Как стать автором
Обновить

Комментарии 47

Идея обвязывать заодно первый уровень коллекций интересная.
По идее, если уж так делать, то можно было бы и следующие уровни обвязывать и не только коллекций но и просто вложенных классов.
Почему коллекции инициализируются только или элементами ли коллекциями? Что если я использую ленивые итераторы?
Непонятно в каком месте реализовывать валидацию.
Посмотрел сгенерированный код projectlombok.org/features/Singular-snippet.html
Метод public SingularExample build() как-то переусложнён — всё это можно было бы спокойно вынести в хелперы.
Почему списки билдера инициализируются ArrayList а не LinkedList?
Непонятно как организовать обязательные поля и как соорудить контекстно-зависимый билдер.
Почему списки билдера инициализируются ArrayList а не LinkedList?

Странный вопрос, на практике LinkedList почти всегда проигрывает ArrayList, как по скорости работы, так и по потребляемой памяти.
И действительно.
habrahabr.ru/post/233797
---Add elements ( 6kk )
LinkedList: 2264 ms
ArrayList: 493 ms
ArrayList is faster
с помощью итератора (когда не нужно ходить по списку каждую итерацию для поиска последнего элемента) LinkedList быстрее.
пруф
Смотрите, такие тесты (вставить тысячу элементов в одно место) мало полезны, так как
1) Если нужно вставить тысячу элементов в начало или середину — создаем новый ArrayList на тысячу элементов и вставляем его как addAll — получится быстрее чем у LinkedList,
2) Если нужно вставлять элементы в произвольные места списка, то сложность итерирования LinkedList съест всякую пользу от скорости вставки по сравнению с ArrayList,
3) Если нужно часто удалять элементы, проще в ArrayList записывать null, а потом null'ы игнорировать — ArrayList будет куда быстрее,
4) Если нужно часто вставлять элементы в произвольные места списка — лучше использовать SortedMap'у c числовым ключом (элементы вставляются быстро по ключу и итерирование будет тоже быстрым).
5) Надо не забывать что в реальных случаях, List надо создать, обработать и куда-то записать результат. Создание и запись результата у LinkedList настолько дорогие, что как правило любая польза от алгоритма это не перевешивает,
6) Не стоит забывать что КАЖДЫЙ элемент LinkedList это отдельный элемент Node с ссылкой на значение и первый и последний элемент, поэтому во-первых, каждый элемент ест минимум в 8 раз больше памяти, создание нового объекта весьма дорого, так и сборка мусора после LinkedList в разы дороже, если ArrayList на миллион элементов собирается как один объект, то в LinkedList как миллион и один объект.
7) плюс LinkedList хранится в памяти в разных блоках, а ArrayList в одном, что ускоряет обработку,

Поэтому такие пруфы в реальных задачах смысла не имеют, использование LinkedList крайне крайне редко оправдано.
В 6 пункте я имел в виду, конечно, что каждый элемент LinkedList это отдельный объект класса Node.
Вся проблема в оверхеде при аллокации памяти при изменении очередности элементов. В LinkedList этой проблемы нету.
Вы рассматриваете абстрактных коней в вакууме, а вам приводят объективные доводы в пользу той или иной структуры хранения данных.

Потому пройдемся по списку:

1) Пример надуман, да и не представляю что создать миллионный массив + 1000 быстрее, чем просто добавить 1000
2) Приведите пример? В чем сложность итерирования вообще чего либо?
3) Это заставит приложение разрастить просто нереально, о эффективном использовании памяти речь не будет идти вовсе. (Не забываем про аллокацию при достижении лимита выделенной памяти под массив)
4) Вода, нужен пруф и тесты
5) Снова вода и снова нужны пруф и тесты
6) Каждый элемент массива — указатель. Конечно памяти ест меньше, но ваше сравнение некорректное, ведь для каждой структуры свои цели — советую почитать что и где лучше использовать.
7) При добавлении неизвестного количества элементов LinkedList даст фору именно по этой причине.

Вы написал «пруфы смысла не имеют» — не надо так. Я сам использую почти всегда ArrayList, но лишь по той причине, что скорость работы и количество аллокаций мне не важны. В BigData разговор был бы совсем другой.
нужен пруф и тесты

Пруф? Официальная документация вам будет достаточным пруфом?

There are two general-purpose List implementations — ArrayList and LinkedList. Most of the time, you'll probably use ArrayList, which offers constant-time positional access and is just plain fast. It does not have to allocate a node object for each element in the List, and it can take advantage of System.arraycopy when it has to move multiple elements at the same time. Think of ArrayList as Vector without the synchronization overhead.

If you frequently add elements to the beginning of the List or iterate over the List to delete elements from its interior, you should consider using LinkedList. These operations require constant-time in a LinkedList and linear-time in an ArrayList. But you pay a big price in performance. Positional access requires linear-time in a LinkedList and constant-time in an ArrayList. Furthermore, the constant factor for LinkedList is much worse. If you think you want to use a LinkedList, measure the performance of your application with both LinkedList and ArrayList before making your choice; ArrayList is usually faster.

Английским же языком сказано, что LinkedList даже при добавлении в центр/середину как правило будет хуже и надо очень хорошо подумать прежде чем его использовать.

>> 2) Приведите пример? В чем сложность итерирования вообще чего либо?
Тем что сложность итерирования в LinkedList O(n), а добавление O(1), сложность итерирования в ArrayList — O(1), а добавления O(n). То есть добавить элемент в случайное место сложность будет одинаковая O(n), но как сказано в официальной документации у LinkedList это O(n) будет намного хуже, чем O(n) ArrayList.

