第6章:方法(Methods)
一、导读
1.1 本章学习目标
第6章聚焦于方法设计的最佳实践。方法是类的行为单元,方法的设计质量直接影响代码的可读性、正确性和可维护性。本章通过9条经验法则,帮助你掌握方法设计的精髓。
本章的核心学习目标包括:
- 掌握参数校验的方法:理解为什么以及如何校验参数。
- 学会防御性拷贝:保护可变对象的内部状态。
- 理解方法重载的规则:避免重载的歧义。
- 掌握 Optional 的使用:理解何时返回 Optional,何时不返回。
- 学会编写方法注释:掌握 Javadoc 的最佳实践。
1.2 为什么第六章如此重要
方法是类的行为单元,也是 API 的基本组成。好的方法设计使得代码易于理解、易于使用、不易出错。差的方法设计则会导致 bug、性能问题和维护困难。
1.3 核心问题引导
为什么公共方法的参数必须校验?私有方法呢?
什么是防御性拷贝?为什么需要它?
方法重载的解析规则是什么?如何避免歧义?
Optional 应该用在什么场景?不应该用在什么场景?
如何编写好的方法注释?
二、核心概念详解
6.1 检查参数的有效性(Item 49)
6.1.1 为什么需要参数校验
// 反模式:不校验参数
public BigInteger multiply(BigInteger val) {
// 如果 val 为 null,会在方法内部抛出 NullPointerException
// 但错误信息不明确,难以调试
return this.val.multiply(val);
}
// 正确做法:校验参数
public BigInteger multiply(BigInteger val) {
Objects.requireNonNull(val, "val must not be null");
return this.val.multiply(val);
}6.1.2 校验的时机
- 公共方法:必须在方法开始时校验参数,使用
Objects.requireNonNull、IllegalArgumentException等。 - 私有方法:可以使用断言(assert)校验,因为可以确保调用者传递了有效参数。
// 使用 assert 校验私有方法参数
private void sort(int[] a, int offset, int length) {
assert a != null : "array must not be null";
assert offset >= 0 : "offset must be >= 0";
assert length >= 0 : "length must be >= 0";
assert offset + length <= a.length;
}6.2 必要时进行防御性拷贝(Item 50)
6.2.1 保护内部可变状态
// 反模式:暴露内部可变对象
public final 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 > end");
}
this.start = start;
this.end = end;
}
public Date start() { return start; }
public Date end() { return end; }
}
// 攻击方式
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // 修改了 p 的内部状态!// 正确做法:防御性拷贝
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException("start > end");
}
}
public Date start() { return new Date(start.getTime()); }
public Date end() { return new Date(end.getTime()); }
}6.2.2 使用不可变对象替代
// 更好的做法:使用不可变的 Instant
public final class Period {
private final Instant start;
private final Instant end;
public Period(Instant start, Instant end) {
if (start.isAfter(end)) {
throw new IllegalArgumentException("start > end");
}
this.start = start;
this.end = end;
}
public Instant start() { return start; }
public Instant end() { return end; }
}6.3 谨慎使用重载(Item 52)
6.3.1 重载的歧义
// 反模式:容易混淆的重载
public class CollectionClassifier {
public static String classify(Set<?> s) { return "Set"; }
public static String classify(List<?> lst) { return "List"; }
public static String classify(Collection<?> c) { return "Unknown Collection"; }
}
// 调用
System.out.println(classify(new HashSet<>())); // Set
System.out.println(classify(new ArrayList<>())); // List
System.out.println(classify((Collection<?>) new HashSet<>())); // Unknown Collection
// 问题:选择哪个重载是在编译时决定的,不是运行时6.3.2 避免重载的歧义
// 正确做法:使用不同的方法名
public static String classifySet(Set<?> s) { return "Set"; }
public static String classifyList(List<?> lst) { return "List"; }
public static String classifyCollection(Collection<?> c) { return "Unknown Collection"; }6.4 返回 Optional 而不是 null(Item 55)
6.4.1 Optional 的优势
// 反模式:返回 null
public String findName(String id) {
User user = database.findUser(id);
return user != null ? user.getName() : null;
}
// 正确做法:返回 Optional
public Optional<String> findName(String id) {
User user = database.findUser(id);
return user != null ? Optional.of(user.getName()) : Optional.empty();
}
// 使用
Optional<String> name = findName("123");
String displayName = name.orElse("Unknown");
name.ifPresent(n -> System.out.println("Found: " + n));6.4.2 Optional 的使用场景
应该使用 Optional 的场景:
- 方法可能没有返回值
- 调用者需要明确处理"无值"的情况
不应该使用 Optional 的场景:
- 返回集合或数组(返回空集合或空数组)
- 返回基本类型(使用
OptionalInt、OptionalLong、OptionalDouble) - 性能敏感的场景(Optional 有对象创建开销)
// 反模式:返回 Optional 的集合
public Optional<List<String>> findNames() { ... }
// 正确做法:返回空集合
public List<String> findNames() {
List<String> result = database.query();
return result != null ? result : Collections.emptyList();
}6.5 为所有序列化后的版本编写 readObject 方法(Item 58 相关)
虽然这主要属于序列化章节,但方法设计中也需要注意:
// 安全的 readObject
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
// 防御性拷贝
start = new Date(start.getTime());
end = new Date(end.getTime());
// 校验不变量
if (start.compareTo(end) > 0) {
throw new InvalidObjectException("start > end");
}
}三、实战案例
6.1 防御性拷贝实战:不可变配置类
public final class AppConfig {
private final String name;
private final Map<String, String> properties;
private final List<String> tags;
public AppConfig(String name, Map<String, String> properties, List<String> tags) {
this.name = Objects.requireNonNull(name, "name");
// 防御性拷贝
this.properties = Collections.unmodifiableMap(new LinkedHashMap<>(
Objects.requireNonNull(properties, "properties")));
this.tags = Collections.unmodifiableList(new ArrayList<>(
Objects.requireNonNull(tags, "tags")));
}
public String getName() { return name; }
public Map<String, String> getProperties() { return properties; }
public List<String> getTags() { return tags; }
public Optional<String> getProperty(String key) {
return Optional.ofNullable(properties.get(key));
}
// Builder 模式
public static class Builder {
private String name;
private Map<String, String> properties = new LinkedHashMap<>();
private List<String> tags = new ArrayList<>();
public Builder name(String name) {
this.name = name;
return this;
}
public Builder property(String key, String value) {
properties.put(key, value);
return this;
}
public Builder tag(String tag) {
tags.add(tag);
return this;
}
public AppConfig build() {
return new AppConfig(name, properties, tags);
}
}
}
// 使用
AppConfig config = new AppConfig.Builder()
.name("MyApp")
.property("db.url", "jdbc:mysql://localhost/mydb")
.property("db.user", "admin")
.tag("production")
.tag("v2")
.build();
// 不可变
// config.getProperties().put("new", "value"); // UnsupportedOperationException6.2 Optional 实战:链式处理
public class UserService {
private final UserRepository userRepository;
private final CacheService cacheService;
public Optional<User> findUser(String id) {
return Optional.ofNullable(id)
.flatMap(cacheService::getCachedUser)
.or(() -> userRepository.findById(id))
.map(user -> {
cacheService.cacheUser(user);
return user;
});
}
public String getDisplayName(String userId) {
return findUser(userId)
.map(User::getDisplayName)
.orElse("Unknown User");
}
public void sendWelcomeEmail(String userId) {
findUser(userId)
.filter(User::isEmailVerified)
.ifPresent(user -> {
EmailService.send(user.getEmail(), "Welcome!", buildWelcomeBody(user));
});
}
public List<User> findActiveUsers(List<String> userIds) {
return userIds.stream()
.map(this::findUser)
.flatMap(Optional::stream)
.filter(User::isActive)
.collect(Collectors.toList());
}
}四、本章小结
第6章系统讲解了方法设计的核心知识:
参数校验:公共方法必须校验参数,使用 Objects.requireNonNull 和异常。
防御性拷贝:保护内部可变状态,优先使用不可变对象。
谨慎使用重载:避免歧义,优先使用不同的方法名。
Optional:用于可能没有返回值的方法,替代 null。
方法注释:使用 Javadoc 记录前置条件、后置条件和副作用。
好的方法设计是高质量代码的基础。记住:方法的设计应该让调用者感到自然和安全。
五、思考与练习
思考题
为什么私有方法可以使用断言而不是显式校验?
防御性拷贝的性能开销如何?如何权衡安全性和性能?
重载解析的优先级规则是什么?
Optional 和 null 相比有什么优势和劣势?
如何编写好的 Javadoc 注释?
编程练习
实现一个安全的 DateRange 类:使用防御性拷贝保护内部状态。
实现一个 UserRepository:使用 Optional 处理可能不存在的用户。
实现一个 ConfigLoader:使用 Builder 模式加载配置,支持参数校验。