Effective Java 一书不仅仅是对于 Java,即使对于整个编程学习来讲,也深有启发。
因此仅以此博客,每日记录一则 Effective Java 规则。
4.24
一、用静态工厂方法代替构造器
BigInteger.valueOf(long)
源码
public static BigInteger valueOf(long value) {
if (value < 0) {
if (value != -1) {
return new BigInteger(-1, -value);
}
return MINUS_ONE;
} else if (value < SMALL_VALUES.length) {
return SMALL_VALUES[(int) value];
} else {// (value > 10)
return new BigInteger(1, value);
}
}
即通过静态方法返回类的实例。
优势
1.有名称。
用户往往无法分辨多个构造器的区别,从而调用错误的构造器。
因此当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器。
注意 这不是工厂方法模式
2.不必每次创建新对象。
类似享元模式
3.可以返回子类型对象。
用户不需要知道子类有哪些,只需要知道被返回的对象是符合要求的即可。这对 API 来说是一种变相减少。
4.可以随参数而变化。
用户不需要也不关心返回的对象,它们只需要知道是某个子类即可。
5.方法返回对象所属的类,在编写包含该静态工厂方法的类时,可以不存在。
这是服务提供者框架的基础。
参考反射
缺点
1.类如果没有 public 或 protected 的构造器,就不能被子类化。
留坑...后续接规则 18
2.程序员很难注意到它们。
直观来讲,使用者更倾向于优先使用构造器,因为构造函数会在 API 文档明确标注,而静态工厂方法不会。
因此,Effective Java 规定一些命名规则,以更快速的发现一个类的静态工厂方法。
命名规则
命名:from
含义:类型转换方法,只有单个参数。
示例:Date d = Date.from(instant);
命名:of
含义:聚合方法,将多个参数合并为一个实例。
示例:Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
命名:valueOf
含义:类型转换方法,只有单个参数。
示例:String oneString = String.valueOf(1);
命名:instance 或 getInstance
含义:返回的实例是通过参数来描述的。
示例:
命名:create 或 newInstance
含义:和上一条的区别是,每次调用都返回一个新实例。
示例:Object newArray = Array.newinstance(classObject, arrayLen);
命名:get<Type>
含义:同 getInstance,但用于工厂方法。
示例:FileStore fs = Files.getFileStore(path);
命名:new<Type>
含义:同 newInstance,但用于工厂方法。
示例:Files.newBufferedReader(buffer);
使用场景
多数情况下,静态工厂方法经常更加合适,因此在 提供公有构造函数之前,优先考虑静态工厂方法。
二、多个构造器参数时优先使用 Builder 模式
背景
静态工厂方法和构造函数有共同的缺点,即不能很好的处理大量可选参数的情况。
举个例子,一本书有很多参数,包括书名、版号、作者、价格等等。
public class Book {
private String name;
private String author;
private int price;
private Date date;
private int version;
}
这些参数有的是必须的,有的是可选的,一般情况下,程序员会选择 重叠构造器模式。第一个构造器拥有必须参数,第二个构造器拥有必须参数外的一个可选参数,后续依次类推。
结果如下:
public class Book {
private String name;
private String author;
private int price;
private Date date;
private int version;
public Book(String name, String author, int price) {
this.name = name;
this.author = author;
this.price = price;
}
public Book(String name, String author, int price, Date date) {
this.name = name;
this.author = author;
this.price = price;
this.date = date;
}
public Book(String name, String author, int price, Date date, int version) {
this.name = name;
this.author = author;
this.price = price;
this.date = date;
this.version = version;
}
}
重叠模式可行,但在参数大量增加的情况下,将很难编写而且难以阅读。而且即使开发时参数颠倒,如果参数类型相同,在编译期也不会出错。
因此有第二种方式,JavaBeans 模式。
public class Book {
private String name; //必须
private String author; //必须
private int price; //必须
private Date date;
private int version = 1; // 默认值
public void setName(String name) {this.name = name;}
public void setAuthor(String author) {this.author = author;}
public void setPrice(int price) {this.price = price;}
public void setDate(Date date) {this.date = date;}
public void setVersion(int version) {this.version = version;}
}
JavaBeans 模式有严重的缺点,它将构造过程分到了几个调用中,导致构造结果需要手动控制,并且多线程情况下存在安全问题。
解决
也就说,我们既需要 JavaBeans 模式的 可读性,同时需要重叠构造器模式的 安全性。
这就需要 建造者模式。
public class Book {
private final String name; //必须
private final String author; //必须
private final int price; //必须
private final Date date;
private final int version;
private Book(Builder builder) {
name = builder.name;
author = builder.author;
price = builder.price;
date = builder.date;
version = builder.version;
}
public static class Builder {
//必须
private final String name; //必须
private final String author; //必须
private final int price; //必须
//可选
private Date date;
private int version = 1; // 默认值
public Builder(String name, String author, int price) {
this.name = name;
this.author = author;
this.price = price;
}
public Builder date(Date date) {
this.date = date;
return this;
}
public Builder version(int version) {
this.version = version;
return this;
}
public Book build() {
return new Book(this);
}
}
}
调用时,得到流式 API:
Book book = new Book.Builder("书籍", "作者", 100).date(new Date()).version(1).build();
Builder 中,还可以对参数进行有效性检查,及时抛出异常。通过 Builder 模式,既保证了类构造时的安全性(构造过程一体化,多线程安全),同时保证了可读性。
此外,Builder 还适用于类层次结构。即抽象类中有抽象的 Builder,具体类中有具体的 Builder,通过使用泛型,可以在具体类中实现具体 Builder 的同时,无需转换类型(协变返回类型)。
最后,使用 Builder 模式,还可以在传入集合参数时,以 add、put 的形式逐个传入集合元素。
由于这俩点不是使用 Builder 模式的主要原因,所以不再详细阐述,有兴趣可以查阅 Effective Java 一书。
使用场景
如果类的构造器或静态工厂方法中具有多个参数,设计这种类时,Builder 模式就是不错的选择。
4.26
三、通过私有构造器强化不可实例化的能力
有些 工具类 是不希望被实例化的,因为实例化对它没有任何意义。然而编译器会自动提供一个公有的、无参的缺省构造器。
为此,让类包含一个私有构造器,它就不能被实例化:
public class UtilClass {
private UtilClass() {
throw new AssertionError();
}
}
AssertionError()
不是必须的,但是它可以避免不小心在类的内部调用构造器。
私有构造器用法的缺点是,它使得一个类不能再被子类化(因为所有的构造器必须显式或隐式的调用超类构造器)。
四、优先考虑依赖注入引入资源
背景
举个例子:
查词需要依赖词典,因此通常会通过静态工具类或单例来实现查词类。
静态工具类
public class DictChecker {
private static final Dict DICT = ...;
private DictChecker() {
...
}
// 词典里是否包含该词
public static boolean contains(String word) {
...
}
// 词的翻译
public static String translation(String word) {
...
}
}
单例类
public class DictChecker {
private static final Dict DICT = ...;
private DictChecker() {
...
}
private static DictChecker INSTANCE = new DictChecker();
// 词典里是否包含该词
public boolean contains(String word) {
...
}
// 词的翻译
public String translation(String word) {
...
}
}
以上俩种实现方式的问题在于,它们都假设只有一本词典可用。
如果添加一个方法来修改词典,则这样的设置会显得很笨拙、容易出错、并且无法并行工作。
因此,静态工具类和 Singleton 类不适合需要引用底层资源的类。
使用依赖注入解决此问题:
public class DictChecker {
private final Dict dict;
private DictChecker(Dict dict) {
this.dict = Objects.requireNonNull(dict);
}
// 词典里是否包含该词
public boolean contains(String word) {
...
}
// 词的翻译
public String translation(String word) {
...
}
}
使用依赖注入极大的提升了灵活性和可测试性,并且注入的对象具有不可变性,因此多个客户端可以共享依赖对象。
依赖注入也有缺点,即会导致项目凌乱不堪,但是可以通过依赖注入框架简单解决,如 Dagger。
使用场景
不要用 Singleton 或静态工具类来实现依赖一个或多个底层资源的类,且该资源会影响到类的行为。
应该将资源或者工厂传给构造器,通过它们来创建类。
[TOC]
网友评论