封装与 JavaBean / 实体类 | JavaSE

封装与 JavaBean / 实体类

一、学习目标

学完本章,你应该能够:

  • 能够解释封装(Encapsulation)的核心思想以及“合理隐藏,合理暴露”的含义。
  • 能够使用 private 隐藏对象内部数据。
  • 能够使用 getter / setter 为外部代码提供受控的数据访问入口。
  • 能够在 setter 中进行基本的数据合法性校验。
  • 能够按照当前 JavaSE 阶段的规范独立设计一个实体类。
  • 能够区分“实体类”“JavaBean 风格类”与普通业务类的职责。
  • 能够将数据对象与业务操作对象进行基本职责拆分。

二、核心知识

2.1 从一个危险的 Student 类开始

假设:

public class Student {

    String name;
    int age;
    double score;
}

外部代码可以直接:

Student student = new Student();

student.name = "张三";
student.age = 18;
student.score = 95;

看起来非常方便。

但问题也非常明显。

外部同样可以:

student.age = -100;
student.score = 99999;

从 Java 语法上来说,只要这些字段允许被直接访问,这些代码完全可能执行。

问题在于:

对象自己的数据完全暴露给了外部代码。

任何调用者都可以任意修改。

这就破坏了对象对自身状态的控制能力。

于是出现了面向对象中最核心的思想之一:

封装(Encapsulation)。


2.2 什么是封装

封装是面向对象程序设计的重要特征之一。

初学阶段可以把它理解成一句话:

合理隐藏对象内部实现,合理暴露对象对外能力。

例如一个银行账户对象:

BankAccount

内部可能保存:

balance

余额当然属于账户对象。

但是我们一般不会希望外部代码直接:

account.balance = -100000000;

更加合理的设计应该是:

account.deposit(1000);
account.withdraw(500);

让对象自己决定:

  • 哪些数据可以修改;
  • 数据怎样修改;
  • 修改前需要什么校验;
  • 什么操作是不允许的。

这就是封装真正的价值。


2.3 面向对象三大特征

Java 入门课程通常把面向对象核心特征概括为:

封装
继承
多态

也就是:

Encapsulation
Inheritance
Polymorphism

当前首先学习:

封装

后面的章节将继续学习:

继承
多态

三者最终共同构成 Java 面向对象设计的重要基础。


2.4 “合理隐藏,合理暴露”

封装并不是:

什么都藏起来。

也不是:

所有东西都公开。

而是:

根据对象职责决定什么应该隐藏,什么应该暴露。

例如:

public class Student {

    private int age;
}

这里:

private

表示该成员只能在当前类内部直接访问。

外部不能:

student.age = 18;

于是对象获得了控制权。

如果确实需要允许外部修改年龄,就可以设计一个公开方法:

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

如果需要允许外部读取:

public int getAge() {
    return age;
}

于是形成:

外部代码
   ↓
public 方法
   ↓
对象内部 private 数据

这就是最基础的封装结构。


2.5 private

private 是 Java 的访问修饰符之一。

例如:

public class Student {

    private String name;
    private int age;
    private double score;
}

这些成员变量只能在 Student 类内部被直接访问。

例如:

public class Student {

    private int age;

    public void printAge() {
        System.out.println(age);
    }
}

合法。

但是其他类:

public class Test {

    public static void main(String[] args) {

        Student student = new Student();

        student.age = 18;
    }
}

会产生编译错误。

因为:

age

已经被 private 隐藏。


2.6 getter 和 setter

如果成员变量私有以后,外部确实需要读取和修改数据怎么办?

通常可以提供访问方法。

例如:

public class Student {

    private int age;

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

    public int getAge() {
        return age;
    }
}

其中:

setAge()

用于设置数据。

getAge()

用于获取数据。

测试:

Student student = new Student();

student.setAge(18);

System.out.println(student.getAge());

于是:

外部不能直接访问 age
        ↓
必须经过 setAge / getAge
        ↓
Student 获得控制数据访问的能力

2.7 setter 为什么非常重要

假设成员变量直接公开:

public int age;

任何外部代码都可以:

student.age = -100;

