Время от времени я нахожу кого-то, кто использует шаблон анти-двойных фигурных скобок (также называемый инициализацией двойных фигурных скобок ) в дикой природе. На этот раз в переполнении стека :
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"); }}); }}); }};
Если вы не понимаете синтаксис, это на самом деле просто. Есть два элемента:
-
Мы создаем анонимные классы, которые расширяются
HashMap
путем написания -
В этом анонимном классе мы используем инициализатор экземпляра для инициализации нового
HashMap
экземпляра анонимного подтипа, написав что-то вроде:{ put("id", "1234"); }
new HashMap() { }
По сути, эти инициализаторы просто код конструктора.
Итак, почему это называется двойной шаблон анти фигурных скобок
На самом деле есть три причины, чтобы это было анти-паттерном:
1. Читабельность
Это наименее важная причина, это удобочитаемость. В то время как это может быть немного легче написать, и немного больше похоже на инициализацию эквивалентной структуры данных в JSON:
{ "firstName" : "John" , "lastName" : "Smith" , "organizations" : { "0" : { "id", "1234" } , "abc" : { "id", "5678" } } }
И да. Было бы здорово,
если бы у Java были литералы коллекций для List
и Map
типов . Использование двойных фигурных скобок для эмуляции, это странно и не совсем правильно, синтаксически.
Но давайте оставим область, где мы обсуждаем вкус и фигурные скобки ( мы делали это раньше ), потому что:
2. Один тип за экземпляр
Мы действительно создаем один тип для двойной инициализации! Каждый раз, когда мы создаем новую карту таким образом, мы также неявно создаем новый одноразовый класс только для этого одного простого экземпляра a HashMap
. Если вы делаете это один раз, это может быть хорошо. Если вы поместите этот код во все огромное приложение, вы возьмете на себя ненужное бремя ClassLoader
, которое хранит ссылки на все эти объекты класса в вашей куче. Не верь этому? Скомпилируйте приведенный выше код и проверьте вывод компилятора. Это будет выглядеть так:
Test$1$1$1.class Test$1$1$2.class Test$1$1.class Test$1.class Test.class
Где
Test.class
единственный разумный класс здесь, включающий класс.
Но это все еще не самая важная проблема.
3. Утечка памяти!
Действительно самая важная проблема — это проблема всех анонимных классов. Они содержат ссылку на свой включающий экземпляр, и это действительно убийца. Давайте представим, что вы помещаете свою умную HashMap
инициализацию в EJB или в любой действительно тяжелый объект с хорошо управляемым жизненным циклом, подобным этому:
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()
, который выполняется в кратчайшие сроки.
Хорошо.
Давайте представим другого разработчика, который реорганизует этот метод для возврата вашей карты или даже частей вашей карты:
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
экземпляром. Не верь этому? Давайте запустим эту программу:
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()); }
Эта программа возвращает
class ReallyHeavyObject
Да, в самом деле! Если вы все еще не верите, вы можете использовать отладчик, чтобы проанализировать возвращаемое map
:
Вы увидите ссылку на экземпляр в вашем анонимном HashMap
подтипе. И все вложенные анонимные HashMap
подтипы также содержат такую ссылку.
Поэтому, пожалуйста, никогда не используйте этот анти-шаблон
Вы можете сказать, что один из способов обойти все проблемы из проблемы 3 — сделать quickHarmlessMethod()
статический метод для предотвращения включения этого экземпляра, и вы правы в этом.
Но самое худшее, что мы видели в приведенном выше коде, это тот факт, что даже если вы знаете, что вы делаете со своей картой, которую вы можете создавать в статическом контексте, следующий разработчик может не заметить этого и static
снова выполнить рефакторинг / удаление , Они могут хранить Map
в каком-то другом экземпляре синглтона, и из самого кода буквально невозможно определить, что это может быть просто бесполезная, бесполезная ссылка ReallyHeavyObject
.
Внутренние классы — зверь. Они вызвали много неприятностей и когнитивного диссонанса в прошлом. Анонимные внутренние классы могут быть еще хуже, потому что читатели такого кода могут действительно не замечать тот факт, что они включают в себя внешний экземпляр и что они передают этот закрытый внешний экземпляр.
Вывод:
Не будь умным, никогда не используй двойную инициализацию