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

Что такое синтаксис инициализации двойной скобки ({{ ... }}) на Яве?

13 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

Больше

Для забавного применения инициализации двойной фигурной скобки см . Массив Двемти в 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 и... кусок бекона!

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

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

{
    // whatever code is needed for initialization goes here
}

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

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

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

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

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

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

  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: https://github.com/speaking-fish/java-sf-builder-simple-example

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

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

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

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

Вы можете поместить некоторые операторы 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));
    }
};

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

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

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

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

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

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

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

  • Java 8
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
  • Java 9
List<String> list = List.of("A", "B", "C");

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

ArrayList<String> friends = new ArrayList<>();
friends.add("Mark");
friends.add("Steve");
invite(friends);

Если вам больше не нужен список массивов, было бы неплохо сделать его анонимным. Но тогда как вы можете добавить элементы? (здесь происходит инициализация двойной скобки) Вот как:

invite(new ArrayList<String>({{ add("Mark"); add("Steve");}});

Обратите внимание на двойные скобки. Внешние скобки делают анонимный подкласс ArrayList, Внутренние скобки являются строительным блоком объекта.

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

Другие вопросы по тегам