引言
本条的核心思想: 对于公有类,不应该直接暴露数据字段,而应该通过访问方法(getter)和修改方法(setter)来封装这些字段。这种做法提供了更好的封装性、灵活性和可维护性。
一、为什么不应该使用公有域
1、问题示例
// 不好的实践 - 直接暴露公有域
public class Point {
public double x;
public double y;
}
2、这种直接暴露公有域的做法存在严重问题
- 失去了对内部表示的控制权:一旦类被广泛使用,改变内部表示几乎不可能,因为客户端代码直接依赖这些字段
- 无法强制不变式:无法保证字段值始终满足某些约束条件
- 无法在字段被访问时采取辅助操作:如日志记录、验证或触发事件等
二、应该使用访问方法的正确方式
1、解决方案示例
// 好的实践 - 使用访问方法
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 void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
2、这种使用访问方法的做法具有以下优势
- 封装内部实现:可以自由改变内部表示而不影响客户端
- 添加约束条件:可以在setter中添加验证逻辑
- 线程安全控制:可以添加同步机制
- 添加辅助行为:可以在访问时记录日志或触发事件
三、特殊情况讨论
1、包级私有或私有嵌套类
原文强调:“如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误”
这种情况下,因为类的可见性已经受限,修改不会影响到外部代码,所以可以直接暴露字段。
示例:
// 包级私有类,可以直接暴露字段
class PackagePrivatePoint {
public double x;
public double y;
}
2、JavaBean模式
作者指出:JavaBean模式(即每个属性都有getter和setter)在某些情况下也有问题,特别是在需要让类不可变时。
2.1 JavaBean模式分析:
JavaBean模式是指遵循特定约定的Java类,通常具有以下特征:
- 一个无参公共构造函数
- 每个属性都有对应的getter和setter方法
- 实现
Serializable接口
2.2 JavaBean模式的标准示例
// 典型的JavaBean
public class PersonBean implements Serializable {
private String name;
private int age;
public PersonBean() {} // 1. 无参构造函数
// 2. 每个属性都有getter和setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
// 其他方法...
}
2.3 JavaBean模式在不可变类中的问题
**不可变类(Immutable Class)**是指其实例在创建后状态不能被修改的类。JavaBean模式与不可变性存在根本冲突,原因如下:
2.3.1 setter方法破坏了不可变性
核心矛盾:不可变类要求实例创建后状态不能改变,而JavaBean的setter方法专门用于修改状态。
// 尝试创建"不可变"的JavaBean - 自相矛盾
public final class ImmutablePerson implements Serializable {
private String name;
private int age;
public ImmutablePerson() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; } // 破坏了不可变性!
public int getAge() { return age; }
public void setAge(int age) { this.age = age; } // 破坏了不可变性!
}
2.3.2 无参构造函数导致不完整初始化
不可变类通常要求在构造时提供所有必要属性,确保对象一旦创建就处于完整、一致的状态。
JavaBean模式下:
// 问题示例:通过无参构造+setter创建的不可变对象
ImmutablePerson person = new ImmutablePerson(); // 此时name=null, age=0
person.setName("Alice"); // 分步设置,中间状态不一致
person.setAge(25);
问题:在setName和setAge调用之间,对象处于不一致状态(有名字无年龄或有年龄无名字)。
2.3.3 正确的不可变类实现方式
// 正确的不可变类实现
public final class ImmutablePerson {
private final String name; // final字段
private final int age; // final字段
// 构造时要求提供所有属性
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
// 只有getter,没有setter
public String getName() { return name; }
public int getAge() { return age; }
// 没有提供修改状态的方法
}
2.3.4 关键区别
- 所有字段声明为
final - 没有无参构造函数
- 构造时必须提供所有属性
- 不提供setter方法
2.3.5 总结
Effective Java的观点:JavaBean模式由于强制要求提供setter方法和无参构造器,使得它无法用于实现真正的不可变类。在设计不可变类时,应该:
- 使所有字段为
final - 不提供setter方法
- 通过构造函数强制完整初始化
- 防止子类化(类声明为
final)
这种模式牺牲了JavaBean的灵活性,但换来了线程安全、易于推理等不可变对象的重要优势。
3、不可变类
对于不可变类,公开final字段是可以接受的,因为无法修改这些字段。
示例:
// 不可变类可以直接公开final字段
public final class Time {
private static final int HOURS_PER_DAY = 24;
private static final int MINUTES_PER_HOUR = 60;
public final int hour;
public final int minute;
public Time(int hour, int minute) {
if (hour < 0 || hour >= HOURS_PER_DAY)
throw new IllegalArgumentException("Hour: " + hour);
if (minute < 0 || minute >= MINUTES_PER_HOUR)
throw new IllegalArgumentException("Min: " + minute);
this.hour = hour;
this.minute = minute;
}
... // 其他方法
}
关键点:即使在这种情况下,如果未来需要改变内部表示,公开字段的方式也会带来问题。
四、总结要点
- 公有类永远不应该暴露可变的字段:这会破坏封装性,使类难以演化
- 让公有类暴露不可变字段的危害较小:但仍有局限性
- 包级私有和私有嵌套类可以更灵活:因为它们的修改不会影响外部代码
最终建议:在公有类中,总是使用私有字段和公共访问方法(getter),而不是直接暴露公有字段。这种面向对象的基本实践提供了必要的灵活性,可以在以后改进类的内部表示而不影响现有客户端代码。

782

被折叠的 条评论
为什么被折叠?



