切勿自作聪明,慎用双括号反模式

Published: 12 Dec 2014 Category: java

我经常发现有人会使用双括号这个反模式来进行编程(也被称为双括号初始化)。Stack Overflow上刚刚又有一个case:

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() {
}
  1. 在该匿名类中,我们通过实例初始化器(instance initializer)来将这个HashMap的匿名子类进行初始化:
{
    put("id", "1234");
}

这些初始化器其实就对应于构造方法里面的代码。

为什么说它是双括号反模式(Double Curly Braces Anti Pattern)?

说它是一个反模式主要出于三方面的原因:

  • 可读性

可读性是最次要的一个原因。尽管它使得程序编写变得更简单,并且看起来跟JSON中数据结构的初始化差不多:

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

没错。如果Java的List和Map类型能有集合字面量就实在是太好了。通过双括号来模拟的话总显得有点怪怪的,有什么不对劲。

不过这个就当作是个人喜好吧,我们先暂且搁下不谈,因为还有更重要的原因:

  • 一个实例,一种类型

通过一次双括号的初始化我们其实就已经创建了一个新类型了!通过这种方式所生成的每一个新map,都会隐式地创建了一个无法重复使用的新类型。如果仅用一次的话也无可厚非。但如果在一个大型的应用中到处都充斥着这种代码的话,无形中会给你的类加载器增加了许多负担,你的堆会持有着这些类的引用。不信么?编译下上述代码并查看下编译器的输出。大概会是这样的:

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

这里只有最外围的Test.class是有意义的。

不过这还不是最重要的问题。

  • 内存泄露!

最重要的问题就是匿名类所造成的。它们持有着外围实例的引用,这简直就是个定时炸弹。想像一下,你把这个看似NB的HashMap初始化放到一个EJB或者是一个很重的包含着这样的生命周期的对象里:

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,或者是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实例。不信么?运行下这段程序看看:

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的内部状态。

image

你会发现外围实例的引用就在这个匿名的HashMap子类中安静地躺着。所有的这些匿名子类型都会持有一个这样的引用。

因此,不要使用这个反模式

你可能会说,如果将quickHarmlessMethod()声明成static的不就好了,这不会出现3中的泄露问题了,你说的没错。

不过上述代码中最糟糕的问题就是即便你知道这个静态上下文中的map该如何使用,但下一个开发人员可能会注意不到,他还可能会把这个static重构或者删除掉。他们还可能会把这个map存储在一个单例中,这样你就很难再从代码中看出哪里会有一个无用的ReallyHeavyObject的引用。

内部类是一头野兽。它已经造成过许多的问题以及认知失衡。匿名内部类则更为严重,因为读到这段代码的人可能完全没有意识到自己已经包装了一个外围实例,并且把这个实例传递到了别处。

结论便是:

不要自作聪明了,别使用双括号来进行初始化。

英文原文链接