但如果隐藏:

private int age;

然后提供:

public void setAge(int age) {

    if (age >= 0 && age <= 150) {
        this.age = age;
    } else {
        System.out.println("年龄数据非法");
    }
}

外部:

student.setAge(-100);

就会被对象自己的规则拦截。

因此 setter 不只是:

给字段赋值的方法。

它还是:

对象控制状态变化的一个入口。


2.8 getter 也不意味着所有数据都必须暴露

很多初学教程会机械地:

每个字段生成 getter 和 setter。

但真正的封装思想不是:

private + 全部 getter/setter

而是:

只暴露业务真正需要暴露的能力。

比如用户密码:

private String password;

是否应该提供:

public String getPassword()

就必须根据实际系统需求判断。

甚至某些数据可能:

  • 只能读,不能写;
  • 只能写,不能读;
  • 只能通过业务方法改变;
  • 完全不应该暴露。

例如:

public class BankAccount {

    private double balance;

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
}

这里没有:

setBalance()

因为余额不应该被调用者任意覆盖。

这才是“合理隐藏、合理暴露”。


2.9 什么是实体类

在当前 JavaSE 学习阶段,可以把实体类理解为:

主要负责保存某类事物数据的 Java 类。

例如:

Student

用于保存学生信息:

姓名
年龄
性别
数学成绩
英语成绩

例如:

public class Student {

    private String name;
    private int age;
    private char sex;
    private double math;
    private double english;
}

它的主要职责不是执行复杂业务,而是:

表达和保存学生数据。


2.10 当前课程中的实体类规范

按照本课程当前阶段的教学规范,一个典型实体类通常设计为:

  1. 成员变量使用 private
  2. 根据需要提供 public getter / setter。
  3. 主动提供无参数构造器。
  4. 可以根据需要提供有参数构造器。
  5. 主要负责数据存取。
  6. 复杂业务逻辑尽量由其他业务类承担。

例如:

public class Student {

    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

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

2.11 JavaBean 和实体类是不是完全相同

原课程为了入门教学,把:

JavaBean
实体类

放在一起讲。

这种方式有利于快速建立编码规范,但需要形成一个更准确的认识:

JavaBean 和“业务实体类”在严格意义上并不是完全相同的概念。

JavaBeans 本身是一套 Java 组件设计约定,支持工具通过标准命名规则识别 Bean 的属性、方法和事件。

例如:

public String getName()

和:

public void setName(String name)

能够形成名为:

name

的 Bean Property。

而我们现在学习的:

Student
User
Movie
Book

更多是在学习一种:

JavaBean-style 的数据类 / 实体类编码方式。

因此本教程后续使用“JavaBean / 实体类”时,要理解成:

当前 JavaSE 阶段采用 JavaBean 风格规范设计的数据承载类。

不要简单记成:

JavaBean = 数据库实体类

这是不严谨的。


2.12 boolean 属性 getter

普通属性:

private String name;

通常:

public String getName() {
    return name;
}

布尔属性在 JavaBeans 命名约定中还常见:

private boolean enabled;

public boolean isEnabled() {
    return enabled;
}

也就是:

getXxx()
setXxx()

以及 boolean 常见的:

isXxx()

这些命名方式未来会在很多 Java 框架中反复出现。


三、使用方法

3.1 封装 Student

首先定义:

public class Student {

    private String name;
    private int age;
    private double score;

    public Student() {
    }

    public Student(
            String name,
            int age,
            double score
    ) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {

        if (age >= 0 && age <= 150) {
            this.age = age;
        } else {
            System.out.println("年龄数据非法");
        }
    }

    public double getScore() {
        return score;
    }

    public void setScore(double score) {

        if (score >= 0 && score <= 100) {
            this.score = score;
        } else {
            System.out.println("成绩必须位于0~100之间");
        }
    }
}

测试:

public class Test {

