Java 新的日期和时间API

By | 3月 6, 2022

现在网上的Java教程,关于Java日期和时间,很多都介绍的是java.util.Date。

Date date = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat ("yyyy-MM-dd hh:mm:ss");
String dateStr = dateFormat.format(date);
Date parsedDate = dateFormat.parse("2022-02-06 10:16:34");

‘java.util.Date’是Java 1.0引入的,Date中的大部分方法在Java 1.1引入Calendar之后被废弃了(标注为Deprecated)。Date的API非常难用,它的实例都是可变的,而且不能处理闰秒等问题。Java 8引入的java.time API纠正了过去的缺陷,将来会长时间为我们服务。

在Java 8新日期和时间API出现之前,大部分项目都使用Joda-Time来处理日期和时间,Java 8新的日期和时间API和Joda-Time很相似,很多类的命名都和Joda-Time一样。自从Java 8之后,Joda-Time官网发布说:

Joda-Time is the de facto standard date and time library for Java prior to Java SE 8. Users are now asked to migrate to java.time (JSR-310).

至此Java 8的java.time API成为标准,Java 8之前的老项目才需要使用Joda-Time。学习Java 8的新日期时间API,要记住它的对象实例都是不可变的,任何操作都只是生成一个新的对象。

1. Instant和Duration

Instant对象表示时间轴上的一个点,原点(也称为元年)被规定为1970年1月1日的午夜,此时本初子午线正在穿过伦敦格林威治皇家天文台。UNIX/POSIX时间也是用了一样的约定。从原点开始,时间按照每天86400秒进行计算,向前向后分别以纳秒为单位。Instant最小值(Instant.MIN)可以回退到10亿年之久;Instant最大值(Instant.MAX)表示1000000000年12月31日。

Duration表示两个Instant之间的距离,使用Duration.between静态方法,下面的例子是计算某算法的运行时间:

Instant start = Instant.now();
runAlgorithm();
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
long millis = timeElapsed.toMillis();
// timeElapsed.toDays();
// timeElapsed.toHours();
// timeElapsed.toMinutes();
// timeElapsed.toNanos();

Instant和Duration的数学操作:

方法描述
plus, minus对当前Instant或Duration,增加或减少一段时间
plusNanos, plusMillis, plusSeconds, plusMinutes, plusHours, plusDays根据指定单位,对当前Instant或Duration添加一段时间
minusNanos, minusMillis, minusSeconds, minusMinutes, minusHours, minusDays根据指定单位,对当前Instant或Duration减少一段时间
multiplieBy, divideBy, negated仅对Duration,乘以,除以一个long值;negated表示取相反数。
isZero, isNegative仅对Duration,判断是否为0,是否为负数

下面代码,检查某个算法是否比另一个算法快10倍。

Duration timeElapsed1 = Duration.between(start. 1, end1);
Duration timeElapsed2 = Duration.between(start2, end2);
boolean overTenTimesFaster = timeElapsed2.multipliedBy(10).minus(timeElapsed1).isNegative();

2. LocalDate和Period

LocalDate表示一个没有时区的日期(年月日),使用LocalDate的now或这of方法来创建LocaDate。

LocalDate today = LocalDate.now();
LocalDate alonzosBirthday = LocalDate.of(1903, 6, 4);
// The eighth day of March 2022
LocalDate womenDay = LocalDate.of(2022, Month.MARCH, 8);
// The 256th day of 2022.
LocalDate programmersDay = LocalDate.ofYearDay(2022, 256);

前面提到两个Instant之间的距离叫做Duration。对于两个LocalDate,它们之间的距离叫做Period,使用until方法得到Period:

LocalDate today = LocalDate.of(2022, 2, 15);
LocalDate nextSpringFestival = LocalDate.of(2023, 1, 22);
Period period = today.until(nextSpringFestival);
System.out.println(period.getYears());  // 0
System.out.println(period.getMonths()); // 11
System.out.println(period.getDays());   // 7
// Get specific years, months, days
long years = today.until(nextSpringFestival, ChronoUnit.YEARS);
long months = today.until(nextSpringFestival, ChronoUnit.MONTHS);
long weeks = today.until(nextSpringFestival, ChronoUnit.WEEKS);
long days = today.until(nextSpringFestival, ChronoUnit.DAYS);
System.out.println(String.format(
    "Years: %s, months: %s, weeks: %s, days: %s", years, months, weeks, days));
// Output: Years: 0, months: 11, weeks: 48, days: 341

LocalDate常用的方法:

方法描述
plusDays, plusWeeks, plusMonths, plusYears增加几天,几周,几个月或者几年
minusdays, minusWeeks, minusMonths, mimusYears减少几天,几周,几个月或者几年
plus, minus添加或减少一个Duration或者Period
withDayOfMonth
withDayOfYear
withMonth
withYear
将月份天数、年份天数,月份,年份修改为指定值,返回一个新的LocalDate
getYear
getMonth
getDayOfYear
getDayOfMonth
getDayOfWeek
获得年份
获得月份:Month枚举值
获得该年的第几天(1~366之间)
获得该月的第几天(1-31之间)
获得星期几:DayOfWeek的枚举
isBefore, isAfter比较两个LocalDate
isLeapYear判断是否为闰年

使用plusXXX 或 minusXXX 修改LocalDate可能会产生不存在的日期。例如,给1月31日增加一个月不应该产生2月31日。与抛出异常相反,这些方法会返回该月最后一个有效的日期。例如:LocalDate.of(2022, 1, 31).plusMonths(1)LocalDate.of(2022, 3, 31).minusMonths(1) 都会返回2016年2月29日。

DayOfWeek是星期的枚举:MONDAY, TUESDAY, WEDNESDAY, etc.

Month是月份的枚举:JANUARY, FEBRUARY, MARCH, etc.

3. TemporalAdjuster(日期校正器)

对于一些需要安排调度的应用程序来说,通常需要计算例如“每个月的第一个周二”这样的日期。TemporalAdjusters类提供了许多静态方法进行常用的校正。例如获得某月的第二个周二:

LocalDate firstTuesday = LocalDate.of(2022, 2, 1)
    .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY));

新API对象都是不可变的,with方法会返回一个新的LocalDate对象,而不会修改原有的对象。下面列出了可用的矫正器, DayOfWeek是星期的枚举:

方法描述
next(DayOfWeek), previous(DayOfWeek)前一个或下一个DayOfWeek
nextOrSame(DayOfWeek),
previousOrSame(DayOfWeek)
同next和previous,不过如果当前日期就是输入的DayOfWeek,返回当前日期
dayOfWeekInMonth(n, DayOfWeek)本月的第n个DayOfWeek
lastInMonth(weekday)本月的最后一个DayOfWeek
firstDayOfMonth(),
firstDayOfNextMonth(),
firstDayOfNextYear()
如方法名所示
lastDayOfMonth(),
lastDayOfPreviousMonth(),
firstDayOfYear(),
如方法名所示

根据需要我们还可以实现自己的校正器。

TemporalAdjuster nextWorkdayAdjuster = TemporalAdjusters.ofDateAdjuster(localDate -> {
  LocalDate result = localDate;
  do {
    result = result.plusDays(1);
  } while (result.getDayOfWeek().getValue() >= 6);
  return result;
});
LocalDate.of(2022, 2, 11).with(nextWorkdayAdjuster);  // 2022-02-14

4. LocalTime

LocalTime表示一天中的某个时间,例如15:30:00。创建LocalTime:

LocalTime rightNow = LocalTime.now();
LocalTime.of(17, 30);           // 17:30
LocalTime.of(17, 30, 12);       // 17:30:12
LocalTime.of(17, 30, 12, 450);  // 17:30:12:450

LocalTime的相关方法:

方法描述
plusHours, plusMinutes,
plusSeconds, plusNanos
增加一段时间
minusHours, minusMinutes,
minusSeconds, minusNanos
减去一段时间
plus, minus增加或前去一个Duration
withHour, withMinute,
withSecond, withNano
修改对应值,并返回新的LocalDate
getHour, getMinute,
getSecond, getNano
获得对应的值
toSecondOfDay, toNanoOfDay转换成秒,纳秒。
isBefore, isAfter比较两个LocalTime

LocalTime本身不关心是AM还是PM,而是交给格式化程序来做这件事。

LocalDateTime 是 LocalDate 和 LocalTime 的合体,它的实例对象包含了 LocalDate 和 LocalTime 的方法。

5. ZonedDateTime

Internet Assigned Numbers Authority(IANA) 维护着一份全球所以已知时区的数据库(https://www.iana.org/time-zones),每年会更新几次,这些更新主要是处理夏令时规则的改变。Java就使用了IANA的数据库。

每个时区都有一个ID,例如America/Los_Angeles或者Asia/Shanghai。调用ZoneId.getAvailableZoneIds()可获得所有的时区,写该博客时有590个。

ZoneId zoneShangHai = ZoneId.of("Asia/Shanghai");
// ZoneId systemZone = ZoneId.systemDefault();
LocalDateTime dateTime = LocalDateTime.of(2022, 1, 1, 12, 30);
ZonedDateTime dateTimeAtShanghai = dateTime.atZone(zoneShangHai);
// Static methods of creating ZonedDateTime
ZonedDateTime.of(localDateTime, zone)
ZonedDateTime.of(date, time, zone)
ZonedDateTime.of(year, month, day, hour, minute, second, nanoOfSecond, zone)
ZonedDateTime.ofInstant(instant, zone)
ZonedDateTime.ofInstant(localDateTime, offset, zone)

上面介绍的LocalDataTime的方法,ZonedDataTime也有,这里不多列举。下面列举些仅ZonedDataTime有的方法:

LocalDateTime dateTime = LocalDateTime.of(2022, 1, 1, 12, 30);
ZonedDateTime dateTimeAtShanghai = dateTime.atZone(ZoneId.of("Asia/Shanghai"));
ZoneId zoneNewYork = ZoneId.of("America/Los_Angeles");
System.out.println(dateTimeAtShanghai.withZoneSameInstant(zoneNewYork));
// Output: 2021-12-31T20:30-08:00[America/Los_Angeles]
System.out.println(dateTimeAtShanghai.withZoneSameLocal(zoneNewYork));
// Output: 2022-01-01T12:30-08:00[America/Los_Angeles]
// Convert to LocalDate, LocalTime, Instant
LocalDate localDate = dateTimeAtShanghai.toLocalDate();
LocalTime localTime = dateTimeAtShanghai.toLocalTime();
Instant instant = dateTimeAtShanghai.toInstant();

ZoneId.of(“UTC”)获得的UTC时区。UTC表示“协调世界时”,该缩写是英语”coordinated universal time“和法语“Temps universel coordonné”二者妥协的产物,与任何一个都不直接匹配。UTC是格林威治皇家天文台的时间,不包含夏令时。

当夏令时开始时,时钟会前进一小时。例如,在2013年,中欧从3月31日2:00进入夏令时,此时时钟会被调快1小时,变成3:00。如果你创建一个不存在的时间点2013年3月2:30,那么实际上你会得到3月31日3:30。

ZonedDateTime.of(
    LocalDate.of(2013, 3, 31),
    LocalTime.of(2, 30),
    ZoneId.of("Europe/Berlin"));
// Create: 2013-3-31-3:30

反过来,当夏令时介绍时,时钟会回退一小时,因此有两个拥有相同本地时间的瞬时点!当你创建这一小时之内的时间时,你会得到二者中较早的哪个。

ZonedDateTime ambiguous = ZonedDateTime.of(
    LocalDate.of(2013, 10, 27), //  End daylight saving time
    LocalTime.of(2, 30),
    ZoneId.of("Europe/Berlin"));
// 2013-10-27T02:30+02:00[Europe/Berlin]
ZonedDateTime anHourLater = ambiguous.plusHours(1);
// 2013-10-27T02:30+01:00[Europe/Berlin]

1小时之后,时间的小时和分钟没变,但是时区偏移量改变了。

当调整一个跨夏令时的时区时,要多加小心。例如,如果你安排了下周的一个会议,不要添加7天的Duration对象。

meeting.plus(Duration.ofDays(7));
// Caution. Cannot handle 'daylight saving time'

相反应当使用Period:

ZonedDateTime nextMeeting = meeting.plus(Period.ofDays(7));
// Right. It takes 'daylight saving time' into account

6. DateTimeFormatter(格式化和解析)

DateTimeFormatter类提供了三种格式话日期/时间的方法:

  • 预定义的标准格式
  • 语言环境相关的格式
  • 自定义的格式

6.1 预定义标准格式

ZonedDateTime newYear = ZonedDateTime.of(
    LocalDateTime.of(2022, 1, 1, 9, 0),
    ZoneId.systemDefault());
String formatted = DateTimeFormatter.ISO_DATE_TIME.format(newYear);
// 2022-01-01T09:00:00+08:00[Asia/Shanghai]

下面表格示例格式化的日期是:2022年1月1日9:00。

格式描述示例
BASIC_ISO_DATE日期没有分隔符20220101+0800
ISO_LOCAL_DATE
ISO_LOCAL_TIME
ISO_LOCAL_DATE_TIME
分隔符为- : T2022-01-01
09:00:00
2022-01-01T09:00:00
ISO_OFFSET_DATE
ISO_OFFSET_TIME
ISO_OFFSET_DATE_TIME
同ISO_LOCAL_XXX,但有时区2022-01-01+08:00
09:00:00+08:00
2022-01-01T09:00:00+08:00
ISO_ZONED_DATE_TIME含有时区和时区ID2022-01-01T09:00:00+08:00[Asia/Shanghai]
ISO_INSTANT用z时区ID表示的UTC时间2022-01-01T01:00:00Z
ISO_ORDINAL_DATE年份-天数2022-001+08:00
ISO_WEEK_DATE年份-第几周-星期几2021-W52-6+08:00
RFC_1123_DATE_TIMERFC定义的邮件时间戳Sat, 1 Jan 2022 09:00:00 +0800

6.2 语言相关的格式化

下面表格示例格式化的日期是:1969年7月16号9:32。EDT表示 Eastern Daylight Time。

风格日期时间
SHORT7/16/699:32 AM
MEDIUMJul 16, 19699:32:00 AM
LONGJuly 16, 19699:32:00 AM EDT
FULLWednesday, July 16, 19699:32:00 AM EDT

因为日期时区是 America/New_York, 所以下面示例默认就按照美国的语言格式化。

ZonedDateTime dateTime = ZonedDateTime.of(
    LocalDateTime.of(1969, 7, 16, 9, 32),
    ZoneId.of("America/New_York"));
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
String dateTimeStr = formatter.format(dateTime);
// July 16, 1969 9:32:00 AM EDT

如果要切换为其它语言,就使用withLocale方法。

formatter.withLocale(Locale.CHINESE).format(dateTime);
// 1969年7月16日 上午09时32分00秒

6.3 自定义格式格式化

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
formatter.format(dateTime);
// 1969-07-16 09:32

6.4 解析日期/时间字符串

使用LocalDate, Localtime, LocalDateTime, ZonedDateTime的parse方法便可。

LocalDate.parse("1903-06-14", DateTimeFormatter.ISO_LOCAL_DATE);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssxx");
ZonedDateTime.parse("1969-07-16 03:32:00-0400", formatter);

7. 与遗留代码互操作

Java 8引入的日期和时间API不得不与已有的类之间互相操作,尤其是无处不在的Java.util.Date, java.util.GregorianCalendar 及 Java.sql.Date/Time/Timestamp。

To遗留类From遗留类
Instant ↔
Java.util.Date
Date.from(instant)date.toInstant()
ZonedDateTime ↔
java.util.GregorianCalendar
GregorianCalendar.from(zonedDateTime)cal.toZonedDateTime()
Instant ↔
java.sql.Timestamp
TimeStamp.from(instant)timestamp.toInstant()
LocalDateTime ↔
java.sql.Timestamp
TimeStamp.valueOf(localDateTime)timeStamp.toLocalDateTime()
LocalDate ↔
java.sql.Time
Date.valueOf(localDate)date.toLocalDate()
LocalTime ↔
java.sql.time
Time.valueOf(localTime)date.toLocalTime()
DateTimeFormatter →
java.text.DateFormat
formatter.toFormat()
java.util.TimeZone ↔
ZoneId
Timezone.getTimeZone(id)timeZone.toZoneId
java.nio.file.attribute.FileTime ↔
Instant
FileTime.from(instant)fileTime.toInstant