>> 3) Это заставит приложение разрастить просто нереально, о эффективном использовании памяти речь не будет идти вовсе. (Не забываем про аллокацию при достижении лимита выделенной памяти под массив)
Вовсе нет, если кол-во null будет больше половины. можно скопировать не null элементы в новый массив это O(n), но все n удаления c перемещениями будут O(n), то есть O(2*n). В то время как в LinkedList перемещения с ударениями будут O(n^2). Учитывая намного большие затраты памяти в LinkedList, даже заранее выделение достаточного лимита и трата лишней памяти в ArrayList будет экономнее

>>4) Вода, нужен пруф и тесты
>>5) Снова вода и снова нужны пруф и тесты
Достаточно просто посчитать сложность алгоритмов (вставка и итерирование Map'ы это O(1), что куда лучше чем O(n) у LinkedList) и посмотреть что написано в офф.документации про LinkedList.

>>7) При добавлении неизвестного количества элементов LinkedList даст фору именно по этой причине.
Не даст, достаточно выделить достаточно памяти под ожидаемый алгоритм если нужно оптимизировать скорость или ограничить память, если небольшая потеря производительности при переаллокации допустима.

>>скорость работы и количество аллокаций мне не важны. В BigData разговор был бы совсем другой.
Вы серьезно? Когда я оптимизировал работу с BigData мне приходилось отказываться не только от LinkedList, но даже от HashMap и ArrayList и работать тупо с массивами.
Про выдержку на английском я тоже согласен, о чем и написал в конце предыдущего сообщения. «тупо массив» использовать не выйдет при неизвестном количестве элементов, и придется писать заново велосипед для расширения массива, который уже внутри ArrayList. Хотя в случае с забиванием null пожалуй соглашусь с вами, потери тут будут минимальны.

А вот по сложности алгоритмов — вы путаете произвольный доступ к случайному элементу и итерирование. Было бы странным при практически равных показателях на операции с чтением всех элементов иметь в одном случае o(n) а в другом o(1).

Также хотел бы ОБЯЗАТЕЛЬНО обратить внимание на пункт с последовательностью в памяти к элементам, что должно было бы упростить доступ к ним — в java в памяти последовательно лежит только массив указателей на указатели, сами же указатели на объекты и объекты будут непоследовательно лежать в памяти, в разных местах.
Когда я оптимизировал работу с BigData мне приходилось отказываться не только от LinkedList, но даже от HashMap и ArrayList и работать тупо с массивами.

Интересно. ArrayList же просто обёртка над массивом. Что в нём было неэффективно и возможна ли другая более эффективная обёртка над массивом?
И чем вы заменили HashMap? Параллельными сортированными массивами? И возможна ли эффективная обёртка?
ArrayList же просто обёртка над массивом. Что в нём было неэффективно и возможна ли другая более эффективная обёртка над массивом?

ArrayList неэффективен если надо работать с примитивными типами, так он всегда использует обертки примитивов, что ест память и производительность. Конечно, существуют trove коллекции, но зачем вводить лишнею обертку над массивами с неизвестным overhead'ом (да и не для всего они подходят), если массива хватало за глаза? Тем более что при больших данных проблема была не в том что неизвестно сколько элементов в массивах, а в том чтобы засунуть работу с миллиардами записей в несчастные сто гигабайт памяти, не убив производительность.

И чем вы заменили HashMap? Параллельными сортированными массивами? И возможна ли эффективная обёртка?

Массивом с похожим функционалов как у HashMap'ы (большую часть тупо скопипастили) но с клонированием ключей и значений вместо создания новых объектов и одновременным поиском/вставкой. Это позволило ускорить работу в пару раз (что в результате позволило сэкономить часы работы сервера ежедневно).
Т.е. эти оптимизации в основном были связаны с неэффективной работой Java с массивами примитивных типов?
Нет, главные оптимизации были с катомными HashMap — клонирование объектов ключа и значения вместо дорого создания объектов и реализация метода getOrCreate, когда объект находится или создается одной итерацией, а не двумя. Примитивные типы это уже поскольку постольку.
У первого подхода есть еще одна неприятность, что поля должны быть инициализированы финальными значениями, а значит что-нибудь простое
public class HelloWorld{
    static class A {
        int a=0;
    }

     public static void main(String []args){
        List<A> as = new ArrayList<>();
        for(int i=0; i<10; ++i)
            as.add(new A(){{a=i;}});
     }
}

не скомпилируется, так как i не final.
Да вы правы, такое не прокатит.
Но, в общем, все уже привыкли не только к особенностям анонимных классов, но и вовсю используют лямбды Java 8 и знают про final/effectively final.
Я например чаще использую for each, тогда в аналогичном коде для Java 8+ ничего не надо будет менять, только для Java < 8 надо будет добавить final:
        for(final int i : new int[] {1,2,3}) {
            new MyClass.Builder(){{ first = i; third = "3"; }}.create();
        }

Чего только не придумают, чтобы не писать на C#. ;-D

Второй пример уже любопытен, но его применимость в реальных проектах под вопросом. Необходимых условий получается многовато:

1. Спец, который может одолеть весь этот матан.
2. Нубы в большой концентрации, которых нужно ограничивать.
3. Более-менее стабильный интерфейс, потому что проделывать это каждый раз невесело.
4. Малое количество вариантов, потому что код растёт экспоненциально (?).
а как вам поможет c# при реализации паттерна builder?
В случае самого простого «билдера» отдельный класс не нужен:

а) Можно задавать read-write свойства в блоке инициализации. Если какие-то свойства обязательны, добавить их в конструктор.
new Foo(1) { Bar = 2, Baz = 3 }
В отличие от Java, отдельный класс для каждого случая генерироваться не будет, никакой контекст захватываться тоже не будет, это просто запись свойств.

б) Можно сделать конструктор или статический метод со всеми доступными свойствами, для всех опциональных задать значения по умолчанию, вызывать с именованными аргументами:
new Foo(foo: 1, bar: 2, baz: 3)
Foo.CreateFromBar(2, baz: 3)
Этот способ даёт возможность пропускать имена аргументов, но можно прописать в конвенции (а также прикрутить проверку в IDE и при коммите по вкусу), что в таких случаях использование именованных аргументов обязательно.

Инициализация коллекций и словарей поддерживается для конструкторов:
new Foo { Bars = { 1, 2, 3 }, Bazs = { [1] = 2, [3] = 4 } }
Кажется, обсуждали поддержку блока инициализации ещё и для статических методов, но не помню, к чему там пришли.

Чуть больше кода в этом комментарии: habrahabr.ru/post/244521/#comment_8154859 (что характерно, тоже заминусованном).
По поводу варианта А — в этом случае наверное не может быть final полей и не могут выполнятся проверки.
Вот вариант Б — да, именованные параметры со значениями по умолчанию сделали бы многие простые билдеры ненужными, однако…
Сейчас я добавил сокращённые версии классов с минимальным билдером. Там по новым ссылкам можно их увидеть github.com/speaking-fish/java-sf-builder-simple-example — классы New*
В них в конструктор класса передаются не все параметры, а только билдер. Т.е. теперь не надо перечислять все параметры в функциях.
Вы же согласны что лучше иметь сложные проверки отдельно от конструктора, в билдере.
Т.е. тогда в C# билдер останется, но не отдельным классом а отдельной функцией, так?
Тогда возникает вопрос, а как будет работать наследование?
Если у меня наследуется класс билдера — то он наследует все свои поля и их потом не надо опять все перечислять.
А что с параметрами функции? Их можно унаследовать или в унаследованных классах придётся как перечислять всё больше и больше параметров и делать какие-то проверки чтобы их случайно не упустить, и в каждом классе потом искать и исправлять дефолтные значения или определять еще для них константы и использовать константы?
Поясните этот момент.
Это вообще скорее не C# касается, а всех языков с возможностью использования именованных параметров функций.
Т.е. в результате, отдельный класс билдера будет проще использовать и в таких языках.
PS. А инициализация коллекций и словарей многоуровневая или только первый уровень поддерживается?
По поводу варианта А — в этом случае наверное не может быть final полей и не могут выполнятся проверки.

Read-only поля и свойства задать так не получится, им можно задавать значение только в конструкторе, а вот проверки можно положить в сеттер свойств, если они read-write.

Вы же согласны что лучше иметь сложные проверки отдельно от конструктора, в билдере.

Почему? Это банальная проверка аргументов в конструкторе.

Тогда возникает вопрос, а как будет работать наследование?

Если честно, я нечасто вижу глубокие иерархии сложных объектов с билдерами на десятки свойств, поэтому вопросом, что делать в общем случае, я не задавался. :) Если количество свойств идёт на десятки, а глубина иерархии дальше одинарной, то я бы не занимался сомнительной копипастой фабричных методов с именованными аргументами, а занялся бы поиском более вменяемого интерфейса, вероятно, с отдельными билдерами, у которых задание свойств собрано в осмысленные выражения. Это тот момент, когда нужно отвлечься от механического кодинга и включить мозг.

Всё-таки мы говорим про простые билдеры. В их рамках копипаста нескольких аргументов займёт меньше места, чем разведение иерархии билдеров. Набивать код проблемы не составит, решарпер всё сам скопипастит. Хотя, конечно, нехорошо, что значения будут дублироваться. Можно в константы вынести, если есть опасения, что значения изменятся.

Скажем, вот код, эквивалентный вашим NewBaseClass.java и NewChildClass.java (где-то в два раза короче):

using static System.Console;

class CsBaseClass
{
    public int First { get; }
    public double Second { get; }
    public string Third { get; }

    public CsBaseClass (int first = -1, double second = double.NaN, string third = null)
    {
        First = first;
        Second = second;
        Third = third;
    }

    public override string ToString () => $"{GetType().Name} - First: {First}, Second: {Second}, Third: {Third}";
}

class CsChildClass : CsBaseClass
{
    public object Fourth { get; }

    public CsChildClass (int first = -1, double second = double.NaN, string third = null, object fourth = null)
        : base(first, second, third)
    {
        Fourth = fourth;
    }

    public override string ToString () => $"{base.ToString()}, Fourth: {Fourth}";
}

class Program
{
    static void Main ()
    {
        WriteLine(new CsBaseClass(first: 1, third: "3"));
        WriteLine(new CsChildClass(first: 1, third: "3", fourth: 4));
    }
}

Если количество свойств доберётся до десяти я так, конечно, делать не буду. :)

В C# 7 планируется сделать что-то вроде этого (не следил за последними обновлениями, могу соврать; там были заморочки с именованием аргументов и свойств):

using static System.Console;

class CsBaseClass (int First = -1, double Second = double.NaN, string Third = null);

class CsChildClass : CsBaseClass (int First = -1, double Second = double.NaN, string Third = null, object Fourth = null);

