объектов - трехмерные массивы java




Эффективность Java «Инициализация двойного брекета»? (10)

В скрытых характеристиках Java в верхнем ответе упоминается Double Brace Initialization , с очень заманчивым синтаксисом:

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

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

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

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

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

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

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

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

В целом, DBI поражает меня как нечто интеллектуальное любопытство. Coobird и другие отмечают, что вы можете добиться такого же эффекта с помощью методов Arrays.asList, varargs, коллекций Google и предлагаемых литератур 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 .


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

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

Пример в вопросе:

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

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

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

Error-Prone настоящее время Error-Proneэтот анти-шаблон .


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

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

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


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

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

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

Предложение коллекции Джошуа Блоха по литературе для проектной монеты было следующим:

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 s и 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.


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

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

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

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


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

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

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

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

            }

        };
    }

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

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 .


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

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

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

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


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

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

и это мое заключение

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

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

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

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

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

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


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

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

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

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

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


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

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

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

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





initialization