6Effective Java(第3版)

方法

方法设计准则

参数校验防御性拷贝方法重载Optional

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第6章:方法(Methods)

一、导读

1.1 本章学习目标

第6章聚焦于方法设计的最佳实践。方法是类的行为单元,方法的设计质量直接影响代码的可读性、正确性和可维护性。本章通过9条经验法则,帮助你掌握方法设计的精髓。

本章的核心学习目标包括:

  • 掌握参数校验的方法:理解为什么以及如何校验参数。
  • 学会防御性拷贝:保护可变对象的内部状态。
  • 理解方法重载的规则:避免重载的歧义。
  • 掌握 Optional 的使用:理解何时返回 Optional,何时不返回。
  • 学会编写方法注释:掌握 Javadoc 的最佳实践。

1.2 为什么第六章如此重要

方法是类的行为单元,也是 API 的基本组成。好的方法设计使得代码易于理解、易于使用、不易出错。差的方法设计则会导致 bug、性能问题和维护困难。

1.3 核心问题引导

为什么公共方法的参数必须校验?私有方法呢?

什么是防御性拷贝?为什么需要它?

方法重载的解析规则是什么?如何避免歧义?

Optional 应该用在什么场景?不应该用在什么场景?

如何编写好的方法注释?


二、核心概念详解

6.1 检查参数的有效性(Item 49)

6.1.1 为什么需要参数校验

java
// 反模式:不校验参数
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)校验,因为可以确保调用者传递了有效参数。
java
// 使用 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 保护内部可变状态

java
// 反模式:暴露内部可变对象
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 的内部状态!
java
// 正确做法:防御性拷贝
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 使用不可变对象替代

java
// 更好的做法:使用不可变的 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 重载的歧义

java
// 反模式:容易混淆的重载
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 避免重载的歧义

java
// 正确做法:使用不同的方法名
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 的优势

java
// 反模式:返回 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 有对象创建开销)
java
// 反模式:返回 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 相关)

虽然这主要属于序列化章节,但方法设计中也需要注意:

java
// 安全的 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 防御性拷贝实战:不可变配置类

java
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");  // UnsupportedOperationException

6.2 Optional 实战:链式处理

java
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 模式加载配置,支持参数校验。


6/10方法
0%