class Program
{
    static void Main ()
    {
        WriteLine(new CsBaseClass(First: 1, Third: "3"));
        WriteLine(new CsChildClass(First: 1, Third: "3", Fourth: 4));
    }
}

В этом случае даже с константами для избавления от копипасты код будет несоразмеримо короче, чем с билдерами.

PS. А инициализация коллекций и словарей многоуровневая или только первый уровень поддерживается?

Многоуровневая.
Пример билдера который никак нельзя воткнуть ни в конструктор ни в основной билдер:
public class LocalBuilderExample {
    
    protected final double base;
    
    public LocalBuilderExample(double base) {
        super();
        this.base = base;
    }

    public class BaseBuilder extends NewBaseClass.BuilderImpl {
        public NewBaseClass create() {
            if(!Double.isNaN(base)) {
                first  = (int) Math.round(base) + first;
                if(!Double.isNaN(base)) {
                    second = base + second;
                } else {
                    second = base;
                }
            }
            return new NewBaseClass(this);
        };
    }
}

Вы предлагаете в таком случае дублировать все описания параметров вместе со всеми дефолтными значениями.

Скажем, вот код, эквивалентный вашим NewBaseClass.java и NewChildClass.java (где-то в два раза короче):

В вашем коде не хватает констант для дефолтных значений — это соответственно увеличит код.
Добавим их и сравним построчно код игнорируя пустые строки (я убрал модификаторы доступа чтобы влезло):
C#                                              Java
class BaseClass {                           =   class BaseClass {                                       
                                             
                                                                                                                       
                                             +  static class BuilderImpl {                                                                                                          
                                             +      int    first  = -1        ;                                                                                                        
                                             +      double second = Double.NaN;                                                                                                        
                                             +      String third  = null      ;                                                                                                        
                                             +  }                                                                                                                                             
                                             +5
                                             +  static class Builder extends BuilderImpl {                                                                                                                                                                
                                             +      BaseClass create() { return new BaseClass(this); }                                                                                                                                              
                                             +  }                                                                                                                                                                                                                
                                             +3                                                                                                                                               
const int    DEFAULT_First  = -1        ; +       
const double DEFAULT_Second = double.NaN; +       
const string DEFAULT_Third  = null      ; +       
                                          +3                                                                         
const int    First  { get; }                =   final int    first ;                                           
const double Second { get; }                =   final double second;                                       
const string Third  { get; }                =   final String third ;                                       
                                                                                                                     
                                             +  int    first () { return first ; }                            
                                             +  double second() { return second; }                            
                                             +  String third () { return third ; }                            
                                             +3                                                                      
BaseClass(                                  =   BaseClass(                        
    int    first  = DEFAULT_First ,         =       BuilderImpl builder                  
    double second = DEFAULT_Second,       +                                       
    string third  = DEFAULT_Third ,       +
) {                                       +2=   ) {                                       
    First  = first;                         =       this.first = builder.first ;                                     
    Second = second;                        =       this.second= builder.second;                                     
    Third  = third;                         =       this.third = builder.third ;                                     
}                                           =   }                               
                                                  
}                                           =   }                                  
                                                                                  
                                            =12                                                                                                                                 
                                          +5 +11
                                             +6
                                          17  23    
                                              
class ChildClass : BaseClass {              =   class ChildClass extends BaseClass {                                      

                                             +  static class BuilderImpl extends BaseClass.BuilderImpl {                                                                    
                                             +      Object fourth = null;                                                                                                         
                                             +3 }                                                                                                                                        
                                                                                                                                                                                         
                                             +  static class Builder extends BuilderImpl {                                                                                        
                                             +      ChildClass create() { return new ChildClass(this); }                                                                    
                                             +3 }
                                                                                                                                                                                        
const object DEFAULT_Fourth = null;       +          
                                          +1    
const object Fourth { get; }                =   final Object fourth;

                                             +  Object fourth() { return fourth; }
                                             +1 
ChildClass (                                =   ChildClass(
    int    first  = DEFAULT_First ,         =       BuilderImpl builder
    double second = DEFAULT_Second,       +     
    string third  = DEFAULT_Third ,       +                                                                          
    object fourth = DEFAULT_Fourth        +                                                  
) : base(                                 +3=   ) { super(                                                                     
    first ,                                 =       builder                        
    second,                               +                                                                          
    third                                 +                                  
) {                                       +2=       );                                                  
    Fourth = fourth;                        =       this.fourth = builder.fourth;                                    
}                                           =   }                                                                    
                                                                                                                       
}                                           =   }                                                                        
                                            =10      
                                          +6 +7
                                             +1    
                                          16 17
                                          
                                            =22
                                         +11 +18
                                             +7
                                         33   40              

В сухом остатке:
Базовый класс в варианте с билдером примерно на четверть больше а не в два раза.
Наследник первого уровня примерно такой же.
Если брать два этих класса суммарно, то в Java будет примерно на четверть больше.
Понятно, что с добавлением уровней наследования вариант с именованными параметрами растёт, а вариант с билдером практически остается одинаковым.
Вывод: Если у вас один класс у которого нет наследников, то однозначно именованные параметры короче.
Если у класса есть наследники второго и более уровней, то однозначно билдер проще.

Ну конечно в варианте с большим количеством параметров гораздо легче ошибиться (несоответствие типа, несоответствие порядка)

В C# 7 планируется сделать что-то вроде этого

Генерация конструкторов? Но тогда проверки сделать не получится.

Многоуровневая.

Можно пример?
Вы предлагаете в таком случае дублировать все описания параметров вместе со всеми дефолтными значениями.

