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

В разделе « Скрытые возможности 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 .

29.05.2009 03:40:13
Код запах я вижу здесь, что Наивный читатель бы ожидать , flavorsчтобы быть HashSet, но , увы , это анонимный подкласс.
Elazar Leibovich 8.09.2011 09:51:48
Если вы рассматриваете запуск вместо загрузки производительности, нет разницы, смотрите мой ответ.
Peter Lawrey 31.01.2013 13:50:39
Мне нравится, что вы создали резюме, я думаю, что это полезное упражнение как для вас, так и для улучшения понимания и сообщества.
Patrick Murphy 9.05.2016 19:52:26
Это не скрыто, на мой взгляд. Читатели должны знать, что двойное ожидание, @ElazarLeibovich уже сказал об этом в своем комментарии . Сам инициализатор с двойными скобками не существует как языковая конструкция, это просто комбинация анонимного подкласса и инициализатора экземпляра. Единственное, что люди должны знать об этом.
MC Emperor 1.02.2017 07:48:21
Java 9 предлагает статические фабричные методы неизменяемого набора, которые могут заменить использование DCI в некоторых ситуациях:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Basil Bourque 26.11.2017 06:37:03
15 ОТВЕТОВ
РЕШЕНИЕ

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

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инициализации с двойной скобкой.

604
25.09.2019 12:53:15
«Вероятно», будучи оперативным словом. Если не измерять, никакие заявления о производительности не имеют смысла.
Instance Hunter 29.05.2009 04:00:52
Вы проделали такую ​​замечательную работу, что вряд ли я это скажу, но время Test1 может зависеть от нагрузки классов. Было бы интересно увидеть, как кто-то запускает один экземпляр каждого теста в цикле for, скажем, 1000 раз, а затем запускает его снова в секунду для цикла 1000 или 10000 раз и выводит разницу во времени (System.nanoTime ()). Первый цикл for должен пройти все эффекты разогрева (JIT, classload, например). Оба теста моделируют разные варианты использования. Я попробую запустить это завтра на работе.
Jim Ferrans 29.05.2009 05:37:08
@Jim Ferrans: Я совершенно уверен, что время Test1 от классовых нагрузок. Но следствием использования инициализации с двойной скобкой является необходимость справляться с нагрузками класса. Я считаю, что большинство случаев использования для инициализации двойной скобки. для однократной инициализации, тест ближе к условиям типичного варианта использования этого типа инициализации. Я полагаю, что несколько итераций каждого теста уменьшат временной интервал выполнения.
coobird 29.05.2009 05:42:48
Это доказывает, что а) двойная инициализация медленнее, и б) даже если вы делаете это 1000 раз, вы, вероятно, не заметите разницы. И это не значит, что это может быть узким местом во внутренней петле. Это налагает крошечный одноразовый штраф на ОЧЕНЬ ХОРОШО.
Michael Myers♦ 29.05.2009 14:44:40
Если использование DBI делает код более читабельным или выразительным, используйте его. Тот факт, что это немного увеличивает работу, которую должна выполнить JVM, сам по себе не является допустимым аргументом против этого. Если бы это было так, то мы также должны беспокоиться о дополнительных вспомогательных методах / классах, предпочитая вместо этого огромные классы с меньшим количеством методов ...
Rogério 27.12.2009 23:26:00

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

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 .

36
29.05.2009 13:34:34
Эдди, очень хорошее объяснение. Если байт-коды JVM так же чисты, как и декомпиляция, скорость выполнения будет достаточно высокой, хотя я был бы несколько обеспокоен дополнительным беспорядком файла .class. Мне все еще любопытно, почему конструктор инициализатора экземпляра видит «this» как новый экземпляр HashSet <String>, а не как экземпляр Test. Это просто явно определенное поведение в последней спецификации языка Java для поддержки идиомы?
Jim Ferrans 29.05.2009 04:13:45
Я обновил свой ответ. Я упустил шаблон класса теста, который вызвал путаницу. Я включил это в свой ответ, чтобы сделать вещи более очевидными. Я также упоминаю раздел JLS для блоков инициализатора экземпляра, используемых в этой идиоме.
Eddie 29.05.2009 04:29:41
@Jim Интерпретация «это» не является частным случаем; он просто ссылается на экземпляр самого внутреннего включающего класса, который является анонимным подклассом HashSet <String>.
Nathan Kitchen 29.05.2009 05:20:46
Извините, что прыгнул через четыре с половиной года спустя. Но хорошая вещь в файле декомпилированного класса (ваш второй блок кода) заключается в том, что он не является допустимым Java! Он имеет super()в качестве второй строки неявного конструктора, но он должен идти первым. (Я проверил его, и он не скомпилируется.)
chiastic-security 25.11.2014 21:14:12
@ chiastic-security: иногда декомпиляторы генерируют код, который не будет компилироваться.
Eddie 19.12.2014 22:44:50
  1. Это будет призывать add()каждого участника. Если вы можете найти более эффективный способ поместить элементы в хэш-набор, используйте его. Обратите внимание, что внутренний класс, скорее всего, будет генерировать мусор, если вы чувствительны к этому.

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

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

2
25.07.2018 08:12:27

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

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

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

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

10
29.05.2009 05:09:21
Arrays.asList () - это то, что я обычно использую, но вы правы, такая ситуация возникает в основном в модульных тестах; Реальный код будет создавать списки из запросов к БД, XML и так далее.
Jim Ferrans 29.05.2009 04:19:34
Остерегайтесь asList, однако: возвращаемый список не поддерживает добавление или удаление элементов. Всякий раз, когда я использую asList, я передаю полученный список в конструктор, new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))чтобы обойти эту проблему.
Michael Myers♦ 29.05.2009 14:50:17

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

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

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

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

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

