不要“自作聪明”:{{是一种反模式

我时常发现有人在用 {{ 这种反模式(也叫做双大括号初始化)。这次是在 stack Overflow 上:

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");
        }});
    }});
}};

如果你不知道它的语法,实际上它非常简单,包含两个元素:

1、以下的代码创建了一个继承 HashMap 的匿名类:

new HashMap() {
}

2、在匿名类内部,我们用一个实例初始者来初始化这个匿名 HashMap 子类:

{
    put("id", "1234");
}

本质上,这些初始者就是构造方法代码。所以,这是为什么我们说{{ 是反模式。

以下是{{是反模式的三个理由:

1、可读性

可读性是最不重要的理由。然而它却容易写,也有点像 JSON 数据的初始化:

{
  "firstName"     : "John"
, "lastName"      : "Smith"
, "organizations" :
  {
    "0"   : { "id", "1234" }
  , "abc" : { "id", "5678" }
  }
}

是的。如果Java有List literal或者Map literal,那真是太赞了。但是从语法上来说,用{{ 来模仿有点奇怪,也感觉不太正确。但是可以先把他留在关于大括号的讨论中(以前我们也这么做过)。

译者注:literal不好翻译,保留英文。list literal指如{1,2,3}这样的列表)。

2、每个实例一种类型

我 们每一次{{实例化,都创建了一种类型。每次我们用这种方式创建一个 Map,我们同时隐式的为这个简单的 hashMap 实例创建了一个不可复用的类。如果你只这么做一次,那还是可行的。但是如果类似这样的代码存在于一个大型的应用中,这将增加 ClassLoader 一些不必要的负担,ClassLoader 需要在堆上持有所有这些类的引用。不相信么?编译一下上面的代码然后查看一下编译结果。它看起来是这样的:

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

在这里,只有 Test.class 是唯一有意义的类,是封装类。但这还不是最严重的问题。

3、内存泄露!

内存泄露是所有匿名类最严重的问题。他们持有他们封装实例的引用,这点是致命的。想象一下,你把你的聪明的 HashMap 实例化放进一个 EJB 或者是其他的有着管理良好的生命周期的重量级 object 里,像下面这样:

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 包含很多资源,一旦 ReallyHeavyObject 被回收,这些资源需要被正确的清除。但是如果你调用马上执行的 quickHarmlessMethod()方法,这个就没有影响了。

好吧。

想象一下其他开发人员,重写了你的方法去返回这个 map,或者仅仅是 map 的一部分。

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 的所有状态暴露给了外界,因为每个内部类都持有它的封装类的引用,也即ReallyHeavyObject 的实例。不相信么,来 run 以下代码:

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

是的,确实如此!如果你还不相信,你可以用 debugger 去检查返回的 map:

 

你可以发现,封装类的引用就存在匿名 HashMap 的子类里。 而且所有的嵌套匿名 HashMap 子类也都持有这样一个引用。

所有,请永远不要使用这个反模式。

你可能会说,把 quickHarmlessMethod()变成静态方法可以绕开内存泄露的问题, 你是对的。但是,在上面的例子中我们看到,事实上你可能可以创建一个静态的上下文,但是下一个开发人员可能 不会注意到,他会改写你的方法,或者直接把 static 去掉。他们可能把 Map 存在其他的一些单例 中,而且通过代码本身是不可能看出它包含 ReallyHeavyObject 的无用引用。内部类非常讨厌,他们过去带来了很多麻烦和明细的不协调。匿名内部类更讨厌,因为看代码的人很容易忽略他们持有一个 外部类的引用而且他们还把这个引用到处传递。 结论就是: 不要自作聪明,千万不要使用{{实例化。

原文链接: JOOQ 翻译: ImportNew.com - poppy
译文链接: http://www.importnew.com/15087.html
[ 转载请保留原文出处、译者和译文链接。]



相关文章

发表评论

Comment form

(*) 表示必填项

1 条评论

  1. blackzwei 说道:

    可以把literal翻译成字面量 其他书上都是这么翻的..

    Thumb up 1 Thumb down 0

跳到底部
返回顶部