为什么我们需要一个新的日期和时间库?
Java长期存在的一个问题是对日期和时间的支持非常不理想。
例如,现有的类(例如java.util.Date和SimpleDateFormatter)不是线程安全的,导致用户可能出现并发问题 - 普通开发人员在编写日期处理代码时并不希望处理这样的问题。
一些日期和时间类的API设计也表现的差强人意。 例如,在Java 1.0中,对日期和时间的支持只能依赖java.util.Date类,但是年份的起始选择是1900年,月份的起始从0开始 - 不是很直观。如果你想创建2014年3月18号,你需要写如下代码:
Date date = new Date(114, 2, 18);
输入如下:
Tue Mar 18 00:00:00 CST 2014
java1.1中对日期和时间的处理又引入了java.util.Calendar
类。但是月份依旧是从0开始(不过,至少Calendar 取消了由1900年开始计算年份这一设计)。
正是因为java原生的日期处理存在这样或那样的问题,所以导致第三方的日期类库反而更加普及,例如Joda-Time。
为了解决这些问题并在JDK中提供更好的支持,为Java SE 8设计了新的日期和时间API。
由 Joda-Time的作者(Stephen Colebourne) 和oracle官方共同主导,再JSR 310 的规范下,新的日期时间处理类出现在Java SE 8的java.time软件包中。
核心理念
新的API包含三个核心思想:
-
Immutable-value classes 在java8以前,处理日期格式化的各种formatters都不是线程安全的,这在日常的开发当中,对于日期处理的需要考虑并发问题带来意想不到的错误。 新API通过确保其所有核心类都是不可变的,以此来避免并发问题。
-
Domain-driven design 新API非常精确地表达其域,其中的类代表了日期和时间的不同用例。 与Java SE 8之前的Java库不同。 例如,java.util.Date表示时间轴上的一个瞬间 - 一个自UNIX元年(传统的设定为UTC时区1970年1月1日午夜时分)以来毫秒数的包装器,但是如果你调用toString(),结果会显示它有一个时区,这容易对开发人员造成困惑。
这种领域驱动的设计,清晰的表达了日期和时间,使得更容易理解,但你若想由先前的API移植到Java SE 8时,可能需要考虑你的应用程序的日期域模型。 -
Separation of chronologies 新API允许人们使用不同的日历系统,以支持世界某些国家和地区(如日本或泰国,他们不采用国际标准ISO-8601)的用户的需求。 但是大多数开发人员无需太多的关心,他们只需要使用标准的日历即可。
LocalDate 和 LocalTime
开始接触新的日期和时间API时,你先碰到的可能是LocalDate类和LocalTime类。他们从默认时区的系统时钟获取当前日期。 还有一个名为LocalDateTime的复合类,它是LocalDate和LocalTime的组合体。
对于由于时区产生的混淆,我们暂时可以放在一边(LocalDate不带时区信息)。当你不需要时区信息的时候,我们应该使用它们,如桌面JavaFX应用程序可能是其中之一的使用场景,这些类甚至可以用于具有一致性时区的分布式系统上。
创建LocalDate 和 LocalTime对象
新API中的所有核心类都是由静态工厂方法构建的。 当通过其传递年月日参数时,工厂方法为of; 从TemporalAccessor类型转换时,工厂方法为from。 还有将将字符串作为参数的parse方法。 如下所示:
LocalDateTime timePoint = LocalDateTime.now(); // The current date and time
LocalDate.of(2012, Month.DECEMBER, 12); // from values
LocalDate.ofEpochDay(150); // middle of 1970
LocalTime.of(17, 18); // the train I took home today
LocalTime.parse("10:15:30"); // From a String
Java SE 8日期类也可以根据标准Java getter约定用于获取值,如下所示:
LocalDate theDate = timePoint.toLocalDate();
Month month = timePoint.getMonth();
int day = timePoint.getDayOfMonth();
timePoint.getSecond();
你还可以更改对象值。 因为所有核心类在新API中都是不可变的,所以你如果想创建修改版,那么调用with、(而不是使用setter),返回修改后的新对象。也会根据不同的参数计算对应的值。如下所示:
LocalDateTime timePoint = LocalDateTime.of(2014, 3, 18,10,0);
// Set the value, returning a new object
LocalDateTime thePast = timePoint.withDayOfMonth(10).withYear(2010); //2010,3,10 10:00
/* You can use direct manipulation methods, or pass a value and field pair */
LocalDateTime yetAnother = thePast.plusWeeks(3).plus(3,
ChronoUnit.WEEKS); // 2010,4,21 10:00
新API还具有调整器的概念 - 一个通用逻辑处理代码块的封装。 你可以设置一个或多个字段的WithAdjuster,也可以添加或减去某些字段的PlusAdjuster。 值类也可以充当调整器,在这种情况下,它们更新它们所代表的字段的值。 新API定义了一些常用的内置调整器,但如果你需要特定业务逻辑,则可以自定义调整器。如下所示:
LocalDate timePoint = LocalDate.of(2014, 3, 18); //2014-03-18
LocalDate foo = timePoint.with(lastDayOfMonth()); //2014-03-31
LocalDate bar = timePoint.with(nextOrSame(DayOfWeek.SUNDAY)); //2014-03-23
// Using value classes as adjusters
LocalDate now = timePoint.with(LocalDate.now()); //当前时间
截断
新API通过提供表示日期,时间和日期与时间的类型来支持不同的精确的时间点,但显然还有更精细的的时间粒度(纳秒)。
truncatedTo方法截断返回原始日期时间的副本,其中小于指定单位的字段设置为零。如下所示:
LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20); //2014-03-18T13:45:20
LocalDateTime truncatedTime = dt1.truncatedTo(ChronoUnit.MINUTES); //2014-03-18T13:45
Time Zones
之前你看到的日期和时间的种类都不包含时区信息。时区的处理是新版日期和时间API新增 加的重要功能,使用新版日期和时间API时区的处理被极大地简化了。时区是按照一定的规则将区域划分成的标准时间相同的区间。在ZoneRules这个类中包含了 40个这样的实例。 时区由它们与通用协调时(UTC)的偏移量定义。 它们大致同步移动,但是按照指定的差异。
时区可以通过两个标识符来引用:缩写,例如“PLT,” 较长标识符,例如“Asia/Karachi.” 。在设计应用程序时,使用时区时发生偏移时,你应该考虑哪个方案更适合。
ZoneId
是区域标识符. 每个ZoneId对应一些定义该位置时区的规则。设计软件时,如果考虑使用诸如“PLT”或“Asia/Karachi,” 则应使用此域类。地区ID都为“{区域}/{城市}”的格式,这些地区集合的设定都由英特网编号分配机构(IANA) 的时区数据库提供。下面展示了时区的使用示例:
LocalDateTime dateTime = LocalDateTime.of(2014, 3, 18,10,0);
// You can specify the zone id when creating a zoned date time
ZoneId id = ZoneId.of("Asia/Shanghai");
ZonedDateTime zoned = ZonedDateTime.of(dateTime, id);
assertEquals(id, ZoneId.from(zoned));
ZoneOffset
当前时区和UTC/格林尼治的固定偏差。这可以在特定时刻针对特定ZoneId提供解决方案。ZoneOffset类,它是ZoneId的一个子类,表示的是当前时间和伦敦格林尼治子午线时间的差异:
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
Time Zone Classes
ZonedDateTime
是包含完全限定时区的日期和时间。 这可以在任何时间点解决偏移。 经验法则是,如果在不依赖于特定服务器的上下文的情况下表示日期和时间,则应使用ZonedDateTime。
ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");
-
OffsetDateTime
是具有已解析偏移的日期和时间。 这对于将数据序列化到数据库中非常有用,如果服务器位于不同的时区,也应该用作记录时间戳的序列化格式。 -
OffsetTime
是具有已解析偏移量的时间。
java.util.TimeZone是一个现有的时区类 - 但Java SE 8不使用它,因为所有JSR 310类都是不可变的,TimeZone是可变的。
Periods
Period表示诸如“3个月和1天”之类的值,其是时间线上的距离。 这与我们到目前为止所看到的其他类不同,其他类是时间轴上的点。
LocalDate oldDate = LocalDate.of(2014, 3, 8);
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
ZoneId romeZone = ZoneId.of("Asia/Shanghai");
ZonedDateTime oldDateTime = dateTime.atZone(romeZone);
// 3 years, 2 months, 1 day
Period period = Period.of(3, 2, 1);
// You can modify the values of dates using periods
LocalDate newDate = oldDate.plus(period); //2017-05-09
ZonedDateTime newDateTime = oldDateTime.minus(period); //2011-01-17T13:45+08:00[Asia/Shanghai]
// Components of a Period are represented by ChronoUnit values
assertEquals(1, period.get(ChronoUnit.DAYS));
Durations
Duration是时间线上按时间测量的距离,它与Period的作用类似,但具有不同的精度,如下所示:
LocalDateTime today = LocalDateTime.now();
LocalDateTime yesterday = today.minusDays(1);
// A duration of 3 seconds and 5 nanoseconds
Duration duration = Duration.ofSeconds(3, 5);
Duration oneDay = Duration.between(today, yesterday);
assertEquals(-1, oneDay.toDays());
可以在Duration实例上执行加法(plus),减法(minus)和 “with” 操作,还可以使用Duration修改日期或时间的值。
Chronologies
为了支持使用非ISO历法系统(non-ISO calendaring) 的开发人员的需求,Java SE 8引入了Chronology,Chronology用于组织和识别日期的日历系统。java.time.chrono包中有大量的接口和实现类提供使用,接口如下:
Chronology
:
ChronoLocalDate
ChronoLocalDateTime
ChronoZonedDateTime
ChronoPeriod
这些类纯粹适用于那些需要考虑本地化日历系统的高度国际化的应用程序,日期及时间API的设计者建议我们使用LocalDate。 一些日历系统甚至没有月或周的概念,并且需要定义一些特殊的日期字段,如Umm Al-Qura变量。
其他 API
Java SE 8还有一些其他常用的类。 如MonthDay,只包含Month和Day,适用于表示生日。 YearMonth类,只包含Year和Month,适用于信用卡开始日期和到期日期的情景。
Java SE 8中的JDBC将支持这些新类型,但没有对公共JDBC API进行更改。 现有的泛型setObject和getObject方法就足够了。
这些类型可以映射到实现了美国国家标准化组织(ANSI) SQL标准的数据库的日期类型;
如下所示:
ANSI SQL | Java SE 8 |
---|---|
DATE | LocalDate |
TIME | LocalTime |
TIMESTAMP | LocalDateTime |
TIME WITH TIMEZONE | OffsetTime |
TIMESTAMP WITH TIMEZONE | OffsetDateTime |
结论
Java SE 8在java.time包中提供了新的日期和时间API,在原生的Java API中提供高质量的日期和时间支持。
JSR 310