类与封装
字段、构造器、getter/setter。
类与封装
这一课你将能做到
- 独立设计一个实体类:私有字段、多个构造器、getter/setter、toString——与 Spring 后端的 User/Order 实体同款结构
- 说清 static 成员和实例成员的区别,用代码验证「static 属于类、实例属于对象」
- 会用 JDK 17 的
record 写不可变数据类,一行顶过去三十行
为什么要学
前 6 课你写的都是「过程式」代码:数据散落在一堆变量里,方法单独存在。真实后端不是这样——本站处理的是一个又一个「用户」「订单」,每个用户有 id、用户名、状态,还有配套的行为(校验、展示)。把数据和操作数据的方法打包在一起,就是面向对象(OOP)。
封装是 OOP 第一支柱:字段设为 private,外界只能通过方法访问。好处:数据坏了能定位到「哪个 setter/方法改的」;校验逻辑只有一处;内部实现随便改,外部不受影响。Spring 后端里实体、DTO、配置类,全是封装思想的具体化。
先建立图像
类 User = 设计图纸 对象 u1 = 按图纸造出来的实例
┌─────────────────────┐ ┌─────────────────────┐
│ - id : long │ │ id = 1001 │
│ - username : String │ new │ username = "tom" │
│ - status : int │ ────▶ │ status = 1 │
├─────────────────────┤ ├─────────────────────┤
│ + getUsername() │ │ (共用同一套方法) │
│ + setStatus(int) │ │ │
└─────────────────────┘ └─────────────────────┘
图纸只有一份,实例可以造任意多个,各自有自己的字段值。
跟着做
步骤 1:写一个标准实体类
新建 User.java(注意:一个文件可以有多个非 public 类,本课示例就放在一个文件里,类不加 public,用 java Xxx.java 直跑):
class User {
// 1. 私有字段:外界不能直接碰
private long id;
private String username;
private int status; // 0=正常 1=封禁
// 2. 无参构造器(给框架反射用,如 MyBatis、Jackson)
public User() {
}
// 3. 全参构造器:new User(1001, "tom", 0) 时调用
public User(long id, String username, int status) {
this.id = id; // this.id 是字段,id 是参数
this.username = username;
this.status = status;
}
// 4. getter/setter:受控的读写通道
public long getId() { return id; }
public String getUsername() { return username; }
public int getStatus() { return status; }
public void setStatus(int status) {
// 封装的价值:校验逻辑只写一次,所有赋值都会经过这里
if (status != 0 && status != 1) {
throw new IllegalArgumentException("非法状态: " + status);
}
this.status = status;
}
@Override
public String toString() { // 打印对象时自动调用
return "User{id=%d, username=%s, status=%d}".formatted(id, username, status);
}
}
public class OopDemo {
public static void main(String[] args) {
User u1 = new User(1001, "tom", 0); // 创建实例
User u2 = new User(1002, "jerry", 1);
System.out.println(u1); // 自动调 toString
System.out.println(u2);
// System.out.println(u1.username); // 编译错!private 字段外部不可见
System.out.println(u1.getUsername()); // 必须走方法
u2.setStatus(0); // 走 setter,合法
u1.setStatus(5); // 运行时报错:非法状态 5
}
}
User{id=1001, username=tom, status=0}
User{id=1002, username=jerry, status=1}
tom
Exception in thread "main" java.lang.IllegalArgumentException: 非法状态: 5
步骤 2:this 的三副面孔
class Account {
private String owner;
private double balance;
public Account(String owner, double balance) {
this.owner = owner; // 面孔 1:区分字段和同名参数
this.balance = balance;
}
public Account deposit(double amount) {
this.balance += amount;
return this; // 面孔 2:返回自己,支持链式调用
}
public Account transfer(Account other, double amount) {
other.deposit(amount); // 也可以直接写字段(同类内 private 互通)
this.balance -= amount;
return this;
}
public void show() {
System.out.printf("%s 余额: %.2f%n", owner, balance);
}
}
public class ThisDemo {
public static void main(String[] args) {
Account a = new Account("tom", 100);
Account b = new Account("jerry", 50);
a.transfer(b, 30).deposit(10).show(); // 链式调用:tom 余额 80.0
b.show(); // jerry 余额 80.0
}
}
(面孔 3 是构造器间调用 this(...),下面步骤 3 出现。)
步骤 3:构造器重载与 this(...) 转发
class Order {
private long id;
private String product;
private int quantity;
public Order(long id, String product, int quantity) { // 主构造器
this.id = id;
this.product = product;
this.quantity = quantity;
}
public Order(String product) {
this(System.nanoTime(), product, 1); // this(...) 必须是构造器第一行
// 默认数量 1,id 用时间戳凑数
}
}
public class CtorDemo {
public static void main(String[] args) {
Order o1 = new Order("键盘", 2);
Order o2 = new Order("鼠标");
System.out.println("两个订单创建成功");
}
}
注意:一旦你手写了任何构造器,编译器就不再自动送无参构造器。框架需要无参构造器时必须显式补一个。
步骤 4:static——属于类而不是对象
class IdGenerator {
static long current = 1000; // 类级别:所有实例共享同一份
long id; // 实例级别:每个对象一份
long nextId() {
current++; // 改的是共享的那份
this.id = current;
return id;
}
}
public class StaticDemo {
public static void main(String[] args) {
IdGenerator g1 = new IdGenerator();
IdGenerator g2 = new IdGenerator();
System.out.println(g1.nextId()); // 1001
System.out.println(g2.nextId()); // 1002:current 是共享的
System.out.println(IdGenerator.current); // 1002:static 用类名访问
}
}
记忆:static 成员在方法区/元空间只有一份,用类名访问;实例成员在堆里每个对象一份,用对象访问。 main 为什么是 static,现在完全通了——JVM 启动时没有对象,static 不需要对象。
步骤 5:record——JDK 17 的不可变数据类
// 一行 = 字段 + 构造器 + getter + equals + hashCode + toString
record Point(int x, int y) {
// 可以加紧凑校验
Point {
if (x < 0 || y < 0) throw new IllegalArgumentException("坐标不能为负");
}
}
public class RecordDemo {
public static void main(String[] args) {
Point p = new Point(3, 4);
System.out.println(p); // Point[x=3, y=4]
System.out.println(p.x()); // 3(访问器名就叫 x,不是 getX)
System.out.println(p.equals(new Point(3, 4))); // true:按值比较
// p.x = 10; // 编译错:record 字段 final 不可变
}
}
适用场景:纯数据载体(DTO、坐标、配置项)。需要可变状态的业务实体(如 User)仍用传统类。
讲清楚要点
- 类是模板,对象是实例:
new 一次造一个,字段值各存各的,方法全体共用。
- 封装三板斧:字段 private、提供 public getter/setter、在 setter/构造器里做校验。「所有字段加 getter/setter」不是目的,「受控访问」才是。
- this 指当前对象:参数与字段同名时必须用它区分;构造器第一行
this(...) 可复用另一个构造器。
- static 的三条纪律:static 方法不能直接访问实例成员(没有对象上下文);static 方法不能写 this;工具类(如
Arrays)全是 static。
- toString/equals 配套:不重写 toString 打印出来是
User@1b6d3586;record 自动生成 equals/hashCode,传统类要自己写(IDE 一键生成)。
- 命名空间:实体类与数据库表对应(User ↔ user 表),DTO 后缀表示传输对象——本站后端用
UserDO/UserDTO 区分层次。
你可能会懵的地方
-
getter/setter 这么啰嗦,直接 public 字段不行吗?
现在不行,明天就出事:一旦外部直接赋值,你加校验、加日志、改内部表示都要动所有调用方。 setter 是唯一改动成本为零的通道,且 Java 生态(MyBatis、Jackson、Spring)全按 getter/setter 反射读写。
-
static 变量线程安全吗?
不安全。多个线程同时 current++ 会丢失更新(读-改-写三步会被打断)。本站后端是多线程服务,计数要用 AtomicLong 或加锁。单机学习阶段先记住结论。
-
什么时候用 static 方法,什么时候用实例方法?
方法逻辑不依赖对象状态(输入全从参数来)→ static 工具方法;要读写实例字段或被 Spring 管理 → 实例方法。Spring Bean 全是实例方法,因为字段是注入的依赖。
-
record 能替代所有实体类吗?
不能。record 不可变、无 setter,MyBatis/Jackson 反序列化对 record 支持已完善(JDK 16+ 的主流版本),但需要可变状态、继承、懒加载的字段仍用传统类。
-
一个对象没有被引用了会怎样?
JVM 的垃圾回收器会在合适时机回收它的堆内存。局部变量出方法作用域后即「不可达」。你不用手动释放——这是相对 C/C++ 的最大解放。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 字段声明成 public | 封装失效,校验无处安放 | private + getter/setter |
构造器里写 id = id; | 参数赋给参数,字段还是 0 | this.id = id; |
写了有参构造器后 new User() 报错 | 编译器不再送无参构造器 | 显式补 public User() {} |
| static 方法里访问实例字段 | 编译错误:无对象上下文 | 创建对象后访问,或把字段也改 static |
new User().setId(1); 然后想再拿这个对象 | 引用没保存,对象已不可达 | 先存变量:User u = new User(); u.setId(1); |
| record 里写 setter | record 字段是 final | 需要可变就用传统类 |
卡点急救
- 打印对象显示
User@1b6d3586:没重写 toString。IDEA 中 Alt+Insert → toString 一键生成,或类改用 record。
- 编译报「无法将类中的构造器应用到给定类型」:参数个数/类型与任何构造器都不匹配。检查你写的构造器签名,或补无参构造器。
- 运行报 NullPointerException(对象版):引用是 null 却调了方法。定位报错行号,检查 new 语句是否真的执行了。
- IDE 提示「Field injection ... access can be private」:你在同类外直接访问了 private 字段。改走 getter/setter。
- this(...) 报「必须是构造器中的第一个语句」:把
this(...) 挪到构造器第一行,且一个构造器最多一个 this(...) 调用。
面试常问
-
什么是封装?为什么需要?
隐藏内部状态,仅暴露受控访问方法。收益:校验集中、实现可替换、降低耦合。举例 setter 校验非法状态。
-
static 成员和实例成员的区别?
归属(类 vs 对象)、内存(一份 vs 每对象一份)、访问方式(类名 vs 对象)、生命周期(类加载到卸载 vs 对象创建到 GC)。static 方法无 this、不能直接访问实例成员。
-
构造器可以被继承/重写吗?
不能继承、不能重写,但可以重载;子类构造器会通过 super(...) 链式调用父类构造器(下一课)。
-
record 和普通类的区别?
不可变、自动生成构造器/访问器/equals/hashCode/toString、不能继承其他类。适合纯数据载体。
-
== 和 equals 在对象上的区别?
== 比地址(是否同一对象);equals 默认等同 ==,业务类应重写为按字段比较(record 自动完成)。重写 equals 必须同时重写 hashCode。
自测三问
- 为什么
new User(1,"tom") 在你补写有参构造器后突然编译失败?
IdGenerator.current++ 在多线程下会出什么问题?生产代码怎么写?
- record
Point(int x, int y) 自动提供了哪些成员?p.getX() 能编译通过吗?
小结一句话
类是模板、对象是实例、封装是「private 字段 + 受控方法」;this 区分成员与参数、static 属于类不属于对象、record 一行搞定不可变数据类——Spring 后端每个实体/DTO 都长这个样子。
延伸
- Oracle:类与对象
- 下一课:继承、接口与多态——Spring 依赖注入的根基,Service 接口 + 实现类的标准姿势。