18. 【Java】封装:保护你的数据

摘要: 本文讲解了Java面向对象编程中的核心概念——封装(Encapsulation)。通过生动的比喻和丰富的代码示例,详细介绍了如何使用访问修饰符(private、public等)将类的属性隐藏起来,并通过公共的getter和setter方法进行安全访问。文章不仅阐述了封装的基本原理和JavaBean规范,还探讨了封装与Record类的选择,并通过综合实例展示了封装如何提升代码的安全性、可维护性和扩展性。

关键词: Java封装, 访问修饰符, getter和setter, JavaBean规范, 面向对象编程


上篇文章,我们理解了“类”和“对象”——学会了如何用设计图(类)造出具体的实物(对象)。但我们当时的设计有一个“隐患”:所有属性都是直接暴露的。你可以随意访问和修改一个对象的 nameage,甚至把它改成任何不合理的值。

比如说,我们定义了一个 Student 类,它有 age 属性。你完全可以把 age 设为 -51500,这在语法上是合法的,但在现实中是完全不合理的。

你会说:“谁会把年龄设为负数呢?”确实,正常情况不会。但错误往往是意外的,而不是故意的。一个好的程序应该能够在源头阻止不合理的数据进入系统,而不是依赖使用者“自觉”。

这就是我们今天要讲的主题 —— 封装(Encapsulation)


1. 封装是什么?—— 把“内部”和“外部”隔开

封装,字面上理解就是“装进一个匣子里,封起来”。在面向对象编程中,它的含义是:

把对象的属性和操作属性的方法绑定在一起,并对外部隐藏内部实现的细节。

说得更直白一点:

  • 属性(数据) 应该对“外界”不可见(不能直接访问),只能通过方法来访问和修改。
  • 方法的内部实现 对外部也是不可见的,外部只需要知道“调用这个方法能达到什么效果”,而不需要知道“它是怎么做到的”。

打个比方:你开一台自动洗衣机。你只需要知道“按这个按钮是标准洗,按那个按钮是快速洗”,你不需要知道洗衣机内部的水泵怎么工作、电机怎么转动、程序怎么控制时间。洗衣机把内部复杂性封装起来了,只给你暴露几个简单的按钮(即方法)。如果洗衣机的电路板直接暴露在外面,任你触摸,那不仅不安全,而且你也无从下手。

这就是封装的核心思想:简化使用,保护安全。


2. 访问修饰符 —— 控制“可见性”的大门

Java 通过访问修饰符(Access Modifiers) 来实现封装。它们决定了“谁能访问这个成员(字段或方法)”。

修饰符同一个类同一个包不同包的子类任何地方
private
default(不写)
protected
public

我们一个一个来看。

private —— 最严格的“隐私保护”

private 修饰的成员只能在当前类内部访问。外部任何地方都不能直接访问它。

这是我们实现封装最常用的修饰符。通常,我们把所有属性都设为 private,然后通过 public 的方法来间接访问它们。

public class Student {
    private String name;   // 私有属性,外部不能直接访问
    private int age;

    // 通过公共方法访问
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public —— 完全公开

public 修饰的成员任何地方都能访问。我们通常把方法设为 public,作为对外提供的“服务入口”。

default(包级私有) —— 同一包内的“朋友”

如果不写任何修饰符,就是 default,也叫“包级私有”。它表示只有同一个包(package) 内的类可以访问。这个用得相对较少。

protected —— 留给“子类”的权限

protected 表示“同一个包内 + 不同包的子类”可以访问。它主要用在继承关系中,我们后面会详细讲。


3. 实践:给 Student 加上封装

让我们把之前那个完全暴露的 Student 类改造一下,加上封装:

public class Student {
    // 所有属性都设为 private
    private String name;
    private int age;
    private String studentId;

    // ---- 构造方法 ----
    public Student(String name, int age, String studentId) {
        // 在构造方法中,我们也可以使用 setter 来做校验
        setName(name);
        setAge(age);
        setStudentId(studentId);
    }

    // ---- Getter 方法 ----
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getStudentId() {
        return studentId;
    }

    // ---- Setter 方法 ----
    public void setName(String name) {
        // 可以加一些基本校验
        if (name == null || name.trim().isEmpty()) {
            System.out.println("警告:姓名不能为空!");
            this.name = "未知";
        } else {
            this.name = name;
        }
    }

    public void setAge(int age) {
        // 校验年龄的合理性
        if (age < 0 || age > 150) {
            System.out.println("警告:年龄 " + age + " 不合理,已设为 0。");
            this.age = 0;
        } else {
            this.age = age;
        }
    }

