← Java 基础教程

第 20 / 40 课 · 免费教程

锁好你的箱子

封装 private 与校验

100–120 分钟本章使用 JDK 21,无需预览功能。面向对象入门

本课作品

制作安全箱子

会用 private 保护对象字段;能通过方法提供受控读取和修改;能用校验守住容量与非负库存;能避免把内部可变数组直接交给外部代码。 前置背包:类、对象、构造器、方法参数与返回值。今天要给数据装门、装锁,还要设计真正好用的按钮。

开始前:建议先完成第 17–19 课

箱子用受控操作维护库存
  1. 请求存入或取出数量
  2. 校验不超容量,不负库存
  3. 状态成功才写入,失败不变

01 / 15

开始前:把示例放对地方

本章使用 JDK 21,不需要预览功能。每个普通示例都给出完整程序,按标注的文件名保存。多个示例各放在自己的文件夹,避免辅助类同名。

终端命令:把中文占位符替换成实际名称

javac -encoding UTF-8 示例文件名.java
java 示例类名

类名区分大小写。完整挑战代码若分成多段,请按顺序接在同一个文件中,不要为续段另建类。运行核对只列程序输出;需要输入的地方另给输入序列。

02 / 15

封装让规则只有一个入口

public 字段像没有门的钻石仓库:任何代码都能把库存改成 -999。封装把数据和维护数据的规则放在同一个类里。

private 把字段藏进箱体
  1. 字段private保存内部状态
  2. 方法公开受控入口
  3. 校验先判断合法性
  4. 规则0到容量内的库存

L20Example01.java · 完整独立示例

class Chest { public int diamonds; }
public class L20Example01 {
    public static void main(String[] args) {
        Chest chest = new Chest();
        chest.diamonds = -999;
        System.out.println(chest.diamonds);
    }
}

运行结果

-999

封装目标

  • 外部不能随意写字段。
  • 所有修改经过同一套检查。
  • 类对外暴露清楚而精简的方法。
  • 通过校验的公开操作应保持对象合法状态。

03 / 15

private 的可见范围

本章把箱子与调用程序写成两个独立类:箱子内部可以维护 private 字段,调用程序使用公开方法。Java 还允许同一最外层类及其嵌套类之间访问 private 成员;不要用这个例外检验外部访问规则。

私有字段只在自己的类体中直接访问
  1. 箱子内部可维护私有字段
  2. 独立外部类不能直接写私有字段
  3. 公开方法提供允许的操作
  4. 例外同一外层类的嵌套成员

BadPrivate.java · 故意错误示例

class Wallet { private int emeralds; }
public class BadPrivate {
    public static void main(String[] args) {
        Wallet wallet = new Wallet();
        wallet.emeralds = 99;
    }
}

04 / 15

getter 提供受控读取

外部若需要查看状态,可提供 getter 或更有意义的查询方法。不是每个字段都必须配一个 getter。

读取方法只交出需要的信息
  1. getCount读取当前库存
  2. remainingSpace读取剩余空间
  3. 查询不修改状态
  4. 最少公开只提供真正需要的信息

L20Example03.java · 完整独立示例

class Chest {
    private final String item;
    private final int capacity;
    private int count;
    public Chest(String item, int capacity) {
        this.item = item == null || item.isBlank() ? "未知物品" : item.trim();
        this.capacity = Math.max(1, capacity);
    }
    public boolean deposit(int amount) {
        if (amount <= 0 || amount > capacity - count) return false;
        count += amount;
        return true;
    }
    public boolean withdraw(int amount) {
        if (amount <= 0 || amount > count) return false;
        count -= amount;
        return true;
    }
    public int getCount() { return count; }
    public int remainingSpace() { return capacity - count; }
    public String describe() { return item + " " + count + "/" + capacity; }
}
public class L20Example03 {
    public static void main(String[] args) {
        Chest chest = new Chest("钻石", 64);
        System.out.println(chest.getCount());
        System.out.println(chest.remainingSpace());
    }
}

运行结果

0
64

05 / 15

行为方法比万能 setter 更安全

setCount(-999) 让调用者承担所有规则。deposit 和 withdraw 则表达真实动作,并能集中检查。

先检查,后修改