Я второй ответ Ната, за исключением того, что я бы использовал цикл вместо создания и немедленного удаления неявного 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;
    }
3
12.10.2009 18:26:03
Почему? Новый объект будет создан в пространстве eden, поэтому для его создания потребуется всего два или три добавления указателя. JVM может заметить, что она никогда не выходит за пределы области действия метода и размещать его в стеке.
Nat 29.05.2009 11:13:10
Да, он может оказаться более эффективным, чем этот код (хотя вы можете улучшить его, указав HashSetпредполагаемую емкость - запомните коэффициент загрузки).
Tom Hawtin - tackline 4.10.2009 00:50:46
Что ж, конструктор HashSet все равно должен выполнять итерацию, поэтому он не будет менее эффективным. Библиотека код , созданный для повторного использования должны всегда стремиться быть очень наилучшим возможным.
Lawrence Dol 12.10.2009 18:29:22

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

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

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

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

7
29.05.2009 05:26:42

Одно свойство этого подхода, которое не было упомянуто до сих пор, состоит в том, что, поскольку вы создаете внутренние классы, весь содержащий класс захватывается в его область действия. Это означает, что до тех пор, пока ваш 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){

            }

        };
    }
105
27.07.2010 22:46:57
Очень хороший момент по скрытой ссылке на содержащий класс. В исходном примере инициализатор экземпляра вызывает метод add () нового HashSet <String>, а не Test.this.add (). Это наводит меня на мысль, что что-то еще происходит. Есть ли анонимный внутренний класс для HashSet <String>, как предполагает Натан Китчен?
Jim Ferrans 29.05.2009 05:58:41
Ссылка на содержащий класс также может быть опасной, если задействована сериализация структуры данных. Содержащий класс также будет сериализован и, следовательно, должен быть Сериализуемым. Это может привести к неясным ошибкам.
Just another Java programmer 7.02.2020 18:32:35

Марио Глейхман описывает, как использовать универсальные функции 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”)

3
22.02.2020 00:46:27

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

Я решил вмешаться. Влияние на производительность включает в себя: работа с диском + распаковка (для 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"));

35
22.08.2013 12:06:24
К счастью, начиная с Java 8, PermGen больше не вещь. Я предполагаю, что есть еще какое-то влияние, но не одно с потенциально очень неясным сообщением об ошибке.
Joey 13.12.2015 09:39:29
@Joey, не имеет значения, если память управляется непосредственно GC (perm gen) или нет. Утечка в метапространстве все еще является утечкой, если только мета не ограничена, OOM_killer в linux не будет запущен OOM (из состава perm gen).
bestsss 6.02.2016 07:50:26

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

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

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

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

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

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

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

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

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

4
24.12.2012 12:17:41
Это не действительный тест. Код создает объекты без их использования, что позволяет оптимизатору исключить создание всего экземпляра. Единственным оставшимся побочным эффектом является продвижение последовательности случайных чисел, чьи накладные расходы в любом случае перевешивают что-либо еще в этих тестах.
Holger 27.05.2015 09:36:24

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

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
19
8.04.2014 02:10:50
Нет разницы, за исключением того, что ваше пространство PermGen испарится, если DBI используется чрезмерно. По крайней мере, так будет, если вы не установите некоторые неясные параметры JVM, чтобы разрешить выгрузку классов и сборку мусора в пространстве PermGen. Учитывая распространенность Java как серверного языка, проблема с памятью / PermGen заслуживает хотя бы упоминания.
aroth 17.06.2015 04:10:52
@aroth, это хороший момент. Я признаю, что за 16 лет работы над Java я никогда не работал над системой, в которой вам приходилось настраивать PermGen (или Metaspace). Для систем, над которыми я работал, размер кода всегда оставался достаточно маленьким.
Peter Lawrey 18.06.2015 07:08:25
Не должны ли условия в compareCollectionsсочетании с, ||а не &&? Использование &&кажется не только семантически неправильным, оно противодействует намерению измерить производительность, поскольку только первое условие будет проверено вообще. Кроме того, умный оптимизатор может признать, что условия никогда не изменятся во время итераций.
Holger 15.09.2016 10:09:00
@aroth как обновление: начиная с Java 8 виртуальная машина больше не использует perm-gen.
Angel O'Sphere 1.10.2019 13:45:31

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

3
18.06.2014 20:46:00

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

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

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"); }};

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

54
17.12.2014 08:56:33
Спаси котят! Хороший ответ!
Aris2World 24.05.2018 06:48:58

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

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

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

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

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

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

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

5
3.04.2018 21:07:11
-1. Несмотря на некоторые правильные моменты, этот ответ сводится к «Как избежать создания ненужного анонимного класса? Используйте платформу с еще большим количеством классов!»
Agent_L 8.11.2018 08:45:04
Я бы сказал, что все сводится к тому, чтобы «использовать правильный инструмент для работы, а не взлом, который может привести к сбою вашего приложения». Guava - довольно распространенная библиотека для приложений (вы определенно пропускаете ее, если не используете), но даже если вы не хотите ее использовать, вы можете и должны избегать двойной инициализации.
dimo414 9.11.2018 21:36:12
И как именно инициализация двойной скобки вызовет утечку памяти?
Angel O'Sphere 1.10.2019 13:46:41
@ AngelO'Sphere DBI - это запутанный способ создания внутреннего класса , поэтому он сохраняет неявную ссылку на включающий его класс (если только он не используется в staticконтекстах). Ссылка на ошибку в нижней части моего вопроса обсуждает это дальше.
dimo414 2.10.2019 20:35:29
Я бы сказал, что это вопрос вкуса. И в этом нет ничего запутывающего.
Angel O'Sphere 4.10.2019 15:00:24