Java中的时间和日期(三):java8中新的时间API介绍

@[toc]
由于java7及以前的版本对时间的处理都存在诸多的问题。自java8之后,引入了新的时间API,现在对这些新的API及其使用进行介绍。

1.Instant

Instant与Date对象类似,都是表示一个时间戳,不同的在于,Instant充分考虑了之前Date精度不足的问题。Date最多支持到毫秒,而cpu对时间精度的要求可能是纳秒。所以Instant在date的基础上进行了扩展,支持纳秒结构。我们可以看看Instant的源码:


/**
 * The number of seconds from the epoch of 1970-01-01T00:00:00Z.
 */
private final long seconds;
/**
 * The number of nanoseconds, later along the time-line, from the seconds field.
 * This is always positive, and never exceeds 999,999,999.
 */
private final int nanos;

/**
 * Constructs an instance of {@code Instant} using seconds from the epoch of
 * 1970-01-01T00:00:00Z and nanosecond fraction of second.
 *
 * @param epochSecond  the number of seconds from 1970-01-01T00:00:00Z
 * @param nanos  the nanoseconds within the second, must be positive
 */
private Instant(long epochSecond, int nanos) {
    super();
    this.seconds = epochSecond;
    this.nanos = nanos;
}

在Instant内部,除了与Date一样用一个long类型来表示毫秒之外,还维护了一个int型的nanos用于表示纳秒数。

/**
 * Constant for the 1970-01-01T00:00:00Z epoch instant.
 */
public static final Instant EPOCH = new Instant(0, 0);
/**
 * The minimum supported epoch second.
 */
private static final long MIN_SECOND = -31557014167219200L;
/**
 * The maximum supported epoch second.
 */
private static final long MAX_SECOND = 31556889864403199L;

在其中维护了EPOCH Time(0ms 0ns)。之后我们可以相对EPOCH轻松的初始化时间,需要注意的是,Instant统一采用的都是systemUTC时间。不再像Date一样根据本地时区进行转换。

System.out.println(Instant.now());
System.out.println(Instant.ofEpochMilli(0));
System.out.println(Instant.ofEpochSecond(0,10));

结果如下:

2020-08-06T08:23:18.652Z
1970-01-01T00:00:00Z
1970-01-01T00:00:00.000000010Z

格式都是统一的yyyy-MM-dd,T表示后面接的是时间。Z表示采用统一的UTC时间。
Instant与时区无关,时钟只输出与格林尼治统一时间。

2.无时区的日期和时间LocalDate、LocalTime、LocalDateTime

与Calendar不同的是,在新版本的API中,将日期和时间做了分离,用单独的类进行处理。LocalDate表示日期,LocalTime表示时间,而LocalDateTime则是二者的综合。

2.1 LocalDate

LocalDate表示日期,内部分别维护了year、month、day三个变量。

/**
 * The year.
 */
private final int year;
/**
 * The month-of-year.
 */
private final short month;
/**
 * The day-of-month.
 */
private final short day;

与Date初始化方法不同的是,这里在不是像之前那样有各种特殊的要求,比如date中构造方法要求year从1900开始,month 0 - 11.
LocalDate定义在ChronoField中,如下:

YEAR("Year", YEARS, FOREVER, ValueRange.of(Year.MIN_VALUE, Year.MAX_VALUE), "year"),

 MONTH_OF_YEAR("MonthOfYear", MONTHS, YEARS, ValueRange.of(1, 12), "month"),
 
 DAY_OF_MONTH("DayOfMonth", DAYS, MONTHS, ValueRange.of(1, 28, 31), "day"),

这样也跟容易理解。
LocalDate可以采用如下办法初始化:

System.out.println(LocalDate.now());
System.out.println(LocalDate.of(2020,8,6));
System.out.println(LocalDate.ofYearDay(2020,100));
System.out.println(LocalDate.ofEpochDay(18000));

输出都是:

2020-08-06
2020-08-06
2020-04-09
2019-04-14

ofYearDay ofEpochDay都非常容易理解。不会再像之前Calendar那么生涩。
不过需要注意的是,LocalDate输出的是默认的系统时区。
还有很多方法如:

方法名 说明
getYear 获取当前年份
getMonthValue 获取当前月份
getDayOfMonth 获取当前日期
getDayOfYear 获取当前是一年中的第几天
isLeapYear 是否闰年
lengthOfYear 一年有多少天
getDayOfWeek 返回星期信息

2.2 LocalTime

与LocalDate类似,LocalTime专注于时间处理,它提供小时、秒、毫秒、微秒、纳秒等各种事件单位的处理。
其内部主要有hour、minute、second、nano等变量:

/**
 * The hour.
 */
private final byte hour;
/**
 * The minute.
 */
private final byte minute;
/**
 * The second.
 */
private final byte second;
/**
 * The nanosecond.
 */
private final int nano;

初了nano 是int类型之外,其他都是byte类型,占一个字节。
of方法提供了很多重载来实现不同参数输入时间的情况。
我们可以如下使用:

System.out.println(LocalTime.now());
System.out.println(LocalTime.of(22,10));
System.out.println(LocalTime.of(22,10,20));
System.out.println(LocalTime.of(22,10,20,1000));

输出结果:

16:51:24.193
22:10
22:10:20
22:10:20.000001

实际上我们可以发现,当用now的时候,精度只有毫秒,这大概还是用的linux的毫秒时间戳。而我们可以让LocalTime显示到纳秒级别。
当然,LocalTime也有很多类似的方法提供:

方法名称 说明
getHour 获取当前小时数
getMinute 获取当前分钟数
getSecond 获取当前秒数
getNano 获取当前纳秒数
withHour 修改当前的Hour
withMinute 修改当前的分钟
withSecond 修改当前的秒数

还有很多方法,但是这些方法都很简单,一看就知道什么意思。

2.2 LocalDateTime

LocalDateTime实际上是LocalDate和LocalTime的综合体:

/**
 * The date part.
 */
private final LocalDate date;
/**
 * The time part.
 */
private final LocalTime time;

其内部提供了date和time两个final的私有变量。之后提供了LocalDate和LocalTime的大部分工具方法。

System.out.println(LocalDateTime.now());
System.out.println(LocalDateTime.of(LocalDate.now(),LocalTime.now()));
System.out.println(LocalDateTime.of(2020,8,6,17,23));
System.out.println(LocalDateTime.of(2020,8,6,17,23,15));
System.out.println(LocalDateTime.of(2020,8,6,17,23,15,100000));

输出结果如下:

2020-08-06T17:24:41.516
2020-08-06T17:24:41.516
2020-08-06T17:23
2020-08-06T17:23:15
2020-08-06T17:23:15.000100

3.与时区相关的时间 ZonedDateTime

前面的LocalDate、LocalTime、LocalDateTime都是与时区无关,默认是本地时区的日期和时间。这也符合Local的定义。但是如果时间需要再多个时区进行转换呢?这就需要ZonedDateTime。

System.out.println(ZonedDateTime.now());
System.out.println(ZonedDateTime.of(LocalDate.now(),LocalTime.now(),ZoneId.of("America/Los_Angeles")));
System.out.println(ZonedDateTime.of(LocalDate.now(),LocalTime.now(),ZoneId.of("EST",ZoneId.SHORT_IDS)));

输出如下:

2020-08-06T17:34:41.337+08:00[Asia/Shanghai]
2020-08-06T17:34:41.337-07:00[America/Los_Angeles]
2020-08-06T17:34:41.340-05:00

这个ZnodeDateTime再初始化的时候,通过of方法,需要传入一个时区。而时区通过简码存储在ZoneId.SHORT_IDS这个Map中。如果需要使用简码,则需要传入这个Map。

 public static ZoneId of(String zoneId, Map aliasMap) {
        Objects.requireNonNull(zoneId, "zoneId");
        Objects.requireNonNull(aliasMap, "aliasMap");
        String id = aliasMap.get(zoneId);
        id = (id != null ? id : zoneId);
        return of(id);
    }
    
 public static ZoneId of(String zoneId) {
        return of(zoneId, true);
    }

我们可以在ZoneId类中查看到其所支持的全世界的时区编码。
而ZnodeDateTime本身与LocalDateTime区别就在于加上了ZnodeID。LocalDateTime则采用本地的时区。ZnodeDateTime则可以根据我们需要的时区进行转换。

    /**
     * The local date-time.
     */
    private final LocalDateTime dateTime;
    /**
     * The offset from UTC/Greenwich.
     */
    private final ZoneOffset offset;
    /**
     * The time-zone.
     */
    private final ZoneId zone;

理解了前面的LocalDateTime,就能很好理解ZnodeDateTime的使用。同时除之前LocalDateTime的一些工具方法之外,还提供若干与时区有关的方法。
需要注意的是,在新版本API中的日期,都是final修饰的内部属性,是不可变类。而Date则是transient的可变类。

4.日期格式化神器DateTimeFormatter

前文介绍了SampleDateFormat等传统的时间格式化工具存在线程安全问题。而且格式化字符串会导致宽松匹配等问题。那么在新版本的DateTimeFormatter中,则很好的解决了这些问题:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(formatter.format(LocalDateTime.now()));
String str = "2019-12-07 07:43:53";
System.out.println(LocalDateTime.parse(str,formatter));

输出结果:

2020-08-06 19:08:55
2019-12-07T07:43:53

在所有的LocalDateTime、LocalDate、LocalTime都有parse方法,这是一个很好的设计模式,值得借鉴。这样把转换的结果对象都放在了所需对象的静态方法中。
上述模式字符串非常严格,有严格的校验规则。如我们不小心将HH写成了hh则会出错:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
System.out.println(formatter.format(LocalDateTime.now()));
String str = "2019-12-07 13:43:53";
System.out.println(LocalDateTime.parse(str,formatter));

输出:

2020-08-06 07:31:12
Exception in thread "main" java.time.format.DateTimeParseException: Text '2019-12-07 13:43:53' could not be parsed: Invalid value for ClockHourOfAmPm (valid values 1 - 12): 13
    at java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1920)
    at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1855)
    at java.time.LocalDateTime.parse(LocalDateTime.java:492)
    at com.dhb.date.test.DateTimeFormatterTest.main(DateTimeFormatterTest.java:12)
Caused by: java.time.DateTimeException: Invalid value for ClockHourOfAmPm (valid values 1 - 12): 13
    at java.time.temporal.ValueRange.checkValidValue(ValueRange.java:311)
    at java.time.temporal.ChronoField.checkValidValue(ChronoField.java:703)
    at java.time.format.Parsed.resolveTimeFields(Parsed.java:382)
    at java.time.format.Parsed.resolveFields(Parsed.java:258)
    at java.time.format.Parsed.resolve(Parsed.java:244)
    at java.time.format.DateTimeParseContext.toResolved(DateTimeParseContext.java:331)
    at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1955)
    at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
    ... 2 more

可以看到,实际上要求非常严格。会提示hh的范围是1-12。这样就解决了之前提到的由于模式字符串匹配宽松导致的隐形BUG。
另外对于线程安全问题,可以看看DateTimeFormatter开篇注释。

 * @implSpec
 * This class is immutable and thread-safe.
 *
 * @since 1.8
 */
public final class DateTimeFormatter {

因为新版本的API都是不可变类,immutable的。这样一来所有的实例都只能赋值一次。之后如果需要用DateTimeFormatter进行转换,实际上是产生了一个新的实例,用这个新的实例输出。用一个不可变的设计模式,永远都不会有线程安全问题。
我们可以回顾在之前旧版本的时候,每个SampleTimeFormat都会持有一个Calendar实例,每次进行格式转换的时候都要对这个实例不断的clear之后重新赋值。
immutable也是一个非常棒的设计模式。

5.时差工具 Period和Duration

新版本的API对于两个时间的差值,专门设计了两个类来实现。Period用于处理两个日期之间的差值。Duration用于处理两个时间之间的差值。

5.1 Period

Period主要处理两个LocalDate之间的差值:

LocalDate now = LocalDate.now();
LocalDate date = LocalDate.of(2019,11,12);
Period period = Period.between(date,now);
System.out.println(period.getYears() + "年" +
                period.getMonths() + "月" +
                period.getDays() + "天");

其结果为:

0年8月25天

主要是用第一个值减去第二个值之间的差异,注意,这个years、months、days得放到一起看才有意义。

5.2 Duration

Duration主要处理两个LocalTime之间的差值:

LocalTime time = LocalTime.of(20,30);
LocalTime time1 = LocalTime.of(23,59);
Duration duration = Duration.between(time,time1);
System.out.println(duration.toMinutes() + "分钟");
System.out.println(duration.toString());

输出结果:

209分钟
PT3H29M

可以看到,这里输出的是总分钟。toString可以看到是3小时29分钟。实际上,我们可以通过方法的命名规则很好的理解,get方法和to方法。get方法是得到实际的单位差值。而to则是将全部的单位差值都转换为这个单位。这在实际操作的过程中需要注意,避免因为理解误差而导致出错。
这一块方法的命名规则也是我们在实际过程中值得参考的。

6.新旧日期格式转换

在java8的Date中增加了和Instant转换的方法。分别是from和toInstant来实现Instant和Date转换。

Instant instant = Instant.now();
System.out.println(Date.from(instant));
Date date = new Date();
System.out.println(date.toInstant());

输出结果:

Thu Aug 06 20:14:49 CST 2020
2020-08-06T12:14:49.935Z

可以看到Date和Instant是非常方便转换的。
Instant也可以在LocalDateTime之间转换。不过需要指定时区。之后LocalDateTime可以转换为LocalDate和LocalTime,我们用系统默认时区示例如下:

System.out.println(LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()));
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime.toLocalDate());
System.out.println(localDateTime.toLocalTime());

输出:

2020-08-06T20:19:47.322
2020-08-06
20:19:47.322

上述就是本文对java8中新版本API的一些介绍。并没设计太深入的源码。作为自我学习的一个过程,后续将值得借鉴的地方进行总结。

你可能感兴趣的:(Java中的时间和日期(三):java8中新的时间API介绍)