Что-то мешает вынести в отдельную функцию?

В вашем коде не хватает констант для дефолтных значений — это соответственно увеличит код.

Тут есть нюанс. Изменение значений по умолчанию — это катаклизм, потому что ломает обратную совместимость. Куда эти значения ни прячь, подобное изменение потенциально ломает весь код. Поэтому значения по умолчанию следует использовать тогда, когда ты уверен, что они не изменятся и ничто не сломают.

Причём в случае значений по умолчанию, прошитых в аргументах конструктора, преступление чуть меньшее, потому что реальные значения вкомпиливаются в код, то есть ломается только сорцовая совместимость, но не бинарная на границе сборок. В случае билдера ломается и то, и другое.

Учитывая такую ситуацию, в случае использования трёх-пяти значений я ограничусь копипастой, даже если это не самый красивый и идеологически верный способ. Потому что, если мне захочется менять значения по умолчанию, чего я делать не должен, я и так всё поломаю к чертям, так зачем же платить больше? Мне всё равно нужно проверить абсолютно все использования конструкторов во всём коде, изменение самого конструктора — это сущая мелочь на фоне этого.

Добавим их и сравним построчно код игнорируя пустые строки (я убрал модификаторы доступа чтобы влезло)

Знаете, ваше форматирование в виде

ChildClass (
    int    first  = DEFAULT_First ,
    double second = DEFAULT_Second,
    string third  = DEFAULT_Third ,
    object fourth = DEFAULT_Fourth
) : base(
    first ,
    second,
    third
) {
    Fourth = fourth;
}

Мне сильно напомнило документацию по Win32 в MSDN. Там тоже аргументы в столбик всегда рисуют. :)

HWND hwnd = CreateWindowEx(
    0,
    CLASS_NAME,
    L"Learn to Program Windows",
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    NULL,
    NULL,
    hInstance,
    NULL
    );

Давайте уж тогда исключим форматирование из уравнения и коллапсируем whitespace ("\s+" => " "). Мерять только строчками некорректно, потому что сложность восприятия строчек разной длины разная.

Сравниваемый код
C#:

using static System.Console;

public class CsBaseClass
{
    public const int DefaultFirst = -1;
    public const double DefaultSecond = double.NaN;
    public const string DefaultThird = null;

    public int First { get; }
    public double Second { get; }
    public string Third { get; }

    public CsBaseClass (int first = DefaultFirst, double second = DefaultSecond, string third = DefaultThird)
    {
        First = first;
        Second = second;
        Third = third;
    }

    public override string ToString () => $"{GetType().Name} - First: {First}, Second: {Second}, Third: {Third}";
}

public class CsChildClass : CsBaseClass
{
    public const object DefaultFourth = null;

    public object Fourth { get; }

    public CsChildClass (int first = DefaultFirst, double second = DefaultSecond, string third = DefaultThird,
        object fourth = DefaultFourth)
        : base(first, second, third)
    {
        Fourth = fourth;
    }

    public override string ToString () => $"{base.ToString()}, Fourth: {Fourth}";

    static void Main ()
    {
        WriteLine(new CsBaseClass(first: 1, third: "3"));
        WriteLine(new CsChildClass(first: 1, third: "3", fourth: 4));
    }
}

Java:

public class NewBaseClass {
    protected static class BuilderImpl {
        public int    first  = -1        ;
        public double second = Double.NaN;
        public String third  = null      ;
    }
    
    public static class Builder extends BuilderImpl {
        public NewBaseClass create() { return new NewBaseClass(this); }
    }
    
    protected final int    first ;
    protected final double second;
    protected final String third ;
    
    protected NewBaseClass(BuilderImpl builder) {
        super();
        this.first = builder.first ;
        this.second= builder.second;
        this.third = builder.third ;
    }

    public int    first () { return first ; }
    public double second() { return second; }
    public String third () { return third ; }
    
    @Override public String toString() {
        return "MyClass [first=" + first + ", second=" + second + ", third=" + third + "]";
    }
}

public class NewChildClass extends NewBaseClass {
    protected static class BuilderImpl extends NewBaseClass.BuilderImpl {
        public Object fourth = null;
    }
    
    public static class Builder extends BuilderImpl {
        public NewChildClass create() { return new NewChildClass(this); }
    }
    
    protected final Object fourth;
    
    protected NewChildClass(BuilderImpl builder) {
        super(builder);
        this.fourth = builder.fourth;
    }

    public Object fourth() { return fourth; }

    @Override public String toString() {
        return "MyChildClass [first=" + first + ", second=" + second + ", third=" + third + ", fourth=" + fourth + "]";
    }

    public static void main(String[] args) {
        System.out.println(new NewBaseClass.Builder(){{ first = 1; third = "3"; }}.create());
        System.out.println(new NewChildClass.Builder(){{ first = 1; third = "3"; fourth = 4; }}.create());
    }
}

Имеем:

C# без констант: 824
C# с константами: 1056
Java с билдером: 1579

Идеологические синтаксические отличия минимальные (extends вместо двоеточия, static у вложенных классов), поэтому, думаю, оценка более-менее точная.

Ещё следует учесть, что у меня 2 класса, а у вас 6. И это гораздо мощнее влияет на сложность, чем спровоцированная мной фаллометрия строчками. :)

Ну конечно в варианте с большим количеством параметров гораздо легче ошибиться (несоответствие типа, несоответствие порядка)

