Статьи

Не будь «умным»: анти-узор с двойными фигурными скобками

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

01
02
03
04
05
06
07
08
09
10
11
12
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");
        }});
    }});
}};

Если вы не понимаете синтаксис, это на самом деле просто. Есть два элемента:

  1. Мы создаем анонимные классы, которые расширяют HashMap , написав:
    1
    2
    new HashMap() {
    }
  2. В этом анонимном классе мы используем инициализатор экземпляра для инициализации нового экземпляра анонимного подтипа HashMap , написав такие вещи:
    1
    2
    3
    {
        put("id", "1234");
    }

    По сути, эти инициализаторы просто код конструктора.

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

На самом деле есть три причины, чтобы это было анти-паттерном:

1. Читабельность

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

1
2
3
4
5
6
7
8
9
{
  "firstName"     : "John"
, "lastName"      : "Smith"
, "organizations" :
  {
    "0"   : { "id", "1234" }
  , "abc" : { "id", "5678" }
  }
}

И да. Было бы здорово, если бы у Java были литералы коллекций для типов List и Map . Использование двойных фигурных скобок для эмуляции, это странно и не совсем правильно, синтаксически.

Но давайте оставим область, где мы обсуждаем вкус и фигурные скобки ( мы делали это раньше ), потому что:

2. Один тип за экземпляр

Мы действительно создаем один тип для каждого экземпляра! Каждый раз, когда мы создаем новую карту таким образом, мы также неявно создаем новый класс, не подлежащий повторному использованию, только для этого простого экземпляра HashMap . Если вы делаете это один раз, это может быть хорошо. Если вы поместите этот вид кода во все огромное приложение, вы ClassLoader некоторую ненужную нагрузку на ваш ClassLoader , который хранит ссылки на все эти объекты класса в вашей куче. Не верь этому? Скомпилируйте приведенный выше код и проверьте вывод компилятора. Это будет выглядеть так:

1
2
3
4
5
Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

Где Test.class является единственным разумным классом здесь, включающий класс.

Но это все еще не самая важная проблема.

3. Утечка памяти!

Действительно самая важная проблема — это проблема всех анонимных классов. Они содержат ссылку на свой включающий экземпляр, и это действительно убийца. Давайте представим, что вы поместили свою умную инициализацию HashMap в EJB или в любой действительно тяжелый объект с хорошо управляемым жизненным циклом, подобным этому:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ReallyHeavyObject {
 
    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;
 
    // This method almost does nothing
    public void 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");
                }});
            }});
        }};
         
        // Some more code here
    }
}

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

Хорошо.

Давайте представим другого разработчика, который реорганизует этот метод для возврата вашей карты или даже частей вашей карты:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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 наружу, потому что каждый из этих внутренних классов содержит ссылку на включающий экземпляр, который является экземпляром ReallyHeavyObject . Не верь этому? Давайте запустим эту программу:

1
2
3
4
5
6
public static void main(String[] args) throws Exception {
    Map map = new ReallyHeavyObject().quickHarmlessMethod();
    Field field = map.getClass().getDeclaredField("this$0");
    field.setAccessible(true);
    System.out.println(field.get(map).getClass());
}

Эта программа возвращает:

1
class ReallyHeavyObject

Да, в самом деле! Если вы все еще не верите, вы можете использовать отладчик, чтобы проанализировать возвращенную map :

отладки output1

Вы увидите ссылку на вложенный экземпляр прямо в вашем анонимном подтипе HashMap . И все вложенные анонимные подтипы HashMap также содержат такую ​​ссылку.

Поэтому, пожалуйста, никогда не используйте этот анти-шаблон

Вы могли бы сказать, что один из способов обойти все трудности из проблемы 3 — это сделать quickHarmlessMethod() статическим методом для предотвращения включения этого экземпляра, и вы правы в этом.

Но самое худшее, что мы видели в приведенном выше коде, это тот факт, что даже если вы знаете, что вы делаете со своей картой, которую вы можете создавать в статическом контексте, следующий разработчик может не заметить этого и выполнить рефакторинг / удаление static еще раз. Они могут хранить Map в каком-то другом экземпляре-синглтоне, и в самом коде буквально невозможно сказать, что это может быть просто бесполезная, бесполезная ссылка на ReallyHeavyObject .

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

Вывод:

Не будь умным, никогда не используй двойную инициализацию