快速翻译
通常建议复用简单对象而不是每次使用的时候都创建新的对象。复用更快也更加优雅,一个不可变对象总是可以复用的。下面是一个反例:
String s = new String("bikini"); // DON'T DO THIS! 不要这么使用
这段代码每次执行的时候都会创建一个新String实例,这些实例的创建是没有必要的,构造函数中的字符串参数bikini,本身就是一个字符串实例,所有通过构造函数创建的相同对象的方法调用,并且在循环中或者被频繁调用的方法,上百万的String实例是没有必要创建的,改进的版本如下:String s = "bikini";
这个版本使用了一个简单的字符串实例,而不是执行的时候每次都创建一个新的实例,更进一步来说,它保证了对象在任何运行的虚拟机上将会被复用。
你通常可以使用静态工厂方法而不是构造函数来避免创建不必要的对象,例如:Boolean.valueOf(String)优先于在java9中废弃的Boolean(String), 构造函数被调用的时候每次都会创建一个对象,而静态工厂方法在实际应用中不会每次创建对象,除了复用不可变对象,如果你知道他们不会被修改,你也可以复用可变对象。
一些对象的创建比其它对象昂贵,如果你想重复使用这些昂贵对象,建议缓存起来使用,但不幸的是,当你创建这样一个对象的时候代价通常不明显,假设你想写一个方法判断一个字符串是不是罗马数字,最简单的方式是使用正则表达式。
// Performance can be greatly improved! 性能可以很大提高 static boolean isRomanNumeral(String s) { return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); }
这个实现的问题是它依赖String.matches(String)方法.尽管String.matches方法是最简单的方法检查一个字符串是否匹配正则,它不适合重复使用在性能敏感的场景。
问题是它内部对每个正则创建一个Pattern实例而只是使用了它一次,然后它就等着被垃圾回收了,创建一个Pattern实例是很昂贵的因为它需要编译正则表达式到有限的状态机中。
为了提高性能,当一个类初始化的时候明确的编译一个正则表达式,并缓存起来,每次调用isRomanNumeral方法的时候复用同一个实例。
// Reusing expensive object for improved performance 复用昂贵对象提高性能 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 isRomanNumeral(String s) { return ROMAN.matcher(s).matches(); } }
当被频繁调用的时候该版本可以明显的提高性能。原始版本当我输入一个8个字符的时候需要耗费1.1微秒,而提高性能的版本只需要0.17微秒,提高了6.5倍的性能,不仅性能提高了,明确性也是可以论证的。
申明为static final 成员变量可以隐式的给它一个名字,它比正则本身更可读,如果类包含了isRomanNumberal方法,它被初始化了但是从来没有被调用,字段ROMAN将没有必要初始化,可以使用懒初始化这个字段来避免初始化,只有方法第一次调用的时候才初始化;但是这样做并不推荐,通常来说懒初始化,他会使得实现复杂,没有可计量的性能提升;
当一个对象是不可变的,很明显它可以被复用。但是也有一些很不明显的场景,甚至可以说是反直觉的。思考一下适配器,或者说是视图,一个适配器是被后面对象委托了的对象,提供了可替换的接口,因为适配器在后面对象的一边没有状态,为一个对象创建适配器没有必要创建多于一个的对象,例如:Map类的keyset的方法返回一个Map对象的set视图,包含map对象的所有key, 很自然的,看起来每个调用keyset的方法会创建一个set 实例,但是每个调用Map的keyset方法可能返回同一个set实例,尽管返回的set对象通常是可变的,所有返回的对象功能相同,当其中一个对象改变的时候,所有其它的返回对象也会改变,因为他们都是在同一个Map实例的后面,虽然创建大量的keyset是无害的,同样也没有必要并且没有任何好处,另外一种创建不必要对象的是自动装箱,让程序员混淆原始类型和包装类型,按照需要进行自动装箱和拆箱,自动装箱很模糊,但是不能抹去原始类型和包装类型差别,有一些细微的语义差别,和并不细微的性能差别;
思考下面的方法,计算所有的正整数的和,为了实现这个,使用了long的算术而不是int,因为int无法足够保存所有的int正数相加,
// Hideously slow! Can you spot the object creation? //非常慢,无法清点创建的对象 private static long sum() { Long sum = 0L; for (long i = 0; i <= Integer.MAX_VALUE; i++) sum += i; return sum; }
这个程序得到了正确的答案,但是它比预想的要慢,因为一个字母的排版错误,变量sum被申明为了Long,而不是long, 这意味着这个程序构造了2的31次方个不必要的Long对象,(大致当i增加1的时候创建一个),把Long改变为long,在我的机器上,运行时间从6.9秒降低为0.59秒,学到的很明显:原生类型优先包装类型,关注非刻意的自动装箱 ,这一条不要误解为:对象创建很昂贵的对象应该避免,相反的,创建和回收(构造函数做一些明显简单的工作)小对象,特别是现代的JVM实现, 创建一些额外的对象来提高明确性,简单性,健壮性是一件好事;相反的,通过维护一个私有的对象池来避免创建对象是一个坏主意,除非对象池里面的对象很重,展示对象池的例子类是数据库连接池,创建一个连接的代价是如此高昂,所以必须意识到对象复用,
通常来说,维护私有对象池让你的代码看起来很乱,增加了内存占用,降低性能,现代的JVM实现充分利用了垃圾回收器比为这些轻便对象提供对象池做的更好。
对应第50条防御复制,当前条目说的是:你应该复用一个已经存在的对象而不是创建一个新的对象;而第50条说的是:当你需要创建一个新对象的时候,别使用已经存在的对象,
注意这一点:在所谓的防御复制中,使用一个已存在的对象的惩罚远大于创建一个重复对象。创建需要的防御型的复制失败可以导致潜在的bug和安全漏洞;创建不必要的对象只是影响风格和性能。
快速记忆
我的理解
如果对象的创建代价很昂贵,应该考虑使用对象池缓存起来使用;如果创建对象的调用次数很多,但是对象却是不可变的,可以考虑重用已有不可变对象,放到类的静态成员变量中是一个比较好的方法,也要特别注意自动装箱和拆箱带来的创建过多不必要对象的问题。
原创不易,转载请注明出处,一起学习Effective java 3,提高代码质量,编程技能。欢迎一起讨论。