1. 简介

在 Java 8 发布之前,Joda-Time 是最主流的日期时间处理库。它的诞生就是为了弥补 JDK 原生 DateCalendar 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 的几大“痛点”:

  • 线程不安全DateSimpleDateFormat 都不是线程安全的,多线程环境下容易出问题。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.tzorg.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


原始标题:Introduction to Joda-Time | Baeldung