java8中的time包

Sep 3, 2016


前言

用java8已经有好一段时间了,不少新特性都已经使用过了,唯独还没用过java8中新的时间包,今天写代码的时候正好需要涉及到时间,那就来研究一下java8中新的时间包(java.time)吧,本博客参考的内容来自Oracle官网,连接如下:

http://www.oracle.com/technetwork/articles/java/jf14-date-time-2125367.html

java8为什么开发新的时间包

其实追溯起来,jdk中关于时间的处理工具已经经过两个时代了,最早的java.util.Date到后来出现的java.util.Canlendar类,都是jdk中提供的时间处理类,为什么Oracle还要在java8中再增加time的包来处理时间呢?原因有以下几个:

  • 原本是时间类(java.util.Date和SimpleDateFormatter等)是非线程安全的
  • API设计不合理,导致很难用
  • 为了在jdk内部更好地支持时间的处理

java8新的时间库在java.time包内,由比较流行的第三方时间库Joda-Time 的作者和Oracle共同开发,遵从JSR 310标准。

设计思想

新的java时间类库主要基于以下几点核心思想设计:

  • 不可变类(Immutable-value classes):新的时间对象会是一个不可变对象,保证线程安全和避免并发问题
  • 领域模型驱动设计(Domain-driven design):新的API会非常明确地区分日期(Date)和时间(Time),不同于以往日期和时间的界限非常模糊
  • 分离年表(Separation of chronologies):新的API允许使用者使用不同的日历系统,以便支持世界不同的地区,比如日本和泰国,他们的日历系统不遵从ISO-8601标准。

两个关键的类

新的时间库中,用来表示时间的两个类有java.time.LocalDatejava.time.Time,它们分别表示本地的日期和时间,另外还有一个包含日期时间的类java.time.LocalDateTime

创建日期对象

创建新的对象

新的API创建日期对象创建通过流式工厂方法创建,一般来说,如果通过要创建对象自身的属性创建,使用of方法,如果是通过其他属性创建,使用from方法。

所谓通过自身属性,指的是自己具有的属性,比如年,月,日是日期自有的属性,因此创建日期或时间对象时,可以采用如下方式创建:

LocalDate date = LocalDate.of(2012, Month.DECEMBER, 12);
LocalTime time = LocalTime.of(17, 18);

from的创建方式如下:

LocalDate date = LocalDate.from(TemporalAccessor accessor);

这种创建方式相对麻烦,需要实现接口TemporalAccessor,这个接口是一个框架级的基础接口,主要用来连接一个只读的时间对象,比如日期,时间,或者时间偏移等等,也有可能是一个复合的对象。

另外,创建方法中还提供了paser接口用来创建对象,比如:

LocalTime time = LocalTime.parse("10:15:30");

通过特定计算创建日期对象

某些情况下,我们需要对得到的日期时间对象进行计算,比如前移一天,后移一天,由于时间日期类都是不可变类,因此我们不能直接通过setter方法来修改,这里类库中提供了with方法来帮助我们计算并将得到的结果以一个新对象的方式返回给我们:

// Set the value, returning a new object
LocalDateTime thePast = timePoint.withDayOfMonth(
    10).withYear(2010);

/* You can use direct manipulation methods, 
    or pass a value and field pair */
LocalDateTime yetAnother = thePast.plusWeeks(
    3).plus(3, ChronoUnit.WEEKS);

通过适配器创建日期对象

新的类库中引入了一个适配器(adjuster)的概念,因此我们也可以通过自己实现一个类WithAdjusterPlusAdjuster来同时设置多个属性创建新对象,这个适配器需要实现接口java.time.temporal.TemporalAdjuster

LocalDateTime timePoint = LocalDateTime.now();
LocalDateTime foo = timePoint.with(TemporalAdjusters.lastDayOfMonth());
LocalDateTime bar = timePoint.with(TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY));
LocalDateTime time = timePoint.with(LocalTime.now());

这里我们需要注意,LocalDateTime这个类,本身就是java.time.temporal.TemporalAdjuster的实现类,实际上我们是可以自己实现的。

获取精确的时间对象

新的时间库还提供了truncatedTo 方法来获得一个精确的时间点,只要提供了明确的日期,时间,或者日期时间,即可,但是开发者可以不关注这里的实现细节,比如:

LocalDateTime truncatedTime = time.truncatedTo(ChronoUnit.SECONDS);

获取属性的接口

日期时间对象提供了一些标准的getter接口用来给我们获取相应的信息,比如:

LocalDate theDate = timePoint.toLocalDate();
Month month = timePoint.getMonth();
int day = timePoint.getDayOfMonth();
timePoint.getSecond();

这里要注意,获取得到的月份不再是一个普通的数值,而是一个Month对象,这个对象是一个枚举类型,从1月到12月。

时区的概念(Time Zone)

在前面提到的几个Local类里,抽象了由时区问题引入的复杂性,时区是一个规则的合集,在使用标准时间的地区,同一个时区的时间是一致的,全球大约有40个时区,时区的定义是通过与世界协调时间(UTC)的偏移量定义的,一般来说不同的时区时间是不一致的,但是时间差是固定的。

时区一般通过两个标识指定

  • 短名(abbreviated):如“PLT”
  • 长名(longer):如“Asia/Karachi”

