当前位置: 首页 > news >正文

Effective Java笔记:要在公有类而非公有域中使用访问方法

在公有类中使用访问方法而非公有域

在面向对象编程中的封装原则(encapsulation)强调,类的实现细节应该对外界隐藏,而只通过控制良好的接口进行交互。这条规则同样适用于类的成员变量(域):不要暴露公共域,而是通过访问方法(getter 和 setter)进行操作。这种方法有助于实现封装、提高代码的灵活性和可维护性、降低耦合。


1. 为什么公有类中不应使用公有域?

1.1 暴露内部实现细节

如果直接使用公有域,外界代码会直接访问或修改类的成员变量,其使用方式可能依赖这些变量的当前实现。一旦你需要修改类的实现(比如改变成员变量的结构或语义),就会破坏依赖这些字段的代码。

例子:

public class Point {public int x; // 公共域public int y;
}

此时,外界可以直接访问并修改 Point 的内容:

Point p = new Point();
p.x = 10; // 直接操作
p.y = 20;

如果有一天你需要用极坐标来表示 Point(使用 radiustheta 替代 xy),你将不可能对其进行修改,因为外界代码严重依赖 xy

1.2 难以添加限制和验证逻辑

直接对公共域赋值,无法限制赋值的合法性或加入额外的验证逻辑。

例子:
假设你希望 Point 的 x 和 y 坐标始终为非负数,直接使用公共域将无法实现此约束:

Point p = new Point();
p.x = -10; // 无法阻止非法赋值
1.3 打破封装,导致不可维护

如果成员变量是公开的,外部代码甚至可能直接修改该变量为非法的状态(即使它对于类而言是不一致或不安全的)。这打破了封装原则,降低了类的维护性。


2. 使用访问方法的好处

更推荐的做法是将类的成员变量声明为私有(private),并提供公有的访问方法(getter 和 setter)来控制对这些变量的访问和修改。访问方法可以提供以下好处:

2.1 提供灵活性:隐藏实现细节

通过访问方法,可以对外暴露一种“接口化”的行为,而隐藏变量的实际实现方式。即使将来更改变量的存储方式,访问方法也可以保持接口稳定。

改进的实现:

public class Point {private int x; // 私有变量private int y;// Getter 方法public int getX() {return x;}public int getY() {return y;}// Setter 方法public void setX(int x) {if (x < 0) { // 添加合法性校验throw new IllegalArgumentException("x must be non-negative");}this.x = x;}public void setY(int y) {if (y < 0) {throw new IllegalArgumentException("y must be non-negative");}this.y = y;}
}

即使未来 Point 从笛卡尔坐标改为极坐标表示(radiustheta),仍然可以通过 getter 和 setter 提供不变的 getX()getY() 接口,从而保证外界代码不受影响。

2.2 支持验证逻辑和额外行为

访问方法可以在读取或写入数据时执行合法性检查、数据转换等操作,而直接暴露公共域就不能做到这一点。

改进后:

public class Temperature {private double celsius; // 温度以摄氏为内部存储单位// Getter:以摄氏为单位返回温度public double getCelsius() {return celsius;}// Setter:合法性校验public void setCelsius(double celsius) {if (celsius < -273.15) { // 合法性校验:避免低于绝对零度throw new IllegalArgumentException("Temperature cannot be below absolute zero");}this.celsius = celsius;}// 额外 Getter:以华氏为单位返回温度public double getFahrenheit() {return celsius * 9 / 5 + 32;}// 额外 Setter:接收以华氏为单位的温度public void setFahrenheit(double fahrenheit) {setCelsius((fahrenheit - 32) * 5 / 9);}
}

优势:
外界可以以摄氏或华氏为单位使用 Temperature 类,而类内部始终以摄氏存储温度值。

2.3 提高类的易维护性和安全性

将数据的内部表示与访问方式解耦,如果未来修改了内部逻辑或存储方式,只需调整访问器方法,而无需修改客户端代码。这种封装可以让类更加容易维护。

2.4 支持只读或条件性访问

访问方法提供更好的权限控制。例如,可以通过只提供 getter 不提供 setter 来实现只读字段;或者根据上下文条件决定访问权限。

示例:只读成员

public class Circle {private final double radius; // 半径public Circle(double radius) {if (radius <= 0) {throw new IllegalArgumentException("Radius must be positive");}this.radius = radius;}public double getRadius() { // 仅提供 getterreturn radius;}
}

