6.4 时间戳窗口:从Date到java时间新API


6.4 时间戳窗口:从 Date 到 java 时间新 API

本节摘要:旧日期 API(Date 与 Calendar)有四大硬伤——可变、月份从零起数、命名误导、格式化类线程不安全。第八版引入的新时间包用"专人专管"重构:LocalDate 管日期、LocalTime 管时刻、LocalDateTime 管两者、Instant 管时间戳、Duration 与 Period 管时长、DateTimeFormatter 管格式。新类型全部不可变,计算方法返回新对象。本节对照新旧用法,完成解析、加减、间隔、格式化的全流程案例。

老 API 的四宗罪

每个维护过老代码的人都撞过这些坑。先看代码里它们长什么样:

import java.util.Date; import java.text.SimpleDateFormat; public class OldDemo { public static void main(String[] args) { Date now = new Date(); System.out.println(now); // 输出类似:Mon Aug 31 10:15:30 CST 2026 本地化格式难用 // 罪一:可变对象 setMonth 直接改掉自己 违反第 4 章讲的不可变设计经验 Date d = new Date(); d.setTime(0); System.out.println(d.getTime()); // 输出:0 内部状态被随意改写 // 罪二:格式化与解析绑死在 SimpleDateFormat 且它线程不安全 SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd"); System.out.println(f.format(new Date())); // 输出类似:2026-08-31 // 多个线程共用同一个 f 时 format 与 parse 可能互相踩踏 输出错乱日期 } }

四宗罪完整清单:可变——setTime、setMonth 随便改内部状态,作为 Map 键或共享数据随时出事(第 5 章可变键的教训);月份从零起数——老 Calendar 的一月是零,写成"一"实际是二月,每年都有新人栽进去;命名误导——Date 其实是时间戳不是"日期",getYear 返回的是与一千九百的差值,getDay 返回的是星期不是天;格式化类线程不安全——SimpleDateFormat 共享即错乱,并发场景要为每个线程各建一份或加锁。

新时间包的设计哲学正好对着治病:专人专管(日期、时刻、时间戳、时长、格式各一个类,不再一个类什么都管)、全部不可变(计算返回新对象,旧值不动,与 String 同思路)、命名直白(getYear 就是年、getDayOfWeek 就是星期)。

图 6-4 新时间包的分工地图

图 6-4 新时间包的分工地图

新 API 的日常四件事

背景:会员系统要处理"注册日期、年龄、会员到期提醒、按指定格式打印与解析"。操作分四步走全流程:

import java.time.LocalDate; import java.time.LocalDateTime; import java.time.Duration; import java.time.Period; import java.time.format.DateTimeFormatter; public class NewApiDemo { public static void main(String[] args) { // 一:获取与构造 LocalDate today = LocalDate.now(); // 今天 LocalDate national = LocalDate.of(2026, 10, 1); // 指定日期 注意 十月就是 10 不再减一 System.out.println("今天:" + today + " 国庆:" + national); // 输出类似:今天:2026-08-31 国庆:2026-10-01 // 二:解析与格式化 DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy年MM月dd日"); LocalDate joined = LocalDate.parse("2023年03月15日", fmt); // 字符串按格式解析 System.out.println("注册日:" + joined); // 输出:注册日:2023-03-15 System.out.println(joined.format(fmt)); // 输出:2023年03月15日 // 三:计算 —— 全部返回新对象 原值不动 LocalDate expire = joined.plusYears(3); // 加三年 System.out.println("到期日:" + expire); // 输出:到期日:2026-03-15 long daysLeft = LocalDate.now().datesUntil(expire).count(); // 也可用间隔算 Period memberAge = Period.between(joined, today); // 间隔:年月日各自多少 System.out.println("会龄:" + memberAge.getYears() + " 年 " + memberAge.getMonths() + " 个月 " + memberAge.getDays() + " 天"); // 输出类似:会龄:3 年 5 个月 16 天(随运行日期变化) // 四:时刻与时长 LocalDateTime visit = LocalDateTime.now(); // 日期加时刻 System.out.println("来访登记:" + visit.format( DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); // 输出带时分秒 Duration halfHour = Duration.ofMinutes(30); System.out.println("办理时限秒数:" + halfHour.getSeconds()); // 输出:办理时限秒数:1800 } }

结果:解析、加减、间隔、格式化四件事各用对应的类完成,没有任何一处修改原对象。解读:plus、minus 系列返回新实例——expire 与 joined 是两个独立对象,这正是不可变设计(第 4 章 String 的思路在新时间包全面兑现),线程安全不证自明;Period 管年月日三段式的"人类间隔"、Duration 管秒纳秒的"机器间隔",选错类会导致跨月的时长算不出(Duration 不认识"月"这个长短不定的单位)。变式:到期提醒判断直接比较 expire.isBefore(today);需要时区时用带区号的类型(时区与夏令时的水深,本教程不展开,知道"涉及时区要换专门的类"即可);时间戳存储用 Instant——它与世界统一时间对齐,跨系统排序最稳。

老代码怎么办

现实项目里 Date 与 Calendar 不会一夜消失,两种类型的转换有官方桥:老转新按系统默认时区取毫秒、新转老同样走毫秒。改造的优先级建议:新代码一律用新 API(没有理由再开旧伤口);老代码遇到才改——顺手把"格式化、加减、解析"三处换掉,收益立现;共享的 SimpleDateFormat 是高危点,并发错乱的日期数据比崩溃更难查,这类实例优先处理。判断口径记住一句话:凡是 2014 年之前设计的日期代码都值得过一眼,四宗罪命中任何一条(尤其共享格式化器与月份魔法数)就地修复。

本节要点回顾

  • 旧 API 四宗罪:可变、月份从零数、命名误导、格式化类线程不安全——每一条都有真实事故对应
  • 新包专人专管:LocalDate 日期、LocalTime 时刻、LocalDateTime 组合、Instant 时间戳、Duration 秒级时长、Period 年月日间隔、DateTimeFormatter 格式化
  • 计算返回新对象:plus、minus 不改原值,不可变带来天然的线程安全——String 的设计思路在时间上的兑现
  • 两种间隔别混用:跨月的自然时长用 Period,纯秒数的机器时长用 Duration
  • 迁移策略:新代码全用新 API,老代码命中高危点(共享格式化器、月份魔法数)优先修

工具房三章到此全部逛完。接下来进差错登记科:流操作里反复出现的那些受检异常——文件不存在、中断、关闭失败——它们的族谱、处置流程与自定义条目,一次性立起章法。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U