Что такое инициализация двойной скобки в Java?

307

Что такое синтаксис инициализации Double Brace ( {{ ... }}) в Java?

sgokhales
источник
2
См. Также stackoverflow.com/q/924285/45935
Джим Ферранс
10
Инициализация двойной скобки является очень опасной функцией и должна использоваться разумно. Это может нарушить контракт и привести к хитрым утечкам памяти. Эта статья описывает детали.
Андрей Полунин

Ответы:

303

Инициализация двойной скобкой создает анонимный класс, производный от указанного класса ( внешние скобки), и обеспечивает блок инициализатора внутри этого класса ( внутренние скобки). например

new ArrayList<Integer>() {{
   add(1);
   add(2);
}};

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

Брайан Агнью
источник
11
Спасибо за разъяснение значения внутренних и внешних скобок. Я удивляюсь, почему вдруг допускаются две фигурные скобки с особым значением, когда они на самом деле являются обычными Java-конструкциями, которые выглядят только как новый магический трюк. Подобные вещи заставляют меня сомневаться в синтаксисе Java. Если вы уже не эксперт, читать и писать может быть очень сложно.
Jackthehipster
4
Подобный «магический синтаксис» существует во многих языках, например, почти все C-подобные языки поддерживают синтаксис «идет к 0» «x -> 0» для циклов, который просто «x--> 0» со странным космическое размещение.
Йоахим Зауэр
17
Мы можем просто сделать вывод, что «инициализация двойной скобки» не существует сама по себе, это всего лишь комбинация создания анонимного класса и блока инициализатора , который после объединения выглядит как синтаксическая конструкция, но на самом деле это не так. т.
MC Emperor
Спасибо! Gson возвращает значение null, когда мы сериализуем что-то с двойной инициализацией из-за анонимного использования внутреннего класса.
Прадип AJ- Msft MVP
295

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

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

1. Вы создаете слишком много анонимных классов

Каждый раз, когда вы используете двойную инициализацию, создается новый класс. Например, этот пример:

Map source = new HashMap(){{
    put("firstName", "John");
    put("lastName", "Smith");
    put("organizations", new HashMap(){{
        put("0", new HashMap(){{
            put("id", "1234");
        }});
        put("abc", new HashMap(){{
            put("id", "5678");
        }});
    }});
}};

... будет производить эти классы:

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

Это довольно много для вашего загрузчика классов - ни за что! Конечно, это не займет много времени инициализации, если вы сделаете это один раз. Но если вы сделаете это 20 000 раз по всему корпоративному приложению ... всю эту кучу памяти только для небольшого количества "синтаксического сахара"?

2. Вы потенциально создаете утечку памяти!

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

public class ReallyHeavyObject {

    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;

    // This method almost does nothing
    public Map quickHarmlessMethod() {
        Map source = new HashMap(){{
            put("firstName", "John");
            put("lastName", "Smith");
            put("organizations", new HashMap(){{
                put("0", new HashMap(){{
                    put("id", "1234");
                }});
                put("abc", new HashMap(){{
                    put("id", "5678");
                }});
            }});
        }};

        return source;
    }
}

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

Утечка памяти прямо здесь

Изображение с http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/

3. Вы можете делать вид, что у Java есть литералы карты

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

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

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

Лукас Эдер
источник
11
«Вы создаете слишком много анонимных классов» - глядя на то, как (скажем) Scala создает анонимные классы, я не слишком уверен, что это серьезная проблема,
Брайан Агнью
2
Разве это не остается верным и хорошим способом объявить статические карты? Если HashMap инициализируется {{...}}и объявляется как staticполе, не должно быть никакой возможной утечки памяти, только один анонимный класс и никаких вложенных ссылок на экземпляры, верно?
Лоренцо-с
8
@ lorenzo-s: Да, 2) и 3) тогда не применяются, только 1). К счастью, с Java 9, наконец, есть Map.of()для этой цели, так что это будет лучшее решение
Lukas Eder
3
Возможно, стоит отметить, что внутренние карты также имеют ссылки на внешние карты и, следовательно, косвенно на ReallyHeavyObject. Кроме того, анонимные внутренние классы захватывают все локальные переменные, используемые в теле класса, поэтому, если вы используете не только константы для инициализации коллекций или карт с этим шаблоном, экземпляры внутреннего класса будут захватывать все из них и продолжать ссылаться на них, даже если они фактически удалены из коллекция или карта. Поэтому в этом случае этим экземплярам не только требуется вдвое больше необходимой памяти для ссылок, но и возникает другая утечка памяти в этом отношении.
Хольгер
41
  • Первая скобка создает новый анонимный внутренний класс.
  • Второй набор фигурных скобок создает инициализаторы экземпляра, такие как статический блок в Class.

Например:

   public class TestHashMap {
    public static void main(String[] args) {
        HashMap<String,String> map = new HashMap<String,String>(){
        {
            put("1", "ONE");
        }{
            put("2", "TWO");
        }{
            put("3", "THREE");
        }
        };
        Set<String> keySet = map.keySet();
        for (String string : keySet) {
            System.out.println(string+" ->"+map.get(string));
        }
    }

}

Как это устроено

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

И второй набор скобок - не что иное, как инициализаторы экземпляра. Если вы напоминаете основные концепции Java, то вы можете легко связать блоки инициализатора экземпляра со статическими инициализаторами из-за сходной фигурной скобки, например struct Разница лишь в том, что статический инициализатор добавляется с ключевым словом static и запускается только один раз; независимо от того, сколько объектов вы создаете.

Больше

Premraj
источник
24

Для забавного применения двойной инициализации см. Здесь Массив Двемти в Java .

Выдержка

private static class IndustrialRaverMonkey
  extends Creature.Base {{
    life = 46;
    strength = 35;
    charisma = 91;
    weapon = 2;
  }}

private static class DwarvenAngel
  extends Creature.Base {{
    life = 540;
    strength = 6;
    charisma = 144;
    weapon = 50;
  }}

А теперь, будьте готовы к BattleOfGrottoOfSausageSmells... и бекону!

akuhn
источник
16

Я думаю, что важно подчеркнуть, что в Java не существует такой вещи, как «инициализация двойной скобки» . На сайте Oracle нет этого термина. В этом примере используются две функции: анонимный класс и блок инициализатора. Похоже, что старый блок инициализатора был забыт разработчиками и вызывает некоторую путаницу в этой теме. Цитирование из документов Oracle :

Блоки инициализатора для переменных экземпляра выглядят так же, как статические блоки инициализатора, но без ключевого слова static:

{
    // whatever code is needed for initialization goes here
}
Алекс Т
источник
11

1. Не существует такой вещи, как двойные скобки:
я хотел бы отметить, что не существует такой вещи, как инициализация двойных скобок. Существует только обычный традиционный один блок инициализации скобки. Второй блок скобок не имеет ничего общего с инициализацией. Ответы говорят, что эти две скобки что-то инициализируют, но это не так.

2- Это не только анонимные классы, но и все классы:
почти все ответы говорят о том, что они используются при создании анонимных внутренних классов. Я думаю, что у людей, читающих эти ответы, будет впечатление, что это используется только при создании анонимных внутренних классов. Но это используется во всех классах. Чтение этих ответов выглядит как новая специальная функция, предназначенная для анонимных классов, и я думаю, что это вводит в заблуждение.

3- Цель состоит только в том, чтобы поместить скобки друг за другом, а не в новую концепцию.
Далее этот вопрос говорит о ситуации, когда вторая открывающая скобка находится сразу после первой открывающей скобки. При использовании в обычном классе обычно есть некоторый код между двумя скобками, но это совершенно одно и то же. Так что это вопрос размещения скобок. Так что я думаю, что мы не должны говорить, что это что-то новое, захватывающее, потому что это то, что мы все знаем, но просто написано с кодом в скобках. Мы не должны создавать новую концепцию под названием «инициализация двойной скобки».

4. Создание вложенных анонимных классов не имеет ничего общего с двумя фигурными скобками:
я не согласен с аргументом, что вы создаете слишком много анонимных классов. Вы создаете их не из-за блока инициализации, а только потому, что создаете их. Они будут созданы, даже если вы не использовали две скобки для инициализации, поэтому эти проблемы могут возникнуть даже без инициализации ... Инициализация не является фактором, который создает инициализированные объекты.

Кроме того, мы не должны говорить о проблеме, созданной с помощью этой несуществующей вещи «инициализация двойной скобкой» или даже обычной инициализацией в одну скобку, потому что описанные проблемы существуют только из-за создания анонимного класса, поэтому он не имеет ничего общего с исходным вопросом. Но все ответы дают читателям впечатление, что это не вина создания анонимных классов, а эта злая (несуществующая) вещь, называемая «инициализация двойной скобкой».

ctomek
источник
9

Чтобы избежать всех негативных последствий инициализации двойной скобки, таких как:

  1. Нарушена "равная" совместимость.
  2. Никаких проверок не выполняется, когда используются прямые назначения.
  3. Возможные утечки памяти.

делать следующие вещи:

  1. Сделайте отдельный класс "Builder" специально для инициализации двойной скобки.
  2. Объявите поля со значениями по умолчанию.
  3. Поместите метод создания объекта в этот класс.

Пример:

public class MyClass {
    public static class Builder {
        public int    first  = -1        ;
        public double second = Double.NaN;
        public String third  = null      ;

        public MyClass create() {
            return new MyClass(first, second, third);
        }
    }

    protected final int    first ;
    protected final double second;
    protected final String third ;

    protected MyClass(
        int    first ,
        double second,
        String third
    ) {
        this.first = first ;
        this.second= second;
        this.third = third ;
    }

    public int    first () { return first ; }
    public double second() { return second; }
    public String third () { return third ; }
}

Использование:

MyClass my = new MyClass.Builder(){{ first = 1; third = "3"; }}.create();

Преимущества:

  1. Просто в использовании.
  2. Не нарушает "равных" совместимости.
  3. Вы можете выполнить проверки в методе создания.
  4. Нет утечек памяти.

Недостатки:

  • Никто.

И, как результат, у нас самый простой шаблон для Java-строителя.

Посмотреть все примеры на github: java-sf-builder-simple-example

Борис Подчезерцев
источник
4

Это - среди прочего - ярлык для инициализации коллекций. Выучить больше ...

Мик
источник
2
Ну, это одна заявка на это, но ни в коем случае не единственная.
Скаффман
4

Вы имеете в виду что-то вроде этого?

List<String> blah = new ArrayList<String>(){{add("asdfa");add("bbb");}};

это инициализация списка массивов во время создания (хак)

dhblah
источник
4

Вы можете поместить некоторые операторы Java как цикл для инициализации коллекции:

List<Character> characters = new ArrayList<Character>() {
    {
        for (char c = 'A'; c <= 'E'; c++) add(c);
    }
};

Random rnd = new Random();

List<Integer> integers = new ArrayList<Integer>() {
    {
         while (size() < 10) add(rnd.nextInt(1_000_000));
    }
};

Но этот случай влияет на производительность, проверьте это обсуждение

Антон Дозорцев
источник
4

Как указывает @Lukas Eder, двойных скобок следует избегать инициализации коллекций.

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

Java 9 ввел удобные методы List.of, Set.ofи Map.of, которые следует использовать вместо. Они быстрее и эффективнее, чем инициализатор с двойными скобками.

Михаил Холодков
источник
0

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

Как и другие указали, это не безопасно использовать.

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

  • Java 8
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
  • Java 9
List<String> list = List.of("A", "B", "C");
Анкит Шарма
источник
-1

Похоже, это то же самое, что ключевое слово with, столь популярное во flash и vbscript. Это метод изменения того, что thisесть, и ничего более.

Чак Восе
источник
На самом деле, нет. Это все равно что сказать, что создание нового класса - это метод изменения того, что thisесть. Синтаксис просто создает анонимный класс (поэтому любая ссылка на него thisбудет ссылаться на объект этого нового анонимного класса), а затем использует блок {...}инициализатора для инициализации вновь созданного экземпляра.
Гринч