文章目录
- Effective第三版
- 前言
- 第二章 创建和销毁对象
- 避免创建不需要的对象
Effective第三版
前言
大家好,这里是 Rocky 编程日记 ,喜欢后端架构及中间件源码,目前正在阅读 effective-java 书籍。
同时也把自己学习该书时的笔记,代码分享出来,供大家学习交流,如若笔记中有不对的地方,那一定是当时我的理解还不够,希望你能及时提出。
如果对于该笔记存在很多疑惑,欢迎和我交流讨论,最后也感谢您的阅读,点赞,关注,收藏~
前人述备矣,我只是知识的搬运工,effective 书籍源码均在开源项目 java-diary 中的 code-effective-third 模块中,其中源代码仓库地址:
https://gitee.com/Rocky-BCRJ/java-diary.git
第二章 创建和销毁对象
避免创建不需要的对象
It is often appropriate to reuse a single object instead of creating a new functionally equivalent object each time it is needed. Reuse can be both faster and more stylish. An object can always be reused if it is immutable (Item 17).
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。重用的方式既快速,又流行。如果对象是不可变(immutable)的(第 17 项),那么就能重复使用它。
As an extreme example of what not to do, consider this statement:
作为一个极端的反面例子,考虑下面的语句:
String s = new String("bikini"); // DON'T DO THIS!
The statement creates a new String instance each time it is executed, and none of those object creations is necessary. The argument to the String constructor (“bikini”) is itself a String instance, functionally identical to all of the objects created by the constructor. If this usage occurs in a loop or in a frequently invoked method, millions of String instances can be created needlessly.
该语句每次被执行的时候都创建一个新的 String 实例,但是这些对象的创建并不都是必要的。传递给 String 构造器的参数("bikini")
本身就是一个 String 实例,功能方面等同于构造器创建的所有对象。如果这种方法是用在一个循环中,或者是在一个被频繁调用的方法中,就会创建出成千上万的不必要的 String 实例。
The improved version is simply the following:
改进后的版本如下:
String s = "bikini";
This version uses a single String instance, rather than creating a new one each time it is executed. Furthermore, it is guaranteed that the object will be reused by any other code running in the same virtual machine that happens to contain the same string literal [JLS, 3.10.5].
这个版本只用了一个 String 实例,而不是每次执行时都创建一个新的实例。除此之外,它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用 [JLS, 3.10.5]。
You can often avoid creating unnecessary objects by using static factory methods (Item 1) in preference to constructors on immutable classes that provide both. For example, the factory method Boolean.valueOf(String) is preferable to the constructor Boolean(String), which was deprecated in Java 9. The constructor must create a new object each time it’s called, while the factory method is never required to do so and won’t in practice. In addition to reusing immutable objects, you can also reuse mutable objects if you know they won’t be modified.
对于同时提供了静态工厂方法(第 1 项)和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,这样可以经常避免创建不必要的对象。例如,这个静态工厂方法Boolean.valueOf(String)
总是优先于在 Java 9 中抛弃的构造器 Boolean(String)
。构造函数必须在每次调用时创建一个新对象,而工厂方法从不需要这样做,也不会在实践中。除了重用不可变对象之外,如果你知道它们不会被修改,你还可以重用可变对象。
Some object creations are much more expensive than others. If you’re going to need such an “expensive object” repeatedly, it may be advisable to cache it for reuse. Unfortunately, it’s not always obvious when you’re creating such an object. Suppose you want to write a method to determine whether a string is a valid Roman numeral. Here’s the easiest way to do this using a regular expression:
有些对象的创建比其他对象的代价大,如果你需要反复创建这种代价大的对象,建议将其缓存起来以便重复使用。不幸的是,当你创建这样一个对象时,并不总是很明显。假设你想编写一个方法来确定一个字符串是否是一个有效的罗马数字。使用正则表达式是最简单的方法:
// 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})$");
}
The problem with this implementation is that it relies on the String.matches method. While String.matches is the easiest way to check if a string matches a regular expression, it’s not suitable for repeated use in performance-critical situations.The problem is that it internally creates a Pattern instance for the regular expression and uses it only once, after which it becomes eligible for garbage collection. Creating a Pattern instance is expensive because it requires compiling the regular expression into a finite state machine.
此实现的问题在于它依赖于String.matches
方法。虽然String.matches
是检查字符串是否与正则表达式匹配的最简单方法,但它不适合在性能关键的情况下重复使用。问题是它在内部为正则表达式创建了一个 Pattern 实例,并且只使用它一次,之后它就可能会被垃圾回收机制回收。创建 Pattern 实例的代价很大,因为它需要将正则表达式编译为有限状态机(because it requires compiling the regular expression into a finite state machine)。
To improve the performance, explicitly compile the regular expression into a Pattern instance (which is immutable) as part of class initialization, cache it, and reuse the same instance for every invocation of the isRomanNumeral method:
为了提高性能,将正则表达式显式编译为 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();
}
}
The improved version of isRomanNumeral provides significant performance gains if invoked frequently. On my machine, the original version takes 1.1 μs on an 8-character input string, while the improved version takes 0.17 μs, which is 6.5 times faster. Not only is the performance improved, but arguably, so is clarity. Making a static final field for the otherwise invisible Pattern instance allows us to give it a name, which is far more readable than the regular expression itself.
如果经常调用的话,改进版本的 isRomanNumeral 可以显着提高性能。在我的机器上,原始版本在 8 个字符的输入字符串上需要 1.1μs,而改进版本需要 0.17μs,这是 6.5 倍的速度。不仅提高了性能,而且功能更加明了。为不可见的 Pattern 实例创建一个静态的 final 字段,我们可以给它一个名字,它比正则表达式本身更具有可读性。
If the class containing the improved version of the isRomanNumeral method is initialized but the method is never invoked, the field ROMAN will be initialized needlessly. It would be possible to eliminate the initialization by lazily initializing the field (Item 83) the first time the isRomanNumeral method is invoked, but this is not recommended. As is often the case with lazy initialization, it would complicate the implementation with no measurable performance improvement (Item 67).
如果初始化包含改进版本的 isRimanNumberal 方法的类时,但是从不调用该方法,则不需要初始化字段 ROMAN。在第一次调用 isRimanNumberal 方法时,可以通过延迟初始化字段(第 83 项)来消除使用时未初始化的影响,但不建议这样做。延迟初始化的做法通常都有一个情况,那就是它会把实现复杂化,从而导致无法测试它的性能改进情况。
When an object is immutable, it is obvious it can be reused safely, but there are other situations where it is far less obvious, even
counterintuitive. Consider the case of adapters [Gamma95], also known as views. An adapter is an object that delegates to a backing object, providing an alternative interface. Because an adapter has no state beyond that of its backing object, there’s no need to create more than one instance of a given adapter to a given object.
当一个对象是不可变的,那么就可以安全地重复使用它,但是在其他情况下,它并不是那么明显,甚至违反直觉。这时候可以考虑使用适配器 [Gamma95],也称为视图。适配器是委托给支持对象的对象(An adapter is an object that delegates to a backing object),它提供一个备用接口。因为适配器的状态不超过其支持对象的状态,所以不需要为给定对象创建一个给定适配器的多个实例。
For example, the keySet method of the Map interface returns a Set view of the Map object, consisting of all the keys in the map. Naively, it would seem that every call to keySet would have to create a new Set instance, but every call to keySet on a given Map object may return the same Set instance. Although the returned Set instance is typically mutable, all of the returned objects are functionally identical: when one of the returned objects changes, so do all the others, because they’re all backed by the same Map instance. While it is largely harmless to create multiple instances of the keySet view object, it is unnecessary and has no benefits.
例如,Map 接口的 keySet 方法返回 Map 对象的 Set 视图,该视图由 Map 中的所有键组成。看起来,似乎每次调用 keySet 都必须创建一个新的 Set 实例,但是对给定 Map 对象上的 keySet 的每次调用都可能返回相同的 Set 实例。尽管返回的 Set 实例通常是可变的,但所有返回的对象在功能上都是相同的:当其中一个返回的对象发生更改时,所有其他对象也会发生更改,因为它们都由相同的 Map 实例支持。虽然创建 keySet 视图对象的多个实例在很大程度上是无害的,但不必要这样做,并且这样做没有任何好处。
Another way to create unnecessary objects is autoboxing, which allows the programmer to mix primitive and boxed primitive types, boxing and unboxing automatically as needed. Autoboxing blurs but does not erase the distinction between primitive and boxed primitive types.There are subtle semantic distinctions and not-so-subtle performance differences (Item 61). Consider the following method, which calculates the sum of all the positive int values. To do this, the program has to use long arithmetic because an int is not big enough to hold the sum of all the positive int values:
创建不必要的对象的另一种方式是自动装箱,它允许程序猿将基本类型和装箱基本类型(Boxed Primitive Type)混用,按需自动装箱和拆箱。自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来,但是并没有完全消除。它们在语义上还有微妙的差别,在性能上也有着比较明显的差别(第 61 项)。请考虑以下方法,该方法计算所有正整数值的总和,为此,程序必须使用 long 类型,因为 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;
}
This program gets the right answer, but it is much slower than it should be, due to a one-character typographical error. The variable sum is declared as a Long instead of a long, which means that the program constructs about 2^31 unnecessary Long instances (roughly one for each time the long i is added to the Long sum). Changing the declaration of sum from Long to long reduces the runtime from 6.3 seconds to 0.59 seconds on my machine. The lesson is clear:prefer primitives to boxed primitives, and watch out for unintentional autoboxing.
这段程序算出的答案是正确的,但是比实际情况要更慢一些,只因为错打了一个字符。变量 sum 被声明成 Long 而不是 long,意味着程序构造了大约 2^31 个多余的 Long 实例(大约每次往 Long sum 中增加 long 时构造一个实例)。将 sum 的声明从 Long 改成 long,在我的机器上运行时间从 43 秒减少到了 6 秒。结论很明显:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
This item should not be misconstrued to imply that object creation is expensive and should be avoided. On the contrary, the creation and reclamation of small objects whose constructors do little explicit work is cheap, especially on modern JVM implementations. Creating additional objects to enhance the clarity, simplicity, or power of a program is generally a good thing.
不要错误地认为本项所介绍的内容暗示着“创建对象的代价非常昂贵,我们就应该尽可能地避免创建对象”。相反,由于小对象的构造器只做很少量的显示工作,所以,小对象的创建和回收动作是非常廉价的,特别是在现代的 JVM 实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。
Conversely, avoiding object creation by maintaining your own object pool is a bad idea unless the objects in the pool are extremely heavyweight. The classic example of an object that does justify an object pool is a database connection. The cost of establishing the connection is sufficiently high that it makes sense to reuse these objects. Generally speaking, however, maintaining your own object pools clutters your code, increases memory footprint, and harms performance. Modern JVM implementations have highly optimized garbage collectors that easily outperform such object pools on lightweight objects.
反之,通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的经典对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。但是,通常来说,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用,而且会影响性能。现代的 JVM 实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。
The counterpoint to this item is Item 50 on defensive copying. The present item says, “Don’t create a new object when you should reuse an existing one,” while Item 50 says, “Don’t reuse an existing object when you should create a new one.” Note that the penalty for reusing an object when defensive copying is called for is far greater than the penalty for needlessly creating a duplicate object. Failing to make defensive copies where required can lead to insidious bugs and security holes; creating objects unnecessarily merely affects style and performance.
与本项对应的是第 50 项的“保护性拷贝”的内容。该项说得是:你应该重用已经存在的对象,而不是去创建一个新的对象。然而第 50 项说的是:你应该创建一个新的对象而不是重用一个已经存在的对象。注意,在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,将会导致潜在的错误和安全漏洞,而不必要地创建对象则只会影响程序的风格和性能。