第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 实践指南
// 反模式:过度暴露
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)
// 反模式:公共字段
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 不可变类的实现
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 继承的问题
继承打破了封装——子类依赖于超类的实现细节。如果超类在后续版本中改变了实现,子类可能会被破坏。
// 反模式:继承导致的脆弱性
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 组合方案
// 正确做法:组合 + 转发
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()); // 32.4.3 包装类模式
组合模式的一种优雅实现是包装类(wrapper class):
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 继承的设计约束
如果决定让一个类被继承,需要注意:
构造器不能调用可被覆盖的方法:超类构造器先于子类构造器执行,如果调用了被覆盖的方法,子类的方法会在子类构造器运行之前执行。
// 危险代码
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)
结合接口和抽象类的优势:
// 接口定义
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 引入的默认方法允许在接口中添加新方法而不破坏现有实现,但它有风险:
// 危险:默认方法可能破坏现有实现
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 类
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 到 103.2 组合优于继承实战:日志装饰器
// 使用组合实现日志功能
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 模式
// 定义通用 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 接口:使用接口和默认方法实现支付系统,支持多种支付方式(信用卡、支付宝、微信)。