如何避免在Java 中使用双括号初始化

 更新时间:2023年07月17日 10:20:46   作者:明明如月学长  
这篇文章主要介绍了如何避免在Java中使用双括号初始化,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

结论先行

避免像这样,在 Java 中使用双括号初始化:

new HashMap<String, String>() {{
     put("key", value);
}};

内存泄漏追踪

我最近正在 LeakCanary 看到了以下内存泄漏追踪信息:

┬───
│ GC Root: Global variable in native code
│
├─ com.bugsnag.android.AnrPlugin instance
│   Leaking: UNKNOWN
│   ↓ AnrPlugin.client
│               ~~~~~~
├─ com.bugsnag.android.Client instance
│   Leaking: UNKNOWN
│   ↓ Client.breadcrumbState
│               ~~~~~~~~~~~~~~~
├─ com.bugsnag.android.BreadcrumbState instance
│   Leaking: UNKNOWN
│   ↓ BreadcrumbState.store
│                       ~~~~~
├─ com.bugsnag.android.Breadcrumb[] array
│   Leaking: UNKNOWN
│   ↓ Breadcrumb[494]
│               ~~~~~
├─ com.bugsnag.android.Breadcrumb instance
│   Leaking: UNKNOWN
│   ↓ Breadcrumb.impl
│                   ~~~~
├─ com.bugsnag.android.BreadcrumbInternal instance
│   Leaking: UNKNOWN
│   ↓ BreadcrumbInternal.metadata
│                           ~~~~~~~~
├─ com.example.MainActivity$1 instance
│   Leaking: UNKNOWN
│   Anonymous subclass of java.util.HashMap
│   ↓ MainActivity$1.this$0
│                       ~~~~~~
╰→ com.example.MainActivity instance
    Leaking: YES (Activity#mDestroyed is true)

当打开一个内存泄漏追踪日志时,我首先会看底部的对象,了解它的生命周期,这将帮助我理解内存泄漏追踪中的其他对象是否应该有相同的生命周期。

在底部,我们看到:

╰→ com.example.MainActivity instance
    Leaking: YES (Activity#mDestroyed is true)

Activity 已经被销毁,应该已被垃圾回收器给回收掉了,但它仍驻留在内存中。

此时,我开始在内存泄漏追踪日志中寻找已知类型,并尝试弄清楚它们是否属于同一个被销毁的范围(=> 正在泄漏)或更高的范围(=> 没有泄漏)。

在顶部,我们看到:

├─ com.bugsnag.android.Client instance
│   Leaking: UNKNOWN

我们的 BugSnag 客户端是一个用于分析崩溃报告单例,由于每个应用我们创建一个实例,所以它没有泄漏。

├─ com.bugsnag.android.Client instance
│   Leaking: NO

所以我们现在需要转变焦点,特别关注从最后一个 Leaking: NO 到第一个 Leaking: YES 的部分:

…
├─ com.bugsnag.android.Client instance
│   Leaking: NO
│   ↓ Client.breadcrumbState
│               ~~~~~~~~~~~~~~~
├─ com.bugsnag.android.BreadcrumbState instance
│   Leaking: UNKNOWN
│   ↓ BreadcrumbState.store
│                       ~~~~~
├─ com.bugsnag.android.Breadcrumb[] array
│   Leaking: UNKNOWN
│   ↓ Breadcrumb[494]
│               ~~~~~
├─ com.bugsnag.android.Breadcrumb instance
│   Leaking: UNKNOWN
│   ↓ Breadcrumb.impl
│                   ~~~~
├─ com.bugsnag.android.BreadcrumbInternal instance
│   Leaking: UNKNOWN
│   ↓ BreadcrumbInternal.metadata
│                           ~~~~~~~~
├─ com.example.MainActivity$1 instance
│   Leaking: UNKNOWN
│   Anonymous subclass of java.util.HashMap
│   ↓ MainActivity$1.this$0
│                       ~~~~~~
╰→ com.example.MainActivity instance
    Leaking: YES (Activity#mDestroyed is true)

BugSnag 客户端保持了一个面包屑的环形缓冲区。这些应该保留在内存中,它们也没有泄漏。

所以让我们跳过上述内容,从下面这里继续分析:

├─ com.bugsnag.android.BreadcrumbInternal instance
│   Leaking: NO

我们只需要关注从最后一个 Leaking: NO 到第一个Leaking: YES 的部分:

…
├─ com.bugsnag.android.BreadcrumbInternal instance
│   Leaking: NO
│   ↓ BreadcrumbInternal.metadata
│                           ~~~~~~~~
├─ com.example.MainActivity$1 instance
│   Leaking: UNKNOWN
│   Anonymous subclass of java.util.HashMap
│   ↓ MainActivity$1.this$0
│                       ~~~~~~
╰→ com.example.MainActivity instance
    Leaking: YES (Activity#mDestroyed is true)
  • BreadcrumbInternal.metadata :内存泄漏追踪通过面包屑实现的元数据字段。
  • MainActivity$1 实例是 java.util.HashMap 的匿名子类:MainActivity$1 是在MainActivity 中定义的 HashMap 的匿名子类。它是从 MainActivity.java 中定义的第一个匿名类(因为是 $1 )。
  • this$0:每个匿名类都有一个隐式字段引用到定义它的外部类,这个字段被命名为 this$0

也就是说:记录到 BugSnag 的面包屑之一有一个元数据映射,这是一个 HashMap 的匿名子类 ,它保留对外部类的引用,这个外部类就是被销毁的 Activity

让我们看看我们在 MainActivity 中记录面包屑的地方:

void logSavingTicket(String ticketId) {
    Map<String, Object> metadata = new HashMap<String, Object>() {{
      put("ticketId", ticketId);
    }};
    bugsnagClient.leaveBreadcrumb("Saving Ticket", metadata, LOG);
}

这段代码利用了一个被称为“双括号初始化” 的有趣的 Java 代码块 。它允许你创建一个 HashMap,并通过添加代码到 HashMap 的匿名子类的构造函数中同时初始化它。

new HashMap<String, Object>() {{
  	put("ticketId", ticketId);
}};

Java 的匿名类总是隐式地引用其外部类。

因此,这段代码:

void logSavingTicket(String ticketId) {
    Map<String, Object> metadata = new HashMap<String, Object>() {{
      put("ticketId", ticketId);
    }};
    bugsnagClient.leaveBreadcrumb("Saving Ticket", metadata, LOG);
}

实际上被编译为:

class MainActivity$1 extends HashMap<String, Object> {
  private final MainActivity this$1;
  MainActivity$1(MainActivity this$1, String ticketId) {
     this.this$1 = this$1;
     put("ticketId", ticketId);
  }
}
void logSavingTicket(String ticketId) {
  Map<String, Object> metadata = new MainActivity$1(this, ticketId);
  bugsnagClient.leaveBreadcrumb("Saving Ticket", metadata, LOG);
}

结果,这个 breadcrumb 就一直持有对已销毁的 activity 实例的引用。

总结

尽管使用 Java 的双括号初始化看起来很"炫酷",但它会无故地额外创建类,可能会导致内存泄漏。因此避免在 Java 中使用双括号初始化。

你可以用下面这种更安全的方式来解决这个问题:

Map<String, Object> metadata = new HashMap<>();
metadata.put("ticketId", ticketId);
bugsnagClient.leaveBreadcrumb("Saving Ticket", metadata, LOG);

或者利用 Collections.singletonMap() 进一步简化代码:

Map<String, Object> metadata = singletonMap("ticketId", ticketId);
bugsnagClient.leaveBreadcrumb("Saving Ticket", metadata, LOG);

或者,直接将文件转换为 Kotlin。

到此这篇关于如何避免在Java 中使用双括号初始化的文章就介绍到这了,更多相关java双括号初始化内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • MyBatis中的@SelectProvider注解源码分析

    MyBatis中的@SelectProvider注解源码分析

    这篇文章主要介绍了MyBatis中的@SelectProvider注解源码分析,@SelectProvider功能就是用来单独写一个class类与方法,用来提供一些xml或者注解中不好写的sql,今天就来说下这个注解的具体用法与源码,需要的朋友可以参考下
    2024-01-01
  • spring boot 3.3.0和mybatis plus 3.5.6版本冲突的问题解决

    spring boot 3.3.0和mybatis plus 3.5.6版本冲突

    这篇文章主要介绍了spring boot 3.3.0和mybatis plus 3.5.6版本冲突的问题解决,文中介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-07-07
  • Java日期时间与正则表达式超详细整理(适合新手入门)

    Java日期时间与正则表达式超详细整理(适合新手入门)

    如果使用得当,正则表达式是匹配各种模式的强大工具,下面这篇文章主要给大家介绍了关于Java日期时间与正则表达式超详细整理的相关资料,本文非常适合新手入门,需要的朋友可以参考下
    2023-04-04
  • Java实现预览与打印功能详解

    Java实现预览与打印功能详解

    在 Java 中,打印功能主要依赖 java.awt.print 包,该包提供了与打印相关的一些关键类,比如 PrinterJob 和 PageFormat,它们构成了 Java 打印框架的核心,接下来我们将一步步实现一个简单的预览和打印功能,需要的朋友可以参考下
    2025-07-07
  • 分布式Netty源码分析EventLoopGroup及介绍

    分布式Netty源码分析EventLoopGroup及介绍

    这篇文章主要介绍了分布式Netty源码分析EventLoopGroup及介绍,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-03-03
  • idea常用习惯操作设置方法图解

    idea常用习惯操作设置方法图解

    这篇文章主要介绍了idea常用习惯操作设置方法,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • SpringBoot各种注解详解

    SpringBoot各种注解详解

    SpringBoot的一个核心功能是IOC,就是将Bean初始化加载到容器中,Bean是如何加载到容器的,可以使用SpringBoot注解方式或者Spring XML配置方式。SpringBoot注解方式减少了配置文件内容,更加便于管理,并且使用注解可以大大提高了开发效率
    2022-12-12
  • Shiro + JWT + SpringBoot应用示例代码详解

    Shiro + JWT + SpringBoot应用示例代码详解

    这篇文章主要介绍了Shiro (Shiro + JWT + SpringBoot应用),本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-06-06
  • java中ReentrantLock实现公平锁和非公平锁

    java中ReentrantLock实现公平锁和非公平锁

    在Java里,公平锁和非公平锁是多线程编程中用于同步的两种锁机制,它们的主要差异在于获取锁的顺序规则,下面就来详细的介绍一下,感兴趣的可以了解一下
    2025-12-12
  • spring装配bean的3种方式总结

    spring装配bean的3种方式总结

    这篇文章主要给大家介绍了关于spring装配bean的3种方式,文中通过示例代码介绍的非常详细,对大家的学习或者使用Spring具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-03-03

最新评论