Эффективность Java "Двойная инициализация скобки"? - PullRequest
760 голосов
/ 29 мая 2009

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

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

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

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

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

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

Резюме: Очень, очень хорошие ответы, спасибо всем. На вопрос (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 '.

Ответы [ 15 ]

573 голосов
/ 29 мая 2009

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

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

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


Предложение Джошуа Блоха Collection Literals для 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);
  }
}

Обратите внимание, что истекшее время для инициализации 1000 ArrayList с и 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 мс.

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

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

96 голосов
/ 29 мая 2009

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

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

Второй вопрос: новый HashSet должен быть «this», используемым в инициализаторе экземпляра ... может кто-нибудь пролить свет на механизм? Я бы наивно ожидал, что «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){

            }

        };
    }
45 голосов
/ 17 декабря 2014

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

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

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. Вы, вероятно, не хотите рисковать этим:

Memory Leak Right Here

Изображение из 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"); }};

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

35 голосов
/ 25 января 2011

утечка

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

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

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

35 голосов
/ 29 мая 2009

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

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 .

19 голосов
/ 31 января 2013

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

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
16 голосов
/ 29 мая 2009

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

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

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

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

9 голосов
/ 29 мая 2009

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

Другим способом достижения декларативного построения списков является использование Arrays.asList(T ...) примерно так:

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

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

7 голосов
/ 29 мая 2009

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

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

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

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

5 голосов
/ 12 апреля 2017

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

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

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

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

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

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

склонен к ошибкам сейчас помечает этот анти-шаблон .

...