数量合法吗?

条件结果执行路径
true写入数量,返回 true
false保持原样,返回 false

↓ 调用者检查结果

L20Example04.java · 完整独立示例

class Chest {
    private final String item;
    private final int capacity;
    private int count;
    public Chest(String item, int capacity) {
        this.item = item == null || item.isBlank() ? "未知物品" : item.trim();
        this.capacity = Math.max(1, capacity);
    }
    public boolean deposit(int amount) {
        if (amount <= 0 || amount > capacity - count) return false;
        count += amount;
        return true;
    }
    public boolean withdraw(int amount) {
        if (amount <= 0 || amount > count) return false;
        count -= amount;
        return true;
    }
    public int getCount() { return count; }
    public int remainingSpace() { return capacity - count; }
    public String describe() { return item + " " + count + "/" + capacity; }
}
public class L20Example04 {
    public static void main(String[] args) {
        Chest chest = new Chest("钻石", 64);
        System.out.println(chest.deposit(12));
        System.out.println(chest.withdraw(3));
        System.out.println(chest.getCount());
    }
}

运行结果

true
true
9

06 / 15

校验守住合法范围

失败操作不应修改对象。先检查所有条件,再写字段。

先检查,后修改

数量合法吗?

条件结果执行路径
true写入数量,返回 true
false保持原样,返回 false

↓ 调用者检查结果

L20Example05.java · 完整独立示例

class Chest {
    private final String item;
    private final int capacity;
    private int count;
    public Chest(String item, int capacity) {
        this.item = item == null || item.isBlank() ? "未知物品" : item.trim();
        this.capacity = Math.max(1, capacity);
    }
    public boolean deposit(int amount) {
        if (amount <= 0 || amount > capacity - count) return false;
        count += amount;
        return true;
    }
    public boolean withdraw(int amount) {
        if (amount <= 0 || amount > count) return false;
        count -= amount;
        return true;
    }
    public int getCount() { return count; }
    public int remainingSpace() { return capacity - count; }
    public String describe() { return item + " " + count + "/" + capacity; }
}
public class L20Example05 {
    public static void main(String[] args) {
        Chest chest = new Chest("钻石", 64);
        chest.deposit(12);
        int before = chest.getCount();
        boolean ok = chest.deposit(Integer.MAX_VALUE);
        System.out.println(!ok && chest.getCount() == before);
    }
}

运行结果

true

07 / 15

boolean 汇报操作结果

修改方法返回 boolean,可以让调用者知道操作是否完成,而不必猜测字段变化。

调用者根据成功或失败决定下一步
  1. true表示操作完成
  2. false表示拒绝且不变
  3. 提示调用者选择反馈
  4. 测试同时检查结果与状态

L20Example06.java · 完整独立示例

class Chest {
    private final String item;
    private final int capacity;
    private int count;
    public Chest(String item, int capacity) {
        this.item = item == null || item.isBlank() ? "未知物品" : item.trim();
        this.capacity = Math.max(1, capacity);
    }
    public boolean deposit(int amount) {
        if (amount <= 0 || amount > capacity - count) return false;
        count += amount;
        return true;
    }
    public boolean withdraw(int amount) {
        if (amount <= 0 || amount > count) return false;
        count -= amount;
        return true;
    }
    public int getCount() { return count; }
    public int remainingSpace() { return capacity - count; }
    public String describe() { return item + " " + count + "/" + capacity; }
}
public class L20Example06 {
    public static void main(String[] args) {
        Chest chest = new Chest("钻石", 64);
        if (chest.deposit(12)) System.out.println("已存入");
        int before = chest.getCount();
        boolean ok = chest.withdraw(999);
        System.out.println(!ok && chest.getCount() == before);
    }
}

运行结果

已存入
true

08 / 15

容量与非负库存

SecureChest 的核心不变条件是 0 <= count <= capacity。构造器先建立合法容量,之后每个方法继续守住它。

上下边界像箱子的底和盖
  1. 0合法的空箱
  2. capacity合法的满箱
  3. 负数不允许库存为负
  4. 越界不允许超过容量

L20Example07.java · 完整独立示例

