在每次需要时重用一个对象而不是创建一个新的相同功能对象通常是恰当的。重用可以更快更流行。如果对象是不可变的,它总是可以被重用。
1. 避免如下使用字符串的方式, 这样每次执行的时候都会创建一个新的对象
String s = new String("bikini"); // DON'T DO THIS!
语句每次执行时都会创建一个新的String实例,而这些对象的创建都不是必需的。String构造方法(“bikini”)
的参数本身就是一个bikini
实例,它与构造方法创建的所有对象的功能相同。如果这种用法发生在循环中,或者在频繁调用的方法中,就可以毫无必要地创建数百万个String实例。
要改成如下
String s = "bikini";
该版本使用单个 String 实例,而不是每次执行时创建一个新实例。此外,它可以保证对象运行在同一虚拟机上的任何其他代码重用,而这些代码恰好包含相同的字符串字面量。
通过使用静态工厂方法,可以避免创建不需要的对象。例如,工厂方法Boolean.valueOf(String)
比构造方法Boolean(String
)更可取,后者在Java 9中被弃用。构造方法每次调用时都必须创建一个新对象,而工厂方法永远不需要这样做,在实践中也不需要。除了重用不可变对象,如果知道它们不会被修改,还可以重用可变对象。
2. 尽量减少创建复杂的对象
有些对象的创建成本要比其他对象高很多,如果重复迭代创建这样的对象,非常消耗资源,因此应该将这样的对象创建好,存放起来,后续直接使用就好了。假设你想写一个方法来确定一个字符串是否是一个有效的罗马数字。 以下是使用正则表达式完成此操作时最简单方法isRomanNumeralNEG。这个实现的问题在于它依赖于 String.matches
方法。 虽然 String.matches
是检查字符串是否与正则表达式匹配的最简单方法,但它不适合在性能临界的情况下重复使用。 问题是它在内部为正则表达式创建一个 Pattern
实例,并且只使用它一次,之后它就有资格进行垃圾收集。 创建 Pattern
实例是昂贵的,因为它需要将正则表达式编译成有限状态机。因此可以使用isRomanNumeralPOS创建一个实例,供后续使用。速度优化接近8倍。
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeralNEG(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
static boolean isRomanNumeralPOS(String s) {
return ROMAN.matcher(s).matches();
}
public static void main(String[] args) {
long start1 = System.currentTimeMillis();
String string = "MCMXCIV";
for(int i = 0; i < 1000000; i ++)
isRomanNumeralNEG(string);
System.out.println("the negtive simple time long : " + (System.currentTimeMillis() - start1));
long start2 = System.currentTimeMillis();
for(int i = 0; i < 1000000; i ++)
isRomanNumeralPOS(string);
System.out.println("the positive simple time long : " + (System.currentTimeMillis() - start2));
}
}
/*
the negtive simple time long : 820
the positive simple time long : 111
*/
3. 避免自动装箱
另一种创建不必要的对象的方法是自动装箱(autoboxing),它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。 有微妙的语义区别和不那么细微的性能差异(条目 61)。 考虑下面的方法,它计算所有正整数的总和。 要做到这一点,程序必须使用long
类型,因为int
类型不足以保存所有正整数的总和:
public class AutoBox {
private static long sum1() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
private static long sum2() {
long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
public static void main(String[] args) {
long start1 = System.currentTimeMillis();
sum1();
System.out.println("the auto boxing use time : " + (System.currentTimeMillis() - start1));
long start2 = System.currentTimeMillis();
sum2();
System.out.println("no auto boxing use time : " + (System.currentTimeMillis() - start2));
}
}
/*
the auto boxing use time : 7077
no auto boxing use time : 502
*/
这个程序的结果是正确的,但由于写错了一个字符,运行的结果要比实际慢很多。变量sum
被声明成了Long
而不是long
,这意味着程序构造了大约2不必要的Long
实例(大约每次往Long
类型的 sum
变量中增加一个long
类型构造的实例),把sum
变量的类型由Long
改为long
更改后,在我的电脑上速度快了十几倍,由7077毫秒,优化到502毫秒。
这个条目不应该被误解为暗示对象创建是昂贵的,而应该避免创建对象。 相反,使用构造方法创建和回收小的对象是非常廉价,构造方法只会做很少的显示工作,,尤其是在现代JVM实现上。 创建额外的对象以增强程序的清晰度,简单性或功能性通常是件好事。
相反,除非池中的对象非常重量级,否则通过维护自己的对象池来避免对象创建是一个坏主意。对象池的典型例子就是数据库连接。建立连接的成本非常高,因此重用这些对象是有意义的。但是,一般来说,维护自己的对象池会使代码混乱,增加内存占用,并损害性能。现代JVM实现具有高度优化的垃圾收集器,它们在轻量级对象上轻松胜过此类对象池。
总结:
- 尽量避免创建新字符串,应该使用缓存中的
- 对于复杂的对象,减少创建,尽量重用
- 避免自动装箱
- 不要对轻量对象维护对象池,小对象可以构造器生成,重量级的大对象要用对象池。