    public static void main(String[] args) {

        Student student = new Student();

        student.setName("张三");
        student.setAge(18);
        student.setScore(95);

        System.out.println(student.getName());
        System.out.println(student.getAge());
        System.out.println(student.getScore());
    }
}

现在外部不能直接:

student.age = -100;

而只能通过:

student.setAge(...)

修改年龄。

对象的数据控制能力因此增强。


3.2 构造器与 setter 的校验问题

注意下面的设计:

public Student(String name, int age) {
    this.name = name;
    this.age = age;
}

虽然:

setAge()

进行了年龄校验:

public void setAge(int age) {
    if (age >= 0 && age <= 150) {
        this.age = age;
    }
}

但调用者仍然可能:

new Student("张三", -100);

因为构造器直接进行了:

this.age = age;

这说明一个非常重要的问题:

如果一个字段存在数据约束,那么对象所有可能修改该字段的入口都应该维护相同的不变量。

初学阶段可以简单复用 setter:

public Student(String name, int age) {
    this.name = name;
    setAge(age);
}

这样可以避免重复:

if (age >= 0 && age <= 150)

更复杂的异常处理策略将在后续异常章节学习。


3.3 完整实体类案例

public class User {

    private long id;
    private String username;
    private String password;
    private String email;

    public User() {
    }

    public User(
            long id,
            String username,
            String password,
            String email
    ) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

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

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

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

注意这里故意没有提供:

getPassword()

这是为了说明:

getter / setter 是否存在应该服务于设计,而不是机械生成。

当然,真实业务中的密码安全问题远比这一示例复杂,这里只用于理解封装思想。


四、原理与进阶

4.1 private 并不等于绝对安全

private 提供的是:

Java 语言层面的访问控制。

它能够阻止普通外部代码直接访问成员。

例如:

student.age

无法访问 private 字段。

但不能把 private 理解成:

加密。

它与:

  • 密码学;
  • 数据库权限;
  • 网络安全;
  • 身份认证;

完全不是同一个层面的概念。

所以:

private String password;

不意味着密码已经安全。


4.2 封装的真正价值是维护对象状态

假设:

public class BankAccount {

    public double balance;
}

外部可以:

account.balance = -999999;

此时对象没有任何机会保护自己。

改成:

public class BankAccount {

    private double balance;

    public void deposit(double amount) {

        if (amount > 0) {
            balance += amount;
        }
    }

    public boolean withdraw(double amount) {

        if (amount <= 0 || amount > balance) {
            return false;
        }

        balance -= amount;
        return true;
    }

    public double getBalance() {
        return balance;
    }
}

调用者只能:

account.deposit(1000);
account.withdraw(500);

余额的改变由:

BankAccount

自己控制。

这才是比“private + getter/setter”更高一层的封装理解:

让对象自己维护自己的合法状态。


4.3 getter / setter 是方法,不是字段本身

例如:

private int age;

public int getAge() {
    return age;
}

调用:

student.getAge();

实际上执行的是一个方法。

因此 getter 完全可以:

public int getAge() {
    System.out.println("正在读取年龄");
    return age;
}

setter 也可以:

  • 校验;
  • 转换;
  • 记录;
  • 拒绝非法数据。

这就是为什么通过方法访问数据比直接暴露字段拥有更强的控制能力。


五、实践应用

5.1 数据与业务逻辑分离

原课程给出了非常重要的一种入门设计方式:

Student
        ↓ 保存数据

StudentOperator
        ↓ 处理学生业务

例如实体类:

public class Student {

    private String name;
    private double math;
    private double english;

    public Student() {
    }

    public Student(
            String name,
            double math,
            double english
    ) {
        this.name = name;
        this.math = math;
        this.english = english;
    }

    public String getName() {
        return name;
    }

    public double getMath() {
        return math;
    }

    public double getEnglish() {
        return english;
    }
}

业务操作类:

public class StudentOperator {

    private Student student;

    public StudentOperator(Student student) {
        this.student = student;
    }

    public void printTotalScore() {

        double total =
                student.getMath()
                        + student.getEnglish();

        System.out.println(
                student.getName()
                        + "的总成绩:"
                        + total
        );
    }

    public void printAverageScore() {

        double average =
                (student.getMath()
                        + student.getEnglish()) / 2;

        System.out.println(
                student.getName()
                        + "的平均成绩:"
                        + average
        );
    }
}

启动类:

public class Test {