示例:条件性访问

public class Account {private double balance;public Account(double initialBalance) {this.balance = initialBalance;}public double getBalance(User user) {if (!user.hasPermission("VIEW_BALANCE")) {throw new SecurityException("User does not have permission to view balance");}return balance;}
}

3. 使用访问方法的注意事项

3.1 避免滥用访问方法

虽然访问方法是一个好的设计实践,但并非所有字段都需要访问方法。只用当字段需要与外界交互时,才需要提供访问器(getter/setter)。否则,保持字段为私有并在类内部管理即可。

3.2 不要过度细化访问方法

访问方法应该符合逻辑需求,而不是一味为所有字段都添加 getter 和 setter。这可能会导致不必要的代码膨胀。

3.3 final 字段与访问方法

当字段确实应该不可变时,可以将其声明为 final,且仅提供 getter


4. 书中案例

原始问题:直接暴露数组

在《Effective Java》中提到了一个常见问题:用 public 数组暴露类的内部数据。

public class Test {public static final String[] VALUES = {"a", "b", "c"}; // 直接暴露
}

问题:外部代码可以通过引用直接修改 VALUES 的内容:

Test.VALUES[0] = "z"; // 修改了内部数组
解决:通过访问方法返回副本

通过访问器返回数组的副本,使得外界不能直接修改 VALUES

public class Test {private static final String[] VALUES = {"a", "b", "c"};public static String[] getValues() {return VALUES.clone(); // 返回副本}
}

这样,外界修改副本时不会影响原数组:

String[] valuesCopy = Test.getValues();
valuesCopy[0] = "z"; // 不影响原数组

5. 总结

  • 封装的核心原则是:将实现隐藏,暴露必要的功能接口。
  • 禁止直接暴露公有域,应使用访问方法(getter 和 setter)来操作私有字段。
  • 使用访问方法的优点包括:
    1. 隐藏实现细节,提高灵活性。
    2. 支持数据校验、转换等附加逻辑。
    3. 提高代码的可维护性,易于以后扩展。
    4. 防止外界直接修改类的内部状态,保证数据一致性。
http://www.dtcms.com/a/330769.html

相关文章:

  • 解决Maven编译时JAVA_HOME配置错误问题:从报错到根治的完整方案
  • 自动驾驶与人形机器人的技术分水岭
  • springboot博客实战笔记02
  • React.memo、useMemo 和 React.PureComponent的区别
  • 智慧城市SaaS平台/专项管理系统
  • 板子识别出来的所有端点号等信息
  • C++中的链式操作原理与应用(三):专注于异步操作延的C++开源库 continuable
  • 决策树 >> 随机森林
  • 智慧工地从工具叠加到全要素重构的核心引擎
  • Claude Code频繁出错怎么办?深入架构层面的故障排除指南
  • 【Linux学习|黑马笔记|Day4】IP地址、主机名、网络请求、下载、端口、进程管理、主机状态监控、环境变量、文件的上传和下载、压缩和解压
  • 【论文阅读】基于表面肌电信号的下肢多关节运动估计:一种深度卷积神经网络方法
  • [小练习]生成54张扑克牌,洗牌。
  • 解决 VSCode 运行 Python 时 ModuleNotFoundError: No module named ‘open_webui‘ 问题
  • 三角洲知识点
  • CI/CD流水线搭建流程
  • 药房发药的“时间密码”:同步时钟用药安全?
  • 抗辐照CANFD通信芯片在高安全领域国产化替代的研究
  • CMake进阶: externalproject_add用于在构建阶段下载、配置、构建和安装外部项目
  • 常见的Jmeter压测问题
  • 飞算 JavaAI 云原生实践:基于 Docker 与 K8s 的自动化部署架构解析
  • 用架构建模工具Sparx EA绘制企业转型路线图
  • C++状态模式详解:从OpenBMC源码看架构、原理与应用
  • NineData云原生智能数据管理平台新功能发布|2025年7月版
  • 云原生俱乐部-k8s知识点归纳(2)
  • 生产环境中Debezium CDC与Kafka实时流处理实战指南
  • 3ds MAX文件/贴图名称乱码?6大根源及解决方案
  • .NET 在鸿蒙系统(HarmonyOS Next)上的适配探索与实践
  • 界面设计风格解析 | ABB 3D社交媒体视觉效果设计
  • 【力扣56】合并区间