Эффективность Java «Двойная скобка инициализации»?


824

В разделе « Скрытые возможности Java» в верхнем ответе упоминается инициализация двойной скобки с очень заманчивым синтаксисом:

Set<String> flavors = new HashSet<String>() {{
    add("vanilla");
    add("strawberry");
    add("chocolate");
    add("butter pecan");
}};

Эта идиома создает анонимный внутренний класс с инициализатором экземпляра, который «может использовать любые [...] методы в содержащей области».

Главный вопрос: это так же неэффективно, как кажется? Следует ли ограничить его использование одноразовыми инициализациями? (И конечно же хвастаться!)

Второй вопрос: новый HashSet должен быть «этим», используемым в инициализаторе экземпляра ... может кто-нибудь пролить свет на механизм?

Третий вопрос: слишком ли неясна эта идиома для использования в рабочем коде?

Резюме: Очень, очень хорошие ответы, спасибо всем. На вопрос (3) люди считали, что синтаксис должен быть ясным (хотя я бы рекомендовал время от времени комментировать, особенно если ваш код будет передаваться разработчикам, которые могут быть не знакомы с ним).

На вопрос (1) сгенерированный код должен работать быстро. Дополнительные файлы .class вызывают беспорядок в jar-файле и слегка замедляют запуск программы (спасибо @coobird за это). @Thilo отметил, что это может повлиять на сборку мусора, и стоимость памяти для дополнительных загруженных классов может быть фактором в некоторых случаях.

Вопрос (2) оказался наиболее интересным для меня. Если я понимаю ответы, то в DBI происходит то, что анонимный внутренний класс расширяет класс объекта, создаваемого оператором new, и, следовательно, имеет значение «this», ссылающееся на создаваемый экземпляр. Очень аккуратный.

В целом, DBI кажется мне чем-то вроде интеллектуального любопытства. Coobird и другие отмечают, что вы можете достичь того же эффекта с помощью Arrays.asList, методов varargs, Google Collections и предложенных литералов Java 7 Collection. Более новые языки JVM, такие как Scala, JRuby и Groovy, также предлагают краткие обозначения для построения списков и хорошо взаимодействуют с Java. Учитывая, что DBI загромождает путь к классам, немного замедляет загрузку классов и делает код немного более непонятным, я бы, вероятно, от этого уклонился. Тем не менее, я планирую рассказать об этом другу, который только что получил свой SCJP и любит добродушные шутки о семантике Java! ;-) Спасибо всем!

7/2017: Baeldung имеет хорошее резюме инициализации двойной фигурной скобки и считает это анти-паттерном.

12/2017: @Basil Bourque отмечает, что в новой Java 9 вы можете сказать:

Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");

Это точно путь. Если вы застряли с более ранней версией, взгляните на ImmutableSet из Google Collections .


33
Код запах я вижу здесь, что Наивный читатель бы ожидать , flavorsчтобы быть HashSet, но , увы , это анонимный подкласс.
Элазар Лейбович

6
Если вы рассматриваете запуск вместо загрузки производительности, нет никакой разницы, смотрите мой ответ.
Питер Лоури

4
Мне нравится, что вы создали резюме, я думаю, что это полезное упражнение как для вас, так и для улучшения понимания и сообщества.
Патрик Мерфи

3
Это не скрыто, на мой взгляд. Читатели должны знать, что двойное ожидание, @ElazarLeibovich уже сказал об этом в своем комментарии . Сам инициализатор с двойными скобками не существует как языковая конструкция, это просто комбинация анонимного подкласса и инициализатора экземпляра. Единственное, что люди должны знать об этом.
MC Emperor

8
Java 9 предлагает статические фабричные методы неизменяемого набора, которые могут заменить использование DCI в некоторых ситуациях:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Василий Бурк

Ответы:


607

Вот проблема, когда я слишком увлекаюсь анонимными внутренними классами:

2009/05/27  16:35             1,602 DemoApp2$1.class
2009/05/27  16:35             1,976 DemoApp2$10.class
2009/05/27  16:35             1,919 DemoApp2$11.class
2009/05/27  16:35             2,404 DemoApp2$12.class
2009/05/27  16:35             1,197 DemoApp2$13.class

/* snip */

2009/05/27  16:35             1,953 DemoApp2$30.class
2009/05/27  16:35             1,910 DemoApp2$31.class
2009/05/27  16:35             2,007 DemoApp2$32.class
2009/05/27  16:35               926 DemoApp2$33$1$1.class
2009/05/27  16:35             4,104 DemoApp2$33$1.class
2009/05/27  16:35             2,849 DemoApp2$33.class
2009/05/27  16:35               926 DemoApp2$34$1$1.class
2009/05/27  16:35             4,234 DemoApp2$34$1.class
2009/05/27  16:35             2,849 DemoApp2$34.class

/* snip */

2009/05/27  16:35               614 DemoApp2$40.class
2009/05/27  16:35             2,344 DemoApp2$5.class
2009/05/27  16:35             1,551 DemoApp2$6.class
2009/05/27  16:35             1,604 DemoApp2$7.class
2009/05/27  16:35             1,809 DemoApp2$8.class
2009/05/27  16:35             2,022 DemoApp2$9.class

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

«Двойная инициализация», как уже упоминалось, является анонимным внутренним классом с блоком инициализации экземпляра, что означает, что для каждой «инициализации» создается новый класс, обычно с целью создания единого объекта.

Учитывая, что виртуальной машине Java необходимо будет читать все эти классы при их использовании, это может привести к некоторому времени в процессе проверки байт-кода и тому подобное. Не говоря уже об увеличении необходимого дискового пространства для хранения всех этих classфайлов.

Кажется, что при использовании инициализации с двойными скобками есть некоторые издержки, так что, вероятно, не такая уж хорошая идея, чтобы зацикливаться на этом. Но, как заметил Эдди в комментариях, невозможно быть абсолютно уверенным в воздействии.


Просто для справки, инициализация двойной скобки следующая:

List<String> list = new ArrayList<String>() {{
    add("Hello");
    add("World!");
}};

Это похоже на «скрытую» особенность Java, но это просто переписывание:

List<String> list = new ArrayList<String>() {

    // Instance initialization block
    {
        add("Hello");
        add("World!");
    }
};

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


Предложение Коллекционных Литералов Джошуа Блоха для Project Coin было следующим:

List<Integer> intList = [1, 2, 3, 4];

Set<String> strSet = {"Apple", "Banana", "Cactus"};

Map<String, Integer> truthMap = { "answer" : 42 };

К сожалению, он не проник ни в Java 7, ни в 8 и был отложен на неопределенный срок.


эксперимент

Вот простой эксперимент, который я протестировал: 1000 ArrayListс элементами "Hello"и "World!"добавление к ним с помощью addметода, используя два метода:

Метод 1: инициализация двойной скобки

List<String> l = new ArrayList<String>() {{
  add("Hello");
  add("World!");
}};

Метод 2: создание ArrayListиadd

List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");

Я создал простую программу для записи исходного файла Java для выполнения 1000 инициализаций, используя два метода:

Тест 1:

class Test1 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    List<String> l1 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    /* snip */

    List<String> l999 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    System.out.println(System.currentTimeMillis() - st);
  }
}

Тест 2:

class Test2 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>();
    l0.add("Hello");
    l0.add("World!");

    List<String> l1 = new ArrayList<String>();
    l1.add("Hello");
    l1.add("World!");

    /* snip */

    List<String> l999 = new ArrayList<String>();
    l999.add("Hello");
    l999.add("World!");

    System.out.println(System.currentTimeMillis() - st);
  }
}

Обратите внимание, что истекшее время инициализации ArrayListрасширения 1000 с и 1000 анонимных внутренних классов ArrayListпроверяется с помощью System.currentTimeMillis, поэтому таймер не имеет очень высокого разрешения. В моей системе Windows разрешение составляет около 15-16 миллисекунд.

Результаты 10 прогонов двух тестов были следующими:

Test1 Times (ms)           Test2 Times (ms)
----------------           ----------------
           187                          0
           203                          0
           203                          0
           188                          0
           188                          0
           187                          0
           203                          0
           188                          0
           188                          0
           203                          0

Как видно, инициализация двойной скобки имеет заметное время выполнения около 190 мс.

Между тем время ArrayListвыполнения инициализации оказалось равным 0 мс. Конечно, следует учитывать разрешение таймера, но оно, вероятно, будет ниже 15 мс.

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

И да, было .classсгенерировано 1000 файлов при компиляции программы Test1инициализации с двойной скобкой.


10
«Вероятно», будучи оперативным словом. Если не измерять, никакие заявления о производительности не имеют смысла.
Охотник за экземплярами

16
Вы проделали такую ​​замечательную работу, что вряд ли я это скажу, но время Test1 может зависеть от нагрузки классов. Было бы интересно увидеть, как кто-то запускает один экземпляр каждого теста в цикле for, скажем, 1000 раз, а затем запускает его снова в секунду для цикла 1000 или 10000 раз и выводит разницу во времени (System.nanoTime ()). Первый цикл for должен пройти все эффекты разогрева (JIT, classload, например). Оба теста моделируют разные варианты использования. Я попробую запустить это завтра на работе.
Джим Ферранс

8
@Jim Ferrans: Я совершенно уверен, что время Test1 от классов нагрузки. Но следствием использования инициализации с двойной скобкой является необходимость справляться с нагрузками класса. Я считаю, что большинство случаев использования для инициализации двойной скобки. для однократной инициализации, тест ближе по условиям к типичному варианту использования этого типа инициализации. Я полагаю, что многократные итерации каждого теста уменьшат временной разрыв выполнения.
coobird

73
Это доказывает, что а) двойная инициализация медленнее, и б) даже если вы делаете это 1000 раз, вы, вероятно, не заметите разницы. И это не значит, что это может быть узким местом во внутренней петле. Это накладывает крошечный одноразовый штраф НА ОЧЕНЬ ХОРОШО.
Майкл Майерс

15
Если использование DBI делает код более читабельным или выразительным, используйте его. Тот факт, что это немного увеличивает работу, которую должна выполнить JVM, сам по себе не является допустимым аргументом против этого. Если бы это было так, то мы также должны беспокоиться о дополнительных вспомогательных методах / классах, предпочитая вместо этого огромные классы с меньшим количеством методов ...
Rogério

105

Одно из свойств этого подхода, которое не было упомянуто до сих пор, состоит в том, что, поскольку вы создаете внутренние классы, весь содержащий класс захватывается в его область видимости. Это означает, что пока ваш Set жив, он будет хранить указатель на содержащий экземпляр (this$0 ) и удерживать его от сбора мусора, что может быть проблемой.

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

Второй вопрос: новый HashSet должен быть «этим», используемым в инициализаторе экземпляра ... может кто-нибудь пролить свет на механизм? Я бы наивно ожидал, что «this» будет ссылаться на объект, инициализирующий «flavors».

Так работают внутренние классы. Они получают свои собственные this, но у них также есть указатели на родительский экземпляр, так что вы также можете вызывать методы для содержащего объекта. В случае конфликта именования внутренний класс (в вашем случае HashSet) имеет приоритет, но вы можете добавить префикс «this» к имени класса, чтобы также получить внешний метод.

public class Test {

    public void add(Object o) {
    }

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // HashSet
              Test.this.add("hello"); // outer instance 
            }
        };
    }
}

Для ясности в отношении создаваемого анонимного подкласса, вы также можете определить методы в нем. Например переопределитьHashSet.add()

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // not HashSet anymore ...
            }

            @Override
            boolean add(String s){

            }

        };
    }

5
Очень хороший момент по скрытой ссылке на содержащий класс. В исходном примере инициализатор экземпляра вызывает метод add () нового HashSet <String>, а не Test.this.add (). Это наводит меня на мысль, что что-то еще происходит. Есть ли анонимный внутренний класс для HashSet <String>, как предполагает Натан Китчен?
Джим Ферранс

Ссылка на содержащий класс также может быть опасной, если задействована сериализация структуры данных. Содержащий класс также будет сериализован и, следовательно, должен быть Сериализуемым. Это может привести к неясным ошибкам.
просто еще один программист на Java

56

Каждый раз, когда кто-то использует двойную инициализацию, котенка убивают.

Помимо того, что синтаксис довольно необычен и не очень идиоматичен (вкус спорен, конечно), вы без необходимости создаете две существенные проблемы в своем приложении, о которых я недавно недавно более подробно писал в блоге .

1. Вы создаете слишком много анонимных классов

Каждый раз, когда вы используете двойную инициализацию, создается новый класс. Например, этот пример:

Map source = new HashMap(){{
    put("firstName", "John");
    put("lastName", "Smith");
    put("organizations", new HashMap(){{
        put("0", new HashMap(){{
            put("id", "1234");
        }});
        put("abc", new HashMap(){{
            put("id", "5678");
        }});
    }});
}};

... будет производить эти классы:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

Это довольно много для вашего загрузчика классов - ни за что! Конечно, это не займет много времени инициализации, если вы сделаете это один раз. Но если вы сделаете это 20 000 раз по всему корпоративному приложению ... всю эту кучу памяти только для небольшого количества "синтаксического сахара"?

2. Вы потенциально создаете утечку памяти!

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

public class ReallyHeavyObject {

    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;

    // This method almost does nothing
    public Map quickHarmlessMethod() {
        Map source = new HashMap(){{
            put("firstName", "John");
            put("lastName", "Smith");
            put("organizations", new HashMap(){{
                put("0", new HashMap(){{
                    put("id", "1234");
                }});
                put("abc", new HashMap(){{
                    put("id", "5678");
                }});
            }});
        }};

        return source;
    }
}

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

Утечка памяти прямо здесь

Изображение с http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/

3. Вы можете делать вид, что у Java есть литералы карты

Чтобы ответить на ваш реальный вопрос, люди использовали этот синтаксис, притворяясь, что в Java есть что-то вроде литералов карты, аналогично существующим литералам массива:

String[] array = { "John", "Doe" };
Map map = new HashMap() {{ put("John", "Doe"); }};

Некоторые люди могут найти это синтаксически стимулирующим.


7
Спаси котят! Хороший ответ!
Aris2World

36

Принимая следующий тестовый класс:

public class Test {
  public void test() {
    Set<String> flavors = new HashSet<String>() {{
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }};
  }
}

и затем декомпилируя файл класса, я вижу:

public class Test {
  public void test() {
    java.util.Set flavors = new HashSet() {

      final Test this$0;

      {
        this$0 = Test.this;
        super();
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
    };
  }
}

Это не выглядит ужасно неэффективным для меня. Если бы я беспокоился о производительности для чего-то подобного, я бы профилировал это. И на ваш вопрос № 2 отвечает приведенный выше код: вы находитесь внутри неявного конструктора (и инициализатора экземпляра) для вашего внутреннего класса, поэтомуthis " относится к этому внутреннему классу.

Да, этот синтаксис неясен, но комментарий может прояснить использование неясного синтаксиса. Чтобы прояснить синтаксис, большинство людей знакомы со статическим блоком инициализатора (JLS 8.7 Статические инициализаторы):

public class Sample1 {
    private static final String someVar;
    static {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

Вы также можете использовать аналогичный синтаксис (без слова " static") для использования в конструкторе (инициализаторы экземпляров JLS 8.6), хотя я никогда не видел, чтобы это использовалось в рабочем коде. Это гораздо менее общеизвестно.

public class Sample2 {
    private final String someVar;

    // This is an instance initializer
    {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

Если у вас нет конструктора по умолчанию, тогда блок кода между { и }превращается компилятором в конструктор. Имея это в виду, распутайте код двойной скобки:

public void test() {
  Set<String> flavors = new HashSet<String>() {
      {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
  };
}

Блок кода между внутренними фигурными скобками превращается компилятором в конструктор. Внешние скобки ограничивают анонимный внутренний класс. Чтобы сделать этот последний шаг сделать все не анонимным:

public void test() {
  Set<String> flavors = new MyHashSet();
}

class MyHashSet extends HashSet<String>() {
    public MyHashSet() {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }
}

В целях инициализации я бы сказал, что нет никаких накладных расходов (или настолько малых, что ими можно пренебречь). Тем не менее, каждое использованиеflavors будет идти не против, HashSetа против MyHashSet. Вероятно, есть небольшие (и вполне возможно незначительные) накладные расходы на это. Но опять же, прежде чем я беспокоюсь об этом, я бы профилировать это.

Опять же, на ваш вопрос № 2, приведенный выше код является логическим и явным эквивалентом инициализации двойной скобки, и это делает очевидным, где "this " относится: к внутреннему классу, который расширяется HashSet.

Если у вас есть вопросы о деталях инициализаторов экземпляров, ознакомьтесь с подробностями в документации JLS .


Эдди, очень хорошее объяснение. Если байт-коды JVM так же чисты, как и декомпиляция, скорость выполнения будет достаточно высокой, хотя я был бы несколько обеспокоен дополнительным беспорядком файла .class. Мне все еще любопытно, почему конструктор инициализатора экземпляра видит «this» как новый экземпляр HashSet <String>, а не как экземпляр Test. Это просто явно определенное поведение в последней спецификации языка Java для поддержки идиомы?
Джим Ферранс

Я обновил свой ответ. Я упустил образец тестового класса, что вызвало путаницу. Я включил это в свой ответ, чтобы сделать вещи более очевидными. Я также упоминаю раздел JLS для блоков инициализатора экземпляра, используемых в этой идиоме.
Эдди

1
@Jim Интерпретация «это» не является частным случаем; он просто ссылается на экземпляр самого внутреннего включающего класса, который является анонимным подклассом HashSet <String>.
Натан Китчен

Извините, что прыгнул через четыре с половиной года спустя. Но хорошая вещь в файле декомпилированного класса (ваш второй блок кода) заключается в том, что он не является допустимым Java! Он имеет super()в качестве второй строки неявного конструктора, но он должен стоять первым. (Я проверил его, и он не скомпилируется.)
chiastic-security

1
@ chiastic-security: иногда декомпиляторы генерируют код, который не будет компилироваться.
Эдди

35

подвержен утечкам

Я решил вмешаться. Влияние на производительность включает в себя: работа с диском + распаковка (для jar), проверка классов, пространство perm-gen (для JVM Sun's Hotspot). Однако хуже всего: это подвержено утечкам. Вы не можете просто вернуться.

Set<String> getFlavors(){
  return Collections.unmodifiableSet(flavors)
}

Таким образом, если набор уходит в любую другую часть, загруженную другим загрузчиком классов, и там сохраняется ссылка, все дерево классов + загрузчик классов будет пропущено. Чтобы избежать этого, необходима копия в HashMap new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}). Уже не так мило. Я сам не использую идиому, вместо этого new LinkedHashSet(Arrays.asList("xxx","YYY"));


3
К счастью, начиная с Java 8, PermGen больше не вещь. Я полагаю, что есть еще какое-то влияние, но не с потенциально очень неясным сообщением об ошибке.
Джои

2
@Joey, не имеет значения, если память управляется непосредственно GC (perm gen) или нет. Утечка в метапространстве все еще является утечкой, если только мета не ограничена, OOM_killer в linux не будет запущен OOM (из состава perm gen).
bestsss

19

Загрузка многих классов может добавить несколько миллисекунд к началу. Если запуск не так критичен, и вы смотрите на эффективность классов после запуска, нет никакой разницы.

package vanilla.java.perfeg.doublebracket;

import java.util.*;

/**
 * @author plawrey
 */
public class DoubleBracketMain {
    public static void main(String... args) {
        final List<String> list1 = new ArrayList<String>() {
            {
                add("Hello");
                add("World");
                add("!!!");
            }
        };
        List<String> list2 = new ArrayList<String>(list1);
        Set<String> set1 = new LinkedHashSet<String>() {
            {
                addAll(list1);
            }
        };
        Set<String> set2 = new LinkedHashSet<String>();
        set2.addAll(list1);
        Map<Integer, String> map1 = new LinkedHashMap<Integer, String>() {
            {
                put(1, "one");
                put(2, "two");
                put(3, "three");
            }
        };
        Map<Integer, String> map2 = new LinkedHashMap<Integer, String>();
        map2.putAll(map1);

        for (int i = 0; i < 10; i++) {
            long dbTimes = timeComparison(list1, list1)
                    + timeComparison(set1, set1)
                    + timeComparison(map1.keySet(), map1.keySet())
                    + timeComparison(map1.values(), map1.values());
            long times = timeComparison(list2, list2)
                    + timeComparison(set2, set2)
                    + timeComparison(map2.keySet(), map2.keySet())
                    + timeComparison(map2.values(), map2.values());
            if (i > 0)
                System.out.printf("double braced collections took %,d ns and plain collections took %,d ns%n", dbTimes, times);
        }
    }

    public static long timeComparison(Collection a, Collection b) {
        long start = System.nanoTime();
        int runs = 10000000;
        for (int i = 0; i < runs; i++)
            compareCollections(a, b);
        long rate = (System.nanoTime() - start) / runs;
        return rate;
    }

    public static void compareCollections(Collection a, Collection b) {
        if (!a.equals(b) && a.hashCode() != b.hashCode() && !a.toString().equals(b.toString()))
            throw new AssertionError();
    }
}

печать

double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 34 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns

2
Нет разницы, за исключением того, что ваше пространство PermGen испарится, если DBI используется чрезмерно. По крайней мере, так будет, если вы не установите некоторые неясные параметры JVM, чтобы разрешить выгрузку классов и сборку мусора в пространстве PermGen. Учитывая распространенность Java как серверного языка, проблема с памятью / PermGen заслуживает хотя бы упоминания.
Аромат

1
@aroth, это хороший момент. Я признаю, что за 16 лет работы над Java я никогда не работал над системой, в которой вам приходилось настраивать PermGen (или Metaspace). Для систем, над которыми я работал, размер кода всегда оставался достаточно маленьким.
Питер Лори

2
Не должны ли условия в compareCollectionsсочетании с, ||а не &&? Использование &&кажется не только семантически неправильным, оно противодействует намерению измерить производительность, поскольку только первое условие будет проверено вообще. Кроме того, умный оптимизатор может признать, что условия никогда не изменятся во время итераций.
Хольгер

@aroth, просто как обновление: начиная с Java 8 виртуальная машина больше не использует perm-gen.
Ангел О'Сфера

16

Для создания наборов вы можете использовать фабричный метод varargs вместо двойной скобки:

public static Set<T> setOf(T ... elements) {
    return new HashSet<T>(Arrays.asList(elements));
}

В библиотеке Google Collections есть много удобных методов, таких как этот, а также множество других полезных функций.

Что касается неясности идиомы, я сталкиваюсь с ней и постоянно использую ее в рабочем коде. Я был бы более обеспокоен программистами, которых смущает идиома, позволяющая писать производственный код.


Хах! ;-) Я на самом деле Рип ван Винкль, вернувшийся в Java с 1,2 дня (я написал голосовой веб-браузер VoiceXML на evolution.voxeo.com на Java). Было весело изучать дженерики, параметризованные типы, коллекции, java.util.concurrent, новый синтаксис цикла for и т. Д. Теперь этот язык стал лучше. На ваш взгляд, даже несмотря на то, что механизм, лежащий в основе DBI, на первый взгляд может показаться неясным, смысл кода должен быть достаточно ясным.
Джим Ферранс

10

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

Еще один способ добиться декларативного построения списков - использовать Arrays.asList(T ...)так:

List<String> aList = Arrays.asList("vanilla", "strawberry", "chocolate");

Ограничение этого подхода состоит в том, что вы не можете контролировать конкретный тип создаваемого списка.


1
Arrays.asList () - это то, что я обычно использую, но вы правы, такая ситуация возникает в основном в модульных тестах; реальный код будет создавать списки из запросов к БД, XML и т. д.
Джим Ферранс

7
Остерегайтесь asList, хотя: возвращенный список не поддерживает добавление или удаление элементов. Всякий раз, когда я использую asList, я передаю полученный список в конструктор, new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))чтобы обойти эту проблему.
Майкл Майерс

7

В этом нет ничего особенно неэффективного. Обычно для JVM не имеет значения, что вы создали подкласс и добавили в него конструктор - это обычная повседневная вещь на объектно-ориентированном языке. Я могу придумать довольно надуманные случаи, когда вы могли бы сделать это неэффективным (например, у вас есть многократно вызываемый метод, который в итоге принимает смесь разных классов из-за этого подкласса, тогда как обычный передаваемый класс будет полностью предсказуемым). - в последнем случае JIT-компилятор может делать оптимизации, которые не осуществимы в первом). Но на самом деле, я думаю, что случаи, когда это будет иметь значение, очень надуманные.

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

В (2) вы находитесь внутри конструктора объекта, поэтому «this» относится к объекту, который вы создаете. Это ничем не отличается от любого другого конструктора.

Что касается (3), это действительно зависит от того, кто поддерживает ваш код, я думаю. Если вы не знаете этого заранее, тогда я бы предложил использовать эталонный тест: «Вы видите это в исходном коде JDK?» (в этом случае я не помню, чтобы видел много анонимных инициализаторов, и, конечно, не в тех случаях, когда это единственный контент анонимного класса). В большинстве проектов среднего размера, я бы сказал, что вам действительно понадобятся ваши программисты, чтобы в какой-то момент понять источник JDK, поэтому любой используемый здесь синтаксис или идиома - это «честная игра». Кроме того, я бы сказал, обучайте людей этому синтаксису, если у вас есть контроль над тем, кто поддерживает код, а также комментируйте или избегайте.


5

Двойная инициализация - это ненужный хак, который может привести к утечкам памяти и другим проблемам.

Нет законной причины использовать этот «трюк». Guava предоставляет хорошие неизменяемые коллекции, которые включают как статические фабрики, так и сборщики, что позволяет вам заполнить вашу коллекцию так, чтобы она была объявлена ​​чистой, читаемой и безопасной. синтаксисе.

Пример в вопросе становится:

Set<String> flavors = ImmutableSet.of(
    "vanilla", "strawberry", "chocolate", "butter pecan");

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

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

Подверженный ошибкам теперь помечает этот анти-шаблон .


-1. Несмотря на некоторые правильные моменты, этот ответ сводится к «Как избежать создания ненужного анонимного класса? Используйте платформу с еще большим количеством классов!»
Agent_L

1
Я бы сказал, что все сводится к тому, чтобы «использовать правильный инструмент для работы, а не взлом, который может привести к сбою приложения». Guava - довольно распространенная библиотека для приложений (вы определенно пропускаете ее, если не используете), но даже если вы не хотите ее использовать, вы можете и должны избегать двойной инициализации.
dimo414

И как именно инициализация двойной скобки вызовет утечку памяти?
Ангел О'Сфера

@ AngelO'Sphere DBI - это запутанный способ создания внутреннего класса , поэтому он сохраняет неявную ссылку на включающий его класс (если только он не используется в staticконтекстах). Ссылка на ошибку в нижней части моего вопроса обсуждает это дальше.
dimo414

Я бы сказал, что это вопрос вкуса. И в этом нет ничего запутывающего.
Angel O'Sphere

4

Я исследовал это и решил сделать более глубокий тест, чем тот, который был дан в действительном ответе.

Вот код: https://gist.github.com/4368924

и это мой вывод

Я был удивлен, обнаружив, что в большинстве тестов запуска внутреннее инициирование было на самом деле быстрее (в некоторых случаях почти вдвое). При работе с большими числами выгода, кажется, исчезает.

Интересно, что случай, который создает 3 объекта в цикле, теряет свою выгоду раньше, чем в других случаях. Я не уверен, почему это происходит, и нужно сделать больше испытаний, чтобы прийти к каким-либо выводам. Создание конкретных реализаций может помочь избежать перезагрузки определения класса (если это то, что происходит)

Тем не менее, очевидно, что в большинстве случаев не наблюдается больших накладных расходов на создание отдельного элемента, даже при большом количестве.

Одним из препятствий может быть тот факт, что каждая инициализация в виде двойной скобки создает новый файл класса, который добавляет целый блок диска к размеру нашего приложения (или около 1 КБ при сжатии). Небольшой след, но если он используется во многих местах, он может оказать влияние. Используйте это 1000 раз, и вы потенциально добавляете к вашему приложению целый MiB, что может быть связано со встроенной средой.

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

Дайте мне знать, что вы думаете :)


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

