方法
1. 检查参数的有效性
空值检查
java 7 提供的 Objects.requireNonNull()
用于空值检查非常有用。
// 源码
@ForceInline
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
// 如果你愿意,甚至可以在使用一个值的同时进行空值检查
this.strategy = Objects.requeireNonNull(strategy,"strategy");
在某些情况下下,有效性检查工作非常昂贵,而且是不必要的
比如在 Collections.sort(List)
中所有对象都必须是可以相互比较的,这在排序过程中就会确认,因此, 提前检查列表中的元素是否可以相互比较,并没有多大意思。
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
2. 必要时进行保护性拷贝
Java 用起来如此舒适的一个因素在于,它是一门安全的语言(safe language)。 这意味着,它对于缓冲区溢出、数组越界、非法指针以及其他的内存破坏错误都自动免疫,而这些错误却困扰着诸如C和C++这样的不安全语言。
即使在安全的语言中,如果不采取一点措施,还是无法与其他的类隔离开来。 假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序。
案例: 表表示一段不可变的时间周期类
class Period {
private final Date start;
private final Date end;
public Period(Date start,Date end) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
...
}
乍看之下,这个类是不可变的,周期的起始时间不能在结束时间之后,然而 Date
本身是可变的
Date start = new Date()
Date end = new Date()
Period p = new Period(start,end)
end.setYear(78) // Date 本身可变 !
Date
已经过时了, Java 8 之后可以使用 Instant
(或 LocalDateTime
) 来替代 Date
,因为 Instant
是不可变的。
为了保护 Period
实例的内部信息避免受到这种攻击,对于构造器的每个可变参数进行保护性拷贝是有必要的
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime())
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}
注意:
- 保护性拷贝是在检查参数有效性之前进行的
- 我们没有使用
clone
来进行保护性拷贝,因为Date
是非final
的,有可能clone
的是它的子类实例
Period
还提供了访问内部成员的方法,因此这里也需要进行保护性拷贝
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
至此, Periond
是真正不可变的了
3. 谨慎设计方法签名
- 谨慎设计方法名称,保证良好的可读性
- 每个方法都应该尽其所能,方法太多会使得类难以学习和使用;只有当一项操作经常被使用的时候,才为它提供快捷方式
- 对于参数类型,要优先使用接口而不是类
- 对于 boolean 参数,要优先使用两个元素的枚举类型
- 避免过长的参数列表,最好不要超过 4 个参数
缩短参数列表的技巧
- 把一个方法分解为多个子方法,每个子方法只需要这些参数的一个子集
public class OrderProcessor {
// 主方法仅保留核心流程,参数由子方法自行处理
public void processOrder(User user, Order order, Payment payment, boolean notifyUser) {
validateOrder(user, order); // 分解验证步骤
double total = calculateTotal(order); // 分解金额计算
processPayment(order, payment, total); // 分解支付处理
saveOrder(user, order); // 分解数据保存
optionallyNotifyUser(user, notifyUser); // 分解通知逻辑
}
// 子方法1:只需验证用户和订单
private void validateOrder(User user, Order order) {}
// 子方法2:只需订单计算
private double calculateTotal(Order order) {}
// 子方法3:支付处理相关的参数
private void processPayment(Order order, Payment payment, double total) {}
// 子方法4:保存到数据库只需用户ID和订单
private void saveOrder(User user, Order order) {}
// 子方法5:通知逻辑只需用户邮箱和标志位
private void optionallyNotifyUser(User user, boolean notifyUser) {}
// 其他辅助方法...
}
- 创建辅助类(helper class),用来保存参数的分组。这些辅助类一般为静态成员类
public class OrderService {
@Getter
public static final class ProductRequest {
private final long productId;
private final int quantity;
private final boolean useDiscount;
public ProductRequest(long productId, int quantity, boolean useDiscount) {
this.productId = productId;
this.quantity = quantity;
this.useDiscount = useDiscount;
}
}
// 参数列表由长度为 3 变为 1
public void createOrder(ProductRequest productRequest) {}
}
- 从对象构建到方法调用都采用 Builder 模式
场景模拟: HTTP 客户端配置与请求执行
RequestConfig
配置类
@Getter
class RequestConfig {
private final String url;
private final int timeout;
private final Map<String,String> headers;
private RequestConfig(Builder builder) {
this.url = builder.url;
this.timeout = builder.timeout;
// 深拷贝 copyOf() 方法来自 jdk 10
this.headers = Map.copyOf(builder.headers);
}
public static class Builder {
private String url;
private int timeout = 30; //默认超时 30 秒
private final Map<String,String> headers = new HashMap<>();
public Builder url(String url) {
this.url = url;
return this;
}
public Builder timeout(int timeout) {
this.timeout = timeout;
return this;
}
public Builder addHeader(String key,String value) {
this.headers.put(key, value);
return this;
}
public Builder headers(Map<String,String> headers) {
if (headers != null) {
this.headers.putAll(headers);
}
return this;
}
public RequestConfig build() {
Objects.requireNonNull(url);
return new RequestConfig(this);
}
}
}
HttpClient
客户端
class HttpClient {
private RequestConfig config;
//私有构造函数 强制 Builder 构建
private HttpClient(RequestConfig config) {
this.config = config;
}
public static Builder newBuilder() {
return new Builder();
}
// 动态添加 Header
public HttpClient addHeader(String key,String value) {
this.config = new RequestConfig.Builder()
.url(config.getUrl())
.timeout(config.getTimeout())
.headers(config.getHeaders())
.addHeader(key, value)
.build();
return new HttpClient(config);
}
public void execute() {
System.out.println(config.getUrl());
System.out.println(config.getTimeout());
System.out.println(config.getHeaders());
}
public static class Builder {
private final RequestConfig.Builder configBuilder = new RequestConfig.Builder();
public Builder url(String url) {
configBuilder.url(url);
return this;
}
public Builder timeout(int timeout) {
configBuilder.timeout(timeout);
return this;
}
public Builder addHeader(String key,String value) {
configBuilder.addHeader(key, value);
return this;
}
public HttpClient build() {
return new HttpClient(configBuilder.build());
}
}
}
- 客户端链式调用
public static void main(String[] args) {
HttpClient.newBuilder()
.url("https://api.example.com")
.timeout(60)
.addHeader("Content-Type", "application/json")
.build()
// 动态添加配置,保留原配置
.addHeader("Authorization", "Bearer abc123")
.execute();
}
4. 谨慎使用重载
class CollectionClassifier {
public static String classify(Set<?> set) {
return "Set";
}
public static String classify(List<?> list) {
return "List";
}
public static String classify(Collection<?> collection) {
return "Unknown Collection";
}
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<Integer>(),
new HashMap<String,String>().values()
};
for (Collection<?> c : collections) {
System.out.println(CollectionClassifier.classify(c));
}
}
你可能期望该程序能打印出 Set
、List
和 Collection
,但实际输出为 3 次 Unknown Collection
。
因为要调用哪个重载方法是在编译时做出决定的,换言之重载方法的选择是静态的,重写方法才是动态选择的。
总结:
- 永远不要导出两个具有相同参数数目的重载方法
- 如果参数数目相同,可以给方法起不同名称,而不使用重载机制
5. 谨慎使用可变参数
在重视性能的场景下,要慎用可变参数,每次调用可变参数方法都会导致一次数组分配和初始化
经验总结: 假设确定对某个方法的绝大部分调用会有 3 个或者更少的参数时,就利用重载加可变参数来解决
public void foo() {}
public void foo(int a1) {}
public void foo(int a1,int a2) {}
public void foo(int a1,int a2,int a3) {}
public void foo(int a1,int a2,int a3,int... args) {}
6. 返回零长度的数组或集合,而不是 null
永远不要返回 null
, 而不返回一个零长度的数组或者集合。
如果返回 null
,那样会使API更难以使用,也更容易出错, 而且没有任何性能优势
7. 谨慎返回 optional
何时应该声明一个方法来返回 Optional<T>
而不是 T
呢
如果无法返回结果并且当没有返回结果时,客户端必须执行特殊的处理,那么就返回 Optional<T>
(比如 Stream
)
8. 位所有导出的 API 元素编写文档注释
为了正确地编写API 文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。