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

762

В Скрытые особенности 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, заключается в том, что анонимный внутренний класс расширяет класс объекта, создаваемого новым оператором, и, следовательно, имеет значение "this", ссылающееся на создаваемый экземпляр. Очень аккуратно.

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

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

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

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

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

  • 24
    Запах кода, который я вижу здесь, заключается в том, что наивный читатель ожидал, что flavors будут HashSet , но, увы, это анонимный подкласс.
  • 5
    Если вы рассматриваете запуск вместо загрузки производительности, нет разницы, смотрите мой ответ.
Показать ещё 11 комментариев
Теги:
performance
collections
initialization

15 ответов

530
Лучший ответ

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

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 необходимо будет прочитать все эти классы при их использовании, это может привести к некоторому времени в процессе bytecode verfication и тому подобное. Не говоря уже о увеличении необходимого дискового пространства для хранения всех этих 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!");
    }
};

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

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.

  • 7
    «Вероятно», будучи оперативным словом. Если не измерять, никакие заявления о производительности не имеют смысла.
  • 0
    Вы говорите: «На самом деле при инициализации с двойными скобками возникают некоторые издержки», но на самом деле это не так. В любом случае, я не верю такому утверждению без профилирования.
Показать ещё 23 комментария
98

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

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

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

Это то, как работают внутренние классы. Они получают свой собственный 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>, как предполагает Натан Китчен?
41

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

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

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

Изображение 1398

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

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

  • 4
    Спаси котят! Хороший ответ!
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 Static Initializers):

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

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

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.

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

склонность к утечке

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

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).
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). Для систем, над которыми я работал, размер кода всегда оставался достаточно маленьким.
Показать ещё 1 комментарий
16

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4

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

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

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

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

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

Однако ясно, что в большинстве случаев не так много накладных расходов для отдельного объекта, даже с большими числами.

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

Мой вывод? Его можно использовать, если он не подвергается насилию.

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

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

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

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

Пример в вопросе будет:

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

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

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

Error-Prone теперь обозначает этот анти-шаблон.

  • 0
    -1. Несмотря на некоторые правильные моменты, этот ответ сводится к «Как избежать создания ненужного анонимного класса? Используйте платформу с еще большим количеством классов!»
  • 1
    Я бы сказал, что все сводится к тому, чтобы «использовать правильный инструмент для работы, а не взлом, который может привести к сбою вашего приложения». Guava - довольно распространенная библиотека для приложений (вы определенно пропускаете ее, если не используете), но даже если вы не хотите ее использовать, вы можете и должны избегать двойной инициализации.
3

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

3

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

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

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

7/10/2014: Если бы это было так просто, как Python's:

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

3

Второй ответ Nat, за исключением того, что я использовал бы цикл вместо создания и немедленного броска неявного списка из asList (элементов):

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 может заметить, что она никогда не выходит за пределы области действия метода и размещать его в стеке.
  • 0
    Да, он может оказаться более эффективным, чем этот код (хотя вы можете улучшить его, сообщив HashSet предполагаемую емкость - запомните коэффициент загрузки).
Показать ещё 1 комментарий
2
  1. Это вызовет add() для каждого члена. Если вы можете найти более эффективный способ поместить элементы в хеш-набор, используйте его. Обратите внимание, что внутренний класс, скорее всего, сгенерирует мусор, если вы чувствительны к этому.

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню