Skip to content

方法

1. 检查参数的有效性

空值检查

java 7 提供的 Objects.requireNonNull() 用于空值检查非常有用。

java
// 源码
@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) 中所有对象都必须是可以相互比较的,这在排序过程中就会确认,因此, 提前检查列表中的元素是否可以相互比较,并没有多大意思。

java
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++这样的不安全语言。

即使在安全的语言中,如果不采取一点措施,还是无法与其他的类隔离开来。 假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序。

案例: 表表示一段不可变的时间周期类

java
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 本身是可变的

java
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 实例的内部信息避免受到这种攻击,对于构造器的每个可变参数进行保护性拷贝是有必要的

java
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 还提供了访问内部成员的方法,因此这里也需要进行保护性拷贝

java
public Date start() {
    return new Date(start.getTime());
}

public Date end() {
    return new Date(end.getTime());
}

至此, Periond 是真正不可变的了

3. 谨慎设计方法签名

  • 谨慎设计方法名称,保证良好的可读性
  • 每个方法都应该尽其所能,方法太多会使得类难以学习和使用;只有当一项操作经常被使用的时候,才为它提供快捷方式
  • 对于参数类型,要优先使用接口而不是类
  • 对于 boolean 参数,要优先使用两个元素的枚举类型
  • 避免过长的参数列表,最好不要超过 4 个参数

缩短参数列表的技巧

  1. 把一个方法分解为多个子方法,每个子方法只需要这些参数的一个子集
java
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) {}

    // 其他辅助方法...
}
  1. 创建辅助类(helper class),用来保存参数的分组。这些辅助类一般为静态成员类
java
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) {}
    
}
  1. 从对象构建到方法调用都采用 Builder 模式

场景模拟: HTTP 客户端配置与请求执行

  • RequestConfig 配置类
java
@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 客户端
java
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());
        }
    }
    
}
  • 客户端链式调用
java
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. 谨慎使用重载

java
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));
    }
}

你可能期望该程序能打印出 SetListCollection,但实际输出为 3 次 Unknown Collection

因为要调用哪个重载方法是在编译时做出决定的,换言之重载方法的选择是静态的,重写方法才是动态选择的

总结:

  • 永远不要导出两个具有相同参数数目的重载方法
  • 如果参数数目相同,可以给方法起不同名称,而不使用重载机制

5. 谨慎使用可变参数

在重视性能的场景下,要慎用可变参数,每次调用可变参数方法都会导致一次数组分配和初始化

经验总结: 假设确定对某个方法的绝大部分调用会有 3 个或者更少的参数时,就利用重载加可变参数来解决

java
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 文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。