3

Я второй ответ Ната, за исключением того, что я бы использовал цикл вместо создания и немедленного удаления неявного List из asList (elements):

static public Set<T> setOf(T ... elements) {
    Set set=new HashSet<T>(elements.size());
    for(T elm: elements) { set.add(elm); }
    return set;
    }

1
Почему? Новый объект будет создан в пространстве eden, поэтому для его создания потребуется всего два или три добавления указателя. JVM может заметить, что она никогда не выходит за пределы области действия метода и размещать его в стеке.
Nat

Да, он может оказаться более эффективным, чем этот код (хотя вы можете улучшить его, указав HashSetпредложенную емкость - запомните коэффициент загрузки).
Том Хотин - tackline

Что ж, конструктор HashSet все равно должен выполнять итерацию, поэтому он не будет менее эффективным. Библиотека код , созданный для повторного использования должны всегда стремиться быть очень наилучшим возможным.
Лоуренс Дол

3

Хотя этот синтаксис может быть удобным, он также добавляет множество ссылок на $ 0, поскольку они становятся вложенными, и может быть трудно выполнить отладку в инициализаторах, если для каждой из них не установлены точки останова. По этой причине я рекомендую использовать это только для банальных сеттеров, особенно для констант, и мест, где анонимные подклассы не имеют значения (например, без сериализации).


