1. 概述
Java 8引入了全新的日期时间API,彻底解决了旧版java.util.Date
和java.util.Calendar
的痛点。本文将先分析旧API的问题,再详解新API如何解决这些缺陷,并重点介绍java.time
包中的核心类:LocalDate
、LocalTime
、LocalDateTime
、ZonedDateTime
、Period
、Duration
及其关键用法。
2. 旧版日期时间API的问题
旧版API存在三大硬伤:
- 线程安全缺失
Date
和Calendar
类非线程安全,开发者需自行处理并发问题,极易踩坑。而Java 8新API的所有类都是不可变且线程安全的,彻底告别并发噩梦。 - API设计反人类
旧API设计混乱,日常操作(如日期加减)需要写大量冗余代码。新API采用ISO标准,提供直观的领域模型(日期/时间/时间段),并内置大量实用方法。 - 时区处理复杂
旧API处理时区需要额外逻辑,新API通过Local
和Zoned
系列类(如ZonedDateTime
)原生支持时区操作。
3. 使用LocalDate、LocalTime和LocalDateTime
这三个类是使用频率最高的核心类,表示不带时区的本地日期/时间,适用于无需显式指定时区的场景。
3.1 LocalDate操作指南
LocalDate
表示ISO格式的日期(yyyy-MM-dd),不含时间信息,适合存储生日、发薪日等场景。
基础创建
// 获取当前日期
LocalDate localDate = LocalDate.now();
// 指定日期创建(两种方式)
LocalDate.of(2015, 02, 20); // 方法1:直接传参
LocalDate.parse("2015-02-20"); // 方法2:解析字符串
常用操作
// 日期加减
LocalDate tomorrow = LocalDate.now().plusDays(1); // 加1天
LocalDate previousMonthSameDay = LocalDate.now().minus(1, ChronoUnit.MONTHS); // 减1个月
// 获取日期信息
DayOfWeek sunday = LocalDate.parse("2016-06-12").getDayOfWeek(); // 获取星期几(返回枚举)
int twelve = LocalDate.parse("2016-06-12").getDayOfMonth(); // 获取当月第几天(返回int)
// 日期判断
boolean leapYear = LocalDate.now().isLeapYear(); // 是否闰年
boolean notBefore = LocalDate.parse("2016-06-12").isBefore(LocalDate.parse("2016-06-11")); // 是否早于指定日期
boolean isAfter = LocalDate.parse("2016-06-12").isAfter(LocalDate.parse("2016-06-11")); // 是否晚于指定日期
// 边界时间获取
LocalDateTime beginningOfDay = LocalDate.parse("2016-06-12").atStartOfDay(); // 当天开始时间(00:00)
LocalDate firstDayOfMonth = LocalDate.parse("2016-06-12").with(TemporalAdjusters.firstDayOfMonth()); // 当月第一天
3.2 LocalTime操作指南
LocalTime
表示不含日期的时间,适合处理每日重复的时间场景(如闹钟时间)。
基础创建
// 获取当前时间
LocalTime now = LocalTime.now();
// 指定时间创建
LocalTime sixThirty = LocalTime.parse("06:30"); // 解析字符串
LocalTime sixThirty = LocalTime.of(6, 30); // 直接传参
常用操作
// 时间加减
LocalTime sevenThirty = LocalTime.parse("06:30").plus(1, ChronoUnit.HOURS); // 加1小时
// 获取时间分量
int six = LocalTime.parse("06:30").getHour(); // 获取小时
// 时间比较
boolean isbefore = LocalTime.parse("06:30").isBefore(LocalTime.parse("07:30")); // 是否早于指定时间
// 特殊时间常量
LocalTime maxTime = LocalTime.MAX; // 23:59:59.999999999(数据库查询常用)
3.3 LocalDateTime操作指南
LocalDateTime
是日期+时间的组合,最常用的日期时间类。
基础创建
// 获取当前日期时间
LocalDateTime.now();
// 指定日期时间创建
LocalDateTime.of(2015, Month.FEBRUARY, 20, 06, 30); // 直接传参
LocalDateTime.parse("2015-02-20T06:30:00"); // 解析字符串
常用操作
// 日期时间加减
localDateTime.plusDays(1); // 加1天
localDateTime.minusHours(2); // 减2小时
// 获取分量
localDateTime.getMonth(); // 获取月份(返回Month枚举)
4. 使用ZonedDateTime API
当需要处理带时区的日期时间时,使用ZonedDateTime
。ZoneId
表示时区标识符(全球约40个时区)。
时区操作
// 创建时区ID
ZoneId zoneId = ZoneId.of("Europe/Paris");
// 获取所有可用时区ID
Set<String> allZoneIds = ZoneId.getAvailableZoneIds();
// LocalDateTime转ZonedDateTime
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, zoneId);
// 解析带时区的字符串
ZonedDateTime.parse("2015-05-03T10:15:30+01:00[Europe/Paris]");
替代方案:OffsetDateTime
OffsetDateTime
表示带UTC偏移量的日期时间(精度到纳秒):
// 创建基础时间
LocalDateTime localDateTime = LocalDateTime.of(2015, Month.FEBRUARY, 20, 06, 30);
// 添加偏移量(+2小时)
ZoneOffset offset = ZoneOffset.of("+02:00");
OffsetDateTime offSetByTwo = OffsetDateTime.of(localDateTime, offset); // 结果:2015-02-20T06:30+02:00
5. 使用Period和Duration
Period
:基于年/月/日的时间段Duration
:基于秒/纳秒的时间段
5.1 Period操作
// 创建日期
LocalDate initialDate = LocalDate.parse("2007-05-10");
// 日期加减(Period)
LocalDate finalDate = initialDate.plus(Period.ofDays(5)); // 加5天
// 获取时间段差值
int five = Period.between(initialDate, finalDate).getDays(); // 获取天数差
long five = ChronoUnit.DAYS.between(initialDate, finalDate); // 直接按天计算差值
5.2 Duration操作
// 创建时间
LocalTime initialTime = LocalTime.of(6, 30, 0);
// 时间加减(Duration)
LocalTime finalTime = initialTime.plus(Duration.ofSeconds(30)); // 加30秒
// 获取时间段差值
long thirty = Duration.between(initialTime, finalTime).getSeconds(); // 获取秒数差
long thirty = ChronoUnit.SECONDS.between(initialTime, finalTime); // 直接按秒计算差值
6. 与Date和Calendar的兼容性
通过toInstant()
方法实现旧API到新API的转换:
// Date转LocalDateTime
LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
// Calendar转LocalDateTime
LocalDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault());
// 从Unix时间戳创建
LocalDateTime.ofEpochSecond(1465817690, 0, ZoneOffset.UTC); // 结果:2016-06-13T11:34:50
7. 日期时间格式化
使用DateTimeFormatter
实现灵活格式化:
// 创建日期时间
LocalDateTime localDateTime = LocalDateTime.of(2015, Month.JANUARY, 25, 6, 30);
// ISO格式化
String localDateString = localDateTime.format(DateTimeFormatter.ISO_DATE); // 2015-01-25
// 自定义格式
localDateTime.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); // 2015/01/25
// 本地化格式(英国风格)
localDateTime.format(
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
.withLocale(Locale.UK)
); // 25-Jan-2015, 06:30:00
8. 向后移植和替代方案
8.1 ThreeTen项目(Java 6/7兼容)
为尚未升级Java 8的项目提供新API的向后移植:
<dependency>
<groupId>org.threeten</groupId>
<artifactId>threetenbp</artifactId>
<version>1.3.1</version>
</dependency>
8.2 Joda-Time库(经典替代)
Java 8日期时间API的灵感来源,功能高度重合:
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.4</version>
</dependency>
9. 总结
Java 8日期时间API通过线程安全设计、直观的API模型和原生时区支持,彻底解决了旧版API的痛点。核心类LocalDate
/LocalTime
/LocalDateTime
覆盖了80%的日常场景,ZonedDateTime
处理复杂时区需求,Period
/Duration
提供精确的时间段计算。建议新项目直接采用新API,旧项目可通过ThreeTen或Joda-Time平滑过渡。
本文代码示例已上传至Java 8 Date/Time示例库