清晰性和简洁性最为重要 :
- 组件的用户永远也不应该被其行为所迷惑
- 组件要尽可能小,但又不能太小(组件”( Component),是指任何可重用的软件元素,从单个方法,到包含多个包的复杂框架, 都可以是一个组件)
- 代码应该被重用,而不是被拷贝
- 组件之间的依赖性应该尽可能地降到最小
- 错误应该尽早被检测出来,最好是在编译时就发现并解决
1. 用静态工厂方法代替构造器
2. 遇到多个构造器参数时要使用构建器
3. 使用私有构造器或者枚举类强化`Singleton`属性
4. 通过私有构造器强化不可实例化的能力
5. 优先考虑依赖注入引用资源
6. 避免创建不必要的对象
7. 消除过期引用
8. 避免使用终结方法和清除方法
9. `try-with-resources` 优先于 `try-finally`
1. 用静态工厂方法代替构造器
使用静态工厂方法代替构造器指的并不是涉及模式中的工厂模式,它仅仅是一个返回类的实例的静态方法,比如在Boolean
类中
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
相较于直接使用共有构造器来构建对象,他有如下优势
① 有名称
- 静态工厂方法的可以自定义名称而不像构造方法一样仅使用参数来区分,这样产生的客户端代码更容易阅读
- 当一个类需要多个带有相同签名的构造器时,只能通过提供多个参数列表上参数顺序不同的构造器来实现,通过参数列表记住这么多构造器对使用者并不友好,用静态工厂方法代替构造器并且仔细地选择名称以便突出静态工厂方法之 间的区别可以很好的解决这个问题
② 不必每次调用他们的时候都创建一个新的对象
- 这使得不可变类可以使用预先构建好的实例,或者将 构建好的实例缓存起来, 进行重复利用,从而避免创建不必要的重复对象
- 可以很好的实践享元模式,单例模式
③ 可以返回原返回类型的任何子类型的对象
- 这项技术适用于基于接口的框架,API可以返回对象同时又不会使对象的类变为公有对外暴露,典型的例子就是
Arrays.asList()
,返回的是Arrays
中的内部类
④ 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在
下面是静态工厂方法的一些惯用名称
//from 类型转换方法,它只有单个参数,返回该类型的一个相对应的实例,例如:
Date d= Date.from(instance) ;
//of 聚合方法,带有多个参数,返回该类型的一个实例,把它们合并起来,例如 :
Set<Rank> faceCards = EnumSet.of (JACK , QUEEN, KING);
//valueOf 比 from 和 of 更烦琐的一种替代方法,例如 :
Biginteger prime = Biginteger.valueOf(Integer.MAX_VALUE);
// instance 或者 getInstance 返回的实例是通过方法的(如有)参数来描述 的,但是不能说与参数具有同样的值,例如 :
StackWalker luke = StackWalker.getInstance(options);
//create 或者 newInstance 像instance 或者 getInstance一样,但 create 或者 newInstance 能够确保每次调用都返回一个新的实例 ,例如:
Object newArray = Array.newInstance(classObject, arraylen);
//getType 像 getInstance一样,但是在工厂方法处于不同的类中的时候使用,Type表示工厂方法所返回的对象类型,例如:
FileStore fs = Files.getFileStore(path);
// newType 像newinstance 一样,但是在工厂方法处于不同的类中的时候使用,Type 表示工厂方法所返回的对象类型,例如:
BufferedReader br= Files.newBufferedReader(path);
// type getType 和 newType 的简版,例如:
List<Complaint> litany = Collections.list(legacylitany);
2. 遇到多个构造器参数时要使用构建器
解决静态工厂和构造器不能扩展到大量的可选参数的局限
比如用一个类表示包装食品外面显示的营养成分标签,这些标签中有几个域是必需的:每份的含量、每罐的含量以及每份的卡路里;还有超过 20 个的可选域: 总脂肪量、饱和脂肪量、转化脂肪、胆固醇、纳,等等
为了构建这样的类保证参数的可选性,有以下三种模式
① 重叠构造器
在这种模式下,提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,依此类推,最后一个构造器包含所有可选的参数
当你想要创建实例的时候,就利用参数列表最短的构造器,但该列表中包含了要设置的所有参数
这种方式可行,但是当有许多参数的时候,客户端代码会很难缩写, 并且仍然较难以阅读
② JavaBeans
模式
先调用一个无参构造器来创建对象,然后再调用 setter
方法来设置每个必要的参数,以及每个相关的可选参数
这种模式弥补了重贴构造器模式的不足,代码具有很好的可读性,但是同时也有以下缺点:
- 因为构造过程被分到了几个调用中, 在构造过程中
JavaBean
可能处于不一致的状态,类无法仅仅通过检验构造器参数的有效性来保证一致性,试图使用处于不一致状态的对象将会导致失败 JavaBeans
模式使得把类做成不可变的可能性不复存在,这就需要额外的去保证它的线程安全
③ 建造者模式
建造者模式
它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静 态工厂),得到一个 builder
对象。 然后客户端在 builder
对象上调用类似于 setter
的方法,来 设置每个相关的可选参数。 最后 客户端调用无参的 build
方法来生成通常是不可变的对 象
//以开头的营养成分标签为例
public class NutritionFacts {
private final int servingSize;//required
private final int servings;
private final int calories;
private final int fat;
public static class Builder{
private final int servingSize;//required
private final int servings;//required
private int calories;
private int fat;
public Builder(int servingSize, int servings){
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int calories){
calories = calories;
return this;
}
public Builder fat(int fat){
fat = fat;
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;
}
}
//使用
NutritionFacts nutritionFacts = new Builder(240, 8)
.calories(100).fat(10).build();
BuiIder
模式模拟了具名的可选参数,如果类的构造器或者静态工厂中具有多个参数,设计这种类时, Builder
模式就是一种不错的选择, 特别是当大多数参数都是可选或者类型相同的时候,与使用 重叠构造器模式相比,使用 Builder
模式的客户端代码将更易于阅读和编写,构建器也比 JavaBeans
更加安全
3. 使用私有构造器或者枚举类强化Singleton
属性
4. 通过私有构造器强化不可实例化的能力
对于一些工具类只对外提供静态方法和静态域,实例化对他是毫无意义的,为了避免使用者无意地将他实例化应该将编译器自动提供的公有的无参的构造器设置为私有的,在Collectos
中的确是这么做的
public final class Collectors {
private Collectors() { }
5. 避免创建不必要的对象
一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个相同功能的新对象;
但是,在创建这种对象的时候,并非总是那么显而易见,假设想要编写一个方法,用它确定一个字符串是否为一个有效的罗马数字,使用一个正则表达式:
//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
方法最易于查看一个字符串是否与正则表达式相匹配, 但并不适合在注重性能的情形中重复使用,它在内部为正则表达式创建了一个 Pattern
实例,却只用了一次,之后就可以进行垃圾回收了;创建 Pattern
实例的成本很高 ,因为需要将正则表达式编译成一个有限状态机
为了提升性能,应该显式地将正则表达式编译成一个 Pattern
实例(不可变),让它成为类初始化的一部分,并将它缓存起来,每当调用 isRomanNumeral
方法的时候就重用同一个实例:
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();
}
6. try-with-resources
优先于 try-finally
根据经验, try-finally
语句是确保资源会被适时关闭的最佳方法
static String firstLineOfFile(String path) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new FileReader(path));
try{
return bufferedReader.readLine();
}finally {
bufferedReader.close();
}
}
但是他也存在一些问题:
- 在
try
块和finally
块中的代码,都会抛出异常,例如在firstLineOfFile
方法中,如果底层的物理设备异常,那么调用readLine
就会抛出异常,基于同样的原因,调用close
也会出现异常,在这种情况下,第二个异常完全抹除了第一个异常,在异常堆 枝轨迹中,完全没有关于第一个异常的记录,这在现实的系统中会导致调试变得非常复杂 - 如果有多个资源,那么代码会变得复杂
static void copy(String src, String dst) throws IOException{
FileInputStream in = new FileInputStream(src);
try{
FileOutputStream out = new FileOutputStream(src);
try {
byte[] buffer = new byte[1024];
int n;
while((n=in.read(buffer))>=0){
out.write(buffer, 0, n);
}
}finally {
out.close();
}
}finally {
in.close();
}
}
使用try-with-resources
可以解决这些问题, 要使用这个构造的资源,必须先实现 AutoCloseable
接口,其中包含了单个返 回 void
的 close
方法。 Java 类库与第三方类库中的许多类和接口,现在都实现或扩展了 AutoCloseable
接口
static void copy(String src, String dst) throws IOException{
try ( FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(src)){
byte[] buffer = new byte[1024];
int n;
while((n=in.read(buffer))>=0){
out.write(buffer, 0, n);
}
}
}