3

Марио Глейхман описывает, как использовать универсальные функции Java 1.5 для имитации литералов Scala List, хотя, к сожалению, вы получаете неизменный списки.

Он определяет этот класс:

package literal;

public class collection {
    public static <T> List<T> List(T...elems){
        return Arrays.asList( elems );
    }
}

и использует это таким образом:

import static literal.collection.List;
import static system.io.*;

public class CollectionDemo {
    public void demoList(){
        List<String> slist = List( "a", "b", "c" );
        List<Integer> iList = List( 1, 2, 3 );
        for( String elem : List( "a", "java", "list" ) )
            System.out.println( elem );
    }
}

Коллекции Google, теперь часть Guava, поддерживают аналогичную идею для построения списка. В этом интервью Джаред Леви говорит:

[...] наиболее часто используемые функции, которые появляются почти во всех написанных мной Java-классах, - это статические методы, которые уменьшают количество повторяющихся нажатий клавиш в вашем Java-коде. Это так удобно - вводить команды, подобные следующим:

Map<OneClassWithALongName, AnotherClassWithALongName> = Maps.newHashMap();

List<String> animals = Lists.immutableList("cat", "dog", "horse");

10.07.2014: Если бы это было так просто, как в Python:

animals = ['cat', 'dog', 'horse']

21.02.2020: В Java 11 теперь можно сказать:

animals = List.of(“cat”, “dog”, “horse”)


2
  1. Это будет призывать add()каждого участника. Если вы можете найти более эффективный способ поместить элементы в хэш-набор, используйте его. Обратите внимание, что внутренний класс, скорее всего, будет генерировать мусор, если вы чувствительны к этому.

  2. Мне кажется, что контекст - это возвращаемый объект new, который является HashSet.

  3. Если вам нужно спросить ... Скорее всего: люди, которые придут за вами, знают это или нет? Легко ли это понять и объяснить? Если вы можете ответить «да» на оба вопроса, не стесняйтесь использовать его.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.