
本文讲解如何通过使用不可变的 java.time 类型替代可变的 java.util.Date,从根本上解决因对象引用共享引发的封装性破坏问题,确保 TripPart 的内部状态不被外部篡改。
本文讲解如何通过使用不可变的 `java.time` 类型替代可变的 `java.util.date`,从根本上解决因对象引用共享引发的封装性破坏问题,确保 `trippart` 的内部状态不被外部篡改。
在 Java 中,封装(Encapsulation)不仅指将字段设为 private,更关键的是防止外部代码通过引用间接修改对象的内部状态。你当前的 TripPart 类虽已将 start 和 end 声明为 private Date,但由于 java.util.Date 是可变(mutable)类型,一旦外部传入一个 Date 实例并被直接赋值给成员变量,调用方后续对原 Date 对象的修改(如 date.setTime(...))会立即反映到 TripPart 内部——这正是测试失败的根本原因。
例如,在以下测试中:
Date dep = getDate(2019,11,28,10,20); TripPart c = new TripPart(dep, getDate(2019,11,28,11,21)); dep.setTime(getDate(2018,11,28,8,0).getTime()); // ⚠️ 外部直接篡改! // 此时 c.getStart() 已悄然变为 2018 年...
尽管 c.getStart() 返回的是 Date 引用,但该引用指向的对象与 dep 完全相同,因此 dep.setTime(...) 直接污染了 c 的内部状态——封装形同虚设。
✅ 正确解法:迁移到 java.time 不可变 API
java.util.Date 和 Calendar 已被官方废弃(自 Java 8 起),其设计存在严重缺陷:方法命名误导(Date 实际表示时间戳而非日期)、线程不安全、API 混乱且大量过时。而 java.time 包中的类型(如 Instant、LocalDateTime、ZonedDateTime)全部是不可变(immutable) 的——所有“修改”操作均返回新实例,原始对象毫发无损。
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
? 迁移步骤(推荐 Instant,适用于精确时间点)
-
替换字段类型与构造逻辑
import java.time.Instant;
public class TripPart { private final Instant start; // 使用 final + 不可变类型,双重保障 private final Instant end;
public TripPart(Instant start, Instant end) {
// 防御性检查(仍需,但无需防御性拷贝)
if (start != null && end != null && start.isAfter(end)) {
throw new IllegalArgumentException("start must be before end");
}
this.start = start;
this.end = end;
}
public TripPart() {
Instant now = Instant.now();
this.start = now;
this.end = now.plusSeconds(3600); // 1 小时后
}
public TripPart(TripPart toCopy) {
this(toCopy.start, toCopy.end);
}
// Getter:直接返回不可变对象,无需拷贝
public Instant getStart() { return start; }
public Instant getEnd() { return end; }
// Setter:若需更新,返回新实例(函数式风格)或抛出异常(强调不可变)
public TripPart withStart(Instant newStart) {
if (end != null && newStart.isAfter(end)) {
throw new IllegalArgumentException("start cannot be after end");
}
return new TripPart(newStart, end);
}
public TripPart withEnd(Instant newEnd) {
if (start != null && newEnd.isBefore(start)) {
throw new IllegalArgumentException("end cannot be before start");
}
return new TripPart(start, newEnd);
}
}
2. **更新测试用例(无需修改逻辑,仅类型适配)**
```java
@Test
public void testConstructor_startEncapsulation() {
Instant argumentStart = Instant.parse("2019-11-28T10:25:00Z");
TripPart c = new TripPart(argumentStart, null);
// ⚡ 关键:Instant 不可变!以下操作无效(编译报错)
// argumentStart.plusSeconds(1); // ❌ 编译失败:Instant 无修改方法
// argumentStart = argumentStart.plusSeconds(1); // ✅ 创建新实例,不影响 c
Instant expectedStart = Instant.parse("2019-11-28T10:25:00Z");
assertEquals(expectedStart, c.getStart()); // 直接比较 Instant,无需 toString()
}
⚠️ 注意事项与最佳实践
- 永远不要使用 Date/Calendar:即使临时修复(如在 setter/getter 中手动 new Date(date.getTime())),也会带来性能开销和维护负担,且易遗漏。
-
优先选择语义明确的类型:
- Instant:UTC 时间戳(推荐用于系统内部逻辑、数据库存储);
- LocalDateTime:无时区的日期时间(适合业务日历、显示层);
- ZonedDateTime:带时区的完整时间(适合跨时区调度)。
- final 字段是强约束:配合不可变类型,能从编译期杜绝意外修改。
- 避免提供“破坏性 setter”:如必须支持修改,采用 withXxx() 函数式方法返回新实例,保持对象不可变性。
通过迁移到 java.time,你不仅修复了封装漏洞,更获得了线程安全、清晰语义、现代化 API 等多重收益——这才是面向对象设计中“封装”的真正意义:控制权在你手中,而非被外部随意劫持。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










