本篇笔记对应原书条目6,介绍几个避免创建不必要对象的方法。

1. 采用更合适的API或工具类减少对象的创建

如果一个方法中的一些对象,每次调用都起到相同的作用,那么就可以被重用。

比如,我们不应该用如下的方式来创建一个String对象:

String str = new String("aaa");

因为当我们往构造方法里传入aaa的时候,其实这个aaa就是一个String实例了。我们等于是创建了两个String实例。

我们应该直接这么写:

String str = "aaa";

根据jdk文档,上述方式实际上等同于:

char data[] = {'a', 'a', 'a'};
String str = new String(data);

传入一个字符数组来创建String,避免了创建重复对象。

再举一个常见的例子[2],我们有时希望遍历一个list,将其中的元素存到一个字符串里,并用逗号分隔。我们可能会用下面这种最low的办法:

public static String listToString(List<String> list) {
    String str = "";
    for (int i = 0; i < list.size(); i++) {
        str += list.get(i);
        if (i < list.size() - 1) {
            str += ",";
        }
    }
    return str;
}

这样其实在每次+=的时候都会重新创建String对象,极大地影响了性能。

我们可以修改一下,采用StringBuilder的方式来拼接list:

public static String listToString(List<String> list) {
    StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < list.size(); i++) {
        stringBuilder.append(list.get(i));
        if (i < list.size() - 1) {
            stringBuilder.append(",");
        }
    }
    return stringBuilder.toString();
}

这种方式每次只会生成两个实例——StringBuilder和最后返回的String

那有没有更好的方法呢?我们可以采用Google Guava的Joiner,这样每次只用生成一个实例,如下所示:

public static String listToString(List<String> list) {
    return Joiner.on(",).join(list);
}

2. 重用相同功能的对象

有时候我们提供的API中有一些每次调用都具备相同功能的对象,那么就可以把这些对象变成静态的不可变对象,只需实例化一次即可。比如下面这个类似书中的例子[1]:

public static boolean isNumeral(String s) {
    return s.matches("^[0-9]*$");
}

这个例子用String.matches()方法来判断字符串是否为数字。每次调用matches()方法,里面都会创建一个Pattern对象,这会对性能造成影响。

因为每次调用isNumeral实际上都会生成一个功能完全相同的Pattern对象,所以我们可以把它抽出来,变成一个静态不可变对象,如下所示:

public static final Pattern NUMBER = Pattern.compile("^[0-9]*$");

public static boolean isNumeral(String s) {
    return NUMBER.matcher(s).matches();
}

上面我们谈到了一个不可变对象的重用,接下来我们再看看可变对象的重用。可变对象的重用可以通过视图(views)来实现。比如,Map的keySet()方法就会返回Map对象所有key的Set视图。这个视图是可变的,但是当Map对象不变时,在任何地方返回的任何一个keySet都是一样的,当Map对象改变时,所有的keySet也会相应的发生改变。[1]

3. 小心自动装箱(auto boxing)

自动装箱允许程序员混用基本类型和包装类型[1],在两者相计算时,程序会构造出基本类型的包装类型实例。例如,我们看书中的这个例子:

private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++) 
        sum += i;
    return sum;    
}

这个例子里的sum += i处用Long型和long型相加,这样每次都会实例化一个值为i的包装类型,一共实例化了约231个不必要的实例,极大地降低了系统的性能。所以我们在日常开发中,方法内尽量用基本类型,只在入出参的地方用包装类型。多留心,切忌无意识地使用到自动装箱。[1]

其他

如果涉及到对象池的应用,除非池中的对象非常重,类似数据库连接,否则最好不要去自己维护一个对象池,因为这样会很复杂。另外,有时考虑到系统的安全性,那么我们需要进行防御性复制,这个在后面会讲到。此时,重复创建对象就是有意义的,因为比起隐含错误和安全漏洞,重复创建对象带来的性能损失是可以接受的。[1]

总结

这个条目要求我们平时多审视我们的代码,在能不重复创建对象的地方,就不要重复创建。不要无脑写代码,而是要多留点心。这是一种非常好的编码习惯,也可以为系统带来性能上的优势。

声明

本文仅用于学习交流,请勿用于商业用途。转载请注明出处,如果涉及任何版权问题,请及时与我联系,谢谢!

参考资料

  1. 《Effective Java(第3版)》
  2. Java之避免创建不必要的对象 https://www.jianshu.com/p/420...
  3. java代码优化——避免创建不必要的对象 https://www.jianshu.com/p/83a...

Dylan117
7 声望3 粉丝

Seek and you shall find.