Как я уже сказал, этот код вручную писать необязательно. «Написание» конструктора для класса-потомка у меня свелось к аккорду Alt+Ins, Enter, Space, Enter.
Вы предлагаете в таком случае дублировать все описания параметров вместе со всеми дефолтными значениями.

Что-то мешает вынести в отдельную функцию?

Создание новой функции приведёт к дублированию всего описания параметров. А в случае билдера — нет.

Сделал одинаковое форматирование и добавил на github: github.com/speaking-fish/java-sf-builder-simple-example/tree/master/comparison
Получилось следующее:
Полный код:
C#: 41 lines (28 sloc) 1.217 kB
Java: 51 lines (35 sloc) 1.616 kB
Java +24% lines +25% sloc +32% байт

Без toString и main:
C#: 30 lines (21 sloc) 0.813 kB
Java: 42 lines (29 sloc) 1.13 kB
Java +40% lines +38% sloc +39% байт

Без полей и getter-ов — рассматриваем только разницу именованные параметры / билдер:
C#: 24 lines (17 sloc) 0.674 kB
Java: 32 lines (22 sloc) 0.875 kB
Java +33% lines +29% sloc +30% байт

Выводы: В худшем синтетическом случае кода больше на 40% и то это сокращение в основном из-за getter-ов. В реальном коде увеличение кода даже в таком минимальном варианте (только два класса) не будет более чем на треть.

Как я уже сказал, этот код вручную писать необязательно. «Написание» конструктора для класса-потомка у меня свелось к аккорду

Обычно надо не только сгенерить код но и поддерживать его.
Что будет в случае следующих рефакторингов?:
Изменение типа поля.
Изменение названия поля.
Изменение значения по умолчанию.
Перенос поля из одного класса в другой.
В случае выделенного объекта билдер это простые контролируемые компилятором операции.
А как с параметрами?

Создание новой функции приведёт к дублированию всего описания параметров. А в случае билдера — нет.

Вообще-то дублирование параметров — неизбежное зло. Вам никогда не приходилось прокидывать аргументы через несколько вызовов по цепочке? Конкретно в этом случае можно выделить обработку конкретно этих двух аргументов в отдельную функцию. Это будет осмысленная функция с нормальным названием и ясным функционалом, поэтому это совершенно нормальное явление.

Вы так говорите про дублирование, как будто слово «first» повторяется в джавовом коде реже, чем в шарповом. Пока что во всех версиях на обоих языках дублирования хоть попой кушай. Хоть как-то приближается к избавлению от дублирования только планируемый синтаксис C# 7, и то там одним упоминанием названия невозможно обойтись.

Выводы: В худшем синтетическом случае кода больше на 40% и то это сокращение в основном из-за getter-ов. В реальном коде увеличение кода даже в таком минимальном варианте (только два класса) не будет более чем на треть.

Для меня 30–50% — это очень даже ощутимое число. И, как я уже сказал, различия в количестве классов даже более важны, чем слоки, байты и непробельные символы.

Что будет в случае следующих рефакторингов?

Ну, будет работа не на часик, а на два. Вроде, даже решарпер не очень поможет, потому что полноценная обработка цепочек конструкторов не реализована. Фиче-реквест был, но, вроде, до него ещё не добрались.

В случае выделенного объекта билдер это простые контролируемые компилятором операции.

Я ж уже говорил: это весьма эфемерный бонус. Тип и имя поля-то вы измените, даже сэкономите 50% времени относительно грязной версии с копипастой, но ведь всё равно основное время будет убито на поиск и исправление всех использований во всём коде. Все перечисленные вами изменения ломают обратную совместимость, не получится изменить только одно место.
Вообще-то дублирование параметров — неизбежное зло. Вам никогда не приходилось прокидывать аргументы через несколько вызовов по цепочке?

Нет никакого неизбежного зла — при необходимости часть или весь список параметров превращается в один объект.
Зависит от того насколько то или иное удобно.
Вы так говорите про дублирование, как будто слово «first» повторяется в джавовом коде реже, чем в шарповом.

Слово first встречается только при певром упоминании — в первом объекте и билдере.
Хоть как-то приближается к избавлению от дублирования только планируемый синтаксис C# 7

Этот сахар опять только для первого объявления а не для всех последующих.
Для меня 30–50% — это очень даже ощутимое число. И, как я уже сказал, различия в количестве классов даже более важны, чем слоки, байты и непробельные символы.

Там не было 50% — максимум 40%. Количество классов само по себе ничему не вредит — например мы пользуемся в больших количествах анонимными классами просто потому что так удобнее.

Что будет в случае следующих рефакторингов?

Ну, будет работа не на часик, а на два. Вроде, даже решарпер не очень поможет, потому что полноценная обработка цепочек конструкторов не реализована. Фиче-реквест был, но, вроде, до него ещё не добрались.

В случае выделенного объекта билдер это простые контролируемые компилятором операции.

Я ж уже говорил: это весьма эфемерный бонус. Тип и имя поля-то вы измените, даже сэкономите 50% времени относительно грязной версии с копипастой, но ведь всё равно основное время будет убито на поиск и исправление всех использований во всём коде. Все перечисленные вами изменения ломают обратную совместимость, не получится изменить только одно место.

Т.е. вы отвечаете примерно так: «Да, рефакторинга никакого быть не может. Значит и вам он не нужен.» или что «раз придётся копировать код, то пусть так и будет».
Я всё же не столь категоричен, т.к. выбор зависит от ситуации — надо только описать как преимущества так и недостатки обоих подходов.
Нет никакого неизбежного зла — при необходимости часть или весь список параметров превращается в один объект.
Зависит от того насколько то или иное удобно.

