Никогда не инициализируйте экземпляр двойными фигурными скобками, если вы не хотите использовать OOM!

Java задняя часть
Никогда не инициализируйте экземпляр двойными фигурными скобками, если вы не хотите использовать OOM!

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

Но кто держит нас молодыми? Лучше потерпеть как можно раньше, и дорога впереди будет лучше.

Выпив этот теплый куриный суп, давайте поговорим о том, что происходит.

Дело в том, что в небольшом проекте Сяо Ван написал такой код:

Map<String, String> map = new HashMap() {{
    put("map1", "value1");
    put("map2", "value2");
    put("map3", "value3");
}};
map.forEach((k, v) -> {
    System.out.println("key:" + k + " value:" + v);
});

Он должен был заменить следующий код:

Map<String, String> map = new HashMap();
map.put("map1", "value1");
map.put("map2", "value2");
map.put("map3", "value3");
map.forEach((k, v) -> {
    System.out.println("key:" + k + " value:" + v);
});

Результат выполнения двух блоков кода абсолютно одинаков:

key:map3 value:value3

key:map2 value:value2

key:map1 value:value1

Итак, Сяо Ван с гордостью представлял этот код новой девушке в отделе, Сяо Тяньтянь, но, к сожалению, Лао Чжан, проходившая мимо, тоже его увидела.

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

img

«Сяо Ван, у вашего кода большая проблема!»

«Как я могу инициализировать экземпляр с помощью двойных фигурных скобок?»

В это время Сяо Ван был ошеломлен, когда его спросили, а в его сердце скакали бесчисленные травяные и грязевые кони.Он думал, что ты, старая корова, даже соревнуешься со мной за эту нежную траву, но было зловещее предчувствие В его сердце. Чтобы проиграть, я мгновенно смутился и не знал, что сказать, поэтому я мог только покраснеть и тихо сказал: «А?»

Лао Чжан: «Использование двойных фигурных скобок для инициализации экземпляров вызовет переполнение памяти! Разве вы не знаете?»

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

Итак, после минутной небрежности, после того как Лао Чжан ушел, он тихо открыл Google и молча начал поиск.

Сяо Ван: О, так это...

Анализ инициализации двойной фигурной скобки

Во-первых, давайте посмотрим, какова природа инициализации с помощью двойных фигурных скобок?

Возьмем наш код в качестве примера:

Map<String, String> map = new HashMap() {{
    put("map1", "value1");
    put("map2", "value2");
    put("map3", "value3");
}};

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

Для этого мы можем использовать командуjavacПосле компиляции кода в байт-код мы обнаружили, что предыдущий класс был скомпилирован в два файла байт-кода (.class), как показано на следующем рисунке:

img

Мы используем Идею, чтобы открытьDoubleBracket$1.classФайл найден:

import java.util.HashMap;

class DoubleBracket$1 extends HashMap {
    DoubleBracket$1(DoubleBracket var1) {
        this.this$0 = var1;
        this.put("map1", "value1");
        this.put("map2", "value2");
    }
}

На данный момент мы можем подтвердить, что это анонимный внутренний класс. Итак, вопрос в том, почему анонимный внутренний класс вызывает переполнение памяти?

«Горшок» анонимных внутренних классов

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

Мысль 1: Зачем проводить внешний класс?

Это начинается с проектирования анонимных внутренних классов.В языке Java есть две основные функции нестатических анонимных внутренних классов.

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

2, Когда анонимный внутренний класс содержит внешний класс, он может напрямую использовать переменные внешнего класса, что может легко завершить вызов, как показано в следующем коде:

public class DoubleBracket {
    private static String userName = "磊哥";
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Map<String, String> map = new HashMap() {{
            put("map1", "value1");
            put("map2", "value2");
            put("map3", "value3");
            put(userName, userName);
        }};
    }
}

Из приведенного выше кода видно, чтоHashMapВнутри метода вы можете напрямую использовать переменные внешнего классаuserName.

Мысль 2: Как он удерживает внешний класс?

Что касается того, как анонимные внутренние классы сохраняют внешние объекты, мы можем узнать, просмотрев байт-код анонимных внутренних классов, мы используемjavap -c DoubleBracket\$1.classкоманда для просмотра, где$1Для байт-кода анонимного класса содержание байт-кода следующее:

javap -c DoubleBracket\$1.class
Compiled from "DoubleBracket.java"
class com.example.DoubleBracket$1 extends java.util.HashMap {
  final com.example.DoubleBracket this$0;

  com.example.DoubleBracket$1(com.example.DoubleBracket);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field this$0:Lcom/example/DoubleBracket;
       5: aload_0
       6: invokespecial #7                  // Method java/util/HashMap."<init>":()V
       9: aload_0
      10: ldc           #13                 // String map1
      12: ldc           #15                 // String value1
      14: invokevirtual #17                 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      17: pop
      18: aload_0
      19: ldc           #21                 // String map2
      21: ldc           #23                 // String value2
      23: invokevirtual #17                 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      26: pop
      27: return
}

