第 1 条:用静态工厂方法代替构造器
对于类而言,获取一个实例的方法,传统是提供一个共有的构造器。
类可以提供一个公有静态工厂方法(static factory method), 它只是一个返回类的实例的静态方法。
示例:Boolean的装箱类,将boolean基本类型值转换成一个Boolean对象引用
public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
静态工厂方法与构造器不同的优点:
第一大优势:有名称。更容易使用,更容易阅读理解。
第二大优势:不必每次调用它们的时候都创建一个新对象。
第三大优势:它们可以返回类型的任何子类型的对象。
例如Java Collections Framework的集合接口45个工具实现,提供了不可修改的集合、同步集合等等。几乎所有都是通过静态工厂方法在一个不可实例化的类(java.util.Collections)中导出。
第四大优势:所有返回对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。
第五大优势: 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。
静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础。例如JDBC(Java数据库连接) API.
静态工厂方法缺点:
第一: 类如果不含共有的或受保护的构造器,就不能被子类化。
第二:程序员很难发现他们。在API文档中没有明确标识出来。
静态工厂方法一些惯用名称:
from:
Date d = Date.from(instant);
of:
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf:
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance或getInstance:
StackWaler luke = StackWaler.getInstance(options);
create或newInstance:
Object newArray = Array.newInstance(classObject, arrayLen);
getType:
FileStore fs = Files.getFileStore(path);
newType:
BufferReader br = Files.newBufferReader(path);
type(getType和newType的简化版):
List<Complaint> litany = Collections.list(legacyLitany);
总结,静态工厂方法和共有构造器各有用处,需要理解各自长处。静态工厂方法经常更加合适,因此切忌第一反应是提供公有构造器,而不是考虑静态工厂方法。
第 2 条:遇到多个构造器参数时要考虑使用构建器
静态工厂和构造器有个共同局限性:都不能很好地扩张到大量的可选参数。
第一种方案(一般解决方法):重叠构造器(telescoping constructor)模式:
提供第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有二个可选参数,以此类推,最后一个构造器包含所有可选参数。
缺点:重叠构造器模式可行,但是有很多参数时,代码很难编写,难以阅读。
第二种方案:JavaBeans模式:
先调用一个无参数构造器来创建对象,然后再调用setter方法来设置每个必要参数和每个可选参数。
缺点:在构造过程中,JavaBeans可能处于不一致的状态。(线程不安全) 另外一点是这种模式就不能把类做成不可变的。
第三种方案:建造者(builder)模式:
例子:
public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { // 必须属性 private final int servingSize; private final int servings; // 可选属性 private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder setCalories(int calories) { this.calories = calories; return this; } public Builder setFat(int fat) { this.fat = fat; return this; } public Builder setSodium(int sodium) { this.sodium = sodium; return this; } public Builder setCarbohydrate(int carbohydrate) { this.carbohydrate = carbohydrate; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; }
使用代码:
NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();
Builder模式便于编写和阅读。
Builder模式也适用于类层次结构。例子:抽象类Pizza, 两个子类,一个NyPizza表示经典纽约风味披萨,一个Calzone表示馅料内置半月形披萨。(代码略)
使用代码:
NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addToping(ONION).build(); Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();
总结: 如果类的构造器或者静态工厂中具有多个参数,Builder模式是一种很好的选择,特别是当大多数参数是可选参数。
第 3 条:使用私有构造器或枚举类型来强制实现 singleton 属性
单例(singleton)就是一个只实例化一次的类。通常用于代表一个无状态的对象,比如函数,或者本质上是唯一的系统组件。
使类成为Singleton会使它的客户端测试变得十分困难。
两种常见实现方式:
1. Public final field: 公有静态成员是一个final域:
// Singleton with public final field public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public void leaveTheBuilding() { ... } }
该方法的优点:
- API 明确表示这个类就是一个单例。公共静态属性是 final 的,所以它总是包含相同的对象引用。
- 这个方法很简单。
2. Singleton with static factory 公有成员是一个静态工厂方法:
// Singleton with static factory public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() { ... } }
该方法的优点:
- 可以方便地将类的实现改为非单例,并且用户代码不需要改变。
- 可以编写一个泛型单例工厂。
- 方法引用可以被用作 supplier,例如 Elvis::instance 等同于 Supplier<Elvis>。
为了实现Singleton类变成可序列化的(Serializable), 需要2点:
1. 声明中加implements Serializable。
2. 必须声明所有实例域都是瞬时的(transient),并提供一个readResolve方法。
否则,每次反序列化一个序列化的实例时,都会创建一个新实例。
// readResolve method to preserve singleton property private Object readResolve() { // Return the one true Elvis and let the garbage collector // take care of the Elvis impersonator. return INSTANCE; }
第三种实现方法:声明一个包含单个元素的枚举类型:
// Enum singleton - the preferred approach public enum Elvis { INSTANCE; public void leaveTheBuilding() { ... } }
这种方法在功能上与公有域方法相似,更加简洁。无偿地提供序列化机制,绝对防止多次实例化。
总结:单元素的枚举类型经常成为实现Singleton的最佳方法。
第 4 条:通过私有构造器强化不可实例化的能力
有时候可能需要编写只包含静态方法和静态域的类。比如java.lang.Math或者java.util.Arrays。
还有java.util.Collections的方式,吧实现特定接口的对象上的静态方法,包括工厂方法组织起来。
企图通过将类做成抽象类来强制该类不可被实例化是行不通的。
该类可以被子类化,并且该子类也可以被实例化。
只有让这个类包含一个私有构造器,他才不能被实例化。
副作用:使得一个类不能被子类化(不能被继承)。所有的构造器都必须显式或隐式地调用超类(superclass)构造器,
而子类就没有可以访问的超类的构造器可调用了。
第 5 条:优先考虑依赖注入来引入资源
有许多类会依赖一个或者多个底层的资源。例如,拼写检查器需要依赖一个或多个词典。
静态工具类和Singleton类不适合于需要引用底层资源的类。
这里需要能够支持类的多个实例,每一个实例都使用客户端指定的资源(本例中的词典)。
满足该条件的模式是:当创建一个新的实例时,就将该资源传到构造器中。(依赖注入的一种形式)
// Dependency injection provides flexibility and testability public class SpellChecker { private final Lexicon dictionary; public SpellChecker(Lexicon dictionary) { this.dictionary = Objects.requireNonNull(dictionary); } public boolean isValid(String word) { ... } public List<String> suggestions(String typo) { ... } }
总之,不要用Singleton和静态工厂类来实现依赖一个或者多个底层资源的类,且该资源的行为会影响到该类的行为,也不要直接用这个类来创建这些资源。
而应该讲这些资源或者工厂传给构造器(或者静态工厂,构建器),通过它们来创建类。这就是依赖注入。
第 6 条:避免创建不必要的对象
通常来讲,重用一个对象比创建一个功能相同的对象更加合适。
重用速度更快,并且更接近现代的代码风格。如果对象是不可变的(immutable)(条款 17),它总是可以被重用。
第一种多余创建对象的场景:String字符串:
String s = new String("bikini"); // DON'T DO THIS!
这个语句每次执行时都会创建一个新的 String 实例,而这些实例的创建都是不必要的。
如果这种用法发生在循环或者频繁调用的方法中,就会创建数百万个毫无必要的 String 实例。
改进后:
String s = "bikini";
第二种多余创建对象的场景:正则表达式:
写一个方法来确定一个字符串是否是一个合法的罗马数字:
// 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.matches 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合在性能临界的情况下重复使用。
因为它在内部为正则表达式创建一个 Pattern 实例,并且只使用一次,之后这个 Pattern 实例就会被 JVM 进行垃圾回收。
创建 Pattern 实例是昂贵的,因为它需要将正则表达式编译成有限状态机(finite state machine)。
为了提高性能,将正则表达式显式编译为一个 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(); } }
第三种多余创建对象的场景:自动装箱(autoboxing)
它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。
// 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实例(每次往 Long 类型的 sum 变量中增加一个 long 类型的 i)。
把 sum 变量的类型由 Long 改为 long 会使性能得到很大提升。这个教训很明显:优先使用基本类型而不是包装的基本类型,也要注意无意识的自动装箱。
这个条目不应该被误解为暗示对象创建是昂贵的,应该避免创建对象。
相反,创建和回收小的对象非常廉价,构造器只会做很少的工作,尤其在现代 JVM 实现上。 创建额外的对象以增强程序的清晰性,简单性或功能性通常是件好事。
反之,通过维护自己的对象池来避免创建对象并不是一个好的做法,除非池中的对象是非常重量级的。正确使用对象池的典型对象示例是数据库连接池。
第 7 条:消除过期对象的引用
Java语言,当你用完对象之后,他们会被自动回收。它很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情了,其实不然。
// Can you spot the "memory leak"? public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) throw new EmptyStackException(); return elements[--size]; } /** * Ensure space for at least one more element, roughly * doubling the capacity each time the array needs to grow. */ private void ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } }
这个程序没有什么明显的错误,但有一个潜在的问题——“内存泄漏”。
由于垃圾回收器的活动的增加,或内存占用的增加,程序的性能会下降。极端情况下导致程序失败(OutOfMemoryError)错误。
程序中哪里发生了内存泄漏?
如果一个栈先增长,后收缩,那么从栈弹出的对象不会被当作垃圾回收掉,即使使用栈的程序不再引用这些对象。
这是因为栈内部维护了对这些对象的过期引用( obsolete references)。 过期引用简单来说就是永远不会再一次被解引用的引用。
在上面这段代码中,数组“活动部分(active portion)”之外的任何引用都是过期的。活动部分是由索引下标小于 size 的那些元素组成的。
这类问题的解决方法很简单:一旦对象引用过期,只需清空这些引用(将它们设置为 null)。
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; }
清空对象引用应该是一种例外,而不是一种规范。消除过期引用的最好方法是让包含引用的变量结束其生命周期。
常见的内存泄漏场景:
1. 只要类是自己管理内存,程序员就应该警惕内存泄漏问题。一旦元素被释放,则该元素中包含的任何对象引用都应该被清空。
2. 内存泄漏的另一个常见来源是缓存。一旦将对象引用放入缓存中,很容易忘记它的存在,并且在它变得无关紧要之后,仍然保留在缓存中。
常见解决方案之一,用WeakHashMap代表缓存,当缓存中的项过期之后,它们就会被自动删除。
常见解决方案之二,缓存应该时不时的清除掉没用的项。清除工作可以由后台线程(ScheduledThreadPoolExecutor)来完成。
3. 监听器和其他回调。如果你实现了一个API——其客户端注册回调(callbacks),但是没有显式地撤销他们的注册。
除非采取一些操作来处理,否则这些回调会积累。确保回调被垃圾回收的一种方法是只存储弱引用(weak references)。
例如,仅将它们保存在 WeakHashMap 的键(key)中。
第 8 条:避免使用终结方法(finalizer)和清除方法(cleaner)
终结方法通常是不可预测的、危险的、一般情况下不必要的。
使用终结方法会导致行为不稳定、性能降低、以及可移植性问题。
在Java9中,用清除方法代替了终结方法。清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢、一般情况下也是不必要的。
Finalizer 和 Cleaner 机制的缺点:
- 不能保证他们能够及时执行。从一个对象变得不可到达开始,到它的终结方法被执行,这段时间是任意长的。
注重时间(time-critical)的任务不应该由终结方法或清除方法来完成。
永远不应该依赖终结方法或清除方法来更新重要的持久状态。例如,释放共享资源(数据库)上的锁。
- 如果忽略在终结过程中被抛出来的未捕获异常,该对象的终结过程也会终止。
未被捕获的异常会使对象处于破坏状态。(corrupt state)
- 严重的性能损失。
- 严重的安全问题。从构造器抛出的异常,应该足以防止对象继续存在;有了终结方法的存在,这一点就做不到了。
为了防止非final类受到终结方法攻击,要编写一个空的final的finalize方法。
如果类的对象中封装的资源(例如文件或线程)确实需要终止,正确做法是:
让类实现AutoCloseable,并且要求客户端在每个实例不再需要时调用close方法,利用try-with-resources确保终止,即使遇到异常也是如此。
终结方法和清除方法的合法用途:
1. 作为一个”安全网”,以防资源的拥有者忘记调用 close 方法。
虽然不能保证 Finalizer 和 Cleaner 机制会及时运行(或者是否运行),但是将资源晚一点释放也要好过永远不释放。
2. 使用Cleaner机制的方法与本地对等类(native peers)有关。
本地对等类是一个由普通对象委托的本地(非 Java)对象。
由于本地对等类不是普通的 Java 对象,所以垃圾收集器并不知道它,当它的 Java 对等对象被回收时,本地对等类也不会被回收。
假设性能是可以接受的,并且本地对等类没有持有关键的资源,那么 Finalizer 和 Cleaner 机制可能是这项任务的合适的工具。
总之,除非作为安全网或者为了终止非关键的本地资源,不要使用清除方法。对于Java9之前的版本,尽量不要使用终结方法。
第 9 条:try-with-resources优先于try-finally
Java 类库中包含许多必须手动调用 close 方法来关闭的资源, 比如InputStream、OutputStream 和 java.sql.Connection。
从以往来看,try-finally 语句是保证资源正确关闭的最佳方式,即使是在程序抛出异常或返回的情况下:
// try-finally - No longer the best way to close resources! static String firstLineOfFile(String path) throws IOException { BufferedReader br = new BufferedReader(new FileReader(path)); try { return br.readLine(); } finally { br.close(); } }
上述例子看起来还行,但是如果添加第二个资源,就会很糟:
// try-finally is ugly when used with more than one resource! static void copy(String src, String dst) throws IOException { InputStream in = new FileInputStream(src); try { OutputStream out = new FileOutputStream(dst); try { byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n = in.read(buf)) >= 0) out.write(buf, 0, n); } finally { out.close(); } } finally { in.close(); } }
例如,在 firstLineOfFile 方法中,由于底层物理设备发生故障,对 readLine 方法的调用可能会引发异常,并且由于相同的原因,调用 close 方法可能也会失败。 在这种情况下,第二个异常完全冲掉了第一个异常。 在异常堆栈跟踪中没有第一个异常的记录,这可能使实际系统中的调试变得非常复杂——通常你想要看到第一个异常来诊断问题。 虽然可以编写代码来抑制第二个异常,保留第一个异常,但是实际上没有人这样做,因为实现起来太繁琐了。
Java 7 引入了 try-with-resources 语句时,所有这些问题都得到了解决。要使用这个构造,资源必须实现 AutoCloseable 接口,该接口由一个返回类型为 void 的 close 方法组成。Java 类库和第三方类库中的许多类和接口现在都实现或继承了 AutoCloseable 接口。如果你编写的类表示必须关闭的资源,那么这个类也应该实现 AutoCloseable 接口。
实例1:
// try-with-resources - the the best way to close resources! static String firstLineOfFile(String path) throws IOException { try (BufferedReader br = new BufferedReader( new FileReader(path))) { return br.readLine(); }
实例2:
// try-with-resources on multiple resources - short and sweet static void copy(String src, String dst) throws IOException { try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst)) { byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n = in.read(buf)) >= 0) out.write(buf, 0, n); } }
使用try-with-resources更简洁,更容易进行诊断。以firstLineOffFile为例,如果调用readLine和close方法都抛出异常,后一个异常会被禁止,以保留第一个异常。
实例3:
// try-with-resources with a catch clause static String firstLineOfFile(String path, String defaultVal) { try (BufferedReader br = new BufferedReader( new FileReader(path))) { return br.readLine(); } catch (IOException e) { return defaultVal; } }
还可以在 try-with-resources 语句中添加 catch 子句,就像在常规的 try-finally 语句中一样。上面例子它不会抛出异常,但是如果它不能打开或读取文件,则返回默认值。
总之,在处理必须关闭的资源时,优先考虑用try-with-resources,而不是try-finally。