Java 8——日期时间工具库(java.time)

一.前言

在介绍Java SE 8中新的日期时间库前,先了解下Java 8之前的日期时间工具的诟病。

在Java SE 8前,日期时间工具库在java.util包中,包括:

  • java.util.Date:表示日期和时间
  • java.util.Calendar以及其实现子类:表示各种日历系统,常用的是格林威治日历java.util.GregorianCalendar
  • java.util.TimeZone以及其实现子类:表示时区偏移量和夏令时

以及辅助其进行格式化和解析的工具库在java.text包中,包括:

  • java.text.DateFormat:格式化日期时间和解析日期时间的工具抽象类
  • java.text.SimpleDateFormat:DateDateFormat的实现

从以上的简述中,对java 8之前的日期时间库,有所宏观视觉。下面简要总结下其设计上的瑕疵和被开发者无限吐槽的诟病:

  • 从以上的api上看,java 8之前的日期时间工具库缺乏年、月、日、时间、星期的单独抽象;
  • Dater日期时间类既描述日期又描述时间,耦合,且Date不仅在java.util包中存在,在java.sql中也存在,重复名称,容易导致bug发生;
  • api的设计上晦涩,难用,不够生动,难以以自然人类的思维理解日期时间。年月日需要从Calendar中获取。q;
  • 最被开发者抱怨的是类型不安全,Calendar类中全局属性是可变的,在多线程访问时,会存在线程安全问题。SimpleDateFormat格式化和解析日期,需要使用年月日时分秒,所以持有了Calendar属性,导致其也是非线程安全;
// 以下都是Calendar中持有的全局属性
// 这些全局属性都是可变的,提供了set
protected int fields[];
transient private int stamp[];
protected long time;
protected boolean  isTimeSet;

// 在其子类GregorianCalendar中
private transient int[] zoneOffsets;
// setTime方法会调用此方法
// 该方法中修改了上述的很多全局属性
public void setTimeInMillis(long millis) {
        // If we don't need to recalculate the calendar field values,
        // do nothing.
        if (time == millis && isTimeSet && areFieldsSet && areAllFieldsSet
            && (zone instanceof ZoneInfo) && !((ZoneInfo)zone).isDirty()) {
            return;
        }
        time = millis;
        isTimeSet = true;
        areFieldsSet = false;
        computeFields();
        areAllFieldsSet = areFieldsSet = true;
}

所以在多线程环境中使用Calendar是非线程安全,多个线程修改其属性域会发生数据一致性和可见性问题。

在DateFormat中持有了Calendar属性,用于解析和格式化日期:

// 从注释上看,Calendar用于计算日期时间域
/**
 * The {@link Calendar} instance used for calculating the date-time fields
 * and the instant of time. This field is used for both formatting and
 * parsing.
 *
 * <p>Subclasses should initialize this field to a {@link Calendar}
 * appropriate for the {@link Locale} associated with this
 * <code>DateFormat</code>.
 * @serial
 */
protected Calendar calendar;
// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo,
                            FieldDelegate delegate) {
    // Convert input date to time field list
    calendar.setTime(date);

    boolean useDateFormatSymbols = useDateFormatSymbols();

    for (int i = 0; i < compiledPattern.length; ) {
        int tag = compiledPattern[i] >>> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
            count = compiledPattern[i++] << 16;
            count |= compiledPattern[i++];
        }

        switch (tag) {
        case TAG_QUOTE_ASCII_CHAR:
            toAppendTo.append((char)count);
            break;

        case TAG_QUOTE_CHARS:
            toAppendTo.append(compiledPattern, i, count);
            i += count;
            break;

        default:
            subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
            break;
        }
    }
    return toAppendTo;
}

format方法中设置了全局成员Calendar的time,多线程访问时每次都会改变Calendar类,导致format格式化时会出现线程安全问题。所以DateFormat和其子类SimpleDateFormat都是非类型安全。

这个可以说是被开发者极度抱怨的。所以每次在使用日期格式工具时大多数都会重新new或者使用ThreadLocal。

基于此诸多问题,java设计者终于在Java SE 8中引入了新的日期时间库。新的日期时间库的易用程度会让你振服!下面开始进入主题,Java SE 8中的日期时间库java.time。

二.概览

先认识下joda项目,joda项目包括:

  • Joda-Time - Basic types for Date and Time
  • Joda-Money - Basic types for Money
  • Joda-Beans - Next generation JavaBeans
  • Joda-Convert - String to Object conversion
  • Joda-Collect - Additional collection data structures

其中joda time是日期时间三方库,但是在java 8之前joda time其实是标准的日期时间库,其出色的语义表达,易用易于理解的api,类型安全的特性,大受开发者的追捧。而且其日历系统遵循的是IOS_8601国际标准,同时还包括其他的非标准的日历系统。支持时区、持续时间、格式化和解析功能。

在java 8之前可以依赖joda time三方库,使用其日期时间库。

但在java 8中提出了JSR 310: Date and Time API规范,该规范即新版的日期时间库java.time规约。可以说JSR-310的设计上汲取了大量的joda time的特性。新版本的日期时间库基于JSR 310: Date and Time API被开发,java.time是基于国际化标准日历系统(International Organization for Standardization)ISO_8601,同时java.time.chrono支持对全球日历系统的扩展。

JSR-310中设计的java.time包括年、月、星期、日期时间、持续时间段、瞬时、时钟、时区的抽象及处理。且api的设计上使用易读易于理解的名称和设计模式,让使用者欣然接受。而且提供旧版和新版api之间的互通以处理兼容性问题。

下面看张概览图,从宏观角度了解下java.time

  • 第一层是对年、月、月中日、星期的抽象;
  • 第二层是对日期、日期时间、时区的抽象,其中时区分为时区Id(Europe/Paris)和时区偏移量(Z/+hh:mm/-hh:mm);
  • 第三层是对区域时间和便宜时间的抽象;
  • 第四层是对瞬时和时钟的抽象;
  • 第五层是对时序时段和持续周期的抽象
  • 右侧层是辅助工具类,如:日期时间格式、日期时间调整器、其他的日历系统;

java 8中日期时间库共分为五个package:

  • java.time:基于ISO_8601日历系统实现的日期时间库
  • java.time.chrono:全球日历系统扩展库,可以自行扩展
  • java.time.format:日期时间格式,包含大量预定义的格式,可以自行扩展
  • java.time.zone:时区信息库
  • java.time.temporal:日期时间调整辅助库

关于日期时间库的使用详细过程,推荐查看oracle提供的java教程The Java™ Tutorials——Trail: Date Time

也可以查看我的github中java 8新特性代码:lixyou/java

三.java.time优点

1.设计

java.time中使用了大量的设计模式

  • 工厂模式:now()工厂方法直接生成当前日期时间或者瞬时;of()工厂方法根据年月日时分秒生成日期或者日期时间;
  • 装饰模式:时区时间ZoneDateTime/便宜时间OffsetDateTime,都是在LocalDateTime的基础上加上时区/偏移量的修饰成为时区时间,然后可以进行时区转换;
  • 建造者模式:Calendar中加入建造者类,用于生成新的Calendar对象;

2.命名

  • java 8中的日期时间库类名、方法名命名上都是极其形象生动,易于理解,让开发者极易于使用——语义清晰精确!如:LocalDate中提供的now表示现在的日期,of用于年月日组成的日期(这里和英文中的of意义非常贴切),plus/minus加减等等;

3.合理的接口设计

  • LocalDate表示日期,由年月日组成,提供了获取所在年,所在月,所在日的api,提供所在一年的第几天api,用于比较日期前后api,替换年份、月份、日的api,这些api使得日期或者日期时间的处理上得到的功能上的极大提升;
  • 抽象出年、月、日、星期、日期、日期时间、瞬时、周期诸多接口,对事物本质有了细腻的抽象,并提供了相互转换的能力——提供极强的处理能力和语言表达能力;
  • 对于遗留的日期时间库Calendar/Date/Timezone和新的日期时间库的互通性;
  • 将全球的非标准日历系统单独抽象并支持扩展,从标准日历系统中隔离(符合设计原则:对修改关闭,对扩展开放)

四.补充

  • 时区:时区是地球上的区域使用同一个时间定义。以前,人们通过观察太阳的位置(时角)决定时间,这就使得不同经度的地方的时间有所不同(地方时)。1863年,首次使用时区的概念。时区通过设立一个区域的标准时间部分地解决了这个问题。
    世界各个国家位于地球不同位置上,因此不同国家,特别是东西跨度大的国家日出、日落时间必定有所偏差。这些偏差就是所谓的时差。
    理论时区以被15整除的子午线为中心,向东西两侧延伸7.5度,即每15°划分一个时区,这是理论时区。理论时区的时间采用其中央经线(或标准经线)的地方时。所以每差一个时区,区时相差一个小时,相差多少个时区,就相差多少个小时。东边的时区时间比西边的时区时间早。为了避免日期的紊乱,提出国际日期变更线的概念
    但是,为了避开国界线,有的时区的形状并不规则,而且比较大的国家以国家内部行政分界线为时区界线,这是实际时区,即法定时区。请参见时区列表。

  • 子午线:经线也称子午线,和纬线一样是人类为度量而假设出来的辅助线,定义为地球表面连接南北两极的大圆线上的半圆弧。任两根经线的长度相等,相交于南北两极点。每一根经线都有其相对应的数值,称为经度。经线指示南北方向。

  • 本初子午线:即0度经线,亦称格林尼治子午线或本初经线,是经过英国格林尼治天文台的一条经线(亦称子午线)。本初子午线的东西两边分别定为东经和西经,于180度相遇。

  • 国际标准ISO 8601:是国际标准化组织的日期和时间的表示方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》。目前是2004年12月1日发行的第三版“ISO8601:2004”以替代1998年的第一版“ISO8601:1988”与2000年的第二版“ISO8601:2000”。
    年由4位数字组成YYYY,或者带正负号的四或五位数字表示±YYYYY。以公历公元1年为0001年,以公元前1年为0000年,公元前2年为-0001年,其他以此类推。应用其他纪年法要换算成公历,但如果发送和接受信息的双方有共同一致同意的其他纪年法,可以自行应用。
    月、日用两位数字表示:MM、DD。
    只使用数字为基本格式。使用短横线"-"间隔开年、月、日为扩展格式。
    ISO 8601:2004不再允许缺省(默认)世纪仅用两位数字表示年,这会与小时数的表示相混淆。而遵循ISO 8601:2000的GB/T 7408-2005,尚还存在这一问题。

  • 协调世界时英语:Coordinated Universal Time,法语:Temps Universel Coordonné,简称UTC):是最主要的世界时间标准,其以原子时秒长为基础,在时刻上尽量接近于格林尼治标准时间。中华民国采用CNS 7648的《资料元及交换格式–资讯交换–日期及时间的表示法》(与ISO 8601类似)称之为世界协调时间。中华人民共和国采用ISO 8601:2000的国家标准GB/T 7408-2005《数据元和交换格式 信息交换 日期和时间表示法》中亦称之为协调世界时。
    协调世界时是世界上调节时钟和时间的主要时间标准,它与0度经线的平太阳时相差不超过1秒[4],并不遵守夏令时。协调世界时是最接近格林威治标准时间(GMT)的几个替代时间系统之一。对于大多数用途来说,UTC时间被认为能与GMT时间互换,但GMT时间已不再被科学界所确定。
    如果时间是以协调世界时(UTC)表示,则在时间后面直接加上一个“Z”(不加空格)。“Z”是协调世界时中0时区的标志。因此,“09:30 UTC”就写作“09:30Z”或是“0930Z”。“14:45:15 UTC”则为“14:45:15Z”或“144515Z”。

  • UTC偏移量:UTC偏移量用以下形式表示:±[hh]:[mm]、±[hh][mm]、或者±[hh]。如果所在区时比协调世界时早1个小时(例如柏林冬季时间),那么时区标识应为“+01:00”、“+0100”或者直接写作“+01”。这也同上面的“Z”一样直接加在时间后面。
    "UTC+8"表示当协调世界时(UTC)时间为凌晨2点的时候,当地的时间为2+8点,即早上10点。

  • 格林尼治平时(英语:Greenwich Mean Time,GMT):是指位于英国伦敦郊区的皇家格林尼治天文台当地的平太阳时,因为本初子午线被定义为通过那里的经线。
    自1924年2月5日开始,格林尼治天文台负责每隔一小时向全世界发放调时信息。
    格林尼治平时的正午是指当平太阳横穿格林尼治子午线时(也就是在格林尼治上空最高点时)的时间。由于地球每天的自转是有些不规则的,而且正在缓慢减速,因此格林尼治平时基于天文观测本身的缺陷,已经被原子钟报时的协调世界时(UTC)所取代。

参考

Trail: Date Time
Class DateTimeFormatter
Java 8新特性终极指南
Is java.util.Calendar thread safe or not?Ask
协调世界时
时区
ISO 8601
经线
[时区列表](https://zh.wikipedia.org/wiki/ %E6%97%B6%E5%8C%BA%E5%88%97%E8%A1%A8#UTC%EF%BC%88WET_-%E6%AD%90%E6%B4%B2%E8%A5%BF%E9%83%A8%E6%99%82%E5%8D%80%EF%BC%8CGMT-_%E6%A0%BC%E6%9E%97%E5%A8%81%E6%B2%BB%E6%A0%87%E5%87%86%E6%97%B6%E9%97%B4%EF%BC%89)

posted @ 2018-08-08 12:57  怀瑾握瑜XI  阅读(13380)  评论(0编辑  收藏  举报