☕ 第三章 面向对象(上)

3.3 封装性

📄 一句话概括

封装就是把属性藏起来(private 上锁),再开两个受控的门(getter 看、setter 改)——让"改数据"这件事必须经过类的安检,垃圾数据进不了门。


1. 为什么需要封装——3.2 留下的隐患

先看 3.2 写的 Car 类,它有个大问题:

public class Car {
    String brand;   // 品牌
    double price;   // 价格
}

属性直接暴露在外,谁拿到对象谁就能随手改:

public static void main(String[] args) {
    Car car = new Car();
    car.brand = "宝马";
    car.price = 200000;    // 合理
    car.price = -99999;    // 价格是负的?!
    car.brand = "一点五辆宝马";  // 随便塞什么都行
    System.out.println(car.price);   // 输出 -99999.0,程序照跑
}
⚠️ 问题所在

car.price = -99999; 这行编译不会报错——在 Java 眼里它完全合法(double 格子确实装得下 -99999)。但业务上是垃圾数据。编译器管不了业务规则,所以得让类自己管。

这不是某个人的错,是设计的漏洞:改数据这件事"谁来改、什么时候改、改成什么"完全失控。封装就是补这个洞的。


2. private——给属性上锁

第一步,把属性藏起来。改一个关键字:

public class Car {
    private String brand;   // 私有:只有本类内部能访问
    private double price;
}

private 是访问修饰符,意思是"私有的"——出了这个类,谁都不许碰。验证一下:

Car car = new Car();
car.price = -99999;   // 编译报错!
✅ 报错信息

java: price 在 Car 中是 private 访问控制

好消息:垃圾数据当场被拦下了。坏消息:合法的赋值(car.price = 200000;)也被拦了——门全关死了,自己人都进不来。

所以还需要第二步:开两扇受控的门。


3. getter 和 setter——两扇带门卫的门

给每个私有属性配两个公开方法:

public class Car {
    private String brand;
    private double price;

    // setter:写入器——别人改值走这扇门,门里有安检
    public void setPrice(double p) {
        if (p >= 0) {              // 安检:业务规则写在这
            price = p;             // 合法,收下
        } else {
            System.out.println("价格不能为负,赋值失败");
        }
    }

    // getter:读取器——别人看值走这扇门
    public double getPrice() {
        return price;
    }
}

调用方(TestCar)现在长这样:

Car car = new Car();
car.setPrice(200000);    // 门卫检查:合法 → 收下
car.setPrice(-99999);    // 门卫检查:负数 → 拒绝,打印提示
double p = car.getPrice();   // 只准看,不准直接摸
💡 命名规范(固定格式,必须照抄)
  • 属性 price → 读的方法叫 getPrice(),写的方法叫 setPrice(参数)
  • 格式:get/set + 属性名首字母大写
  • boolean 属性的 getter 惯例用 isXxx(),比如 isRich()

这个格式是全 Java 圈的统一惯例,不是语法强制的,但没人敢不遵守——很多框架就靠这个名字找方法。

为什么要分两扇门? 因为门是可以单独控制的:


4. this 关键字——"我这个对象的"

上面 setter 里的参数名 p 其实偷懒了。标准写法参数名应该和属性名一致:

public void setPrice(double price) {   // 参数也叫 price
    if (price >= 0) {
        this.price = price;   // this.price 是属性,price 是参数
    }
}

问题来了:两个 price,到底谁是谁?

💡 this 的读音

把 this 读成"我这个"。this.price = price; 的意思是:"把我这个对象的 price 属性,赋成传进来的参数 price"。左边是属性,右边是参数,靠 this 区分。

省略 this 行不行?行,但只在不撞名的时候:

public void setPrice(double p) {
    price = p;        // 不撞名,this 可以省
}
⚠️ 误区 1:以为 this 是"类"

this 指的不是 Car 这个图纸,而是当前正在执行方法的那个具体对象。car1.setPrice(...) 执行时,this 就是 car1;car2.setPrice(...) 执行时,this 就是 car2。3.2 学的"new 一次造一辆",每辆车各有自己的 price 格子,this 就是站在某辆车面前说"你这辆车"。


5. 封装的标准姿势(三件套)

以后每写一个类,标准流程就三步,一个都不能少:

步骤 代码 作用
① 属性全部 private private double price; 上锁,外界摸不到
② 配 getter public double getPrice() 开"看"的门
③ 配 setter public void setPrice(double price) 开"改"的门,门里安检

方法一般 public(方法是对外的功能接口,藏着别人没法用),属性一般 private(属性是内部状态,藏着才安全)——记住这句口诀。

❓ 疑问:多写这么一堆方法,不累吗?直接 public 属性多省事?

省的是眼前几行代码,赔的是将来整个项目。举例:项目跑了一年,100 处代码直接改 car.price,现在要求"价格改动必须写日志"——你得翻 100 个地方。封装后只有 setter 一个门,改一处,全项目生效。封装就是把"变化"圈在一个地方。这是它真正的价值,安检只是顺手的好处。


