一、简介

Joda-Time是在Java 8发布之前使用最广泛的日期和时间处理库。它的目的是提供一个直观的API来处理日期和时间,并解决Java Date/Time API中存在的设计问题。这个库中实现的核心概念在Java 8版本发布时引入到了JDK中。

  • Joda-Time的由来

    Java 8之前的日期/时间API存在多个设计问题:

    • Date和SimpleDateFormatter类不是线程安全的;为了解决这个问题,Joda-Time使用不可变类来处理日期和时间。

    • Date类并不表示实际的日期,而是指定一个瞬间;日期中的年份是从1900年开始的,而大多数日期操作通常使用从1970年1月1日开始的时间。

    • 日期的日、月和年偏移量是违反直觉的:天从0开始,月从1开始,而且要访问它们中的任何一个,必须使用Calendar类。

为此,Joda-Time提供了一个干净流畅的API来处理日期和时间;Joda-Time还支持八种日历系统,而Java只支持两种:GregorianCalendar和JapaneseImperialCalendar。

  • 安装

添加依赖:

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.12.5</version>
</dependency>
  • 结构

Joda-Time使用org.joda.time包中的类对日期和时间的概念进行建模:用org.joda.time.format包中的类解析和格式化日期;用org.joda.time.chronoorg.joda.time.tz包中的类表示日历系统和特定于时区。

二、API

1、日期和时间

  • LocalDate:日期(不带时间)
import org.joda.time.LocalDate;

LocalDate currentDate = LocalDate.now();
System.out.println(currentDate);

输出:

2023-09-10
  • LocalTime:时间(不带时区)
import org.joda.time.LocalTime;

LocalTime currentTime = LocalTime.now();
System.out.println(currentTime);

输出:

14:49:02.141
  • LocalDateTime:日期和时间(不带时区)
import org.joda.time.LocalDateTime;

LocalDateTime currentDateAndTime = LocalDateTime.now();
System.out.println(currentDateAndTime);

输出:

2023-09-10T14:49:42.957

LocalDateTime对象可以通过toDateTime()方法获取DateTime对象(带时区):

DateTime dateTime = currentDateAndTime.toDateTime();

LocalDateTime对象可以通过toLocalDate()方法获取LocalDate对象:

LocalDate localDate = currentDateAndTime.toLocalDate();

LocalDateTime对象可以通过toLocalTime()方法获取LocalTime对象:

LocalTime localTime = currentDateAndTime.toLocalTime();

LocalDateTime对象可以通过toDate()方法获取Java Date对象:

Date date = currentDateAndTime.toDate();

LocalDate、LocalTime、LocalDateTime可以接受一个DateTimeZone对象来表示指定时区的日期或时间:

DateTimeZone timeZone = DateTimeZone.forID("UTC");
LocalDate date = LocalDate.now(timeZone);
System.out.println(date);
LocalTime time = LocalTime.now(timeZone);
System.out.println(time);
LocalDateTime dateTime = LocalDateTime.now(timeZone);
System.out.println(dateTime);

输出:

2023-09-10
06:50:58.905
2023-09-10T06:50:58.907

Joda-Time还提供了与Java日期和时间对象的转换:

import java.util.Date;
import org.joda.time.LocalDateTime;

LocalDateTime dateTime = new LocalDateTime(new Date());
System.out.println(dateTime);
Date date = dateTime.toDate();
System.out.println(date);

输出:

2023-09-10T14:51:33.939
Sun Sep 10 14:51:33 CST 2023

2、自定义日期时间

Joda-Time提供了自定义日期和时间的方式:

  • 构造函数
import java.util.Date;
import org.joda.time.DateTime;
import org.joda.time.Instant;

Date oneMinuteAgoDate = new Date(System.currentTimeMillis() - (60 * 1000));
Instant oneMinuteAgoInstant = new Instant(oneMinuteAgoDate);
DateTime dateTimeFromInstant = new DateTime(oneMinuteAgoInstant);
DateTime dateTimeFromJavaDate = new DateTime(oneMinuteAgoDate);
DateTime dateTimeFromString = new DateTime("2022-10-01T08:00:00.100");
DateTime dateTimeFromParts = new DateTime(2022, 10, 1, 8, 0, 0, 100);

System.out.println(oneMinuteAgoDate);
System.out.println(oneMinuteAgoInstant);
System.out.println(dateTimeFromInstant);
System.out.println(dateTimeFromJavaDate);
System.out.println(dateTimeFromString);
System.out.println(dateTimeFromParts);

输出:

Sun Sep 10 17:16:15 CST 2023
2023-09-10T09:16:15.340Z
2023-09-10T09:16:15.340Z
2023-09-10T17:16:15.340+08:00
2022-10-01T08:00:00.100+08:00
2022-10-01T08:00:00.100+08:00
  • 解析ISO格式的日期和时间字符串
DateTime parsedDateTime = DateTime.parse("2022-10-01T08:00:00.100");
System.out.println(parsedDateTime);

输出:

2022-10-01T08:00:00.100+08:00
  • 使用自定义DateTimeFormatter
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
DateTime parsedDateTime = DateTime.parse("2023-05-01 06:08:00", dateTimeFormatter);
System.out.println(parsedDateTime);

输出:

2023-05-01T06:08:00.000+08:00

3、时刻

Instant:表示从Java纪元(epoch)1970-01-01T00:00:00Z到给定时刻的毫秒数

  • 当前时刻

可以使用默认构造函数或now()方法来获取当前时刻

import org.joda.time.Instant;

Instant instant = new Instant();
System.out.println(instant);
Instant now = Instant.now();
System.out.println(now);

输出:

2023-09-10T07:12:19.184Z
2023-09-10T07:12:19.244Z
  • 自定义时刻

如果要创建自定义时刻,可以使用构造函数传入ISO格式日期和时间的字符串、Java Date对象或从1970-01-01T00:00:00Z开始的毫秒数:

Instant instantFromString = new Instant("2022-05-05T22:10:01");
System.out.println(instantFromString);

Long milliSeconds = System.currentTimeMillis() - 60 * 1000;
Instant instantFromDate = new Instant(new Date(milliSeconds));
System.out.println(instantFromDate);

Instant instantFromTimestamp = new Instant(milliSeconds);
System.out.println(instantFromTimestamp);

输出:

2022-05-05T22:10:01.000Z
2023-09-10T07:12:14.291Z
2023-09-10T07:12:14.291Z

也可以使用ofEpochMilli()指定毫秒数或使用ofEpochSecond()指定秒:

Long oneMinuteAgoMilliSeconds = System.currentTimeMillis() - 60 * 1000;
Instant instantOfEpochMilli = Instant.ofEpochMilli(oneMinuteAgoMilliSeconds);
System.out.println(instantOfEpochMilli);
Instant instantOfEpochSecond = Instant.ofEpochSecond(oneMinuteAgoMilliSeconds / 1000);
System.out.println(instantOfEpochSecond);

输出:

2023-09-10T07:15:29.591Z
2023-09-10T07:15:29.000Z
  • 转换

Instant可以转换为DateTime对象或Java Date对象:

Instant instant = Instant.now();
DateTime dateTimeFromInstant = instant.toDateTime();
Date dateFromInstant = instant.toDate();
System.out.println(dateTimeFromInstant);
System.out.println(dateFromInstant);

输出:

2023-09-10T15:23:17.997+08:00
Sun Sep 10 15:23:17 CST 2023
  • 比较大小

Instant实现了Comparable接口,可以使用compareTo()方法比较两个对象的大小;Instant也实现了Joda-Time的ReadableInstant接口,也可以用isAfter()isBefore()等方法比较:

Instant instantNow = Instant.now();
Instant oneMinuteAgoInstant = Instant.ofEpochMilli(System.currentTimeMillis() - 60 * 1000);
System.out.println(instantNow.toDate());
System.out.println(oneMinuteAgoInstant.toDate());

System.out.println(instantNow.compareTo(oneMinuteAgoInstant));
System.out.println(instantNow.isAfter(oneMinuteAgoInstant));
System.out.println(oneMinuteAgoInstant.isBefore(instantNow));
System.out.println(oneMinuteAgoInstant.isBeforeNow());
System.out.println(instantNow.isEqual(oneMinuteAgoInstant));

输出:

Sun Sep 10 16:07:19 CST 2023
Sun Sep 10 16:06:19 CST 2023
1
true
true
true
false
  • 获取日期时间的一部分

可以通过Instant的get(DateTimeFieldType type)方法获取日期和时间的一部分,例如:年份、小时等:

Instant instant = Instant.now();
int year = instant.get(DateTimeFieldType.year());
int month = instant.get(DateTimeFieldType.monthOfYear());
int day = instant.get(DateTimeFieldType.dayOfMonth());
int hour = instant.get(DateTimeFieldType.hourOfDay());
int minute = instant.get(DateTimeFieldType.minuteOfHour());
int second = instant.get(DateTimeFieldType.secondOfMinute());
System.out.println(String.format("%s-%s-%s %s:%s:%s", year, month, day, hour, minute, second));

输出:

2023-9-10 7:28:18

4、持续时间、期间和间隔

  • Duration:表示两个时间点之间的持续时间,以毫秒为单位

关于持续时间格式的介绍可以参考:ISO 8601日期格式与持续时间格式

import org.joda.time.Duration;

long currentTimestamp = System.currentTimeMillis();
long oneHourAgo = currentTimestamp - 60 * 60 * 1000;
Duration duration = new Duration(oneHourAgo, currentTimestamp);
System.out.println(duration);
Instant afterAnHour = Instant.now().plus(duration);
System.out.println(afterAnHour);

输出:

PT3600S
2023-09-10T08:34:40.057Z

通过Duration对象,可以获取持续时间对应的天、小时、分钟、秒和毫秒:

long currentTimestamp = System.currentTimeMillis();
long oneDayAgo = currentTimestamp - 24 * 60 * 60 * 1000;
Duration duration = new Duration(oneDayAgo, currentTimestamp);
System.out.println(duration.getStandardDays());
System.out.println(duration.getStandardHours());
System.out.println(duration.getStandardMinutes());
System.out.println(duration.getStandardSeconds());
System.out.println(duration.getMillis());

输出:

1
24
1440
86400
86400000
  • Period:与Duration类似,但允许访问日期和时间对象的一部分,如年、月、日等。

Period和Duration之间的主要区别在于Period是根据其日期和时间(年、月、小时等)定义的,并不表示精确的毫秒数。当使用Period日期和时间作计算时,会考虑时区和夏令时(Daylight Saving Time)。

例如:当前是2月1日,计算1个月之后的时间,使用Period可以正确计算,而使用Duration只能指定一个固定的时间量:

import org.joda.time.Period;

//1个月
Period period = new Period().withMonths(1);
LocalDateTime datePlusPeriod = new LocalDateTime(2023, 2, 1, 8, 0).plus(period);
System.out.println(datePlusPeriod);

//30天
Duration duration = new Duration(30 * 24 * 60 * 60 * 1000);
LocalDateTime datePlusDuration = new LocalDateTime(2023, 2, 1, 8, 0).plus(duration);
System.out.println(datePlusDuration);

输出:

2023-03-01T08:00:00.000
2023-01-12T14:57:12.704
  • Interval:表示两个固定时间点之间的日期和时间间隔

一分钟的间隔:

import org.joda.time.Interval;

Instant instantNow = Instant.now();
Instant oneMinuteAgoInstant = Instant.ofEpochMilli(System.currentTimeMillis() - 60 * 1000);

Interval interval = new Interval(oneMinuteAgoInstant, instantNow);
System.out.println(interval);

输出:

2023-09-10T08:07:23.652Z/2023-09-10T08:08:23.652Z

可以用overlap()方法来检查两个间隔是否重叠或者计算它们之间的差值,此方法返回重叠的Interval,如果不重叠则返回null:

Instant startInterval1 = new Instant("2022-10-01T08:00:00");
Instant endInterval1 = new Instant("2022-10-01T10:00:00");
Interval interval1 = new Interval(startInterval1, endInterval1);
		
Instant startInterval2 = new Instant("2022-10-01T09:00:00");
Instant endInterval2 = new Instant("2022-10-01T11:00:00.000");
Interval interval2 = new Interval(startInterval2, endInterval2);

Interval overlappingInterval = interval1.overlap(interval2);
System.out.println(overlappingInterval);

输出:

2022-10-01T09:00:00.000Z/2022-10-01T10:00:00.000Z

两个间隔之前的差异可以使用gap()方法计算:

Instant startInterval1 = new Instant("2022-10-01T08:00:00");
Instant endInterval1 = new Instant("2022-10-01T09:00:00");
Interval interval1 = new Interval(startInterval1, endInterval1);
		
Instant startInterval2 = new Instant("2022-10-01T11:00:00");
Instant endInterval2 = new Instant("2022-10-01T12:00:00.000");
Interval interval2 = new Interval(startInterval2, endInterval2);

Interval gap = interval1.gap(interval2);
System.out.println(gap);

输出:

2022-10-01T09:00:00.000Z/2022-10-01T11:00:00.000Z

如果想知道一个间隔的结束是否等于另一个间隔的开始时,可以使用abuts()方法:

Instant startInterval1 = new Instant("2022-10-01T08:00:00");
Instant endInterval1 = new Instant("2022-10-01T10:00:00");
Interval interval1 = new Interval(startInterval1, endInterval1);
		
Instant startInterval2 = new Instant("2022-10-01T10:00:00");
Instant endInterval2 = new Instant("2022-10-01T12:00:00.000");
Interval interval2 = new Interval(startInterval2, endInterval2);

System.out.println(interval1.abuts(interval2));//true

5、日期和时间操作

日期和时间常见操作是加、减和转换,Joda-time为LocalDate、LocalTime、LocalDateTime和DateTime的每个类提供特定的方法;这些类是不可变的,每次方法调用都会创建一个其类型的新对象。

LocalDateTime currentDate = LocalDateTime.now();
System.out.println(currentDate);
//添加一天
LocalDateTime nextDay = currentDate.plusDays(1);
System.out.println(nextDay);
//添加一个周期
Period oneMonthPeriod = new Period().withMonths(1);
LocalDateTime nextMonth = currentDate.plus(oneMonthPeriod);
System.out.println(nextMonth);
//减去一年
LocalDateTime lastYear = currentDate.minusYears(1);
System.out.println(lastYear);
//设置时、分、秒
LocalDateTime currentDateAtHour6 = currentDate.withHourOfDay(6).withMinuteOfHour(0).withSecondOfMinute(0);
System.out.println(currentDateAtHour6);

输出:

2023-09-10T16:40:58.707
2023-09-11T16:40:58.707
2023-10-10T16:40:58.707
2022-09-10T16:40:58.707
2023-09-10T06:00:00.707

6、时区

Joda-Time中的抽象类DateTimeZone用来表示关于时区的所有方面,Joda-Time使用的默认时区是从Java系统属性user.timezone中选择的;Joda-Time允许对每个类或计算单独指定应该使用的时区。

设置默认时区:

DateTimeZone.setDefault(DateTimeZone.UTC);

上面设置完默认时区后,所有的日期和时间的操作,如果没有另外指定将在UTC时区表示。

可以使用getAvailableIDs()方法查看所有可用的时区:

Set<String> availableIDs = DateTimeZone.getAvailableIDs();

如果需要表示特定时区的日期或时间,需要在LocalTime、LocalDate、LocalDateTime、DateTime的构造函数中指定DateTimeZone;另外,

import java.util.Date;
import java.util.TimeZone;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;


DateTime dateTimeInShanghai = new DateTime(DateTimeZone.forID("Asia/Shanghai"));
LocalDateTime localDateTimeInShanghai  = new LocalDateTime(DateTimeZone.forID("Asia/Shanghai"));
System.out.println(dateTimeInShanghai);
System.out.println(localDateTimeInShanghai);

DateTime dateTimeInNewYork = localDateTimeInShanghai.toDateTime(DateTimeZone.forID("America/New_York"));
Date dateInNewYork = localDateTimeInShanghai.toDate(TimeZone.getTimeZone("America/New_York"));
System.out.println(dateTimeInNewYork);
System.out.println(dateInNewYork);

输出:

2023-09-10T17:03:00.549+08:00
2023-09-10T17:03:00.578
2023-09-10T17:03:00.578-04:00
Mon Sep 11 05:03:00 CST 2023
参考资料

Introduction to Joda-Time

Introduction to the Java 8 Date/Time API