封装概念
封装的本质是什么?
很多人会回答:「把数据和操作放在一起」「把字段设成 private,然后加 getter/setter」。这些都对,但不深刻。
换个问法:封装是为了防谁?
防的是两类人:一是使用者——不让他们看到不需要看到的东西;二是未来的自己——不让内部的改动,波及到外部依赖。
真正的封装,是给一个东西筑起边界:边界内的东西随便改,边界外的人不受影响。
访问修饰符:边界的四层闸门
Java 提供了四个级别的访问控制,从宽到严:
| 修饰符 | 本类 | 同包 | 子类 | 其他包 |
|---|---|---|---|---|
| public | ✅ | ✅ | ✅ | ✅ |
| protected | ✅ | ✅ | ✅ | ❌ |
| default(无修饰符) | ✅ | ✅ | ❌ | ❌ |
| private | ✅ | ❌ | ❌ | ❌ |
private 是最常用的边界手段。把它想象成一道单向门:内部可以出去,外面进不来。
protected 是个特殊存在:同包能访问,跨包子类也能访问。这是为了让继承体系内的协作更灵活,但实际用的时候要谨慎——protected 成员也是 API 的一部分,子类会依赖它。
default 通常是无意间产生的。如果你不写修饰符,Java 默认给你一个包内可见。这不一定是坏事:包内协作类之间不需要严格的边界,暴露给同包的代价可控。
public 是最大胆的选择。一旦 public,就是对外承诺:这个接口我负责维护,不会乱改。所以在设计 public API 时,要格外谨慎。
封装的三个层次
大多数教程把封装等同于「属性私有化 + getter/setter」,这是最浅的理解。封装实际上分三个层次:
第一层:数据隐藏
把不想让人直接操作的字段藏起来:
public class Person {
private String name;
private int age;
// 外部不能直接改,只能通过方法
public void setAge(int age) {
if (age >= 0 && age <= 150) {
this.age = age;
} else {
throw new IllegalArgumentException("年龄不合理");
}
}
}这里的关键不是「private」,而是「验证逻辑」。如果 setter 里没有校验,private 就只是一个形式。
第二层:内部表示隐藏
数据隐藏只是开始。更重要的是:内部怎么存,不重要;对外提供什么能力,才重要。
public class Person {
// 内部可以用 String 存,也可以用 Date 存
// 对外只暴露「操作日期的能力」,不暴露存储细节
private LocalDate birthDate;
public int getAge() {
return Period.between(birthDate, LocalDate.now()).getYears();
}
public boolean isAdult() {
return getAge() >= 18;
}
}调用方不需要知道 birthday 是用什么类型存的,也不需要知道年龄怎么算的。他们只需要「问一句」就能得到答案。哪天要把 LocalDate 换成 long 时间戳,调用方不需要改一行代码。
第三层:行为封装
最高层次的封装是:不暴露数据,只暴露行为。
public class FileReader {
private final String path;
public FileReader(String path) {
this.path = path;
}
// 不暴露文件句柄、字符流
// 只暴露「读取」这个行为
public String read() throws IOException {
return Files.readString(Path.of(path));
}
}调用方不需要知道底层用的是 FileInputStream 还是 NIO,不需要管理资源。他们只需要调用 read(),得到结果。这就是封装带来的透明性。
基本封装示例
把三个层次合在一起看:
public class EncapsulationConceptDemo {
static class Person {
// 第一层:数据隐藏
private String name;
private int age;
// 构造时也做校验
public Person(String name, int age) {
this.name = name;
setAge(age); // 通过 setter 校验
}
// 第二层:提供行为,不暴露字段
public String getName() {
return name;
}
public int getAge() {
return age;
}
// setter 带验证逻辑
public void setAge(int age) {
if (age >= 0 && age <= 150) {
this.age = age;
} else {
throw new IllegalArgumentException("年龄必须在 0-150 之间");
}
}
// 第三层:行为方法
public boolean isAdult() {
return age >= 18;
}
public String describe() {
return name + "," + age + "岁,"
+ (isAdult() ? "已成年" : "未成年");
}
}
public static void main(String[] args) {
Person person = new Person("张三", 25);
System.out.println(person.describe()); // 张三,25岁,已成年
person.setAge(-5); // 抛出异常,数据不会被污染
}
}封装的价值
封装不是写代码的硬性要求,而是软件设计的必然选择:
对使用者:只需要关心接口,不需要理解实现。越简单越好用。
对维护者:内部改,不影响外部。越独立越好改。
对系统:边界清晰,依赖关系简单。越清晰越稳定。
好封装的标志是什么?当内部实现全换掉,外部没有任何感知。
这就是封装的终极目标。
