Java8 时间和日期API

旧API的问题:

在Java 1.0中,对日期和时间的支持只能依赖java.util.Date类。 存在的问题有:

  1. 年份的起始选择是1900年
  2. 月份的起始从0开始

在Java 1.1中, Date类中的很多方法被废弃了,取而代之的是java.util.Calendar类 ,仍然有很多问题:

  1. 月份依旧是从0开始计算
  2. 同时存在Date和Calendar这两个类,增加了程序员的疑惑
  3. 有的特性只在某一个类有提供,比如DateFormat只在Date类里有
  4. DateFormat不是线程安全的
  5. Date和Calendar类都是可变的

所有这些缺陷和不一致导致用户们转投第三方的日期和时间库,比如Joda-Time。为了解决这些问题, Oracle决定在原生的Java API中提供高质量的日期和时间支持。所以,你会看到Java 8在java.time包中整合了很多Joda-Time的特性。

先给出几个新API的示例,可以看到,Java8中对时间、日期的操作是非常优雅和易于理解的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LocalDate today = LocalDate.now(); 
// 获取两个日期的间隔
LocalDate today = LocalDate.now();
LocalDate someDay = LocalDate.of(2021, Month.MAY, 14);
Period period = Period.between(today, someDay);
// interval : 2 years, 0 months, 10 days
System.out.printf("interval : %d years, %d months, %d days ", period.getYears(), period.getMonths(), period.getDays());

// 取下一天:
LocalDate tomorrow = today.plusDays(1);
// 取本月第1天:
LocalDate secondDay = today.withDayOfMonth(1);
// 取本月最后一天
LocalDate lastDay = today.with(TemporalAdjusters.lastDayOfMonth());
// 取本月第一个周一
LocalDate firstMonday = today.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));

// 获取当前时刻
LocalTime now = LocalTime.now();
// 将当前时间加上2小时
LocalTime then = now.plusHours(2);
// 获取一个时间段的毫秒数
long window = Duration.ofMinutes(45).toMillis();

时间、日期、时间戳

LocalDate

该类的实例是一个不可变对象,它只包含日期,并不含时间。另外,它不附带时区信息

1
2
3
4
5
6
7
8
9
10
// 创建一个LocalDate对象并读取其值
LocalDate date = LocalDate.of(2014, 3, 18); // 2014-03-18
int year = date.getYear();
Month month = date.getMonth();
int day = date.getDayOfMonth(); // 18
DayOfWeek dow = date.getDayOfWeek();
int len = date.lengthOfMonth(); // 31
boolean leap = date.isLeapYear(); // false 是否闰年
// 你还可以使用工厂方法从系统时钟中获取当前的日期:
LocalDate today = LocalDate.now();

LocalTime

一天中的时间,比如13:45:20,可以使用LocalTime类表示 :

1
2
3
4
LocalTime time = LocalTime.of(13, 45, 20);
int hour = time.getHour();
int minute = time.getMinute();
int second = time.getSecond();

LocalDate和LocalTime都可以通过字符串创建:

1
2
LocalDate date = LocalDate.parse("2014-03-18");
LocalTime time = LocalTime.parse("13:45:20");

LocalDateTime

LocalDateTime,是LocalDate和LocalTime的合体。它同时表示了日期和时间,但不带有时区信息,你可以直接创建,也可以通过合并日期和时间对象构造,如下所示。

1
2
3
4
5
6
7
8
9
10
11
// 直接创建LocalDateTime对象,或者通过合并日期和时间的方式创建
// 2014-03-18T13:45:20
LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
// 另外,也可以通过它们各自的atTime或者atDate方法,向LocalDate传递一个时间对象,或者向LocalTime传递一个日期对象的方式,创建LocalDateTime对象
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
// 从LocalDateTime中提取LocalDate或者LocalTime
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();

时间戳类Instant

Unix元年时间(UTC时区1970年1月1日午夜时分)开始所经历的秒数

1
2
3
4
5
6
7
8
9
10
11
// 静态工厂方法now,它能够帮你获取当前时刻的时间戳
Instant.now();

/*
以通过向静态工厂方法ofEpochSecond传递一个代表秒数的值创建一个该类的实例。
还有一个增强的重载版本,它接收第二个以纳秒为单位的参数值,对传入作为秒数的参数进行 调整。
*/
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000);// 2秒加100万纳秒(1秒)
Instant.ofEpochSecond(4, -1_000_000_000);// 4秒之前的100万纳秒(1秒)

可以通过Duration和Period类使用Instant 。

Duration 、Period

Duration类主要用于以秒和纳秒衡量时间的长短 。

1
2
3
4
// 获取两个LocalTime对象、两个LocalDateTime对象,或者两个Instant对象之间的duration
Duration d1 = Duration.between(time1, time2);
Duration d1 = Duration.between(dateTime1, dateTime2);
Duration d2 = Duration.between(instant1, instant2);
  • 由于LocalDateTime和Instant是为不同的目的而设计的,一个是为了便于人阅读使用,另一个是为了便于机器处理,所以你不能将二者混用。如果你试图在这两类对象之间创建duration,会触发一个DateTimeException异常。
  • Duration类主要用于以秒和纳秒衡量时间的长短,所以不能向between方法传递LocalDate对象做参数。

使用Period类,得到两个LocalDate之间的时长

1
2
Period tenDays = Period.between(LocalDate.of(2014, 3, 8),
LocalDate.of(2014, 3, 18));

最后, Duration和Period类都提供了很多非常方便的工厂类,直接创建对应的实例 。

1
2
3
4
5
6
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);

Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

操纵、解析和格式化日期

截至目前,我们介绍的这些日期/时间对象都是不可变的 。

如果你已经有一个LocalDate对象,想要创建它的一个修改版副本,最直接也最简单的方法是使用withAttribute方法。

1
2
3
4
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.withYear(2011);
LocalDate date3 = date2.withDayOfMonth(25);
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);

或者以声明的方式操纵LocalDate对象

1
2
3
4
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.plusWeeks(1);
LocalDate date3 = date2.minusYears(3);
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);

TemporalAdjuster

有的时候,你需要进行一些更加复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象

1
2
3
4
import static java.time.temporal.TemporalAdjusters.*;
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));
LocalDate date3 = date2.with(lastDayOfMonth());

另外,还也已创建自定义的TemporalAdjuster,例如获得当前日期的下一个工作日。见《Java 8 in Action》。

打印输出及解析日期、时间对象

和老的java.util.DateFormat相比较,所有的DateTimeFormatter实例都是线程安全 的。所以,你能够以单例模式创建格式器实例,就像DateTimeFormatter预定义的那些格式器常量,并能在多个线程间共享这些实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 格式化输出
LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);// 20140318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);// 2014-03-18
// 解析
LocalDate date1 =
LocalDate.parse("20140318",DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 =
LocalDate.parse("2014-03-18",DateTimeFormatter.ISO_LOCAL_DATE);

// DateTimeFormatter类还支持一个静态工厂方法,它可以按照某个特定的模式创建格式器
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);

// ofPattern方法也提供了一个重载的版本,使用它你可以创建某个Locale的格式器
DateTimeFormatter italianFormatter =
DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date.format(italianFormatter); // 18. marzo 2014
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);

最后,如果你还需要更加细粒度的控制, DateTimeFormatterBuilder类还提供了更复杂 的格式器,你可以选择恰当的方法,一步一步地构造自己的格式器。另外,它还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精 确 地 匹 配 指 定 的 模 式 )、 填 充 ,以及在格式器中指 定可选节 。比 如 , 你可以通 过DateTimeFormatterBuilder 自己编程实现italianFormatter,代码清单如下。

1
2
3
4
5
6
7
8
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.ITALIAN);

时区

日期和时间的类都不包含时区信息。

时区的处理是新版日期和时间API新增加的重要功能,使用新版日期和时间API时区的处理被极大地简化了。新的java.time.ZoneId类是老版java.util.TimeZone的替代品。

可以通过调用ZoneId的getRules()得到指定时区。每个特定的ZoneId对象都由一个地区ID标识,比如: ZoneId romeZone = ZoneId.of("Europe/Rome); 地区ID都为“{区域}/{城市}”的格式,这些地区集合的设定都由英特网编号分配机构(IANA)的时区数据库提供。

还可以通过Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId: ZoneId zoneId = TimeZone.getDefault().toZoneId();

一旦得到一个ZoneId对象,你就可以将它与LocalDate、 LocalDateTime或者是Instant对象整合起来,构造为一个ZonedDateTime实例,它代表了指定时区的时间点,代码清单如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 为时间点添加时区信息
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);

// 通过ZoneId,可以将LocalDateTime转换为Instant:
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
Instant instantFromDateTime = dateTime.toInstant(romeZone);
// 还可以通过反向的方式得到LocalDateTime对象:
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);

Comment