JDK8新时间Api

267 阅读8分钟

前言

JDK8之前,我们经常使用到的时间API包括(DateCalendar),Date与字符串之间的转换使用 SimpleDateFormat进行转换(parse()format() 方法),然而SimpleDateFormat不是线程安全的。 在设计上也是存在一些缺陷的,比如有两个Date类,一个在java.util包中,一个在java.sql包中。

JDK8中,引入了一套全新的时间日期API,这套 API 在设计上比较合理,使用时间操作也变得更加方便。并且支持多线程安全操作。

旧版日期时间API存在的问题

  • 设计很差:在java.utiljava.sql的包中都有日期类。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,此外用于格式化和解析的类又在java.text包中定义。
  • 非线程安全:java.util.Date 是非线程安全的,所有的日期类都是可变的,这是 java 日期类最大的问题之一。
  • .时区处理麻烦:日期类并不提供国际化,没有时区支持。因此java引入了java.util.Calendarjava.util.TimeZone类,但他们同样存在上述所有的问题。
public class DateDemo01 {
    public static void main(String[] args) {
        //旧版日期时间 API 存在的问题
        //1.设计不合理(JDK8 Date类已经 @Deprecated 注释,不推荐使用)
        Date now =new Date(1985,9,23);
        System.out.println(now);
 
        //2.时间格式化和解析是线程不安全的
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 50; i++) {
            new Thread(()->{
                try {
                    Date date = sdf.parse("2019-09-09");
                    System.out.println("date:"+date);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

//多线程测试结果:(会有日期格式化错误的,还有直接报错的,说明线程是不安全)
date:Tue Jun 09 00:00:00 CST 2026
date:Tue Sep 09 00:00:00 CST 990
date:Tue Sep 09 00:00:00 CST 990
date:Tue Sep 09 00:00:00 CST 990
date:Sun Dec 01 00:00:00 CST 2019
date:Mon Sep 09 00:00:00 CST 2019(这个才是正确的)
date:Mon Sep 09 00:00:00 CST 2019
date:Mon Sep 09 00:00:00 CST 2019
Exception in thread "Thread-35" Exception in thread "Thread-37" java.lang.NumberFormatException: For input string: "1909E.21909"
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)

新日期时间 API 介绍

JDK8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于 java.time包下,如下是一些该包下的关键类:

  • LocalDate:表示日期,包含:年月日。格式为:2020-01-13。
  • LocalTime:表示时间,包含:时分秒。格式为:16:39:09.307。
  • LocalDateTime:表示日期时间,包含:年月日 时分秒。格式为:2020-01-13T16:40:59.138。
  • DateTimeFormatter:日期时间格式化类。
  • Instant:时间戳类。
  • Duration:用于计算 2 个时间(LocalTime,时分秒)之间的差距。
  • Period:用于计算 2 个日期(LocalDate,年月日)之间的差距。
  • ZonedDateTime:包含时区的时间。

Java中使用的历法是ISO-8601日历系统,他是世界民用历法,也就是我们所说的公历。平年有365天,闰年是366天。此外Java8还提供了4套其他历法,分别是:

  • ThaiBuddhistDate:泰国佛教历。
  • MinguoDate:中华民国历。
  • JapaneseDate:日本历。
  • HijrahDate:伊斯兰历。

LocalDate,LocalTime,LocalDatetime

这些类是线程安全的,对于它的任何操作都会产生一个新的实例,这和 String 类是一样的。其次它不存储或表示时区。 相反,它是对日期时间的描述。它不存储时区,但不代表它没有时区。通过一张图来理解三者的关系

方法

案例

LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();

//当前的年月日
System.out.println(localDate);
//当前的时分秒
System.out.println(localTime);
//当前的年月日加时分秒
System.out.println(localDateTime);

//输出
2018-06-06
14:43:04.889017
2018-06-06T14:43:04.889065

of指定时间

LocalDate of1 = LocalDate.of(2017, 8, 5);
System.out.println(of1);

LocalTime of2 = LocalTime.of(5, 11, 20);
System.out.println(of2);

LocalDateTime of3 = LocalDateTime.of(2017, 8, 5, 11, 11, 23);
System.out.println(of3);

//输出
2017-08-05
05:11:20
2017-08-05T11:11:23

getXxx获取值

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime);
//getXxx
//年份的天数
System.out.println(localDateTime.getDayOfYear());
//月份的天数
System.out.println(localDateTime.getDayOfMonth());
//返回星期几,英文
System.out.println(localDateTime.getDayOfWeek());
//返回月份,因为
System.out.println(localDateTime.getMonth());
//月份
System.out.println(localDateTime.getMonthValue());
//小时
System.out.println(localDateTime.getHour());
//分钟
System.out.println(localDateTime.getMinute());

//输出
2018-06-06T14:54:02.650016
157
6
WEDNESDAY
JUNE
6
14
54

withXxx修改时间

修改了后会返回新的对象,老的对象不变,体系了日期对象的不可变性。

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

//修改月份
LocalDateTime localDateTime1 = localDateTime.withDayOfMonth(20);
System.out.println(localDateTime1);

//修改小时
System.out.println(localDateTime.withHour(12));

//输出
2018-06-06T15:01:43.102993
2018-06-20T15:01:43.102993
2018-06-06T12:01:43.102993

plus添加时间

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

//向前添加3天
LocalDateTime localDateTime1 = localDateTime.plusDays(3);
System.out.println(localDateTime1);

//向前添加2小时
System.out.println(localDateTime.plusHours(2));

//输出
2018-06-06T15:10:56.921629
2018-06-09T15:10:56.921629
2018-06-06T17:10:56.921629

minus减去时间

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

//减去2天
System.out.println(localDateTime.minusDays(2));

//减去3小时
System.out.println(localDateTime.minusHours(3));

//输出
2018-06-06T15:12:25.612975
2018-06-04T15:12:25.612975
2018-06-06T12:12:25.612975

判断时间是不是在之前或者之后

LocalDateTime localDateTime = LocalDateTime.now();

//加2天
LocalDateTime localDateTime1 = localDateTime.plusDays(2);

//看时间在不在前面
boolean before = localDateTime1.isBefore(localDateTime);
//看时间在不在后面
boolean after = localDateTime1.isAfter(localDateTime);
//看时间是不是一样
boolean equal = localDateTime1.isEqual(localDateTime);

System.out.println(before);
System.out.println(after);
System.out.println(equal);

//输出
false
true
false

Instant

在处理时间和日期的时候,我们通常会想到年,月,日,时,分,秒。然而,这只是时间的一个模型,是面向人类的。第二种通用模型是面向机器的,或者说是连续的。在此模型中,时间线中的一个点表示为一个很大的数,这有利于计算机处理。在UNIX中,这个数从1970年开始,以秒为的单位;同样在Java中,也是从1970年开始,但以毫秒为单位。

java.time包通过值类型Instant提供机器视图,不提供处理人类意义上的时间单位。Instant表示时间线上的一点,而不需要任何上下文信息,例如,时区。概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。因为java.time包是基于纳秒计算的,所以Instant的精度可以达到纳秒级。(1 ns = 10-9 s)

方法

时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。

案例

//now获得实例
//表示1970到现在的秒数
Instant instant = Instant.now();

System.out.println(instant);

//atOffset 得到带有偏移量的日期时间
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));
System.out.println(offsetDateTime);

//得到时间戳
long milli = instant.toEpochMilli();
System.out.println(milli);

//根据秒数,得到时间点的对象,使用的话差8小时
Instant instant1 = Instant.ofEpochMilli(milli);
System.out.println(instant1);

//输出
2018-06-06T07:50:02.951177Z
2018-06-06T15:50:02.951177+08:00
1528271402951
2018-06-06T07:50:02.951Z

ZonedDateTime

java.time包下的LocalDate、LocalTime、LocalDateTimeInstant基本能满足需求。当你不可回避时区时,ZonedDateTime类可以满足我们的需求。

其中每个时区都对应着ID,地区ID都为 {区域}/{城市}的格式,例如:Asia/Shanghai等。

  • now():使用系统时间获取当前的ZonedDateTime
  • now(ZoneId zone):返回指定时区的ZonedDateTime

ZoneId:该类中包含了所有的时区信息

  • getAvailableZoneIds() : 静态方法,可以获取所有时区时区信息。
  • of(String id) :静态方法, 用指定的时区信息获取ZoneId对象。

案例

//查询所有时区。
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
for (String s : zoneIds) {
    System.out.println(s);
}

查询上海时区对应的时间。
//Asia/Shanghai
LocalDateTime localDateTime = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
System.out.println(localDateTime);    //2019-10-20T18:52:10.383302

//获取本时区的ZonedDateTime
ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println(zonedDateTime);

//获取指定时区的ZonedDateTime
ZonedDateTime zonedDateTime1 = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
System.out.println(zonedDateTime1);

//输出
2019-10-20T18:52:34.053441+08:00[Asia/Shanghai]
2019-10-20T18:52:34.054067+08:00[Asia/Shanghai]

Duration

Duration:用于计算两个时间间隔,以秒和纳秒为基准。 DurationSpringBoot 项目中,配置也很方便

token:
 effective-time: 7d  
 # d:天 , h:小时 , m:分钟 , s:秒

在实体类中,可以使用 @ConfigurationProperties 或者 @Value 将它直接映射成 Duration 对象,当然这依赖于 SpringBoot 中提供的丰富的类型转换器

方法

案例

LocalDateTime localDateTime = LocalDateTime.now();

LocalDateTime of = LocalDateTime.of(2018, 6, 5, 17, 25, 10);

Duration duration = Duration.between(localDateTime, of);
System.out.println(duration);

//秒数
System.out.println(duration.getSeconds());
//纳秒
System.out.println(duration.getNano());
//相隔的天数
System.out.println(duration.toDays());

//是否过期
Duration duration = Duration.between(token.getCreateTime(), LocalDateTime.now());
boolean negative = duration.minus(effective).isNegative();

//输出
PT-23H-59M-42.433722S
-86383
566278000
0

PT那个负的表述少,这边的意思是少23小时,59分钟,后面是秒。

Period

Period:用于计算两个“日期”间隔,以年、月、日衡量。

方法

案例

//求2个日期相隔的年月日。
LocalDate localDate = LocalDate.now();
LocalDate localDate1 = LocalDate.of(2017, 3, 5);

Period period = Period.between(localDate, localDate1);
System.out.println(period);

//取年
System.out.println(period.getYears());
//取月
System.out.println(period.getMonths());
//取天
System.out.println(period.getDays());

//输出
P-1Y-3M-1D
-1
-3
-1

//指定间隔多少后的Period对象。
LocalDate localDate = LocalDate.now();
LocalDate localDate1 = LocalDate.of(2017, 3, 5);

Period period = Period.between(localDate, localDate1);
System.out.println(period);

Period period1 = period.withYears(2);
System.out.println(period1);

//输出
P-1Y-3M-1D
P2Y-3M-1D

TemporalAdjuster,TemporalAdjusters

  • TemporalAdjuster : 时间校正器。有时我们可能需要获取例如:将日期调整到“下一个工作日”等操作。
  • TemporalAdjusters : 该类通过静态方法(firstDayOfXxx()/lastDayOfXxx()/nextXxx())提供了大量的常用TemporalAdjuster的实现。

案例


LocalDateTime.now().with(TemporalAdjusters.firstDayOfMonth());//当月第一天
LocalDateTime.now().with(TemporalAdjusters.firstDayOfNextMonth());//下个月第一天
LocalDateTime.now().with(TemporalAdjusters.dayOfWeekInMonth(2,DayOfWeek.MONDAY));//第N个星期几
LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY));//下个星期几

//当前日期的下一个周日
TemporalAdjuster temporalAdjuster = TemporalAdjusters.next(DayOfWeek.SUNDAY);
LocalDateTime localDateTime = LocalDateTime.now().with(temporalAdjuster);
System.out.println(localDateTime);
//输出
2018-06-10T17:58:27.288545

OffsetDateTime

带有时区偏移量的日期时间类,相当于 OffsetDateTime = LocalDateTime + ZoneOffset

ZonedDateTime

真正带有完整时区信息的日期时间类

Date 和 LocalDateTime 互转

可以使用 Java8 提供的 Instant 将两者互转来简化一些业务代码操作

案例

//Date 转 LocalDateTime
LocalDateTime.ofInstant(new Date().toInstant(), ZoneId.systemDefault());

// LocalDateTime 转 Date
Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant());