第二章:创建和销毁对象
第1条:用静态工厂方法代替构造器
-
静态工厂方法相比构造器的优势:前四点比较容易理解,第5点:静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。后文又提到服务提供者框架。服务提供者框架类图如图所示。
典型的服务提供者框架应用JDBC。
-
静态工厂方法的缺点:
-
类如果不含公有的或者受保护的构造器,就不能被子类实例化。这一点可以通过复合来解决。
-
程序员很难发现他们。因此,需要遵守标准的命名习惯。
-
常见命名:
-
from 类型转换方法
-
of 聚合方法
-
valueof
-
instance/getInstance
-
create/newInstance
-
getTYPE: 例如:Files.getFileStore(path);
-
newTYPE: 例如:Files.newBufferedReader(path);
-
TYPE: 例如 Collections.list(..);
第2条:遇到多个构造器参数时要考虑使用构建器
-
类多个属性时,构造对象有三种选择:
-
重叠构造器:客户端代码很难编写
-
JavaBeans模式:构造过程被分到了几个调用中,在构造过程中,JavaBean可能处于不一致的状态。JavaBeans模式使得把类做成不可变性的可能性不复存在。
-
建造者(Builder)模式:如果类的构造器或者静态工厂中具有多个参数,设计这种类,Builder模式就是一种不错的选择。
/**建造者模式创建对象样例*/
NutritionFacts fact = new NutritionFacts.Builder(140, 40).calories(100).sodium(35).carbohydrate(27).build();
//---------------------------------
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 calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
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;
}
}
第3条:用私有构造器或者枚举类型强化Singleton属性
-
单元素的枚举类型经常成为实现Singleton的最佳方法。
-
规避反射攻击
-
规避序列化问题
-
博客 为什么要用枚举实现单例模式(避免反射、序列化问题)
public enum SingletonEnum {
INSTANCE;
private String content;
private SingletonEnum() {}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public SingletonEnum getInstance() {
return INSTANCE;
}
}
第4条:通过私有构造器强化不可实例化的能力
-
让这个类包含一个私有构造器,它就不能被实例化
public class UtilityClass {
//suppress default constructor for noninstantiability
//抑制默认构造函数的非实例化
private UtilityClass() {
throw new AssertionError();
}
}
第5条:优先考虑依赖注入来引用资源
-
静态工具类和Singleton类不适合与需要应用底层资源的类。
-
笨拙,易出错,无法并行工作
-
需要能够支持类的多个实例,每一个实例都使用客户端指定的资源。当创建一个新的实例时,就将该资源传到构造器中。依赖注入的对象资源具有不可变性。
-
总而言之,不要用Singleton和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为;也不要直接用这个类来创建这些资源。这个实践就被称作依赖注入,它极大地提升了类的灵活性、可重用性和可测试性。
/**
* 拼写检查器
* @author qiweiwei
* @date 2019年7月1日-下午4:47:02
* @describle
*
*/
public class SpellChecker {
private final String dictionary;
public SpellChecker(String dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isVlid(String word) {
//TODO
return false;
}
public List<String> suggestions(String typo) {
//TODO
return null;
}
}
第6条:避免创建不必要的对象
-
虽然String.matches方法最易于查看一个字符串是否与正则表达式相匹配,但并不适合在注重性能的情形中重复使用。问题在于,它在内部为正则表达式创建了一个Pattern实例,却只用了一次,之后就可以进行垃圾回收了。创建Pattern实例的成本很高,因为需要将正则表达式编译成一个有限状态机。
public static Map<String, Pattern> patterns = new HashMap<String, Pattern>();
public static Pattern getPattern(String regex) {
if (patterns.containsKey(regex)) {
return patterns.get(regex);
} else {
Pattern pattern = Pattern.compile(regex);
patterns.put(regex, pattern);
return pattern;
}
}
-
自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来,但是并没有完全消除。他们在语义上有着微妙的差别,在性能上有着比较明显的差别。
-
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
-
不要错误的认为“创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象”,相反,由于小对象的构造器只做很少量的显示工作,所以小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。
-
反之,通过维护自己的对象池来避免创建对象并不是一种好的做法,除非池中的对象是非常重要级的。正确使用对象池的典型对象示例就是数据库连接池。
第7条 消除过期对象的引用
-
在支持垃圾回收的语言中,内存泄露是很隐蔽的。如果一个对象引用被无意识地保留下来了,那么垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。
-
清空过期引用的另一个好处是,如果它们以后又被错误地接触引用,程序就会立即抛出NullPointerException异常,而不是悄悄地错误运行下去。
-
清空对象引用应该是一种例外,而不是一种规范行为。
-
消除过期引用最好的方法是让包含该引用的变量结束其生命周期。如果你是在紧凑的作用域范围内定义每一个变量,这种情形就会自然而然地发生。
-
一般来说,只要类是自己管理内存,程序员就应该警惕内存泄露问题。(栈Stack类是自己管理内存)
-
内存泄露的另一个常见来源是缓存。WeakHashMap:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。“缓存项的声明周期是否有意义”并不是很容易确定。随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时地清楚掉没用的项。
-
内存泄露的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取这些动作,否则它们就会不断地堆积起来。确保回调立即被垃圾回收的最佳方法是只保存它们的弱引用,例如,只将它们保存成WeakHashMap中的键。
-
内存泄露问题通常不会表现成明显的失败,往往只有通过仔细检查代码,或者借助于Heap剖析工具(Heap Profiler)才能发现内存泄露问题。
第8条 避免使用终结方法和清除方法
-
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定、性能降低,以及可移植性问题。
-
在Java9中,用清除方法代替了终结方法。清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的。
-
终结方法和清除方法的缺点在于不能保证会被及时执行。注重时间的任务不应该由终结方法或者清楚方法来完成。
-
永远不应该依赖终结方法或者清除方法来更新重要的持久状态。
-
使用终结方法的另一个问题是:如果忽略在终结过程中被抛出来的未被捕获的异常,该对象的终结过程也会终止。
-
使用终结方法和清除方法有一个非常严重的性能损失。
-
终结方法有一个严重的安全问题:它们为终结方法共计(finalizer attack)打开了类的大门。终结方法攻击背后的思想很简单:如果从构造器或者它的序列化对等体抛出异常,恶意子类的终结方法就可以在构造了一部分的应该已经半途夭折的对象上运行。从构造器抛出的异常,应该足以放置对象继续存在;有了终结方法的存在,这一点就做不到了。为了防止非final类受到终结方法攻击,要编写一个空的final的finalize方法。
-
如果类的对象中封装的资源确实需要终止,只需让类实现AutoCloseable,并要求其客户端在每个实例不再需要的时候调用close方法。
-
总而言之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用清除方法,对于在Java9之前的发行版本,则尽量不要使用终结方法。
第9条 try-with-resources优先于try-finally
-
try-finally语句存在些许不足,因为在try和finally块中的代码,都会抛出异常。
-
Java7引入try-with-resource。要使用这个构造的资源,必须先实现AutoCloseable接口,其中包含了单个返回void的close方法。
try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("D://singletonEnum.obj"))) {
outputStream.writeObject(s);
outputStream.flush();
}
try (ObjectInputStream inputStream2 = new ObjectInputStream(new FileInputStream("D://singletonEnum.obj"))){
SingletonEnum s1 = (SingletonEnum)inputStream2.readObject();
}