从Java 8 开始,java.time包提供了新的日期和时间API,主要类型有

  • 本地日期和时间: LocalDateTimeLocalDateLocalTime
  • 带时区的日期和时间:ZonedDateTime
  • 时刻:Instant
  • 时区:ZoneIdZoneOffset
  • 时间间隔 : Duration

以及一套新的用于取代SimpleDateFormat的格式化类型DateTimeFormatter

和旧的API相比,新API严格区分了时刻、本地日期、本地时间和带时区的日期时间,并且,对日期和时间进行运算更加方便。
此外,新API修正了旧API不合理的常量设计

  • Month的范围用1~12 表示 1月到12月
  • Week的范围用1~7表示周一到周日

最后,新API的类型几乎全部是不变类型,可以放心使用,不必担心被修改。

LocalDateTime

我们首先来看最常用的LocalDateTime,它表示一个本地日期和时间。

  1. public class Main {
  2. public static void main(String[] args) {
  3. LocalDate d = LocalDate.now();
  4. LocalTime t = LocalTime.now();
  5. LocalDateTime dt = LocalDateTime.now();
  6. System.out.println(d);
  7. System.out.println(t);
  8. System.out.println(dt);
  9. }
  10. }

本地日期和时间通过now() 获取到的总是以当前默认时区返回的,和旧APi不同,LocalDateTimeLocalDateLocalTime默认严格按照ISO 8601规定的日期和时间格式进行打印。

创建LocalDateTime的三种方式

上述代码其实有一个小问题,在获取3个类型的时候,由于执行一行代码总会消耗一点时间。因此,3个类型的日期和时间很可能对不上(时间的毫秒数基本上不同)。为了保证获取到同一时刻的日期和时间,可以改写

  1. public class Main {
  2. public static void main(String[] args) {
  3. LocalDateTime dt = LocalDateTime.now();
  4. LocalDate d = dt.toLocalDate();
  5. LocalTime t = dt.toLocalTime();
  6. }
  7. }

反过来,通过指定的日期和时间创建LocalDateTime可以通过of()方法

  1. LocalDate d2 = LocalDate.of(2091,12,12);
  2. LocalTime t2 = LocalTime.of(14:14:13);
  3. LocalDateTime dt2 = LocalDateTime.of(2022,12,12,12,12,12);
  4. LocalDateTime dt3 = LocalDateTime.of(d2,t2);

因为严格按照ISO 8601的格式,因此,将字符串转换为LocalDateTime就可以传入标准格式。

  1. LocalDateTime dt = LocalDateTime.parse("2019-11-13T14:14:14");
  2. LocalDate d = LocalDate.parse("2019-11-12");
  3. LocalTime t = LocalTime.parse("14:13:12");

注意ISO 8601规定的日期和时间分隔符是 T。标准格式如下

  • 日期: yyyy-MM-dd
  • 时间: HH:mm:ss
  • 带毫秒的时间,HH:mm:ss.SSS
  • 日期和时间: yyyy-MM-dd’T’HH:mm:ss.SSS
  • 带毫秒的日期和时间:yyyy-MM-dd’T’HH:mm:ss.SSS

    格式化日期时间DateTimeFormatter

    如果要自定义输出的格式,或者要把一个非ISO 8601格式的字符串解析成LocalDateTime,可以使用新的DateTimeFormatter

    1. public class Main {
    2. public static void main(String[] args) {
    3. //自定义格式化
    4. DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
    5. System.out.println(dtf.format(LocalDateTime.now()));
    6. //用自定义格式解析
    7. LocalDateTime dt2 = LocalDateTime.parse("2019/11/39 14:14:14", dtf);
    8. System.out.println(dt2);
    9. }
    10. }

    LocalDateTime提供了对日期和时间进行加减的非常简单的链式调用

    1. public class Main {
    2. public static void main(String[] args) {
    3. LocalDateTime dt = LocalDateTime.of(2020,10,21,12,21,12);
    4. System.out.println(dt);
    5. //加5天减3小时
    6. LocalDateTime dt2 = dt.plusDays(5).minusHours(3);
    7. System.out.println(dt2);
    8. //System.out.println(dt); dt不变
    9. //减1月
    10. LocalDateTime dt3 = dt2.minusMonth(1);
    11. System.out.println(dt3);
    12. }
    13. }

    注意到月份加减会自动调整日期,例如从2019-10-31减去一个月得到的结果是2019-09-30 ,因为9月没有31日。

调整日期 (with)

对日期和时间进行调整则使用withXxxx()方法,例如,withHour(15)会把10:11:12变成15:11:12

  • 调整年: withYear()
  • 调整月: withMonth()
  • 调整日: withDayOfMonth()
  • 调整时: withHour()
  • 调整分: withMinute()
  • 调整秒: withSecond()

    1. public class Main {
    2. public static void main(String[] args) {
    3. LocalDateTime ldt1 = LocalDateTime.of(2020,12,12,12,12,12);
    4. System.out.pritnln(ldt1);
    5. LocalDateTime ldt2 = ldt1.withDayOfMonth(31);
    6. System.out.println(ldt2);
    7. LocalDateTime ldt3 = Ldt2.withMonth(9);
    8. System.out.println(ldt3);
    9. }
    10. }

    同样注意到调整月份时,会调整日期,即把2019-10-31的月份调整为9时,日期也自动变为30
    实际上,LocalDateTime还有一个通用的with()方法允许我们做更复杂的运算。

    1. public class Main {
    2. public static void main(String[] args) {
    3. //本月第一天0:00时刻
    4. //LocalDateTime ldt = LocalDateTime.now().withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0);
    5. LocalDateTime ldt1 = LocalDateTime.now().withDayOfMonth(1).atStartOfDay();//Java8运行失败
    6. //本月最后一天
    7. LocalDateTime ldt2 = LocalDateTime.now().with(TemporalAdjusters.lastDayOfMonth());
    8. System.out.println(lastDay);
    9. //下月第一天
    10. LocalDateTime ldt3 = LocalDateTime.now().with(TemporalAdjusters.firstDayOfNextMonth());
    11. System.out.println(ldt3);
    12. //本月第一个周一
    13. LocalDateTime firstWeekDay = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
    14. System.out.println(firstWeekDay);
    15. }
    16. }

    对于计算某个月第一个周日这样的问题,新的API可以轻松完成。

判断两个LocalDateTime的先后 (isBefore,isAfter)

要判断两个LocalDateTime的先后,可以使用isBefore()isAfter()方法,对于 LocalDateLocalTime类似。

  1. public class Main {
  2. public static void main(String[] args) {
  3. LocalDateTime now = LocalDateTime.now();
  4. LocalDateTime target = LocalDateTime.of(2021,8,18,18,18,18);
  5. System.out.println(now.isBefore(target));
  6. System.out.println(LocalDate.now().isBefore(LocalDate.of(2021,8,18)));
  7. System.out.println(LocalTime.now().isAfter(LocalTime.parse("16:18:07")));
  8. }
  9. }

注意到LocalDateTime无法与时间戳进行转换,因为LocalDateTime没有时区,无法确定某一时刻。后面我们要介绍的ZoneDateTime相当于LocalDateTime加时区的组合,它具有时区,可以与Long表示的时间戳进行转换。

Duration和Period (时间间隔)

Duration表示两个时刻之间的时间间隔。另一个类似的Period表示两个日期之间的天数。

  1. public class Main {
  2. public static void main(String[] args) {
  3. LocalDateTime now = LocalDateTime.now();
  4. LocalDateTime ldt = LocalDateTime.of(2022,8,23,21,32,12);
  5. Duration d = Duration.between(now,ldt);
  6. System.out.pirntln(d);
  7. Period p = LocalDate.of(2018,1,2).until(LocalDate.of(2020,2,10));
  8. System.out.println(p);
  9. }
  10. }

注意到两个LocalDateTime之间的差值用Duration表示,类似PT-14080H-54M-13.765S,表示14080小时54分13秒。而两个LocalDate之间的差值用Period表示,类似P-1Y-7M-8D,表示一年七个月八天。

DurationPeriod的表示方法也符合ISO 8601的格式,它以P...T..的形式表示,P...T...之间表示日期间隔,T后面表示时间间隔。如果是PT...的格式表示仅有时间间隔。 自用ofSXxx()或者parse()方法也可以直接创建Duration

  1. Duration d1 = Duration.ofHours(10);//10hours
  2. Duration d2 = Duration.parse("P1DT2H3M");

小结

Java 8 引入了新的日期和时间API,,它们是不变类,默认按ISO 8601标准格式化和解析
使用LocalDateTime可以非常方便地对日期和时间进行加减,或者调整日期时间,它总是返回新对象
使用isBefore()isAfte()可以判断日期和时间的先后。
使用DureationPeriod可以表示两个日期和时间的区间间隔