2Effective Java(第3版)

类和接口

类与接口的设计原则

信息隐藏不可变类组合优于继承接口设计

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

第2章:类和接口(Classes and Interfaces)

一、导读

1.1 本章学习目标

第2章聚焦于 Java 编程中最核心的设计单元——类和接口。类是面向对象编程的基础,接口是实现抽象和松耦合的关键工具。本章通过10条经验法则,帮助你掌握类和接口设计的精髓,写出更清晰、更灵活、更可维护的代码。

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

  • 掌握信息隐藏的原则与实践:理解为什么封装是面向对象设计的基石,以及如何正确使用访问修饰符。
  • 理解不可变类的设计方法:掌握创建不可变类的五条规则,理解不可变性的优势与代价。
  • 掌握组合优于继承的原则:理解继承的缺陷,学会用组合和委托来替代继承。
  • 学会正确设计接口:理解接口的局限性,掌握默认方法和接口抽象类的权衡。
  • 理解类层次结构的设计:掌握何时使用抽象类、何时使用接口、何时使用类。

1.2 为什么第二章如此重要

类和接口是 Java 程序的基本构建单元。一个程序的质量,很大程度上取决于类和接口的设计质量。好的类设计使得代码易于理解、易于修改、易于测试;差的类设计则会导致代码僵化、脆弱、难以复用。

本章的重要性在于它教授的不是语法知识,而是设计思维。如何决定一个方法应该是 public 还是 private?一个类应该是 final 的还是可继承的?应该使用接口还是抽象类?这些决策直接影响代码的架构质量。

1.3 核心问题引导

在阅读本章时,请带着以下问题思考:

为什么应该使类和成员的可访问性尽可能小?过度暴露 API 有什么危害?

不可变类有哪些优势?为什么 String 被设计为不可变的?

继承为什么被称为"打破了封装"?组合如何弥补这个缺陷?

接口中的默认方法是好是坏?它解决了什么问题,又引入了什么问题?

为什么应该优先使用接口类型作为变量类型?


二、核心概念详解

2.1 使类和成员的可访问性最小化(Item 15)

2.1.1 封装的好处

封装(信息隐藏)是软件设计的基本原则之一。良好的封装带来以下好处:

模块解耦:各个模块可以独立开发、测试和优化。

并行开发:不同的开发者可以同时开发不同的模块。

易于理解:不需要理解其他模块的内部实现就能使用它。

性能优化:可以独立优化各个模块而不影响其他模块。

代码复用:封装良好的模块更容易在其他项目中复用。

2.1.2 访问控制规则

Java 提供了四种访问级别:

修饰符类包子类世界
private✓
默认(包私有)✓✓
protected✓✓✓
public✓✓✓✓

核心原则:对于每个类或成员,应该赋予尽可能小的访问级别。

2.1.3 实践指南

java
// 反模式:过度暴露
public class Point {
    public double x;
    public double y;
}

// 正确做法:封装内部表示
public class Point {
    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() { return x; }
    public double getY() { return y; }

    public double distanceFrom(Point other) {
        return Math.sqrt(Math.pow(x - other.x, 2) + Math.pow(y - other.y, 2));
    }
}

关键规则:

  • 实例字段不应该是公共的(除非是不可变类)。
  • 包含公共可变字段的类通常不是线程安全的。
  • 对于公共静态字段,只应暴露常量(public static final)。
  • 方法不应返回对内部可变对象的引用。

2.2 在公共类中使用访问器方法,而不是公共字段(Item 16)

java
// 反模式:公共字段
public class Temperature {
    public double celsius;
}

// 正确做法:私有字段 + 访问器
public class Temperature {
    private double celsius;

    public Temperature(double celsius) {
        this.celsius = celsius;
    }

    public double getCelsius() {
        return celsius;
    }

    public void setCelsius(double celsius) {
        if (celsius < -273.15) {
            throw new IllegalArgumentException("Temperature below absolute zero");
        }
        this.celsius = celsius;
    }

    public double getFahrenheit() {
        return celsius * 9.0 / 5.0 + 32;
    }
}

2.3 使可变性最小化(Item 17)

2.3.1 不可变类的五条规则

不要提供任何修改对象状态的方法(setter)。

保证类不会被扩展(使用 final 或私有构造器)。

使所有字段都是 `final` 的。

使所有字段都成为私有的。

确保对任何可变组件的排他性访问:如果类有指向可变对象的字段,确保客户端无法获取这些对象的引用。

2.3.2 不可变类的优势

线程安全:不可变对象天然是线程安全的,不需要同步。

易于共享:可以自由地共享和缓存。

易于构建:构造器只建立不变量约束。

失败原子性:如果抛出异常,对象不会处于不一致状态。

2.3.3 不可变类的实现

java
public final class Complex {
    private final double re;
    private final double im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public double realPart() { return re; }
    public double imaginaryPart() { return im; }

    // 不修改自身,而是返回新对象
    public Complex add(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

    public Complex subtract(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    public Complex multiply(Complex c) {
        return new Complex(re * c.re - im * c.im,
                          re * c.im + im * c.re);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Complex)) return false;
        Complex c = (Complex) o;
        return Double.compare(re, c.re) == 0
            && Double.compare(im, c.im) == 0;
    }

    @Override
    public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }

    @Override
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

// 使用——函数式风格
Complex a = new Complex(1, 2);
Complex b = new Complex(3, 4);
Complex sum = a.add(b);  // a 和 b 不变

2.3.4 不可变性的代价

不可变类的唯一缺点是:对于每个不同的值都需要一个单独的对象。如果创建大量中间对象,可能带来性能问题。解决方案:

提供可变配套类:如 String 对应 StringBuilder。

猜测哪些值会被频繁使用:预先创建并缓存(如 Boolean.TRUE)。

2.4 复合优于继承(Item 18)

2.4.1 继承的问题

继承打破了封装——子类依赖于超类的实现细节。如果超类在后续版本中改变了实现,子类可能会被破坏。

java
// 反模式:继承导致的脆弱性
public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

// 测试
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("a", "b", "c"));
// 期望 addCount = 3,实际 addCount = 6!
// 因为 HashSet.addAll 内部调用了 add,导致重复计数

2.4.2 组合方案

java
// 正确做法:组合 + 转发
public class InstrumentedSet<E> implements Set<E> {
    private final Set<E> innerSet;
    private int addCount = 0;

    public InstrumentedSet(Set<E> innerSet) {
        this.innerSet = innerSet;
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return innerSet.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return innerSet.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }

    // 转发方法
    @Override public void clear() { innerSet.clear(); }
    @Override public boolean contains(Object o) { return innerSet.contains(o); }
    @Override public boolean isEmpty() { return innerSet.isEmpty(); }
    @Override public int size() { return innerSet.size(); }
    @Override public Iterator<E> iterator() { return innerSet.iterator(); }
    @Override public boolean remove(Object o) { return innerSet.remove(o); }
    // ... 其他转发方法
}

// 使用——可以包装任何 Set 实现
Set<String> set = new InstrumentedSet<>(new HashSet<>());
set.addAll(List.of("a", "b", "c"));
System.out.println(((InstrumentedSet<String>) set).getAddCount()); // 3

2.4.3 包装类模式

组合模式的一种优雅实现是包装类(wrapper class):

java
public class Wrapper<E> implements Set<E> {
    private final Set<E> delegate;

    public Wrapper(Set<E> delegate) {
        this.delegate = Objects.requireNonNull(delegate);
    }

    // 所有方法都委托给 delegate
    @Override
    public boolean add(E e) {
        return delegate.add(e);
    }

    // ... 其他方法
}

2.5 如果继承不适用,就不要使用它(Item 19)

2.5.1 继承的设计约束

如果决定让一个类被继承,需要注意:

构造器不能调用可被覆盖的方法:超类构造器先于子类构造器执行,如果调用了被覆盖的方法,子类的方法会在子类构造器运行之前执行。

java
// 危险代码
public class Super {
    public Super() {
        overrideMe();  // 在构造器中调用可覆盖的方法
    }

    public void overrideMe() {
        System.out.println("Super.overrideMe()");
    }
}

public class Sub extends Super {
    private final Instant instant;

    public Sub() {
        instant = Instant.now();  // 这行在 super() 之后执行
    }

    @Override
    public void overrideMe() {
        System.out.println(instant);  // 打印 null!
    }
}

必须文档化所有可覆盖的方法:使用 @implSpec 注解说明内部调用关系。

保护性拷贝构造器:提供受保护的构造器,让子类可以安全地创建超类实例的副本。

2.5.2 替代方案

对于工具类或辅助类,优先使用组合而非继承。只有当确实存在"is-a"关系时,才使用继承。

2.6 接口优于抽象类(Item 20)

2.6.1 接口的优势

允许多重实现:一个类可以实现多个接口。

不强制继承层次:不会限制类的继承关系。

支持渐进式增强:通过默认方法可以安全地增强接口。

2.6.2 接口的骨架实现(Skeletal Implementation)

结合接口和抽象类的优势:

java
// 接口定义
public interface Map<K, V> {
    int size();
    boolean isEmpty();
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    V get(Object key);
    V put(K key, V value);
    V remove(Object key);
    void putAll(Map<? extends K, ? extends V> m);
    void clear();
    Set<K> keySet();
    Collection<V> values();
    Set<Map.Entry<K, V>> entrySet();

    default boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Map)) return false;
        Map<?, ?> m = (Map<?, ?>) o;
        if (m.size() != size()) return false;
        try {
            for (Entry<K, V> e : entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                if (value == null) {
                    if (!(m.get(key) == null && m.containsKey(key)))
                        return false;
                } else {
                    if (!value.equals(m.get(key)))
                        return false;
                }
            }
        } catch (ClassCastException | NullPointerException unused) {
            return false;
        }
        return true;
    }
}

// 骨架实现
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
    @Override
    public V setValue(V value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Map.Entry)) return false;
        Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
        return Objects.equals(getKey(), e.getKey())
            && Objects.equals(getValue(), e.getValue());
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
}

2.7 为后代设计接口(Item 21)

2.7.1 默认方法的陷阱

Java 8 引入的默认方法允许在接口中添加新方法而不破坏现有实现,但它有风险:

java
// 危险:默认方法可能破坏现有实现
public interface Collection<E> {
    default boolean removeIf(Predicate<? super E> filter) {
        boolean removed = false;
        for (E e : this) {
            if (filter.test(e)) {
                remove(e);  // 假设 remove 是 O(1),实际可能是 O(n)
                removed = true;
            }
        }
        return removed;
    }
}

2.7.2 实践建议

谨慎使用默认方法:在添加默认方法之前,仔细考虑所有可能的实现。

测试默认方法:使用多种实现来测试默认方法。

优先使用抽象类:如果接口需要大量默认方法,考虑使用抽象类。


三、实战案例

3.1 不可变类实战:Range 类

java
public final class Range {
    private final int min;
    private final int max;

    private Range(int min, int max) {
        if (min > max) {
            throw new IllegalArgumentException(
                "min (" + min + ") > max (" + max + ")");
        }
        this.min = min;
        this.max = max;
    }

    public static Range of(int min, int max) {
        return new Range(min, max);
    }

    public static Range of(int value) {
        return new Range(value, value);
    }

    public int getMin() { return min; }
    public int getMax() { return max; }

    public boolean contains(int value) {
        return value >= min && value <= max;
    }

    public boolean contains(Range other) {
        return other.min >= this.min && other.max <= this.max;
    }

    public boolean overlaps(Range other) {
        return this.min <= other.max && other.min <= this.max;
    }

    public Range intersection(Range other) {
        if (!overlaps(other)) {
            throw new IllegalArgumentException("Ranges do not overlap");
        }
        return Range.of(Math.max(this.min, other.min),
                       Math.min(this.max, other.max));
    }

    public Range union(Range other) {
        if (!overlaps(other) && this.max + 1 < other.min && other.max + 1 < this.min) {
            throw new IllegalArgumentException("Ranges are not adjacent or overlapping");
        }
        return Range.of(Math.min(this.min, other.min),
                       Math.max(this.max, other.max));
    }

    public int size() {
        return max - min + 1;
    }

    public IntStream stream() {
        return IntStream.rangeClosed(min, max);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Range)) return false;
        Range r = (Range) o;
        return min == r.min && max == r.max;
    }

    @Override
    public int hashCode() {
        return 31 * min + max;
    }

    @Override
    public String toString() {
        return "[" + min + ", " + max + "]";
    }
}

// 使用
Range r1 = Range.of(1, 10);
Range r2 = Range.of(5, 15);

System.out.println(r1.contains(5));       // true
System.out.println(r1.overlaps(r2));      // true
System.out.println(r1.intersection(r2));  // [5, 10]
System.out.println(r1.union(r2));         // [1, 15]
System.out.println(r1.size());            // 10

r1.stream().forEach(System.out::println); // 1 到 10

3.2 组合优于继承实战:日志装饰器

java
// 使用组合实现日志功能
public class LoggingSet<E> implements Set<E> {
    private final Set<E> delegate;
    private final Logger logger;

    public LoggingSet(Set<E> delegate, Logger logger) {
        this.delegate = Objects.requireNonNull(delegate);
        this.logger = Objects.requireNonNull(logger);
    }

    @Override
    public boolean add(E e) {
        logger.info("Adding element: " + e);
        boolean result = delegate.add(e);
        logger.info("Add result: " + result + ", size: " + delegate.size());
        return result;
    }

    @Override
    public boolean remove(Object o) {
        logger.info("Removing element: " + o);
        boolean result = delegate.remove(o);
        logger.info("Remove result: " + result);
        return result;
    }

    @Override
    public void clear() {
        logger.info("Clearing set, size was: " + delegate.size());
        delegate.clear();
    }

    // 转发方法
    @Override public int size() { return delegate.size(); }
    @Override public boolean isEmpty() { return delegate.isEmpty(); }
    @Override public boolean contains(Object o) { return delegate.contains(o); }
    @Override public Iterator<E> iterator() { return delegate.iterator(); }
    @Override public Object[] toArray() { return delegate.toArray(); }
    @Override public <T> T[] toArray(T[] a) { return delegate.toArray(a); }
    @Override public boolean containsAll(Collection<?> c) { return delegate.containsAll(c); }
    @Override public boolean addAll(Collection<? extends E> c) { return delegate.addAll(c); }
    @Override public boolean retainAll(Collection<?> c) { return delegate.retainAll(c); }
    @Override public boolean removeAll(Collection<?> c) { return delegate.removeAll(c); }
}

// 使用——可以组合不同的装饰器
Set<String> set = new LoggingSet<>(
    new HashSet<>(),
    Logger.getLogger("SetLogger")
);

3.3 接口设计实战:Repository 模式

java
// 定义通用 Repository 接口
public interface Repository<T, ID> {
    T findById(ID id);
    List<T> findAll();
    T save(T entity);
    void delete(T entity);
    long count();

    default boolean existsById(ID id) {
        return findById(id) != null;
    }

    default List<T> saveAll(List<T> entities) {
        List<T> result = new ArrayList<>();
        for (T entity : entities) {
            result.add(save(entity));
        }
        return result;
    }
}

// 内存实现
public class InMemoryRepository<T, ID> implements Repository<T, ID> {
    private final Map<ID, T> store = new LinkedHashMap<>();
    private final Function<T, ID> idExtractor;

    public InMemoryRepository(Function<T, ID> idExtractor) {
        this.idExtractor = idExtractor;
    }

    @Override
    public T findById(ID id) {
        return store.get(id);
    }

    @Override
    public List<T> findAll() {
        return new ArrayList<>(store.values());
    }

    @Override
    public T save(T entity) {
        ID id = idExtractor.apply(entity);
        store.put(id, entity);
        return entity;
    }

    @Override
    public void delete(T entity) {
        ID id = idExtractor.apply(entity);
        store.remove(id);
    }

    @Override
    public long count() {
        return store.size();
    }
}

// 使用
class User {
    private final String id;
    private final String name;

    public User(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() { return id; }
    public String getName() { return name; }
}

Repository<User, String> userRepo = new InMemoryRepository<>(User::getId);
userRepo.save(new User("1", "Alice"));
userRepo.save(new User("2", "Bob"));

System.out.println(userRepo.count());           // 2
System.out.println(userRepo.findById("1"));     // User{id='1', name='Alice'}
System.out.println(userRepo.existsById("3"));   // false

四、本章小结

第2章通过10条经验法则,系统讲解了类和接口设计的核心原则:

最小化可访问性:封装是软件设计的基础,每个类或成员都应该赋予尽可能小的访问级别。

不可变类:不可变对象简单、安全、可靠,是并发编程的理想选择。

组合优于继承:继承打破了封装,组合提供了更好的灵活性和可维护性。

接口优于抽象类:接口允许多重实现,不强制继承层次。

谨慎使用默认方法:默认方法可能破坏现有实现,需要仔细测试。

这些原则不是教条,而是指导设计决策的思维工具。在实践中,你需要根据具体场景灵活运用这些原则。记住:好的设计不是做加法,而是做减法——用最少的复杂度满足需求。


五、思考与练习

思考题

不可变类在什么场景下会带来性能问题?如何解决?

继承在什么情况下是合理的?给出一个使用继承的正确场景。

接口的默认方法有哪些潜在风险?如何避免?

包装类模式的性能开销在哪里?如何优化?

为什么 String 被设计为不可变的?这带来了哪些好处?

编程练习

实现一个不可变的 Matrix 类:支持矩阵加法、乘法、转置操作,所有操作返回新对象。

实现一个 DelegatingList 类:使用组合模式包装 List 接口,添加元素访问日志功能。

设计一个 Payment 接口:使用接口和默认方法实现支付系统,支持多种支付方式(信用卡、支付宝、微信)。


2/10类和接口
0%