    public static void main(String[] args) {

        Student student =
                new Student(
                        "张三",
                        90,
                        95
                );

        StudentOperator operator =
                new StudentOperator(student);

        operator.printTotalScore();
        operator.printAverageScore();
    }
}

于是形成:

Student
负责保存数据

StudentOperator
负责操作数据

Test
负责启动程序和组织对象协作

这是以后 Java Web 中:

Entity / DTO
Service
Controller

等职责拆分思想的非常早期雏形。

但现阶段不要提前套框架,只需要理解:

一个类最好拥有清晰的主要职责。


5.2 Movie 案例

数据类:

public class Movie {

    private String name;
    private double score;
    private double price;

    public Movie() {
    }

    public Movie(
            String name,
            double score,
            double price
    ) {
        this.name = name;
        this.score = score;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public double getScore() {
        return score;
    }

    public double getPrice() {
        return price;
    }
}

未来可以再设计:

MovieOperator

负责:

  • 展示电影;
  • 查询电影;
  • 统计评分;
  • 计算价格。

而不是把所有逻辑全部塞入:

Movie

中。


六、常见问题

6.1 封装是不是把成员变量全部 private?

这是常见实现方式,但不是封装思想的完整定义。

更准确地说:

封装关注的是隐藏不应该暴露的实现,并提供合理的公开接口。

private 只是实现封装的重要工具。


6.2 private 成员变量还能访问吗?

可以。

本类内部可以直接访问:

private int age;

public void printAge() {
    System.out.println(age);
}

外部代码则不能直接访问。


6.3 private 字段是不是一定要提供 getter 和 setter?

不是。

根据需求决定。

例如:

private double balance;

可能只提供:

getBalance()

而不提供:

setBalance()

余额只能通过:

deposit()
withdraw()

修改。


6.4 getter 和 setter 有什么意义?

主要意义包括:

  • 控制读取与修改权限;
  • 校验输入;
  • 维护对象合法状态;
  • 隐藏内部实现;
  • 为外部提供稳定访问接口。

6.5 实体类一定必须有无参构造器吗?

从 Java 语言本身来说:

普通类并没有“实体类必须提供无参构造器”的语法规则。

但在本课程当前 JavaBean-style 实体类规范中,我们主动提供:

public Student() {
}

这样也有利于后续一些框架、工具或对象创建方式使用。

因此应该区分:

Java语言强制规则

和:

课程 / 框架 / 工程约定

不能混为一谈。


6.6 JavaBean 就等于实体类吗?

不严格等同。

当前课程为了入门方便将二者一起讲解。

JavaBean 本质上来自 JavaBeans 组件规范与命名约定,而业务实体类强调:

对业务数据的建模和承载。

很多实体类采用 JavaBean 风格设计,所以二者经常长得非常相似。


6.7 getter 必须叫 getXxx 吗?

按照 JavaBeans 属性命名约定,常见:

getName()
setName()

对于 boolean 属性还常见:

isEnabled()

这些命名未来会被大量 Java 工具和框架识别。


6.8 实体类是不是不能有任何业务方法?

不能机械理解。

实体对象可以拥有与自身状态密切相关的行为。

当前课程将数据与业务操作拆开,是为了帮助初学者先建立:

数据职责与业务职责分离

的意识。

真正的领域建模是否采用贫血模型、充血模型等设计,将属于更高层次的软件设计问题,不需要在 JavaSE 入门阶段展开。


七、练习与验收

7.1 知识问答

  1. 面向对象通常所说的三大特征是什么?
  2. 什么是封装?
  3. “合理隐藏,合理暴露”是什么意思?
  4. private 有什么作用?
  5. private 成员变量为什么不能被其他类直接访问?
  6. 外部代码如何合理访问 private 数据?
  7. getter 和 setter 分别承担什么职责?
  8. setter 为什么可以成为数据合法性校验入口?
  9. 为什么不建议把所有成员变量都直接声明成 public?
  10. private 字段是不是一定要同时提供 getter 和 setter?
  11. 什么是实体类?
  12. 当前课程中的实体类通常采用什么编码规范?
  13. 为什么实体类通常主动提供无参构造器?
  14. JavaBean 与业务实体类是不是严格相同概念?
  15. 为什么 StudentStudentOperator 可以进行职责拆分?

7.2 代码阅读

阅读:

public class Student {

