永远不要使用双花括号初始化实例!!!

本贴最后更新于 2705 天前,其中的信息可能已经斗转星移

说在前面

一段时间以来,经常发现有人使用类似如下代码装逼耍酷

Map source = new HashMap(){{
    put("firstName", "John");
    put("lastName", "Smith");
    put("organizations", new HashMap(){{
        put("it", new HashMap(){{
            put("id", "1234");
        }});
        put("oa", new HashMap(){{
            put("id", "5678");
        }});
    }});
}};

我们称之为双花括号初始化(构造)实例,看起来是挺酷的!

如果你对这个语法只是拿来用过,知其然不知其所以然,没关系,实际上它挺简单的,一共做了两件事

  • 使用下面的代码实例化一个继承自 Map 的匿名类
	new HashMap(){} 
  • 然后使用下面的代码 继续实例化一个匿名类
{
	put("id", "1234");
}

原理详见 Oracle 官方文档:Initializing Fields

实际上,这就是一个执行构造函数的过程~

为什么需要拒绝以这种方式初始化实例?

可读性 --这是最不重要的一个原因

双括号初始化实例的代码片段对于没有接触过的人来看,很难直观的理解他的作用,起码,从语法上来讲,就觉得别扭(虽然写的人挺爽);

每个实例一个类型

看起来,这段代码只是一个简单的实例化对象的过程,除此之外,什么事情都没有发生 ,实际上,他同时也会创建一个不可复用的(匿名内部)类!
如果你只是偶尔这么做一次,那还勉强可以接受,如果你在一个体量庞大的企业级应用中大范围的使用这种做法,那对 ClassLoader 来说简直就是灾难,他需要在堆内存中为这些生成的所有的匿名内部类保持一个引用!
如果你不信的话,你尝试建一个 Test 类,然后 copy 上面的代码,然后编译,你就会发现生成了如下文件:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

holy shit!并不是你想象中理所当然的就一个 Test 类的 class...

最重要的原因是这样做会导致内存泄漏!!!

这是所有匿名内部类都有的一个致命的问题--他们会在内部维护一个外部容器类的引用,想像一下,你在一个庞大的应用中创建了一个如下代码类:

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 被 GC 的时候清理,然而你可能并没有意识到这一点,你只是:若无其事的调用了 quickHarmlessMethod(),完成了 ReallyHeavyObject 的使命,而且以后可能不会再调用第二次 ```

如果有一天,另一个开发者改动了你的代码,在 quickHarmlessMethod 方法中返回了 source 对象,如下代码所示:

	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 这个类通过此方法暴露了出去;
如果不信的话,请尝试以下代码:

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 持有了外部类的引用,就连他的子类,也持有!

延伸

也许你会讲,第三个问题可以通过将方法静态化来解决,这没错!但这是基于你对以上几点有深刻的认知的前提下,万一哪天你的小弟在不知情的前提下,将 static 关键字删掉,问题又会出现了!
古往今来,使用匿名类已经带来够多的麻烦了,而匿名内部类,那简直更危险!因为无心的人根本不会注意到,他们通过一个匿名内部类将外部容器类的引用暴露了出去!

结论

SO,我们的结论是 没有蛀牙 永远不要使用双花括号初始化实例!!!

参考文章:Don’t be “Clever”: The Double Curly Braces Anti Pattern

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3453 回帖 • 204 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • 代码
    467 引用 • 631 回帖 • 9 关注
  • 编程
    53 引用 • 266 回帖 • 3 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • zonghua

    我看 golang 好多这个使用,声明结构体直接初始化,生命方法后面就执行。。

  • 其他回帖
  • beibeiquan

    我感觉既然你用了反射的话,暴露不暴露都没有啥影响了,用了反射啥都会暴露,另外我测试了下创建一百万匿名 HashMap,并没有 OOM,JVM 能自动 GC 掉垃圾,我的是 1.8,是不是我姿势不对 ,以下是我测试的方式和结果

    TIM 截图 20200523122713.png

  • someone

    啊哦,跨语言的不是很懂,不予评论~

  • someone

    厉害了~:heart:

  • 查看全部回帖