封装:属性与方法
这是什么:封装(Encapsulation) = 把数据藏起来,只留规定的入口让人进出;配套的 private、getter、setter 是它的三件套。
为什么用:上一章类与对象里,Dog 的名字和年龄都裸露在外,谁都能改。封装就像自动售货机只留投币口和取货口——你没法伸手进货柜乱拿,把内部藏好,只开规定的口子。
如何用:本章先看封装代码长什么样,再认识 private 到 public 这一族修饰符,最后拿银行账户练一个只读属性。
为什么要封装
想象 int score 是公开的,谁都能写一句 s.score = 999; 塞个非法值进去,程序不会当场报错,脏数据却会一路蔓延。
蔓延有多可怕?看这条链:成绩 999 被存进去 → 排名计算时拿它排序,小明"霸榜" → 平均分被拉高,全班统计全错 → 数据存进文件,错误被永久保存。源头只是一个等号,污染的却是整条数据链——而且越往下游排查越难找到病根。
封装的思路:成员变量用 private 藏起来,对外只提供 getter / setter 方法。
private:被它修饰的成员,只有本类内部能访问,外面一碰就报错setter:赋值入口,在里面做校验,非法数据进不来getter:读取出口,想给就给,不想给可以不给
两种写法摆在一起对比,高下立判:
| 对比项 | 字段裸奔(public) | 封装(private + 方法) |
|---|---|---|
| 非法值 | 直接塞进去,无声无息 | setter 里拦下,当场提示 |
| 改内部实现 | 牵一发动全身,所有 s.score 都得改 |
只改类内部,外部一行不动 |
| 排查脏数据 | 全项目大海捞针 | 入口只有 setter,一查一个准 |
| 给别人用 | 心惊胆战,生怕被乱改 | 只暴露方法,用得放心 |
代码演示
public class Student {
private String name; // 私有:外部改不了
private int score;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setScore(int score) {
if (score < 0 || score > 100) { // 封装的价值:校验在这里
System.out.println("成绩必须在 0~100 之间,设置失败");
return;
}
this.score = score;
}
public int getScore() {
return score;
}
public static void main(String[] args) {
Student s = new Student();
s.setName("小明");
s.setScore(59);
s.setScore(120); // 非法值,被拦下
System.out.println(s.getName() + " 的成绩:" + s.getScore());
}
}
运行后输出:
成绩必须在 0~100 之间,设置失败
小明的成绩:59
逐行解读
| 代码 | 作用 |
|---|---|
private int score; |
私有成员变量:外部写 s.score = 999; 直接编译报错,数据被保护 |
public void setScore(int score) |
setter:外部赋值的唯一入口,校验逻辑写在这 |
public int getScore() |
getter:外部读值的唯一出口 |
s.setName("小明") |
使用对象时不再直接碰字段,一切经由方法 |
IDE 小贴士:IDEA / VS Code 里右键 → Generate(Alt+Insert)可以批量生成 getter / setter。实际项目没人手写几十个,但生成出来的东西你必须看得懂。
修饰符家族:private 只是其中一档
Java 用访问修饰符控制"谁能看见谁",从最严到最松一共四档:
| 修饰符 | 本类 | 同一个包 | 子类(下一章讲) | 任何地方 |
|---|---|---|---|---|
private |
✅ | ❌ | ❌ | ❌ |
| 不写(默认) | ✅ | ✅ | ❌ | ❌ |
protected |
✅ | ✅ | ✅ | ❌ |
public |
✅ | ✅ | ✅ | ✅ |
入门阶段记两条就够:成员变量一律 private,需要被外部调用的方法用 public。其余两档等学了"包"和"继承"再来细究,现在会认即可。
进阶示例:银行账户
封装最经典的战场是钱。余额一旦裸奔,acc.balance = 100000; 一句就"暴富",还查无实据。看封装后的账户——存取都是方法,每笔都过安检:
public class BankAccount {
private String owner;
private double balance; // 余额藏起来,外部碰不到
public BankAccount(String owner) {
this.owner = owner;
}
public void deposit(double amount) { // 存钱入口
if (amount <= 0) {
System.out.println("存款金额必须大于 0");
return;
}
balance += amount;
System.out.println(owner + " 存入 " + amount + ",余额 " + balance);
}
public void withdraw(double amount) { // 取钱入口
if (amount <= 0) {
System.out.println("取款金额必须大于 0");
return;
}
if (amount > balance) {
System.out.println("余额不足,当前只有 " + balance);
return;
}
balance -= amount;
System.out.println(owner + " 取出 " + amount + ",余额 " + balance);
}
public double getBalance() { // 余额只读:查可以,改不行
return balance;
}
public static void main(String[] args) {
BankAccount acc = new BankAccount("小青蛙");
acc.deposit(100);
acc.withdraw(30);
acc.withdraw(500); // 超额,被拦下
acc.deposit(-5); // 非法金额,被拦下
System.out.println("最终余额:" + acc.getBalance());
}
}
你会看到:
小青蛙 存入 100.0,余额 100.0
小青蛙 取出 30.0,余额 70.0
余额不足,当前只有 70.0
存款金额必须大于 0
最终余额:70.0
注意这里没有 setBalance——余额不该被外部随意"整笔改写",只提供 deposit / withdraw 两个业务动作。这就是封装的更高境界:暴露的不是字段,是行为。
只读属性:只有 getter 没有 setter
有些数据"只许读不许改",比如身份证号、订单编号——只写 getter,不写 setter。这类字段建议再配上 final,把"只初始化一次"也钉死:
public class Order {
private final String orderId; // final + 无 setter = 只读
public Order(String orderId) {
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
// 没有 setOrderId,外部无法修改
}
final 保证引用只能赋值一次(在构造方法里),没有 setter 堵住唯一的修改口子,双重保险。三类属性怎么选,一张表定夺:
| 属性类型 | 配方 | 例子 |
|---|---|---|
| 可读可写 | getter + 带校验的 setter | 昵称、收货地址 |
| 只读 | getter,不写 setter(可加 final) | 订单号、身份证号 |
| 只写不给读 | 只写 setter,不写 getter(少见) | 临时验证码 |
封装让内部实现随便换
封装最隐蔽也最值钱的收益:外部只认方法签名,内部怎么实现随便改。比如 Student 想升级为"自动按出生年份算年龄",外部代码一行不用动:
public class Person {
private int birthYear; // 内部改存出生年份
public Person(int birthYear) {
this.birthYear = birthYear;
}
public int getAge() { // 对外还是"年龄",内部随便换算法
return 2026 - birthYear;
}
}
调用方依旧写 p.getAge(),完全感知不到内部从"存年龄"换成了"存年份再算"。要是字段裸奔,这个改动会让全项目所有 p.age = ... 的地方集体遭殃。方法签名是合同,内部实现是自由——这句记住,工作后天天受益。
记忆口诀
数据藏 private,进出走 getter / setter,校验加中间,安全又体面。
❓ 常见问题
Q1:所有成员变量都要 private 吗?
不是绝对。入门阶段建议"成员变量默认 private,需要外部访问时再加 getter/setter"。这样数据安全,后期想加校验也只改 setter 内部,外部代码一行不用动。
Q2:getter / setter 名字能乱起吗?
建议遵循惯例:getXxx() / setXxx()。很多框架(如 Spring)和工具会自动识别这种命名,乱起名会出奇迹。
Q3:final 变量和 private 有什么区别?
final 是"只赋一次值,之后不能改";private 是"外部类看不见"。两者可以组合:private final 表示"外部看不见,且只能初始化一次"。
Q4:布尔类型的 getter 有什么讲究?
按惯例,private boolean vip; 的 getter 不叫 getVip() 而叫 isVip()——读起来就是一句问话"是不是 VIP?"。很多框架按这个惯例找方法,布尔属性切记用 is 开头。
Q5:setter 只做打印 + return,非法数据其实还是没存,这够吗?
学习阶段够用。正经项目里更常见的做法是抛异常(throw new IllegalArgumentException("成绩非法"))让调用方明确处理失败,而不是悄悄 return——异常是后面章节的内容,现在先用"提示 + return"把校验思想立起来,思路是相通的:非法数据必须在入口处拦下。
Q6:书里说 s.score = 999 会报错,我跟着写却编译通过了?
八成你把这句写进了 Student 类自己的 main 方法里——private 的准确含义是"只有本类内部能访问",main 就在本类里,当然畅通无阻。把这句挪到另一个类(比如新建 Test.java)里再试,编译器立刻翻脸报 score has private access。顺带一个冷知识:private 锁的是"类",不是"对象"——在本类内部,通过另一个同类对象的引用访问 private 字段也是合法的。
⚠️ 新手翻车现场
翻车一:在 main 里直接访问 private 字段
// 错误:score 是 private,外部一碰就报错
Student s = new Student();
s.score = 999; // ❌ 编译报错
问题:private 把数据锁起来,外部类没权限直接读写。
修正:走 setScore(999) / getScore() 这对 getter/setter。
翻车二:setter 里忘了校验,非法值照进
public void setScore(int score) {
this.score = score; // ❌ 没校验,120 也能塞进去
}
问题:封装的意义就是在 setter 里拦下脏数据,漏了校验等于白封装。
修正:在 setter 里加 if (score < 0 || score > 100) 判断,非法就返回。
翻车三:getter / setter 命名拼错
public void SetScore(int score) { // ❌ 首字母大写,不符合惯例
this.score = score;
}
问题:Java 惯例是 setScore(小写开头,驼峰)。框架可能识别不到,导致自动注入失效。
修正:public void setScore(int score)。
翻车四:构造方法绕过 setter,校验形同虚设
public Student(int score) {
this.score = score; // ❌ 绕过 setScore,120 直接进来了
}
public void setScore(int score) {
if (score < 0 || score > 100) { return; } // setter 里的校验白写了
this.score = score;
}
问题:入口有两个(构造方法、setter),校验只守了一个——new Student(120) 照样塞进脏数据。
修正:让构造方法复用 setter:public Student(int score) { setScore(score); }。校验逻辑只写一份,所有入口都过同一道安检。
🎯 什么时候用封装
- 数据有取值范围(如成绩 0~100、年龄 0~150)
- 数据只读不允许修改(如订单编号、身份证号)
- 修改数据时需要联动其他逻辑(如改库存时同时记日志)
- 多人协作时,你的类要交给同事用——封装就是给数据装护栏,防止误用
- 字段的算法将来可能变(比如"存年龄"改成"按出生年份算")——封装好,换实现零成本
🛠 封装实战清单
- 成员变量一律先 private:要读再加 getter,要写再加 setter——按需开门,别一上来全开
- setter 的第一件事永远是校验:非法就拦;规则变了也只改这一处
- 核心数据不给裸 set:余额、库存、密码这类字段,提供业务方法(存款、扣库存、改密码)而不是
setBalance - getter 里别藏惊喜:读取最好无副作用;写操作会联动什么,要写清楚
- 类与类只通过 public 方法打交道:谁也不许伸手进别人口袋,这是团队协作的默契
📝 本章小结
private把成员变量藏起来,外部无法直接读写getter/setter是数据进出的唯一入口,校验逻辑写在 setter 里- 只读属性:只写 getter 不写 setter,或加
final;布尔属性的 getter 用is开头 - 封装后,内部实现怎么变,外部代码一行不用动
- 更高境界:暴露的不是字段而是行为(如存款取款方法),余额这类核心数据甚至不给 setter
- 校验逻辑只写一份,构造方法复用 setter,别让任何一个入口漏检
- 记忆口诀:数据藏 private,进出走 getter / setter,校验加中间,安全又体面。
动手改
- 把
setScore的校验改成"只允许 0~150",传入 160 看看输出 - 在 setter 里加一句
System.out.println("成绩已更新"),观察每次赋值都会打印 - 故意在
main里写s.score = 59;,亲眼看 private 给你的编译报错 - 给
Student加一个只读属性studentId(用final+ 无 setter),在构造方法里赋值
下一章:继承与多态,让子类站在父类的肩膀上 🐸