    private int age;

    public void setAge(int age) {

        if (age >= 0 && age <= 150) {
            this.age = age;
        }
    }

    public int getAge() {
        return age;
    }
}

测试:

Student student = new Student();

student.setAge(18);
student.setAge(-100);

System.out.println(student.getAge());

不要运行代码,分析:

  1. 第一次调用 setAge() 后对象状态是什么?
  2. 第二次调用为什么不会正常修改年龄?
  3. 最终 getAge() 返回什么?
  4. 如果把 age 改成 public,这种保护还能否保证?

阅读:

public class User {

    private String username;
    private boolean enabled;

    public String getUsername() {
        return username;
    }

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

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
}

回答:

  1. 哪两个 JavaBean Property 被暴露?
  2. 为什么 boolean 属性可能使用 isEnabled()
  3. 外部是否可以直接写 user.enabled = true
  4. 应该通过什么方法修改?

7.3 手写代码

任务一:Student

设计:

Student

包含:

name
age
score

要求:

  • 所有字段合理私有化。
  • age 合法范围为 0~150。
  • score 合法范围为 0~100。
  • 提供无参构造器。
  • 提供完整参数构造器。
  • 提供合理 getter / setter。
  • 创建对象进行测试。

任务二:User 实体类

设计:

User

包含:

id
username
password
email

要求:

  1. 成员变量私有。
  2. 提供无参数构造器。
  3. 提供有参数构造器。
  4. 根据业务含义判断每个字段是否都应该同时拥有 getter 和 setter。
  5. 写出你的设计理由。

任务三:Movie

分别设计:

Movie
MovieOperator
Test

要求:

Movie
负责保存电影数据

MovieOperator
负责电影数据的展示和查询

Test
负责创建对象和组织调用

不要把全部职责写在一个类中。


7.4 Debug

代码:

public class Student {

    private String name;
    private int age;
}

测试:

Student student = new Student();

student.name = "张三";
student.age = 18;

回答:

  1. 为什么编译失败?
  2. 是对象不存在,还是访问权限有问题?
  3. 应该如何在保持封装的前提下修改代码?

继续分析:

public class Student {

    private int age;

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

业务要求:

年龄只能位于0~150

回答:

  1. 当前 setter 是否真正保护了对象状态?
  2. 应该在哪里加入校验?
  3. 如果构造器也可以设置 age,构造器是否也应该维护这一规则?

7.5 综合训练

设计一个:

BankAccount

包含:

accountNumber
owner
balance

要求:

  1. 不允许外部直接修改 balance
  2. 提供查询余额的能力。
  3. 提供 deposit(double amount)
  4. 提供 withdraw(double amount)
  5. 存款金额必须大于 0。
  6. 取款金额必须大于 0。
  7. 取款金额不能超过余额。
  8. 不允许提供能够直接任意设置余额的 setBalance()

完成后回答:

为什么这种设计比简单的 private balance + getBalance() + setBalance() 更符合封装思想?


7.6 本章验收

不查看资料,确认自己是否能够:

  • [ ] 解释面向对象三大特征。
  • [ ] 用自己的话解释封装。
  • [ ] 解释“合理隐藏、合理暴露”。
  • [ ] 独立使用 private 隐藏数据。
  • [ ] 手写 getter。
  • [ ] 手写 setter。
  • [ ] 在 setter 中进行数据合法性校验。
  • [ ] 解释为什么不是所有字段都必须拥有 getter / setter。
  • [ ] 从零手写一个规范的 Student 实体类。
  • [ ] 解释 JavaBean 与实体类为什么不是严格同义词。
  • [ ] 将 StudentStudentOperator 的职责进行拆分。
  • [ ] 设计一个不能被外部任意修改余额的 BankAccount

如果你只是会让 IDEA 自动生成 getter / setter,却无法解释为什么需要封装,那么这一章仍然没有真正掌握。