Смущение в жизни есть везде, иногда просто хочется просто притвориться, но какие-то «старые товарищи» всегда ненароком дают тебе безжалостный пинок, пинают так сильно, что ты едва дышишь.
Но кто держит нас молодыми? Лучше потерпеть как можно раньше, и дорога впереди будет лучше.
Выпив этот теплый куриный суп, давайте поговорим о том, что происходит.
Дело в том, что в небольшом проекте Сяо Ван написал такой код:
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-градусной горячей воды к вчерашнему лайчи, но это был шанс показать свои навыки перед Сяо Тяньтянем, поэтому он обычно разобрался. включен.
«Сяо Ван, у вашего кода большая проблема!»
«Как я могу инициализировать экземпляр с помощью двойных фигурных скобок?»
В это время Сяо Ван был ошеломлен, когда его спросили, а в его сердце скакали бесчисленные травяные и грязевые кони.Он думал, что ты, старая корова, даже соревнуешься со мной за эту нежную траву, но было зловещее предчувствие В его сердце. Чтобы проиграть, я мгновенно смутился и не знал, что сказать, поэтому я мог только покраснеть и тихо сказал: «А?»
Лао Чжан: «Использование двойных фигурных скобок для инициализации экземпляров вызовет переполнение памяти! Разве вы не знаете?»
Сяо Ван на мгновение замолчал, но, судя по прошлому опыту, в этом «старичке» еще что-то есть, поэтому он небрежно сказал «о~», как будто понимал, что происходит, но на самом деле он все еще был в замешательстве. , чтобы не дать узнать другим коллегам, пришлось действовать так.
Итак, после минутной небрежности, после того как Лао Чжан ушел, он тихо открыл Google и молча начал поиск.
Сяо Ван: О, так это...
Анализ инициализации двойной фигурной скобки
Во-первых, давайте посмотрим, какова природа инициализации с помощью двойных фигурных скобок?
Возьмем наш код в качестве примера:
Map<String, String> map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
}};
этот кодФактически создается анонимный внутренний класс, а затем выполняется блок кода инициализации..
Для этого мы можем использовать командуjavac
После компиляции кода в байт-код мы обнаружили, что предыдущий класс был скомпилирован в два файла байт-кода (.class), как показано на следующем рисунке:
Мы используем Идею, чтобы открыть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
,Как показано ниже:
Результат выполнения приведенного выше кода:
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;
}
Почему использование «может» вместо «конечно» вызывает утечку памяти?
Это потому, что когда это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;
}
Какие? ты не веришь!
Неважно, мы говорим фактами, используя приведенный выше код, мы перекомпилируем байт-код и просмотрим содержимое анонимного класса следующим образом:
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... .
Суммировать
В этой статье мы говорили о проблеме утечек памяти, вызванных инициализацией двойной фигурной скобкой, поскольку она содержит ссылку на внешний класс, и продемонстрировали эту проблему на уровне байт-кода и отражения.
Способ гарантировать отсутствие утечки памяти при инициализации двойных фигурных скобок также очень прост, простоstatic
Его можно изменить, но потенциальный риск при этом все же есть, и он может быть кем-то случайно удален, поэтому мы искали его и обнаружили, что можем использовать Stream в Java8 или фабрику коллекций в Java9.of
метод вместо "{{".
последние слова
Оригинальность непростая, нажмите "отличный"Пойдем!
Ссылки и благодарности
Ууууу, покойся с миром java.com/article/129…
cloud.Tencent.com/developer/ ах…
Подпишитесь на официальный аккаунт «Java Chinese Community» и ответьте на «Галантные товары», чтобы получить 50 оригинальных галантерейных товаров.Топ-лист.