刚接触Java 8时间日期API时,很多人会被LocalDate、LocalTime和LocalDateTime这三个长得像三胞胎的类搞晕。作为从传统Date/Calendar切过来的开发者,我在实际项目中踩过不少坑之后,终于摸清了它们的门道。这三个类都来自java.time包,是Java 8日期时间改革的核心成员,专为解决旧API的种种反人类设计而生。
它们仨最大的特点就是"纯"——不带时区信息,就像你手腕上的机械表,走到哪都显示同样的时间。这种设计特别适合处理生日、节假日这类与时区无关的场景。我接手过一个跨国电商项目,就是因为之前混用了带时区的ZonedDateTime处理商品促销时间,导致不同地区用户看到的促销时段错乱,改用LocalDateTime后才彻底解决问题。
java复制LocalDate birthday = LocalDate.of(1990, Month.DECEMBER, 25);
System.out.println("我的生日是:" + birthday); // 输出:1990-12-25
这个类专门处理年月日,连构造函数都设计得贴心——Month枚举避免了月份从0开始的反人类设计。我常用它来计算会员有效期:
java复制LocalDate today = LocalDate.now();
LocalDate expiryDate = today.plusYears(1).minusDays(1); // 正好一年会员期
坑点警示:获取当月天数时,记得考虑闰年二月的情况。有次我写会员系统时硬编码30天,结果2月注册的用户集体投诉。
java复制LocalTime meetingTime = LocalTime.of(14, 30); // 14:30
System.out.println("会议时间:" + meetingTime.plusHours(1)); // 输出:15:30
它专注处理时分秒纳秒,适合考勤系统:
java复制LocalTime checkInTime = LocalTime.parse("09:15:30");
if(checkInTime.isAfter(LocalTime.of(9, 0))){
System.out.println("迟到!");
}
java复制LocalDateTime flightTime = LocalDateTime.of(2023, 7, 20, 15, 40);
System.out.println("航班时间:" + flightTime); // 2023-07-20T15:40
这是前两者的合体,我最近做的订单系统就用它记录下单时间:
java复制LocalDateTime orderTime = LocalDateTime.now();
LocalDateTime payDeadline = orderTime.plusMinutes(30); // 30分钟内支付
计算两个日期间隔再也不用自己写闰年判断了:
java复制Period between = Period.between(
LocalDate.of(2023, 1, 1),
LocalDate.of(2023, 12, 31)
);
System.out.println(between.getMonths()); // 输出11个月
时间差计算更精准:
java复制Duration duration = Duration.between(
LocalTime.of(9, 0),
LocalTime.of(18, 30)
);
System.out.println(duration.toHours()); // 输出9小时
DateTimeFormatter比SimpleDateFormat线程安全得多:
java复制DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
String formatted = LocalDateTime.now().format(formatter);
LocalDateTime parsed = LocalDateTime.parse("2023/07/20 15:30", formatter);
经验之谈:定义全局常量保存常用格式,避免重复创建formatter对象。我在性能测试中发现频繁创建formatter会导致GC压力激增。
兼容遗留代码的转换方法:
java复制// Date转LocalDateTime
Instant instant = new Date().toInstant();
LocalDateTime dt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
// LocalDateTime转Date
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
这三个类都是不可变对象,意味着每次计算都会产生新对象。在高频调用的支付超时检查中,我最初这样写:
java复制// 反例:每次调用都新建对象
public boolean isPayTimeout(LocalDateTime orderTime) {
return LocalDateTime.now().isAfter(orderTime.plusMinutes(30));
}
优化后版本:
java复制// 正例:复用Duration对象
private static final Duration PAY_TIMEOUT = Duration.ofMinutes(30);
public boolean isPayTimeout(LocalDateTime orderTime) {
return LocalDateTime.now().isAfter(orderTime.plus(PAY_TIMEOUT));
}
MyBatis处理LocalDateTime的TypeHandler配置:
xml复制<resultMap type="Order">
<result column="create_time" property="createTime"
typeHandler="org.apache.ibatis.type.LocalDateTimeTypeHandler"/>
</resultMap>
JPA实体类中的正确姿势:
java复制@Entity
public class Event {
@Column
private LocalDateTime startTime;
// 必须添加的转换器
@Convert(converter = LocalDateTimeConverter.class)
private LocalDate reminderDate;
}
虽然这三个类不带时区,但now()方法其实暗藏玄机:
java复制// 在UTC+8时区服务器上执行
LocalDateTime now = LocalDateTime.now(); // 获取的是服务器本地时间
解决方案是明确时区:
java复制LocalDateTime now = LocalDateTime.now(Clock.systemUTC()); // 强制使用UTC时间
判断同一天不能直接用equals:
java复制LocalDateTime dt1 = LocalDateTime.of(2023,7,20,23,59);
LocalDateTime dt2 = LocalDateTime.of(2023,7,21,0,1);
// 错误方式
if(dt1.equals(dt2)) {...}
// 正确方式
if(dt1.toLocalDate().equals(dt2.toLocalDate())) {...}
即使不用时区,夏令时也可能通过系统时钟影响你:
java复制// 在实行夏令时的地区,以下代码可能在特定日期出现1小时误差
LocalDate date = LocalDate.of(2023,3,26); // 欧洲夏令时切换日
LocalDateTime dt = date.atTime(LocalTime.of(2,30)); // 可能不存在的时间
防御性编程建议:
java复制if(!date.isSupported(ChronoField.EPOCH_DAY)){
throw new DateTimeException("无效日期");
}
结合TemporalAdjusters实现:
java复制LocalDate date = LocalDate.of(2023,7,20); // 周四
LocalDate nextWorkday = date.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
更复杂的节假日判断需要自定义TemporalAdjuster:
java复制public class HolidayAdjuster implements TemporalAdjuster {
private static final Set<LocalDate> HOLIDAYS = Set.of(
LocalDate.of(2023,10,1),
LocalDate.of(2023,10,2)
);
@Override
public Temporal adjustInto(Temporal temporal) {
LocalDate date = LocalDate.from(temporal);
do {
date = date.plusDays(1);
} while(HOLIDAYS.contains(date) || date.getDayOfWeek() == DayOfWeek.SATURDAY
|| date.getDayOfWeek() == DayOfWeek.SUNDAY);
return temporal.with(date);
}
}
结合Spring Scheduler实现每天定点执行:
java复制@Scheduled(cron = "0 0 9 * * ?") // 每天9点执行
public void morningTask() {
LocalTime now = LocalTime.now();
if(now.isBefore(LocalTime.of(9, 30))) {
// 执行晨间任务
}
}
java复制log.info("事件发生时间:{}", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
在最近开发的金融对账系统中,我全面采用LocalDateTime处理交易时间,配合精心设计的日期计算工具类,使对账周期从原来的4小时缩短到30分钟。这让我深刻体会到,用好这三个时间类,绝对能让你的代码摆脱日期计算的噩梦。