    public void setStudentId(String studentId) {
        if (studentId == null || studentId.trim().isEmpty()) {
            System.out.println("警告:学号不能为空!");
            this.studentId = "未知";
        } else {
            this.studentId = studentId;
        }
    }

    // ---- 业务方法 ----
    public void sayHello() {
        System.out.println("你好,我是 " + name + ",今年 " + age + " 岁,学号 " + studentId);
    }
}

现在,外部代码不能再随意赋值了:

public class Main {
    public static void main(String[] args) {
        Student s1 = new Student("张三", 20, "2024001");
        s1.sayHello();   // 输出:你好,我是 张三,今年 20 岁,学号 2024001

        // 尝试设置不合理的数据
        s1.setAge(-5);   // 会输出警告,并把 age 设为 0
        s1.setName("");  // 会输出警告,并把 name 设为 "未知"
        s1.sayHello();   // 输出:你好,我是 未知,今年 0 岁,学号 2024001
    }
}

你看,封装让我们的类变得“有脑子”了——它能够主动保护自己的数据,拒绝不合理的信息,而不是傻傻地接受任何值。


4. 封装不仅仅是“私有化” —— 更高层的含义

① 把变化隔离

如果某个属性的存储方式发生变化(比如把 ageint 改为 String 存储),只要 getAge()setAge() 的方法签名不变,外部调用者完全不需要修改任何代码。变化被隔离在了类内部。

② 提供“只读”或“只写”的权限

有时候,你希望某个属性只能读,不能改。那你只需要提供 getter,不提供 setter

private String id;   // ID 一旦生成就不能修改

public String getId() {
    return id;
}
// 没有 setter,外部无法修改 id
③ 在方法内部做额外的逻辑

除了参数校验,你还可以在 getter/setter 中做更多的事情,比如:

  • 记录日志(谁在什么时候读取或修改了某个属性)
  • 触发其他事件(修改了价格,自动重新计算总价)
  • 延迟加载(从数据库读取)

5. JavaBean 规范 —— 一种“约定俗成”的写法

在 Java 开发中,尤其是 Spring 等框架中,你经常会听到 JavaBean 这个词。它不是一个类,也不是一种技术,而是一套编码规范

  1. 类是 public 的。
  2. 所有属性都是 private 的。
  3. 每个属性都有对应的 public getter 和 setter 方法(除非该属性是只读的)。
  4. 有一个无参构造方法(即使有有参构造,也建议保留无参构造)。
  5. 实现 Serializable 接口(可选,用于序列化)。

我们平时的 POJO(Plain Old Java Object,普通 Java 对象)类,就是按照这套规范写的。

public class User implements Serializable {
    private String username;
    private String password;
    private int age;

    public User() {}   // 无参构造

    public User(String username, String password, int age) {
        this.username = username;
        this.password = password;
        this.age = age;
    }

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

这套规范的优点是什么?框架能够通过反射机制识别你的类结构,自动完成很多工作(比如序列化、数据绑定、JSON 转换等)。如果你不遵守这个规范,很多框架功能就无法生效。


6. 封装与 Record —— 新方式与旧方式的选择

你可能会想:“我们现在写的 getter/setter 好啰嗦啊,那上一篇学的 Record 不香吗?”

确实,Record 自动生成了构造方法、访问器(name()age())和 toString(),但它不生成 setter,因为 Record 是不可变的。

两者各有适用场景:

场景推荐
数据只读,不需要修改Record,简洁优雅
数据需要修改,有业务逻辑校验✅ 普通类 + 封装(private + getter/setter)
需要兼容框架(如 Spring)普通类更常见,但 Record 也逐渐被支持

在初学阶段,我建议你把普通类的封装写法练熟,因为它在实际工作中仍然是主流。等你熟练之后,再根据场景灵活选择 Record。


7. 一个综合例子:带封装的商品类

我们来写一个稍微复杂的例子——一个 Product(商品)类,包含封装、构造方法、业务方法:

public class Product {
    // ---- 私有属性 ----
    private String id;
    private String name;
    private double price;
    private int stock;   // 库存数量

    // ---- 构造方法 ----
    public Product(String id, String name, double price, int stock) {
        this.id = id;
        setName(name);
        setPrice(price);
        setStock(stock);
    }

    // ---- Getter 方法 ----
    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }

    public int getStock() {
        return stock;
    }