Среди них код ключа находится вputfieldЭта строка, эта строка указывает на то, что есть параDoubleBracketСсылка на хранится вthis$0Другими словами, этот анонимный внутренний класс содержит ссылку на внешний класс.

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

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class DoubleBracket {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Map map = new DoubleBracket().createMap();
        // 获取一个类的所有字段
        Field field = map.getClass().getDeclaredField("this$0");
        // 设置允许方法私有的 private 修饰的变量
        field.setAccessible(true);
        System.out.println(field.get(map).getClass());
    }
    public Map createMap() {
        // 双花括号初始化
        Map map = new HashMap() {{
            put("map1", "value1");
            put("map2", "value2");
            put("map3", "value3");
        }};
        return map;
    }
}

Когда мы включаем режим отладки, мы видим, чтоmapудерживает внешние предметы вDoubleBracket,Как показано ниже:

img

Результат выполнения приведенного выше кода:

class com.example.DoubleBracket

Из вывода вышеуказанной программы видно, что:Анонимный внутренний класс содержит ссылку на внешний класс, поэтому мы можем использовать$0Получить внешние классы в обычном режиме и вывести соответствующую информацию о классе.

Что может вызвать утечку памяти?

Когда мы помещаем следующий нормальный код (без возвращаемого значения):

public void createMap() {
    Map map = new HashMap() {{
        put("map1", "value1");
        put("map2", "value2");
        put("map3", "value3");
    }};
    // 业务处理....
}

При изменении на следующее (возврат коллекции карт) это может вызвать утечку памяти:

public Map createMap() {
    Map map = new HashMap() {{
        put("map1", "value1");
        put("map2", "value2");
        put("map3", "value3");
    }};
    return map;
}

Почему использование «может» вместо «конечно» вызывает утечку памяти?

img

Это потому, что когда этоmapКогда он назначается другим атрибутам класса, это может привести к тому, что сборщик мусора не будет очищать этот объект во время сбора, что приведет к утечке памяти. Можете обратить внимание на мое "Java Chinese Community" и позже я напишу статью по этому вопросу.

Как сделать так, чтобы память не утекала?

Чтобы убедиться, что номер двойной цветочной пряжки не протекает, метод также очень прост.mapобъект объявлен какstaticСтатический тип в порядке, код выглядит следующим образом:

public static Map createMap() {
    Map map = new HashMap() {{
        put("map1", "value1");
        put("map2", "value2");
        put("map3", "value3");
    }};
    return map;
}

Какие? ты не веришь!

img

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

javap -c  DoubleBracket\$1.class
Compiled from "DoubleBracket.java"
class com.example.DoubleBracket$1 extends java.util.HashMap {
  com.example.DoubleBracket$1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/util/HashMap."<init>":()V
       4: aload_0
       5: ldc           #7                  // String map1
       7: ldc           #9                  // String value1
       9: invokevirtual #11                 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      12: pop
      13: aload_0
      14: ldc           #17                 // String map2
      16: ldc           #19                 // String value2
      18: invokevirtual #11                 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      21: pop
      22: aload_0
      23: ldc           #21                 // String map3
      25: ldc           #23                 // String value3
      27: invokevirtual #11                 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      30: pop
      31: return
}

Из этого кода мы видим, чтобольше никогдаputfieldКлючевое слово в этой строке означает, что статические анонимные классы не будут содержать ссылок на внешние объекты..

Почему статический внутренний класс не будет содержать ссылку на внешний класс?

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

Альтернатива двойным фигурным скобкам

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

Причина очень проста: проекты обычно требуют командной работы, если парень по незнаниюstaticудалить его? Это эквивалентно настройке невидимой «ямы», в которую случайно прыгают другие люди, которые не знают, поэтому мы можем попробовать некоторые другие решения, такие как Stream API в Java8 и фабрику коллекций в Java9.

Альтернатива 1: поток

Вместо этого используйте Stream API в Java8, пример ниже. Оригинальный код:

List<String> list = new ArrayList() {{
    add("Java");
    add("Redis");
}};

Альтернативный код:

List<String> list = Stream.of("Java", "Redis").collect(Collectors.toList());

Альтернатива 2: Фабрика коллекций

с помощью фабрики коллекцийofАльтернативы способа, примеры следующие. Оригинальный код:

Map map = new HashMap() {{
    put("map1", "value1");
    put("map2", "value2");
}};

Альтернативный код:

Map map = Map.of("map1", "Java", "map2", "Redis");

Очевидно, что использование решения в Java9 отлично работает для нас, просто и круто, но мы все еще разочарованы Java 6...6...6... .

img

Суммировать

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

Способ гарантировать отсутствие утечки памяти при инициализации двойных фигурных скобок также очень прост, простоstaticЕго можно изменить, но потенциальный риск при этом все же есть, и он может быть кем-то случайно удален, поэтому мы искали его и обнаружили, что можем использовать Stream в Java8 или фабрику коллекций в Java9.ofметод вместо "{{".

последние слова

Оригинальность непростая, нажмите "отличный"Пойдем!

Ссылки и благодарности

Ууууу, покойся с миром java.com/article/129…

cloud.Tencent.com/developer/ ах…

Hashtag.com/article/149…

Подпишитесь на официальный аккаунт «Java Chinese Community» и ответьте на «Галантные товары», чтобы получить 50 оригинальных галантерейных товаров.Топ-лист.