🐸 青蛙编程网
首页 / Java / 面向对象 / 2. 封装

封装:属性与方法

这是什么:封装(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,校验加中间,安全又体面。

动手改

  1. 把 setScore 的校验改成"只允许 0~150",传入 160 看看输出
  2. 在 setter 里加一句 System.out.println("成绩已更新"),观察每次赋值都会打印
  3. 故意在 main 里写 s.score = 59;,亲眼看 private 给你的编译报错
  4. 给 Student 加一个只读属性 studentId(用 final + 无 setter),在构造方法里赋值

下一章:继承与多态,让子类站在父类的肩膀上 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。