Java 日期时间类全面解析:从传统到现代的演进
一、发展历程概览
二、传统日期类(Java 8前)
1. java.util.Date
- 日期表示类
Date now = new Date(); // 当前日期时间
System.out.println(now); // Wed May 15 09:30:45 CST 2023
// 特定时间创建
Date specificDate = new Date(121, 4, 15); // 2021-05-15 (已弃用)
主要问题:
- 年份从1900开始计数
- 月份0-11(实际需+1)
- 线程不安全
- 时区处理混乱
2. java.util.Calendar
- 日期操作类
Calendar cal = Calendar.getInstance();
cal.set(2023, Calendar.MAY, 15); // 2023-05-15
// 日期计算
cal.add(Calendar.DAY_OF_MONTH, 7); // 加7天
// 获取值
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH) + 1; // 月份需+1
缺点:
- API设计臃肿
- 可变对象(线程不安全)
- 仍存在月份偏移问题
3. java.text.SimpleDateFormat
- 日期格式化
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formatted = sdf.format(new Date()); // 2023-05-15 09:30:45
// 解析日期
Date parsedDate = sdf.parse("2023-05-15 12:00:00");
致命缺陷:
- 非线程安全(必须在方法内创建或使用ThreadLocal)
三、现代日期时间API(Java 8+)
java.time
包提供了全新的日期时间处理API:
1. 核心类对比表
类名 | 描述 | 示例值 |
---|---|---|
LocalDate | 只包含日期 | 2023-05-15 |
LocalTime | 只包含时间 | 09:30:45 |
LocalDateTime | 日期+时间 | 2023-05-15T09:30:45 |
ZonedDateTime | 带时区的日期时间 | 2023-05-15T09:30:45+08:00[Asia/Shanghai] |
Instant | 时间戳(Unix时间) | 1684114245 |
Period | 日期间隔(年/月/日) | P1Y2M3D |
Duration | 时间间隔(秒/毫秒) | PT8H30M |
2. 基础使用示例
// 获取当前日期
LocalDate today = LocalDate.now();
// 创建特定日期
LocalDate birthday = LocalDate.of(1990, Month.DECEMBER, 25);
// 日期计算
LocalDate nextWeek = today.plusWeeks(1);
LocalDate previousMonth = today.minusMonths(1);
// 日期比较
boolean isAfter = today.isAfter(birthday);
boolean isLeapYear = today.isLeapYear();
3. 时间操作
LocalTime nowTime = LocalTime.now();
LocalTime meetingTime = LocalTime.of(14, 30); // 14:30
// 时间加减
LocalTime lunchTime = nowTime.plusHours(1).plusMinutes(30);
// 时间判断
if (nowTime.isBefore(LocalTime.NOON)) {
System.out.println("上午好!");
}
4. 日期时间组合
LocalDateTime meetingDateTime = LocalDateTime.of(
2023, Month.MAY, 15, 14, 30);
// 转换为时区时间
ZonedDateTime shanghaiMeeting = meetingDateTime.atZone(ZoneId.of("Asia/Shanghai"));
// 转换为其他时区
ZonedDateTime newYorkMeeting = shanghaiMeeting.withZoneSameInstant(
ZoneId.of("America/New_York"));
5. 时间间隔计算
// 日期间隔(年/月/日)
Period period = Period.between(
LocalDate.of(2020, 1, 1),
LocalDate.now()
);
System.out.println(period.getYears() + "年" +
period.getMonths() + "月");
// 时间间隔(精确时间)
Duration duration = Duration.between(
LocalTime.of(9, 0),
LocalTime.now()
);
System.out.println(duration.toMinutes() + "分钟");
四、格式化与解析
1. DateTimeFormatter
- 替代SimpleDateFormat
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
// 格式化
String formatted = LocalDateTime.now().format(formatter);
// 解析
LocalDateTime parsed = LocalDateTime.parse("2023-05-15 14:30:00", formatter);
2. 预定义格式器
// ISO标准格式
String isoFormat = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
// 本地化格式
DateTimeFormatter localizedFormatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM)
.withLocale(Locale.CHINA);
String chineseFormat = LocalDateTime.now().format(localizedFormatter);
五、与传统类的转换
// Date 转 Instant
Date legacyDate = new Date();
Instant instant = legacyDate.toInstant();
// Instant 转 Date
Date newDate = Date.from(Instant.now());
// Calendar 转 ZonedDateTime
Calendar calendar = Calendar.getInstance();
ZonedDateTime zdt = ZonedDateTime.ofInstant(
calendar.toInstant(),
calendar.getTimeZone().toZoneId()
);
六、最佳实践指南
-
版本选择:
- Java 8+项目:始终使用java.time
- 旧项目兼容:使用Joda-Time(第三方库)
-
时区处理原则:
// 明确时区而非依赖系统默认 ZonedDateTime zonedNow = ZonedDateTime.now(ZoneId.of("Asia/Tokyo")); // UTC时间优先存储 Instant utcInstant = Instant.now();
-
避免使用枚举值:
// 推荐 LocalDate date = LocalDate.of(2023, Month.MAY, 15); // 不推荐(可能因月份偏移引发错误) LocalDate date = LocalDate.of(2023, 5, 15);
-
线程安全实践:
// DateTimeFormatter线程安全可共享 public static final DateTimeFormatter GLOBAL_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; // SimpleDateFormat必须每个线程独立实例 private static final ThreadLocal<SimpleDateFormat> threadLocalSdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
七、经典问题解决方案
1. 工作日计算
LocalDate start = LocalDate.of(2023, 1, 1);
LocalDate end = LocalDate.of(2023, 12, 31);
long workDays = Stream.iterate(start, date -> date.plusDays(1))
.limit(ChronoUnit.DAYS.between(start, end))
.filter(date -> date.getDayOfWeek() != DayOfWeek.SATURDAY)
.filter(date -> date.getDayOfWeek() != DayOfWeek.SUNDAY)
.count();
2. 夏令时处理
ZoneId zone = ZoneId.of("America/New_York");
ZonedDateTime springTime = ZonedDateTime.of(2023, 3, 12, 2, 30, 0, 0, zone);
// 自动处理时间跳变(会调整为03:30)
System.out.println(springTime); // 2023-03-12T03:30-04:00[America/New_York]
3. 精确时间计算
Instant start = Instant.now();
// 执行操作...
Instant end = Instant.now();
Duration elapsed = Duration.between(start, end);
System.out.printf("操作耗时: %d毫秒", elapsed.toMillis());
八、Joda-Time(老项目备用方案)
// 引入依赖
implementation 'joda-time:joda-time:2.12.5'
// 使用示例
DateTime now = new DateTime();
DateTime nextWeek = now.plusWeeks(1);
DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd");
String formatted = fmt.print(now);
总结思考
-
演进本质:
- 从可变对象 → 不可变对象(线程安全)
- 从过程式操作 → 流畅API
- 从隐含规则 → 显式表达
-
选择策略:
场景 推荐方案 新项目 java.time Android项目 java.time + ThreeTenABP Java 7及以下 Joda-Time 简单日期操作 LocalDate/LocalTime 全球时间系统 Instant/ZonedDateTime -
设计启示:
- 清晰性:消除月份偏移等隐含规则
- 类型安全:专用类型处理不同时间概念
- 领域驱动:方法命名直击业务语义(plusDays()、isAfter())
最终建议:所有新项目应优先使用java.time API,老项目逐步迁移。掌握现代日期时间API,是构建健壮时间敏感型应用的基石。