1. 简介
在 Java 8 发布之前,Joda-Time 是最主流的日期时间处理库。它的诞生就是为了弥补 JDK 原生 Date
和 Calendar
API 的设计缺陷,并提供一套更直观、易用的 API。
✅ 核心亮点:Joda-Time 的设计理念非常成功,以至于 Java 8 直接将其核心思想引入了 JDK,形成了现在的 java.time
包(即 JSR-310)。我们之前写过一篇 Java 8 时间 API 入门,讲的就是这个新体系。
⚠️ 重要提醒:自 Java 8 发布后,Joda-Time 官方已明确表示该项目“基本完成”,建议新项目直接使用 java.time
,老项目也应逐步迁移。本文更多是帮助理解历史代码或过渡知识。
2. 为什么曾经需要 Joda-Time?
JDK 8 之前的日期时间 API 被吐槽多年,问题多多,Joda-Time 正是为了解决这些“坑”而生。
❌ JDK 原生 API 的几大“痛点”:
- 线程不安全:
Date
和SimpleDateFormat
都不是线程安全的,多线程环境下容易出问题。Joda-Time 采用不可变对象(immutable) 设计,天然规避此问题。 - 语义混乱:
Date
类名误导人,它其实表示的是一个时间戳(毫秒级的瞬时点),而非“日期”。年份从 1900 开始算,月份从 0 开始,反人类操作。 - 操作繁琐:想取年月日?得借助
Calendar
,代码啰嗦且易错。 - 日历系统支持弱:JDK 只原生支持公历(Gregorian)和日本年号历法,而 Joda-Time 支持多达 8 种日历系统。
✅ Joda-Time 的优势:提供了一套流畅、清晰、面向对象的 API,让日期操作变得简单粗暴又安全。
3. 环境配置
要在项目中使用 Joda-Time,只需引入 Maven 依赖:
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.12.5</version>
</dependency>
4. 核心概念概览
Joda-Time 的核心类位于 org.joda.time
包下,主要分为以下几类:
✅ 常用日期时间类
类名 | 说明 |
---|---|
LocalDate |
仅日期,无时间、无时区 |
LocalTime |
仅时间,无日期、无时区 |
LocalDateTime |
日期+时间,无时区 |
Instant |
瞬时点,以毫秒计,从 1970-01-01T00:00:00Z 开始 |
Duration |
两个时间点之间的毫秒数(精确时长) |
Period |
以年、月、日等逻辑单位表示的时间段(会考虑闰年、夏令时) |
Interval |
两个 Instant 之间的时间区间 |
✅ 其他重要模块
- 格式化/解析:
org.joda.time.format
包下的DateTimeFormatter
,用于自定义日期字符串的格式。 - 时区与日历系统:
org.joda.time.tz
和org.joda.time.chrono
包,支持丰富的时区和日历实现。
5. 日期时间的表示
5.1 获取当前时间
获取当前日期(不含时间):
LocalDate currentDate = LocalDate.now();
获取当前时间(不含日期):
LocalTime currentTime = LocalTime.now();
获取当前日期时间(不含时区):
LocalDateTime currentDateAndTime = LocalDateTime.now();
类型转换
LocalDateTime
可以方便地转成其他类型:
DateTime dateTime = currentDateAndTime.toDateTime(); // 转为带时区的 DateTime
LocalDate localDate = currentDateAndTime.toLocalDate(); // 转为 LocalDate
LocalTime localTime = currentDateAndTime.toLocalTime(); // 转为 LocalTime
指定时区
所有 now()
方法都可传入 DateTimeZone
参数,获取指定时区的当前时间:
LocalDate currentDate = LocalDate.now(DateTimeZone.forID("America/Chicago"));
与 JDK 类型互转
Joda-Time 与 java.util.Date
兼容性良好:
// 从 Date 创建
LocalDateTime currentDateTimeFromJavaDate = new LocalDateTime(new Date());
// 转回 Date
Date currentJavaDate = currentDateTimeFromJavaDate.toDate();
5.2 创建自定义时间
Joda-Time 提供了多种方式创建自定义时间点:
// 从毫秒偏移(1分钟前)
Date oneMinuteAgoDate = new Date(System.currentTimeMillis() - (60 * 1000));
Instant oneMinutesAgoInstant = new Instant(oneMinuteAgoDate);
// 多种构造方式
DateTime customDateTimeFromInstant = new DateTime(oneMinutesAgoInstant);
DateTime customDateTimeFromJavaDate = new DateTime(oneMinuteAgoDate);
DateTime customDateTimeFromString = new DateTime("2018-05-05T10:11:12.123");
DateTime customDateTimeFromParts = new DateTime(2018, 5, 5, 10, 11, 12, 123);
字符串解析
使用 ISO 格式字符串解析:
DateTime parsedDateTime = DateTime.parse("2018-05-05T10:11:12.123");
使用自定义格式解析:
DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("MM/dd/yyyy HH:mm:ss");
DateTime parsedDateTimeUsingFormatter = DateTime.parse("05/05/2018 10:11:12", dateTimeFormatter);
6. 日期时间操作
6.1 使用 Instant
Instant
表示一个精确的时间点(毫秒级)。
Instant instant = new Instant(); // 当前时间点
Instant.now(); // 同上,更常用
创建自定义 Instant
Instant instantFromEpochMilli = Instant.ofEpochMilli(milliesFromEpochTime);
Instant instantFromEpocSeconds = Instant.ofEpochSecond(secondsFromEpochTime);
// 从字符串、Date 或时间戳创建
Instant instantFromString = new Instant("2018-05-05T10:11:12");
Instant instantFromDate = new Instant(oneMinuteAgoDate);
Instant instantFromTimestamp = new Instant(System.currentTimeMillis() - (60 * 1000));
解析自定义格式
Instant parsedInstant = Instant.parse("05/05/2018 10:11:12", dateTimeFormatter);
比较与转换
Instant
实现了 Comparable
,可直接比较:
assertTrue(instantNow.compareTo(oneMinuteAgoInstant) > 0);
assertTrue(instantNow.isAfter(oneMinuteAgoInstant));
assertTrue(oneMinuteAgoInstant.isBefore(instantNow));
assertTrue(oneMinuteAgoInstant.isBeforeNow());
assertFalse(oneMinuteAgoInstant.isEqual(instantNow));
转换为其他类型:
DateTime dateTimeFromInstant = instant.toDateTime();
Date javaDateFromInstant = instant.toDate();
获取时间字段
int year = instant.get(DateTimeFieldType.year());
int month = instant.get(DateTimeFieldType.monthOfYear()); // 注意:1-12
int day = instant.get(DateTimeFieldType.dayOfMonth());
int hour = instant.get(DateTimeFieldType.hourOfDay());
6.2 Duration、Period 与 Interval
Duration(精确时长)
表示两个时间点之间的毫秒数,不考虑时区和日历逻辑,适合做固定时长的加减。
long currentTimestamp = System.currentTimeMillis();
long oneHourAgo = currentTimestamp - 60 * 60 * 1000; // 注意:原文此处为24小时,应为1小时
Duration duration = new Duration(oneHourAgo, currentTimestamp);
Instant.now().plus(duration); // 当前时间加1小时
获取时长的各个单位:
long durationInDays = duration.getStandardDays();
long durationInHours = duration.getStandardHours();
long durationInMinutes = duration.getStandardMinutes();
long durationInSeconds = duration.getStandardSeconds();
long durationInMilli = duration.getMillis();
Period(逻辑时间段)
以年、月、日等逻辑单位表示,会考虑闰年、月份天数、夏令时等。这是它与 Duration
的核心区别。
// 加1个月,会自动处理2月29日等边界情况
Period period = new Period().withMonths(1);
LocalDateTime datePlusPeriod = localDateTime.plus(period);
Interval(时间区间)
表示两个 Instant
之间的区间,常用于判断重叠、间隙等。
Interval interval = new Interval(oneMinuteAgoInstant, instantNow);
判断区间重叠:
Instant startInterval1 = new Instant("2018-05-05T09:00:00.000");
Instant endInterval1 = new Instant("2018-05-05T11:00:00.000");
Interval interval1 = new Interval(startInterval1, endInterval1);
Instant startInterval2 = new Instant("2018-05-05T10:00:00.000");
Instant endInterval2 = new Instant("2018-05-05T11:00:00.000");
Interval interval2 = new Interval(startInterval2, endInterval2);
Interval overlappingInterval = interval1.overlap(interval2); // 返回重叠部分,无重叠则为 null
计算区间间隙与连接:
// 判断 interval1 的结束是否紧接下一个区间的开始
assertTrue(interval1.abuts(new Interval(
new Instant("2018-05-05T11:00:00.000"),
new Instant("2018-05-05T13:00:00.000"))));
6.3 常见操作
所有 Joda-Time 的核心类都是不可变的,每次操作都返回新对象。
加减操作
LocalDateTime currentLocalDateTime = LocalDateTime.now();
// 加1天
LocalDateTime nextDayDateTime = currentLocalDateTime.plusDays(1);
// 加1个月(使用 Period)
Period oneMonth = new Period().withMonths(1);
LocalDateTime nextMonthDateTime = currentLocalDateTime.plus(oneMonth);
// 减1天
LocalDateTime previousDayLocalDateTime = currentLocalDateTime.minusDays(1);
设置时间字段
使用 withXxx
方法链设置特定字段:
LocalDateTime currentDateAtHour10 = currentLocalDateTime
.withHourOfDay(0)
.withMinuteOfHour(0)
.withSecondOfMinute(0)
.withMillisOfSecond(0); // 设置为当天 00:00:00.000
类型转换
dateTime = localDateTime.toDateTime(); // 转 DateTime
localDate = localDateTime.toLocalDate(); // 转 LocalDate
localTime = localDateTime.toLocalTime(); // 转 LocalTime
javaDate = localDateTime.toDate(); // 转 java.util.Date
7. 时区处理
Joda-Time 的 DateTimeZone
类让时区操作变得简单。
默认时区
// 设置全局默认时区
DateTimeZone.setDefault(DateTimeZone.UTC);
此后未指定时区的操作将使用 UTC。
查看所有时区
Set<String> availableIDs = DateTimeZone.getAvailableIDs();
创建指定时区的时间
DateTime dateTimeInChicago = new DateTime(DateTimeZone.forID("America/Chicago"));
DateTime dateTimeInBucharest = new DateTime(DateTimeZone.forID("Europe/Bucharest"));
LocalDateTime localDateTimeInChicago = new LocalDateTime(DateTimeZone.forID("America/Chicago"));
转换时区
// LocalDateTime 转为另一时区的 DateTime
DateTime convertedDateTime = localDateTimeInChicago.toDateTime(DateTimeZone.forID("Europe/Bucharest"));
// 转为另一时区的 JDK Date
Date convertedDate = localDateTimeInChicago.toDate(TimeZone.getTimeZone("Europe/Bucharest"));
8. 总结
Joda-Time 曾是 Java 日期处理领域的标杆,其优秀的设计直接推动了 java.time
的诞生。
✅ 核心价值:解决了 JDK 旧 API 的线程安全、易用性、语义清晰度等根本问题。
⚠️ 现状与建议:
- 官方已将其视为“基本完成”的项目。
- 强烈建议新项目直接使用 Java 8 的
java.time
包。 - 对于维护老项目,理解 Joda-Time 有助于读懂历史代码,但长远看应规划迁移。
本文的示例代码可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-date-operations-1