Вы будете ради двух-трёх аргументов заводить отдельный класс ради использования один раз?

Слово first встречается только при певром упоминании — в первом объекте и билдере.

Откройте файл, нажмите Ctrl+F, введите «first» и посчитайте. Вот это всё — дублирование.

Количество классов само по себе ничему не вредит

Это усложняет восприятие кода. Может быть, вы супермен, и для вас разницы между 2000 и 6000 классов нет, а вот для меня есть.
Вы будете ради двух-трёх аргументов заводить отдельный класс ради использования один раз?

Это усложняет восприятие кода.

Нет конечно.
Но когда когда количество аргументов функции увеличится вместе с увеличением числа пробросов этих аргументов то для упрощения кода я скорее всего объединю их в класс.
Всё зависит от того что в итоге получается удобнее — пробрасывать каждый раз все параметры но увеличить количество классов, или каждый раз пробрасывать все параметры не увеличивая количество классов.

Откройте файл, нажмите Ctrl+F, введите «first» и посчитайте. Вот это всё — дублирование.

Только мы обсуждаем сейчас не автоматическую генерацию геттеров, а именованые параметры vs объекты.
Только мы обсуждаем сейчас не автоматическую генерацию геттеров, а именованые параметры vs объекты.

Сразу видно, что не последовали совету. :) Посмотрите самую первую версию классов, которую я привел на C# (там, где ни констант, ни билдеров). Слово «first» повторяется 8 раз, да даже слово «fourth» 6 раз (не считая Main). В версии с константами — 11 и 8. С джавой и билдерами — 10 и 8 (без геттеров — 8 и 6). А вот в планируемой версии C# 7 — 2 и 1, и это при том, что функциональности даже больше реализовано (GetHashCode, в частности). Вот это — реально избавление от дублирования. Всё остальное — так, игрушки.

Что я хочу сказать: вы видите только одно дублирование — дублирование аргументов. А вообще-то в обоих языках дублирования дофига, и оно совсем в другом месте.