6. IDEA 快捷生成——门卫不用手写

getter/setter 是纯格式化代码,IDEA 一键生成(保姆式):

  1. 在类里右键 → Generate(或按 Alt + Insert)
  2. 弹出菜单选 Getter and Setter
  3. 按住 Ctrl 点选要生成属性的 getter 和 setter(或 Ctrl+A 全选)
  4. 回车——全部生成完毕
💡 生成的代码和自己手写的一模一样,但注意看一遍

IDEA 生成的 setter 是最朴素版(没有安检):this.price = price; 直接赋值。安检的业务规则永远要你手动补——机器懂格式,不懂你的业务。


7. 动手部分:把 3.2 的 Car 封装一遍

第 1 步:在 IDEA02 项目 src 下新建类 Student(换个人物练,顺便巩固)。

第 2 步:抄入:

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) {   // 分数必须在 0~100 之间
            this.score = score;
        } else {
            System.out.println("分数不合法(0~100),赋值失败");
        }
    }

    public int getScore() {
        return score;
    }

    public void showInfo() {
        System.out.println("姓名:" + name + ",分数:" + score);
    }
}

第 3 步:新建测试类 TestStudent:

public class TestStudent {
    public static void main(String[] args) {
        Student stu = new Student();
        stu.setName("波仔");
        stu.setScore(92);
        stu.setScore(150);     // 越界 → 门卫拒绝
        stu.showInfo();        // 姓名:波仔,分数:92(150 没进得来)
    }
}

看到什么算成功:控制台先打印"分数不合法(0~100),赋值失败",再打印"姓名:波仔,分数:92"。150 被门卫拦在门外——这就是封装的现场。

三个 30 秒实验:

  1. 把 stu.setScore(92) 改成 stu.score = 92——编译报错,亲眼看看"上锁"长什么样
  2. 用 Alt + Insert 一键重新生成 getter/setter,对比手写的(生成版 setter 没安检,别用它覆盖你加了安检的)
  3. 把 setter 安检里的 else 分支删掉会怎样?——不合法的值被默默忽略,连提示都没有(不报错但更迷惑,所以提示很重要)

8. 常见误区

⚠️ 误区 1:封装就是把属性设成 private

只上锁不开门,属性彻底没法用——那不叫封装,叫自断后路。封装 = 藏起来 + 受控地暴露,两个动作缺一不可。

⚠️ 误区 2:getter/setter 是语法规定必须写的

不是语法规定,是工程惯例。Java 语法只管 private 挡外人,getter/setter 是全行业的标准接口约定,框架靠它工作,不写寸步难行。

⚠️ 误区 3:this 能省就省

参数名和属性名撞名时,this 不能省(省了就全指参数,自己给自己赋值,属性永远收不到值)。不撞名时省略没问题,但初学建议全写上——写清楚了才不会糊涂。

⚠️ 误区 4:private 了外面还能 new,锁了个寂寞

new 造对象和访问属性是两回事。private 锁的是属性访问(car.price 这种点号直取),不影响你造对象。想连 new 都管控,那是后面"构造方法"的事。

⚠️ 误区 5:安检逻辑写在外面的调用方

if (score >= 0 && score <= 100) { stu.setScore(score); } ——安检写在调用方,等于门卫失业:谁绕过你这个 if,垃圾数据照样进。规则必须写在类的 setter 里,和属性锁在一起才跑不掉。


9. 自测题

❓ Q1:`private` 的作用是什么?只写 private 不写 getter/setter,会发生什么?

private 把属性限制在本类内部可访问,类外(包括 main 方法里)用点号直接访问会编译报错。只锁不开门,外界既不能读也不能写——属性成了摆设。所以封装必须配 getter/setter。

❓ Q2:`this.price = price;` 这行里,两个 price 分别指什么?

左边 this.price 是当前对象的属性(堆里那辆车自己的格子);右边 price 是方法参数(刚传进来的值)。this 就是"我这个",靠它区分撞名的两个变量。

❓ Q3:`setScore(150)` 传入 150,程序会崩溃吗?属性值会变吗?

都不会。安检代码发现 150 不在 0~100 之间,走 else 分支打印提示,赋值语句没执行,属性保持原值。程序照常跑完——这正是封装的价值:垃圾数据被拦,程序不崩。

❓ Q4:String 类型的属性 name,它的 getter 该怎么写?返回值类型是什么?

public String getName() { return name; }——返回值类型是 String(和属性类型一致),方法名 getName(get + 首字母大写的属性名)。没有参数,因为它不需要你给它什么,只要把值交出去。

❓ Q5(挑战):为什么说"安检写在 setter 里"比"写在调用方"更安全?用一句完整的话说清楚。

因为 setter 是属性的唯一入口(private 锁死了其他路),安检写在门里就谁也绕不过去;写在调用方则只是"某一个调用者自觉",任何新代码或别人绕开它直接调 setter,垃圾数据照样进——规则和门锁在一起,才是真的门禁。


10. 下一步学什么


相关笔记