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
![](https://images2018.cnblogs.com/blog/1286175/201808/1286175-20180808125921134-702165352.png)
- 第一层是对年、月、月中日、星期的抽象;
- 第二层是对日期、日期时间、时区的抽象,其中时区分为时区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
经线
时区列表