class Chest {
    private final String item;
    private final int capacity;
    private int count;
    public Chest(String item, int capacity) {
        this.item = item == null || item.isBlank() ? "未知物品" : item.trim();
        this.capacity = Math.max(1, capacity);
    }
    public boolean deposit(int amount) {
        if (amount <= 0 || amount > capacity - count) return false;
        count += amount;
        return true;
    }
    public boolean withdraw(int amount) {
        if (amount <= 0 || amount > count) return false;
        count -= amount;
        return true;
    }
    public int getCount() { return count; }
    public int remainingSpace() { return capacity - count; }
    public String describe() { return item + " " + count + "/" + capacity; }
}
public class L20Example07 {
    public static void main(String[] args) {
        Chest chest = new Chest("钻石", 0);
        System.out.println(chest.remainingSpace());
        System.out.println(chest.deposit(1));
        System.out.println(chest.deposit(1));
        System.out.println(chest.withdraw(1));
    }
}

运行结果

1
true
false
true
验收表
输入是否允许原因
存入0否数量必须大于0
存入到刚好满是等于容量合法
取出全部是库存可以为0
取出超过库存否不能为负数

09 / 15

别暴露内部可变数组

private 数组若被 getter 原样返回,外部仍能通过引用修改内部内容。字段虽然 private,后门却敞开了。

把数组引用交出去等于把后门钥匙送人
  1. 内部数组private仍是可变对象
  2. 直接返回会共享内部引用
  3. copyOf新数组,元素引用浅复制
  4. 安全性String不可变,对象仍需检查

L20Example08.java · 完整独立示例

class ItemBox {
    private final String[] items = {"钻石", "火把"};
    public String[] getItemsCopy() {
        return java.util.Arrays.copyOf(items, items.length);
    }
    public String getItem(int index) {
        if (index < 0 || index >= items.length) return null;
        return items[index];
    }
}
public class L20Example08 {
    public static void main(String[] args) {
        ItemBox box = new ItemBox();
        String[] copy = box.getItemsCopy();
        copy[0] = "泥土";
        System.out.println(copy[0]);
        System.out.println(box.getItem(0));
        System.out.println(box.getItem(9));
    }
}

运行结果

泥土
钻石
null

10 / 15

设计箱子的公开接口

这里的“公开接口”指调用者可使用的方法集合,并非 Java 的 interface 关键字。先写调用者需要做什么,再设计最少的方法集合。

先列允许的操作 再决定字段
  1. deposit受控存入
  2. withdraw受控取出
  3. getCount库存查询
  4. remainingSpace余量查询
验收表
公开方法输入返回保证
deposit正数量boolean不超过容量
withdraw正数量boolean不低于0
getCount无int只读取库存
remainingSpace无int只读取空间
describe无String提供摘要

11 / 15

三类测试锁住规则

封装测试不仅看返回值,还要通过公开查询确认对象状态。

正常 边界和错误测试都要检查状态
  1. 正常存入12个
  2. 边界正好存满、正好取空
  3. 错误满箱存入、空箱取出
  4. 状态拒绝后不得改变

L20Example10.java · 完整独立示例

class Chest {
    private final String item;
    private final int capacity;
    private int count;
    public Chest(String item, int capacity) {
        this.item = item == null || item.isBlank() ? "未知物品" : item.trim();
        this.capacity = Math.max(1, capacity);
    }
    public boolean deposit(int amount) {
        if (amount <= 0 || amount > capacity - count) return false;
        count += amount;
        return true;
    }
    public boolean withdraw(int amount) {
        if (amount <= 0 || amount > count) return false;
        count -= amount;
        return true;
    }
    public int getCount() { return count; }
    public int remainingSpace() { return capacity - count; }
    public String describe() { return item + " " + count + "/" + capacity; }
}
public class L20Example10 {
    public static void main(String[] args) {
        Chest chest = new Chest("钻石", 64);
        System.out.println(chest.deposit(12));
        System.out.println(chest.deposit(52));
        System.out.println(chest.deposit(1));
        System.out.println(chest.getCount());
        System.out.println(chest.withdraw(64));
        System.out.println(chest.withdraw(1));
        System.out.println(chest.getCount());
    }
}

运行结果