这里官网有一句话我并不是特别理解,摘抄如下:

When designing your application, you should consider what scenarios are appropriate for using time zones and when offsets are appropriate.

大体的意思是在设计程序的时候,我们需要考虑脚本(或者说代码?)对于使用的时区的偏移是恰当的。

在java8的时间类库里,使用java.time.ZoneId来标识一个时区,我们可以通过本地时间配合ZoneId来获取当前时区的时间,如下:

LocalDateTime time = timePoint.with(LocalTime.now());
// You can specify the zone id when creating a zoned date time
ZoneId id = ZoneId.of("Europe/Paris");
ZonedDateTime zoned = ZonedDateTime.of(time, id);

类库中还提供了java.time.ZoneOffset来表示时区时间和标准时间(UTC)的时差,如:

ZoneOffset offset = ZoneOffset.of("+2:00");

新类库中的时区类

前面我们已经接触到一些时区相关的类了,现在我们再来深入一点看看新的时间类库中提供了跟时区相关的那些类。

ZonedDateTime

ZonedDateTime 类我们在前面已经看过了,这个类表示的是一个日期和时间,可以通过这个类可以计算得到所有时区的对应时间,如果希望得到一个跟时区无关的日期和时间对象,建议使用这个类,类的创建如下:

ZonedDateTime zoned =ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");

OffsetDateTime

OffsetDateTime这个类我们前面也见过了,它表示了一个时间偏移,一般用于序列化存储到数据库,如果我们的服务器在不同的时区,也应当用这个类序列化为日志的时间戳以便服务器日志时间的统一,使用示例如下:

ZoneOffset offset = ZoneOffset.of("+2:00");
OffsetTime time = OffsetTime.now();
// changes offset, while keeping the same point on the timeline
OffsetTime sameTimeDifferentOffset = time.withOffsetSameInstant(
    offset);
// changes the offset, and updates the point on the timeline
OffsetTime changeTimeWithNewOffset = time.withOffsetSameLocal(
    offset);
// Can also create new object with altered fields as before
changeTimeWithNewOffset
 .withHour(3)
 .plusSeconds(2);

需要注意的是,在旧的类库中,已经存在一个java.util.TimeZone了,但是并没有在新的类库中使用这个类,主要是因为新的类库要求时间类是不可变对象,而这个已有的类是可变的。

java.time.Period

java.time.Period表示一个周期,与我们前面看到的类都不同,这个类表示的是一个时间段,比如3个月多1天这个时间段,而不是一个日期或者时间点,使用示例如下:

LocalDate oldDate = LocalDate.now();
ZonedDateTime oldDateTime = ZonedDateTime.now();
// 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);
ZonedDateTime newDateTime = oldDateTime.minus(period);

java.time.Duration

java.time.Duration类也是表示一个时间段,和Period类似,使用如下:

// A duration of 3 seconds and 5 nanoseconds
Duration duration = Duration.ofSeconds(3, 5);
Duration oneDay = Duration.between(today, yesterday);

java.time.Durationjava.time.Period的不同之处在于,Duration是允许加减和with操作的,因此也可以用它来改变一个日期或者时间的值。

注:这里其实我也并不是非常理解两者的区别,上面的解释是根据官方文档翻译得出的。这里摘抄一下原文: It’s possible to perform normal plus, minus, and “with” operations on a Duration instance and also to modify the value of a date or time using the Duration.

年表(Chronologies)

年表主要的设计目的是支持使用非ISO标准日历系统的开发者使用的,java8引入了年表(Chronology)的概念,它主要应用于给一个时间点作为一个工厂来表明一个日历系统和动作,这句话理解起来 有点拗口,我们分开来解读一下。

首先,Chronology是一个工厂,用来创建一个时间点对象的,这个时间点对象是在标准ISO日历系统中的。 其次,Chronology表明了一个时间点,这个时间点是在非标准ISO日历系统中的。 因此,Chronology实际上是把非标准日历时间点和标准日历时间点一一对应起来的对象,一个Chronology表明一个非标准时间点,然后通过Chronology可以创建出一个和它说表示的非标准时间点对应的标准时间点。

跟年表相关的类有如下几个:

java.time.Chronology:
java.time.ChronoLocalDate
java.time.ChronoLocalDateTime
java.time.ChronoZonedDateTime

实际上,如果不是必须的情况下,我们都不应该使用年表对象,官方也不建议我们使用。

公用类

除了上面说的,java8还提供了一些公用的类:

java.time.MonthDay;//包含了一个Month对象和Day对象表示月和日
java.time.YearMonth;//包含了年和月的对象

上面两个类比较好理解,不多说了。

在java8之后,jdbc的标准中也增加了对新的时间类的支持,但是并没有改变原来的接口规范,而是通过原有的setObjectgetObject接口来支持的。

数据库类型和java类型的对应表如下:

ANSI SQL Java SE 8
DATE LocalDate
TIME LocalTime
TIMESTAMP LocalDateTime
TIME WITH TIMEZONE OffsetTime
TIMESTAMP WITH TIMEZONE OffsetDateTime

总结

java8中新增的时间包提供了更多的特性支持,接口的使用也简单了许多,所以在需要使用时间对象的时候,我们应该优先选择使用这个新的类库,当然,在使用之前要注意,使用的数据库对应的版本号是否支持使用java8的时间对象。