    // ---- Setter 方法(带校验) ----
    public void setName(String name) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("商品名称不能为空");
        }
        this.name = name;
    }

    public void setPrice(double price) {
        if (price < 0) {
            throw new IllegalArgumentException("商品价格不能为负数");
        }
        this.price = price;
    }

    public void setStock(int stock) {
        if (stock < 0) {
            throw new IllegalArgumentException("库存数量不能为负数");
        }
        this.stock = stock;
    }

    // ---- 业务方法 ----
    /**
     * 减少库存(发货时调用)
     */
    public void reduceStock(int quantity) {
        if (quantity <= 0) {
            System.out.println("❌ 减少数量必须大于 0");
            return;
        }
        if (stock < quantity) {
            System.out.println("❌ 库存不足!当前库存:" + stock + ",需要:" + quantity);
            return;
        }
        stock -= quantity;
        System.out.println("✅ 库存已减少 " + quantity + ",剩余库存:" + stock);
    }

    /**
     * 增加库存(补货时调用)
     */
    public void increaseStock(int quantity) {
        if (quantity <= 0) {
            System.out.println("❌ 增加数量必须大于 0");
            return;
        }
        stock += quantity;
        System.out.println("✅ 库存已增加 " + quantity + ",当前库存:" + stock);
    }

    /**
     * 计算折扣价(打 8 折)
     */
    public double getDiscountedPrice(double discountRate) {
        if (discountRate < 0 || discountRate > 1) {
            System.out.println("❌ 折扣率必须在 0~1 之间");
            return price;
        }
        return price * (1 - discountRate);
    }

    /**
     * 显示商品信息
     */
    public void display() {
        System.out.println("商品ID:" + id);
        System.out.println("商品名称:" + name);
        System.out.println("商品价格:¥" + price);
        System.out.println("库存数量:" + stock);
    }
}
public class Main {
    public static void main(String[] args) {
        Product book = new Product("P001", "Java编程思想", 99.0, 50);

        book.display();
        System.out.println("-------------------");

        // 尝试打折
        double discounted = book.getDiscountedPrice(0.2);
        System.out.println("8折后价格:¥" + discounted);

        // 尝试减少库存
        book.reduceStock(10);   // 库存减少 10
        book.reduceStock(100);  // 库存不足,报错

        // 尝试设置不合理的数据(会抛异常)
        try {
            book.setPrice(-10);
        } catch (IllegalArgumentException e) {
            System.out.println("捕获异常:" + e.getMessage());
        }
    }
}

这个例子展示了封装如何让一个类变得安全(数据校验)和自包含(数据 + 操作数据的方法放在一起)。


8. 封装与面向对象的关系

封装是面向对象三大特性中的第一个(另外两个是继承和多态)。它奠定了面向对象的基础:

  • 没有了封装,对象的属性可以被随意篡改,数据完整性无法保证。
  • 没有了封装,对象的内部实现对外部透明,一旦实现改变,所有依赖的代码都要修改。
  • 没有了封装,对象就只是一个“数据袋子”,而不是一个有“行为”和“职责”的实体。

可以说,封装是面向对象的“第一块砖”


今天的总结

今天,我们彻底理解了封装:

  • 封装:把数据和操作数据的方法绑在一起,对外部隐藏内部细节。
  • 访问修饰符private(最严格)、default(包级)、protected(继承相关)、public(完全公开)。
  • 通常的做法:属性用 private,方法用 public
  • Getter/Setter 不只是“读取/设置”,它们提供了校验、隔离变化、控制权限的机会。
  • JavaBean 规范:private 属性 + public getter/setter + 无参构造。
  • 封装让代码更安全、更可维护、更易扩展

现在你写的类,已经有模有样了。它不再是一堆数据的堆砌,而是一个能自我保护的“小实体”。


动手试试

  1. 把上篇文章的 Book 类改造成符合封装规范的版本:所有属性 private,提供 getter/setter,在 setter 中加入合理的校验(价格不能为负、书名不能为空等)。
  2. 设计一个 BankAccount(银行账户)类,包含 accountNumberbalance 属性。提供 deposit(double amount)withdraw(double amount) 方法,确保余额不能为负,取款不能超出余额。
  3. 写一个 Temperature 类,内部用 double celsius 存储温度。提供 getCelsius()setCelsius()getFahrenheit()setFahrenheit() 方法,实现摄氏度和华氏度的自动转换。(公式:F = C * 9/5 + 32
  4. (挑战)结合第 6 篇的“名片生成器”和第 17 篇的 Person 类,把 Person 类的属性改为 private,在 setter 中加入校验(年龄 0~150,姓名非空),并重构 printCard() 方法直接使用私有属性。

多写几个封装的类,你会越来越习惯“把数据藏起来”的思维方式。这种“藏”不是不让人用,而是让人通过正规的、受控的途径去用。

我们下一篇见。😃

📌 获取本系列示例代码请访问 GitCode

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值