Это всё равно что оптимизировать в 10 раз скорость выполнения кода, который отнимает меньше 1% времени. Звучит круто, но бесполезно.
Без констант будет не эквивалентный код так как дефолтные значения буду размазаны по всем классам.
Тогда и без сгенерированных геттеров, сравним — мы же сравниваем только именованные параметры vs builder?:
1: public BaseClass(int first = -1...
2: First =
3:  first;
против
1: public int first = -1
2: protected int first;
3: first =
4:  src.first;

builder проигрывает на одно упоминание в базовом классе
1: public ChildClass(int first = -1...
2: base(first...
против
0:

в производном builder выигрывает на два упоминания
итого builder выигрывает на одно упоминание даже у кода с размазанными по всем классам константами

Это всё равно что оптимизировать в 10 раз скорость выполнения кода, который отнимает меньше 1% времени.

Повторюсь — поэтому надо самому определять что удобнее в каждой ситуации.
А наследование в общем-то для того и нужно, чтобы не дублировать код — странно было бы не пользоваться им при необходимости.
Можно пример?

class Foo
{
    public Foo Foo { get; set; }
    public List<Foo> FooList { get; } = new List<Foo>();
    public Dictionary<Foo, Foo> FooDic { get; } = new Dictionary<Foo, Foo>();

    static void Main ()
    {
        new Foo {
            Foo = new Foo {
                FooList = {
                    new Foo(),
                    new Foo()
                },
                FooDic = {
                    [new Foo()] = new Foo(),
                    [new Foo()] = new Foo()
                },
                Foo = new Foo {
                    FooList = {
                        new Foo {
                            FooList = {
                                new Foo {
                                    FooList = {
                                        new Foo()
                                    }
                                }
                            }
                        }
                    },
                    FooDic = {
                        [new Foo {
                            Foo = new Foo()
                        }] = new Foo {
                            Foo = new Foo()
                        },
                        [new Foo {
                            FooDic = {
                                [new Foo()] = new Foo()
                            }
                        }] = new Foo {
                            FooDic = {
                                [new Foo()] = new Foo()
                            }
                        }
                    }
                }
            }
        };
    }
}

Есть ещё возможность к любой коллекции добавить метод Add с произвольными аргументами и добавлять элементы в фигурных скобках, но это в основном используется для словарей, а для них в C# 6 добавили отдельный синтаксис с квадратными скобками. Но вообще фича осталась, иногда полезна.

Ещё есть нюанс, что в зависимости от того, нужно ли создать объект или просто изменить его, либо используется new, либо не используется. В примере выше объект всегда создаётся в иницилизаторе, а коллекции всегда создаются сами. Но можно и наоборот. new Foo { Foo = { Foo = new Foo() } } тоже будет работать, если свойство не будет null после вызова конструктора (но конкретно в этом случае будет stack overflow).
Конструкция конечно вложенная но инициализации тут одноуровневые.
Вот например сначала отдельно инициализируется класс, который потом используется при инициализации Foo
new Foo {
Foo = new Foo {...}
}
А это уже похоже:
new Foo { Foo = { Foo =… } }
А как инициализировать List<List> или Dictionary<Foo, List>?
А как инициализировать List<List> или Dictionary<Foo, List>?

Как-то так:

using System.Collections.Generic;

class Foo
{
    static void Main ()
    {
        var foos = new List<Dictionary<List<Dictionary<Foo, Foo>>, List<List<Foo>>>> {
            new Dictionary<List<Dictionary<Foo, Foo>>, List<List<Foo>>> {
                [new List<Dictionary<Foo, Foo>> {
                    new Dictionary<Foo, Foo> {
                        [new Foo()] = new Foo()
                    }
                }] = new List<List<Foo>> {
                    new List<Foo> {
                        new Foo()
                    }
                }
            }
        };
    }
}
Понятно. Но все так же не синтаксис инициализации двумерного массива.
Как вы себе это представляете? У коллекций могут быть наследники, поэтому без указания классов не выйдет.
Да я понял что многоуровневого аналога как для двумерного массива нет
Я предполагал что это могло бы выглядеть наподобие:
var foos = new List<Dictionary<List<Dictionary<Foo, Foo>>, List<List<Foo>>>> {
    [
        [
            [[new Foo() = new Foo()], [new Foo() = new Foo()]] = [[new Foo(), new Foo()], [new Foo(), new Foo()]],
            [[], [new Foo() = new Foo()]] = [[], []]
        ],
        [
            [] = [[new Foo(), new Foo()], []],
        ];
    ]
};
Выглядит ужасающе, чёрт ногу сломит. :) Но от полноценных литералов для списков и словарей я бы не отказался.
Если компактный код выглядит ужасающие, можно разбавить его комментариями :)
Я если честно, не совсем понял как приведенный код относится к паттерну builder? Это же вроде обычная инициализация объекта. В Java тоже можно сделать конструкторы и сеттере без дополнительного класса. Паттерн builder нужен же для конструирования сложных объектов, чего нету в вашем примере. Отличным примером является написание query builder'а. Если вы напишите пример и он будет выглядеть много красивее, чем на Java, то я скажу вам огромное спасибо.
Автор под «простым билдером» подразумевает билдер, который просто задаёт значения некоторых свойств, без каких-либо украшений. Такой тривиальный билдер в шарпе «встроенный» за счёт именованных параметров и значений аргументов по умолчанию, в джаве тривиальная «встроенная» версия будет приводить к созданию классов и заглатыванию контекста, поэтому нужен более сложный код.

В случае осмысленного сложного билдера каких-либо принципиальных отличий не будет. Ну, что-то сэкономится по мелочам за счёт синтаксического сахара, не более.
На самом деле для использования даже сложных билдеров знание «матана» не нужно, так как это обычно всё вызывается в fluent-стиле и промежуточные шаги нигде явно не пристутствуют.

Яркий пример использования подобных билдеров — библиотека JOOQ:
Result<...> query = create.select(BOOK.AUTHOR_ID, BOOK.TITLE)
      .from(BOOK)
      .groupBy(AUTHOR_ID)
      .fetch();

>> Для использовании наследования, Builder разделяется на две части (один с полями, другой — с методом создания) следующим образом:
Так делать не стоит, сейчас объясню почему:

1. Если Вы используете билдеры и наследование, то оно Вам для чего-либо нужно. И обычно не просто для экономии места при написании переменных, а для того, чтобы переиспользовать билдер:
List<? extends BaseEntity> entities;

public void addEntity(BaseEntity.Builder builder) {
  entities.add(builder.build());
}

2. После присвоения базового параметра при такой реализации будет возвращена ссылка на базовый билдер, т.е. нельзя будет сделать такое:
ExtendedEntity.newBuilder().baseParameter(value).extendedParameter(value2).build();

Это можно обойти хитрым использованием дженериков при наследовании. Могу написать код, если он кому-либо интересен.

По поводу «Mega Builder» — можете привести пример реализации и использования?
>> Для использовании наследования, Builder разделяется на две части (один с полями, другой — с методом создания) следующим образом:
Так делать не стоит, сейчас объясню почему

Это написано про минимальный билдер, который сооружается из double brace initialization, а он не является fluent-интерфейсом.
Mega Builder является fluent-интерфейсом, но он не может поддерживать такого сокращённого наследования с помощью пареадресации унаследованному билдеру, поскольку в унаследованном классе могут быть вообще совершенно другие валидные сочетания параметров, и поэтому их надо полностью описывать заново.
В статье есть все ссылки на описываемый код: github.com/speaking-fish/java-sf-builder-mega github.com/speaking-fish/java-sf-builder-simple-example
В тему: есть прекрасный проект Joda-Beans, который решает многие задачи и исправляет косяки спецификации Java Beans: генерирует автоматически boilerplate code для properties (в том числе и билдеры), добавляет метаданные, equals/hashcode, immutability, etc… Другая хорошая альтернатива — Groovy, которая при необходимости также может генерировать билдеры для объектов.
> Другая хорошая альтернатива — Groovy, которая при необходимости также может генерировать билдеры для объектов.

А еще лучше Scala с кейс-классами и статической типизацией.
Scala — это все-таки отдельная область, с собственной системой типов, стандартной библиотекой, экосистемой, и парадигмами, которые имеют лишь определенную совместимость с Java. Смешивать в решении одновременно Java и Scala, довольно нецелесообразно, хотя и можно.

Groovy же натурально интегрируется с Java, не выходя из ее парадигмы. В данном случае я имел ввиду такой мощный инструмент Groovy как AST. В стандартной библиотеке уже есть гибкие трансформации для создания properties и билдеров. При указании @CompileStatic Groovy AST генерирует код, который 100% интероперабелен с Java и не требует присутствия groovy в рантайме.
Есть ещё project lombok, который используется в compile-time и удобен для описания POJO (умеет генерировать геттеры, сеттеры, toString, hashCode/equals, билдеры). Просто POJO-класс аннотируется и lombok.jar добавляется в classpath.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории