美文网首页Java
Java中的时间和日期(三):java8中新的时间API介绍

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

作者: 冬天里的懒喵 | 来源:发表于2020-08-23 18:12 被阅读0次

    @[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<String, String> 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介绍

        本文链接:https://www.haomeiwen.com/subject/aohwjktx.html