如何避免在 Java 中使用双括号初始化
发布于 2023-09-03 17:04:56 阅读()作者:admin
已有9352成功领取POS机
如何避免在 J*a 中使用双括号初始化
如何避免在 J*a 中使用双括号初始化
【编者按】本文介绍了一个使用了J*a的双括号初始化语法导致内存泄漏的案例。作者分析了泄漏的原因,提出了几种解决的方法,并给出了代码示例。
链接:https://blog.p-y.wtf/*oid-j*a-double-brace-initialization
未经允许,禁止转载!
作者|Pierre-YvesRicau责编|明明如月
责编|夏萌
出品|CSDN(ID:CSDNnews)
结论先行
避免像这样,在J*a中使用双括号初始化:
newHashMap--{{put-"key",value-;
内存泄漏追踪
我最近正在LeakCanary看到了以下内存泄漏追踪信息:
┬───│GCRoot:Globalvariableinnativecode├─com.bugsnag.android.AnrPlugininstance│Leaking:UNKNOWN│↓AnrPlugin.client│~~~~~~├─com.bugsnag.android.Clientinstance│Leaking:UNKNOWN│↓Client.breadcrumbState│~~~~~~~~~~~~~~~├─com.bugsnag.android.BreadcrumbStateinstance│Leaking:UNKNOWN│↓BreadcrumbState.store│~~~~~├─com.bugsnag.android.Breadcrumb[]array│Leaking:UNKNOWN│↓Breadcrumb[494]│~~~~~├─com.bugsnag.android.Breadcrumbinstance│Leaking:UNKNOWN│↓Breadcrumb.impl│~~~~├─com.bugsnag.android.BreadcrumbInternalinstance│Leaking:UNKNOWN│↓BreadcrumbInternal.metadata│~~~~~~~~├─com.example.MainActivity$1instance│Leaking:UNKNOWN│Anonymoussubclassofj*a.util.HashMap│↓MainActivity$1.this$0│~~~~~~╰→com.example.MainActivityinstanceLeaking:YES-Activity#mDestroyedistrue-当打开一个内存泄漏追踪日志时,我首先会看底部的对象,了解它的生命周期,这将帮助我理解内存泄漏追踪中的其他对象是否应该有相同的生命周期。
在底部,我们看到:
╰→com.example.MainActivityinstanceLeaking:YES-Activity#mDestroyedistrue-
Activity已经被销毁,应该已被垃圾回收器给回收掉了,但它仍驻留在内存中。
此时,我开始在内存泄漏追踪日志中寻找已知类型,并尝试弄清楚它们是否属于同一个被销毁的范围(=>正在泄漏)或更高的范围(=>没有泄漏)。
在顶部,我们看到:
├─com.bugsnag.android.Clientinstance│Leaking:UNKNOWN
我们的BugSnag客户端是一个用于分析崩溃报告单例,由于每个应用我们创建一个实例,所以它没有泄漏。
├─com.bugsnag.android.Clientinstance│Leaking:NO
所以我们现在需要转变焦点,特别关注从最后一个Leaking:NO到第一个Leaking:YES的部分:
├─com.bugsnag.android.Clientinstance│Leaking:NO│↓Client.breadcrumbState│~~~~~~~~~~~~~~~├─com.bugsnag.android.BreadcrumbStateinstance│Leaking:UNKNOWN│↓BreadcrumbState.store│~~~~~├─com.bugsnag.android.Breadcrumb[]array│Leaking:UNKNOWN│↓Breadcrumb[494]│~~~~~├─com.bugsnag.android.Breadcrumbinstance│Leaking:UNKNOWN│↓Breadcrumb.impl│~~~~├─com.bugsnag.android.BreadcrumbInternalinstance│Leaking:UNKNOWN│↓BreadcrumbInternal.metadata│~~~~~~~~├─com.example.MainActivity$1instance│Leaking:UNKNOWN│Anonymoussubclassofj*a.util.HashMap│↓MainActivity$1.this$0│~~~~~~╰→com.example.MainActivityinstanceLeaking:YES-Activity#mDestroyedistrue-
BugSnag客户端保持了一个面包屑的环形缓冲区。这些应该保留在内存中,它们也没有泄漏。
所以让我们跳过上述内容,从下面这里继续分析:
├─com.bugsnag.android.BreadcrumbInternalinstance│Leaking:NO
我们只需要关注从最后一个Leaking:NO到第一个Leaking:YES的部分:
├─com.bugsnag.android.BreadcrumbInternalinstance│Leaking:NO│↓BreadcrumbInternal.metadata│~~~~~~~~├─com.example.MainActivity$1instance│Leaking:UNKNOWN│Anonymoussubclassofj*a.util.HashMap│↓MainActivity$1.this$0│~~~~~~╰→com.example.MainActivityinstanceLeaking:YES-Activity#mDestroyedistrue-
BreadcrumbInternal.metadata:内存泄漏追踪通过面包屑实现的元数据字段。
MainActivity$1实例是j*a.util.HashMap的匿名子类:MainActivity$1是在MainActivity中定义的HashMap的匿名子类。它是从MainActivity.j*a中定义的第一个匿名类(因为是$1)。
this$0:每个匿名类都有一个隐式字段引用到定义它的外部类,这个字段被命名为this$0。
也就是说:记录到BugSnag的面包屑之一有一个元数据映射,这是一个HashMap的匿名子类,它保留对外部类的引用,这个外部类就是被销毁的Activity。
让我们看看我们在MainActivity中记录面包屑的地方:
voidlogS*ingTicket-StringticketId-{Mapmetadata=newHashMap--{{put-"ticketId",ticketId-;bugsnagClient.le*eBreadcrumb-"S*ingTicket",metadata,LOG-;
这段代码利用了一个被称为“双括号初始化”的有趣的J*a代码块。它允许你创建一个HashMap,并通过添加代码到HashMap的匿名子类的构造函数中同时初始化它。
newHashMap--{{put-"ticketId",ticketId-;J*a的匿名类总是隐式地引用其外部类。
因此,这段代码:
voidlogS*ingTicket-StringticketId-{Mapmetadata=newHashMap--{{put-"ticketId",ticketId-;bugsnagClient.le*eBreadcrumb-"S*ingTicket",metadata,LOG-;
实际上被编译为:
classMainActivity$1extendsHashMap{privatefinalMainActivitythis$1;MainActivity$1-MainActivitythis$1,StringticketId-{this.this$1=this$1;put-"ticketId",ticketId-;voidlogS*ingTicket-StringticketId-{Mapmetadata=newMainActivity$1-this,ticketId-;bugsnagClient.le*eBreadcrumb-"S*ingTicket",metadata,LOG-;
结果,这个breadcrumb就一直持有对已销毁的activity实例的引用。
总结
尽管使用J*a的双括号初始化看起来很"炫酷",但它会无故地额外创建类,可能会导致内存泄漏。因此避免在J*a中使用双括号初始化。
你可以用下面这种更安全的方式来解决这个问题:
Mapmetadata=newHashMap--;metadata.put-"ticketId",ticketId-;bugsnagClient.le*eBreadcrumb-"S*ingTicket",metadata,LOG-;
或者利用Collections.singletonMap--进一步简化代码:
Mapmetadata=singletonMap-"ticketId",ticketId-;bugsnagClient.le*eBreadcrumb-"S*ingTicket",metadata,LOG-;
或者,直接将文件转换为Kotlin。
你是否在使用J*a时遇到过内存泄漏的问题?
为什么不建议使用双括号初始化?
原文地址: https://alphahinex.github.io/2020/07/12/why-does-not-suggest-to-use-double-brace-initialization/
description: "Double Brace Initialization in J*a"
date: 2020.07.12 19:26
categories:
- J*a
tags: [J*a]
keywords: J*a, HashMap, Map, J*a Collections, Double Brace Initialization
以下内容引自 The J*a? Tutorials - Initializing Fields :
虽然双括号初始化看上去还算美,但却不建议使用。
J*a 8、9 也提供了一些其他的初始化方式,还有各种三方类库也提供了很多其他方式(详见上节中的参考资料)。
什么?还不满意?
忍一时风平浪静,退一步海阔天空。
有能耐你别用 J*a 啊!
比如 Groovy 了解一下:
eclipse和myeclipse中怎么关闭自动补全括号,花括号,双引号等功能
首先打开eclipse或myeclipse, 单击window选择preferences:
依次选择j*a->editor->content assist:
把insert single proposals automatically勾去掉!
帮忙看下j*a错误,为什么去掉两个花括号就不对呢?
你写了个私有的语句块!
如果你把括号去了就有
panel.setOpaque(false);
panel.add(new JButton("Hello"));
setContentPane(panel);
上面三句只能出现在方法或语句块里面(类的成员有属性,方法,语句块,这三句都不符合)
不信你可以把前面的括号下移到
panel.setOpaque(false);
这句的前面试试
所以就...
几个j*a小题目,想请教大家
我来解释一下吧,我不知道你的水平,所以可能讲的比较罗嗦了点,希望你能有所收获:
1 j*a程序的运行原理和编译执行一个j*a程序的步骤:
j*a程序是通过j*a虚拟机解释,将其中的代码(字节码)翻译为具体的操作指令来执行的。
***设要编译并执行位于d:\j*ademo目录中的Test.j*a:
一、首先设置classpath和path环境变量: 在我的电脑上点右键 > 属性 > 高级 > 环境变量 > 上面一半的部分是当前用户的环境变量,只对当前用户适用,用其它用户登录后就变为另一个用户的环境变量内容了。下面一半是整个系统的环境变量,对电脑中的每个用户都适用。建议设置系统的环境变量 > 新建 > 变量名填classpath,变量值填一个英文句号(.),注意要是英文的句号 > 确定 > 设置path变量,path变量是已经存在的,我们只要找到该变量名(这里名称是大小写无关的,PATH、path、Path,只要找到就行),双击它或点击它之后点编辑按钮 > 在变量值一栏中的最前面(注意不要将原先的值删掉)加上你的j*a安装目录下的bin目录的路径,然后加一个英文分号。如你的j*a安装路径是c:\j*a,那么这里要加上“c:\j*a\jdkxxx\bin;(这后面是原先的值)”(这里的jdkxxx会因为你安装的版本不同而不同,所以你要自己查一下) > 完成之后一直点确定就行了
这里讲一下环境变量的作用:环境变量又叫系统变量,和我们程序中的变量类似,是储存一些系统运行期间经常用到的值的,操作系统或应用程序如果需要用到环境变量,它可以根据环境变量的变量名(如刚才定义的classpath)来查找对应的值(如刚才的英文句号)。
那classpath到底是干嘛的呢?它是给j*a虚拟机用的,当我们需要运行一个j*a程序时,j*a虚拟机查找classpath变量的值,根据这些值定位j*a程序所在的位置,刚才定义的英文句号的意思是命令行所处的当前目录,下面例子中会详细讲到。
(以上的环境变量只要设置一次,以后再编译或运行j*a程序时就不用再重复设置了)
二、然后打开命令行:开始菜单 > 运行 > 输入cmd后确定
将目录切换到j*a文件所在的位置,输入以下命令(输入一行命令按一次回车键):
d:
cd j*ademo
解释:
d: 的作用是切换盘符
cd j*ademo 的作用是切换目录,该目录只能在当前盘符下进行目录切换,如果在c盘下输入cd d:\j*ademo是没有效果的,必须先将盘符切换到d盘。该命令可以一次切换多层目录,如在d盘根目录下要切换到d:\a\b\c目录下,可以写cd a\b\c,就几层写几层就行了。
三、编译j*a程序
经过前一步,当前目录已经是Test.j*a所在的目录了,因此我们输入以下命令编译:
j*ac Test.j*a
这一步命令是大小写无关的,因此也可以输入j*ac test.j*a。但注意,文件本身的文件名是大小写有关的,只是输入这个命令的时候可以忽略它的大小写(见第五步)。
四、执行j*a程序
第三步编译后会生成一个Test.class文件,输入以下命令载入这个文件并执行它:
j*a Test
注意,这一步Test是大小写相关的,并且不能加.class后缀,因此不能写成j*a test 或 j*a Test.class
五、补充,下面是Test.j*a的一个样本,可以直接拿来做实验:
import j*ax.swing.JOptionPane;
public class Test
{
public static void main(String[] args)
{
JOptionPane.showDialogMessage(null, "如果你见到这个,就说明你已经成功了");
System.exit(0);
}
}
说明:这个程序定义了一个公有类叫Test,因此它的文件名必须叫Test.j*a,包括大小写。如果你改变了类的名字,那么文件名也必须跟着改变。
2.类和对象的关系:
其实只要明白一点:类是我们预先定义好的,它就像一个模板,对象由这个模板来创建,因此由一个类可以“复制”出任意多个对象。
举一个直观点了例子,类就好像印刷时用的模板,每一次印刷都会产生一件印刷品(报纸、书、宣传单等等)。模板本身并不太能胜任“给人看”这样一个任务,首先因为它太重,其次因为它的字是反的。类也一样,我们一般不直接用类来完成一件事情,而是先由它生成一个对象,由对象来执行具体的任务。
3.构造方法又叫构造函数,它的任务是创建它所在类的对象。一个类必须至少有一个构造方法,否则这个类无法创建对象(因此也就没用了)。所以如果我们没有在类中自己定义构造方法,编译器会给我们添加一个(这就保证了任何一个类都能产生对象)。
4.方法重载:
举个例子,你想编写一个方法(或叫函数),它返回传入的两个参数中较大的那个,在c中(c不支持方法重载)你可以这样写:
int larger(int x, int y)
{
if (x>=y)
{
return x;
}
else
{
return y;
}
}
但问题是,这个方法只能完成两个整数的比较,如果我还想传入两个浮点数并返回其中的较大者,在c中只能另外定义一个不同名称的函数,因为两个函数的名称相同在c中是不允许的。
而j*a中可以利用方法重载方便的解决这个问题,这个double型的方法可以这样写:
double larger(double x, double y)
{
....
}
虽然定义了两个都叫larger的方法,但由于参数的类型不同,所以它们是不同的方法。但是这种不同对使用这个方法的人来说是透明的,也就是说,任何人都可以这样调用:
larger(a, b);
不管a和b是整型或double型,j*a虚拟机都能够从所有重载的方法中正确选择一个合适的方法来执行。
而在c中,由于不能进行方法重载,编写方法的人必须定义不同的名称来完成相似的功能,例如上面例子中两个方法可能分别叫做intLarger和dblLarger,这样使用方法的人也必须根据参数的不同选择一个方法去调用。
相关文章推荐
-
pos机汇开店余额怎么提出来 08-15
-
汇付天下pos机开户(汇付天下pos机开户流程) 09-19
-
汇付天下POS机(汇付天下pos机正规吗) 08-03
-
汇付天下pos机功能() 09-04
-
汇付天下蓝牙pos机(汇付天下蓝牙pos机招商) 09-09
-
2019年闪电宝pos机安全吗(闪电宝安全吗) 10-16
-
pos机刷卡汇付天下(汇付天下pos机使用步骤) 10-16