Вопрос-ответ

Efficiency of Java "Double Brace Initialization"?

Эффективность Java "Инициализации в двойных скобках"?

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

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

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

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

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

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

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

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

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

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

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

Переведено автоматически
Ответ 1

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

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

Пожалуйста, обратите внимание, что время, затраченное на инициализацию 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 тестовой программы инициализации в двойных скобках.

Ответ 2

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

}

};
}
Ответ 3

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

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

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

... will produce these classes:

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

That's quite a bit of overhead for your classloader - for nothing! Of course it won't take much initialisation time if you do it once. But if you do this 20'000 times throughout your enterprise application... all that heap memory just for a bit of "syntax sugar"?

2. You're potentially creating a memory leak!

If you take the above code and return that map from a method, callers of that method might be unsuspectingly holding on to very heavy resources that cannot be garbage collected. Consider the following example:

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

The returned Map will now contain a reference to the enclosing instance of ReallyHeavyObject. You probably don't want to risk that:

Memory Leak Right Here

Image from http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/

3. You can pretend that Java has map literals

To answer your actual question, people have been using this syntax to pretend that Java has something like map literals, similar to the existing array literals:

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

Some people may find this syntactically stimulating.

Ответ 4

Taking the following test class:

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

and then decompiling the class file, I see:

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.

2023-12-11 15:28 java performance collections