Статьи

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

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

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путем написания

  2. new HashMap() {
    }
  3. В этом анонимном классе мы используем инициализатор экземпляра для инициализации нового HashMapэкземпляра анонимного подтипа, написав что-то вроде:

    {
        put("id", "1234");
    }

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

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

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

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.

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

Вывод:

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