true
true
false
64
true
false
0
验收表
测试操作预期
正常容量64存入12true 库存12
边界再存入52true 库存64
错误满箱再存1false 库存64
边界取出64true 库存0
错误空箱取1false 库存0

12 / 15

封装故障诊所

编译器能发现类外直接访问 private,却不能自动发现 setter 漏校验或数组引用泄漏。

private 报错和规则漏洞要分开处理
  1. 直接访问编译器阻止
  2. 漏校验编译器不会代做
  3. 先改后拒绝已经破坏状态
  4. 数组泄漏公开了内部可变引用

L20Example11.java · 完整独立示例

class BrokenChest {
    private int count = 0;
    public boolean withdraw(int n) {
        count -= n;
        return count >= 0;
    }
    public int getCount() { return count; }
}
public class L20Example11 {
    public static void main(String[] args) {
        BrokenChest c = new BrokenChest();
        System.out.println(c.withdraw(3));
        System.out.println(c.getCount());
    }
}

运行结果

false
-3

13 / 15

挑战 实现 安全箱子

挑战 程序叫 SecureChest。箱子保存一种物品、容量和数量,外部只能使用公开方法。

private 字段和公开方法共同守住库存规则
  1. 构造器初始化容量与名字
  2. deposit余量足够才加库存
  3. withdraw库存足够才扣除
  4. 查询通过公开方法验证

SecureChest.java · 完整独立示例

class Chest {
    private final String item;
    private final int capacity;
    private int count;
    public Chest(String item, int capacity) {
        this.item = item == null || item.isBlank() ? "未知物品" : item.trim();
        this.capacity = Math.max(1, capacity);
    }
    public boolean deposit(int amount) {
        if (amount <= 0 || amount > capacity - count) return false;
        count += amount;
        return true;
    }
    public boolean withdraw(int amount) {
        if (amount <= 0 || amount > count) return false;
        count -= amount;
        return true;
    }
    public int getCount() { return count; }
    public int remainingSpace() { return capacity - count; }
    public String describe() { return item + " " + count + "/" + capacity; }
}
public class SecureChest {
    public static void main(String[] args) {
        Chest chest = new Chest("钻石", 64);
        System.out.println(chest.deposit(12));
        System.out.println(chest.deposit(52));
        System.out.println(chest.deposit(1));
        System.out.println(chest.describe());
        System.out.println(chest.withdraw(64));
        System.out.println(chest.withdraw(1));
        System.out.println(chest.describe());
        System.out.println(chest.remainingSpace());
    }
}

运行结果

true
true
false
钻石 64/64
true
false
钻石 0/64
64

14 / 15

拓展任务与单元复盘

第17至20课完成了面向对象第一阶段:蓝图、引用、出生和封装。作业要同时提交接口表与测试表。

三档任务继续加固对象边界
  1. 木镐Mob生命改为private
  2. 铁镐维护0到20饥饿值
  3. 钻石镐设计有状态的密码箱
  4. 复盘封装是约束代码访问

三档作业

  • 木镐:把 Mob 的 health 改为 private,提供 takeDamage、heal 和 getHealth。
  • 铁镐:设计 FoodBar,饥饿值始终保持0到20,非法操作返回false。
  • 钻石镐:设计三次输错会锁定的密码箱,不允许外部直接修改密码和锁定状态。
验收表
自评能还要练
我会使用private□□
我会设计行为方法□□
我会验证失败不改状态□□

15 / 15

自己的验收记录

运行之前写预期,运行之后记录实际结果。正常案例说明程序会做事;边界案例说明它知道何时停;错误案例说明它能拒绝不合理的请求。

把证据写进记录
类别你选择的输入预期与实际
正常一个常用值或一次成功操作是否逐行一致?
边界最小值、最大值及相邻值有没有少一格、多一步?
错误非法数量、缺失对象或错误语法是否明确拒绝?状态是否保持?
  • 把一个 Bug 写成:触发条件、现象、原因、最小修复。
  • 整理一个值得记住的规则,用自己的话解释,不能只复制定义。
  • 改变一个示例,保留测试记录,让结果证明你的理解。

TAKE IT WITH YOU

下载本课 PDF

完整课文、代码、图解与练习,选择你需要的语言。