JSR310新日期API(二)-日期时间API

前提

这篇文章主要介绍一下日期时间API中最常用的类库,分别是:

  • java.time.Clock:时钟。
  • java.time.Instant:瞬时时间,时间戳java.sql.Timestamp的替代类。
  • java.time.LocalDate:本地日期,ISO-8601日历系统下的日期表示,不包含时区的概念,只能表示年月日。
  • java.time.LocalDateTime:本地日期时间,ISO-8601日历系统下的日期时间表示,不包含时区的概念,只能表示年月日时分秒。
  • java.time.LocalTime:本地时间,ISO-8601日历系统下的时间表示,不包含时区的概念,只能表示时分秒。
  • java.time.OffsetTime:带有时间偏移量的时间,ISO-8601日历系统下的带有UTC/GMT时间偏移量的时间表示。
  • java.time.OffsetDateTime:带有时间偏移量的日期时间,ISO-8601日历系统下的带有UTC/GMT时间偏移量(不包含基于ZoneRegion的时间偏移量)的日期时间表示。
  • java.time.ZonedDateTime:带有时间偏移量的日期时间,ISO-8601日历系统下的带有UTC/GMT时间偏移量(包含基于ZoneRegion的时间偏移量)的日期时间表示。

其他的类库还有YearMonthDayOfWeekMonthDayYearMonth等。值得注意的是:JSR-310增加的日期API是严格区分年月日-时分秒格式的日期表示类,例如XXXDateTime一定表示为年月日时分秒(纳秒),XXXTime只能表示时分秒(纳秒),XXXDate只能表示年月日。

值得注意的是:这些新增的日期时间类都是不可变类,每次通过其方法更变或者修改都是返回一个全新的对象,因此它们都是线程安全的。

Clock

java.time.Clock是一个抽象类,它表示时钟,一般情况下,它需要结合时区使用,提供获取当前时刻的功能。Clock主要提供下面四个方法,其他方法都是静态工厂方法:

// 获取用于创建时钟的时区。
public abstract ZoneId getZone()

// 获取时钟的当前瞬时对象。
public abstract Instant instant()

// 获取时钟的当前毫秒数值
public long millis()

// 返回当前时钟实例的一个新的拷贝时钟实例,并且使用入参作为新时钟实例的时区
public abstract Clock withZone(ZoneId zone)

静态工厂方法如下:

方法 功能
public static Clock systemUTC() 获取可以返回当前时刻的系统时钟,使用UTC(零)时区进行进行时间转换[SystemClock]
public static Clock systemDefaultZone() 获取可以返回当前时刻的系统时钟,使用默认时区进行时间转换[SystemClock]
public static Clock system(ZoneId zone) 获取可以返回当前时刻的系统时钟,使用指定时区ID进行时间转换[SystemClock]
public static Clock tickMillis(ZoneId zone) 获取以整数毫秒返回当前时刻的时钟,使用指定时区ID进行时间转换[TickClock]
public static Clock tickSeconds(ZoneId zone) 获取以整数秒返回当前时刻的时钟,使用指定时区ID进行时间转换[TickClock]
public static Clock tickMinutes(ZoneId zone) 获取以整数分钟返回当前时刻的时钟,使用指定时区ID进行时间转换[TickClock]
public static Clock tick(Clock baseClock, Duration tickDuration) 返回一个以基础时钟和时钟记录基础单位为构造的时钟[TickClock]
public static Clock fixed(Instant fixedInstant, ZoneId zone) 获得一个始终返回同一时刻的时钟,使用指定时区ID进行时间转换[FixedClock]
offset​(Clock baseClock, Duration offsetDuration) 返回一个以基础时钟和固定时间偏移量为构造的时钟[OffsetClock]

java.time.Clock主要有四个实现,它们都是java.time.Clock的内部类,上面的工厂方法创建的实例一定是这四个实现之一:

  • SystemClock:总是基于System#currentTimeMillis()返回最新的时间SystemClock.UTC是典型的实现。
  • FixedClock:总是返回相同的瞬时时间,可以认为是一个固定时刻的时钟,通常使用于测试。
  • OffsetClock:基于一个确定的Clock实现,为它添加一个时间偏移量,时间偏移量的单位是Duration
  • TickClock:基于一个确定的Clock实现,为它添加一个时间偏移量,时间偏移量的单位是纳秒。

上面比较难理解的是TickClock,这里举个简单的例子:

public class TickClockMain {

public static void main(String[] args) throws Exception{
Clock tickMillis = Clock.tickMillis(ZoneId.systemDefault());
Clock tickSeconds = Clock.tickSeconds(ZoneId.systemDefault());
System.out.println(tickMillis.millis());
System.out.println(tickSeconds.millis());
}
}

//输出结果
1546010945575
1546010945000

简单来说,Clock#tickMillis()构造的时钟的计时单位是毫秒,而Clock#tickSeconds()构造的时钟的计时单位是秒(毫秒部分会被截断),以此类推。

FixedClock是一个固定时刻的时钟,一般用于测试

public class FixedClockMain {

public static void main(String[] args) throws Exception {
Clock fixed = Clock.fixed(Instant.now(), ZoneId.systemDefault());
System.out.println(fixed.millis());
System.out.println(fixed.millis());
System.out.println(fixed.millis());
}
}

//输出结果
1546011492590
1546011492590
1546011492590

最常用的是默认ZoneId下的系统时钟SystemClock和UTC系统时钟:

public class SystemClockMain {

public static void main(String[] args) throws Exception {
Clock clock = Clock.systemDefaultZone();
System.out.println(clock.millis());
Clock utc = Clock.systemUTC();
System.out.println(utc.millis());
System.out.println(System.currentTimeMillis());
}
}

//某个时刻的输出结果
1546011686413
1546011686413
1546011686413

Instant

java.time.Instant字面意思是瞬时时间,它是java.sql.Timestamp的对应类,代表时间线(Time-Line)上的一个瞬时时间点,准确来说,它内部持有一个long类型的纪元秒属性(seconds)和一个int类型的纳秒属性(nanos,nanos的取值范围是[0,999_999_999]),纪元秒如果为正数,表示该瞬时时间点位于格林威治新纪元1970-01-01T00:00:00Z之后,而纪元秒如果为负数,则表示该瞬时时间点位于格林威治新纪元之前。因此Instant能表示的时间点其实是有上下界的,逻辑上的界限就是1970-01-01T00:00:00Z - 31557014167219200秒1970-01-01T00:00:00Z + 31556889864403199秒 + 999_999_999纳秒或者表示为Instant#MINInstant#MAX,这个范围很大,因此暂时不需要考虑超限的问题。Instant中已经提供了一个公有静态实例用于表示格林威治新纪元,它就是Instant#EPOCH,代表1970-01-01T00:00:00Z这个瞬时时间点。先看一下Instant的常用静态工厂方法(Instant没有公有构造器,必须通过工厂方法构造实例):

// 当前时刻的瞬时时间点
public static Instant now()

// 基于时钟实例获取瞬时时间点
public static Instant now(Clock clock)

// 基于距离新纪元的秒数创建瞬时时间点
public static Instant ofEpochSecond(long epochSecond)

// 基于距离新纪元的秒数和纳秒创建瞬时时间点
public static Instant ofEpochSecond(long epochSecond, long nanoAdjustment)

// 基于毫秒数创建瞬时时间点
public static Instant ofEpochMilli(long epochMilli)

// 基于其他日期时间API创建瞬时时间点
public static Instant from(TemporalAccessor temporal)

// 基于特定格式字符串创建瞬时时间点,如2007-12-03T10:15:30.00Z
public static Instant parse(final CharSequence text)

当然还有其他常用的方法:

// 获取当前Instant实例对于不同计时单位的值,见ChronoField
public long getLong(TemporalField field)

// 获取当前Instant实例的纪元秒属性
public long getEpochSecond()

// 获取当前Instant实例的纳秒属性
public int getNano()

// 获取当前Instant实例的毫秒值
public long toEpochMilli()

// 基于TemporalField实例(TemporalField)和新的值调整并且创建一个新的Instant
public Instant with(TemporalField field, long newValue)

// 当前Instant实例基于TemporalUnit(ChronoUnit)截断并且返回一个新的Instant
public Instant truncatedTo(TemporalUnit unit)

// 顾名思义,基于一个时间基准单位进行时间量增加,返回一个新的Instant
public Instant plus(long amountToAdd, TemporalUnit unit)

// 顾名思义,基于一个时间基准单位进行时间量减少,返回一个新的Instant
public Instant minus(long amountToSubtract, TemporalUnit unit)

// 计算当前Instant实例和入参endExclusive基于时间基准单位unit之间的时间量
public long until(Temporal endExclusive, TemporalUnit unit)

举个使用例子:

public class InstantMain {

public static void main(String[] args) throws Exception {
Instant instant = Instant.now();
System.out.println(String.format("Second:%d,Nano:%d", instant.getEpochSecond(), instant.getNano()));
instant = Instant.now(Clock.systemDefaultZone());
System.out.println(String.format("Second:%d,Nano:%d", instant.getEpochSecond(), instant.getNano()));
instant = Instant.ofEpochSecond(new Date().toInstant().getEpochSecond());
System.out.println(String.format("Second:%d,Nano:%d", instant.getEpochSecond(), instant.getNano()));
instant = Instant.ofEpochMilli(System.currentTimeMillis());
System.out.println(String.format("Second:%d,Nano:%d", instant.getEpochSecond(), instant.getNano()));
instant = Instant.from(Instant.now());
System.out.println(String.format("Second:%d,Nano:%d", instant.getEpochSecond(), instant.getNano()));
instant = Instant.parse("2018-12-31T10:15:30.00Z");
System.out.println(String.format("Second:%d,Nano:%d", instant.getEpochSecond(), instant.getNano()));
}
}
// 某个时刻的输出
Second:1546187685,Nano:261861900
Second:1546187685,Nano:291941900
Second:1546187685,Nano:0
Second:1546187685,Nano:292000000
Second:1546187685,Nano:292946400
Second:1546251330,Nano:0

LocalDate

java.time.LocalDate代表ISO-8601日历系统中不包含时区的日期(当然也不包含具体的时间)表示,例如2007-12-03。LocalDate是一个不可变的日期对象,也就是只能表示日期,通常的表示格式为年-月-日,同时提供其他日期字段的访问,例如一年中的第几日(day-of-year)、星期几(day-of-week)和一年中的第几周(week-of-year)等。不同的LocalDate之间的比较只能通过LocalDate#equals()方法,其他比较操作如==或者hash()方法会产生无法预知的结果。LocalDate提供的常量:

// -999999999-01-01
public static final LocalDate MIN = LocalDate.of(Year.MIN_VALUE, 1, 1)

// 999999999-12-31
public static final LocalDate MAX = LocalDate.of(Year.MAX_VALUE, 12, 31)

// 1970-01-01
public static final LocalDate EPOCH = LocalDate.of(1970, 1, 1)

LocalDate的工厂方法比较多,这里只列举部分常用的:

// 基于当前日期获取LocalDate实例
public static LocalDate now()

// 基于当前日期和时区获取LocalDate实例
public static LocalDate now(ZoneId zone)

// 基于当前日期和时钟获取LocalDate实例
public static LocalDate now(Clock clock)

// 基于年月(枚举)日获取LocalDate实例
public static LocalDate of(int year, Month month, int dayOfMonth)

// 基于年月日获取LocalDate实例
public static LocalDate of(int year, int month, int dayOfMonth)

// 基于年和具体该年中的某一日获取LocalDate实例
public static LocalDate ofYearDay(int year, int dayOfYear)

// 基于新纪元1970-01-01的偏移天数获取LocalDate实例
public static LocalDate ofEpochDay(long epochDay)

LocalDate的实例方法也比较多,这里也列举部分常用的:

// 获取年份值
public int getYear()

// 获取月份值,范围是1-12
public int getMonthValue()

// 获取月份枚举
public Month getMonth()

// 返回当前LocalDate实例的该年中具体的第几天
public int getDayOfYear()

// 返回当前LocalDate实例的具体是星期几
public DayOfWeek getDayOfWeek()

// 是否闰年
public boolean isLeapYear()

// 返回当前LocalDate实例月份长度
public int lengthOfMonth()

// 返回当前LocalDate实例年份长度
public int lengthOfYear()

// 基于一个日期属性修改对应的值返回一个新的LocalDate实例
public LocalDate with(TemporalField field, long newValue)

// 基于一个日期时间基准单位增加对应的值返回一个新的LocalDate实例
public LocalDate plus(long amountToAdd, TemporalUnit unit)

// 基于一个日期时间基准单位减去对应的值返回一个新的LocalDate实例
public LocalDate minus(long amountToSubtract, TemporalUnit unit)

// 基于一个日期时间基准单位计算以入参为endExclusive计算日期或者时间的间隔
public long until(Temporal endExclusive, TemporalUnit unit)

// 返回基于新纪元年1970-1-1的偏移天数
public long toEpochDay()

// 如果入参为LocalDate类型功能和equals一致,否则通过基于纪元年的偏移天数比较
public boolean isEqual(ChronoLocalDate other)

// 只有年月日三个成员同时相等此方法才返回true
public boolean equals(Object obj)

举个简单的使用例子:

public class LocalDateMain {

public static void main(String[] args) throws Exception {
LocalDate localDate = LocalDate.now();
System.out.println(localDate);
localDate = LocalDate.of(2018, 12, 31);
System.out.println(localDate);
localDate = localDate.plus(1, ChronoUnit.DAYS);
System.out.println(localDate);
System.out.println(localDate.equals(LocalDate.of(2019,1,1)));
System.out.println(localDate.toEpochDay());
}
}
// 某天执行的输出结果
2018-12-31
2018-12-31
2019-01-01
true
17897

LocalTime

java.time.LocalTime代表ISO-8601日历系统中不包含时区的时间(当然也不包含具体的日期)表示,例如10:15:30。LocalTime是一个不可变的时间对象,也就是只能表示时间,通常的表示格式为时:分:秒,也可以包含一个纳秒属性(nano取值范围[0,999999999]),通俗来说,它表示的就是挂钟上所见的时间的描述。同样,不同的LocalTime实例必须通过LocalTime#equals()方法比较。LocalTime提供的静态实例如下:

// 一天的起始时间 - 00:00
public static final LocalTime MIN

// 一天的结束时间 - 23:59:59.999999999
public static final LocalTime MAX

// 午夜 - 00:00 其实和MIN是一样的
public static final LocalTime MIDNIGHT

// 中午 - 12:00
public static final LocalTime NOON

LocalTime常用的工厂方法:

// 基于当前时间构造LocalTime实例
public static LocalTime now()

// 基于当前时间和时区ID构造LocalTime实例
public static LocalTime now(ZoneId zone)

// 基于当前时间和时钟实例构造LocalTime实例
public static LocalTime now(Clock clock)

// 基于小时、分钟(、秒和纳秒)构造LocalTime实例
public static LocalTime of(int hour, int minute)
public static LocalTime of(int hour, int minute, int second)
public static LocalTime of(int hour, int minute, int second, int nanoOfSecond)

// 基于瞬时时间实例和时区ID构造LocalTime实例
public static LocalTime ofInstant(Instant instant, ZoneId zone)

// 基于一天当中的具体秒数构造LocalTime实例,secondOfDay范围是[0,24 * 60 * 60 - 1]
public static LocalTime ofSecondOfDay(long secondOfDay)

// 基于一天当中的具体纳秒数构造LocalTime实例,nanoOfDay[0,24 * 60 * 60 * 1,000,000,000 - 1]
public static LocalTime ofNanoOfDay(long nanoOfDay)

LocalTime常用的实例方法有很多,套路和上面章节提到过的方法类似,这里不啰嗦分析:

// 返回小时值,范围[0,23]
public int getHour()

// 返回分钟值,范围[0,59]
public int getMinute()

// 返回秒数值,范围[0,59]
public int getSecond()

// 返回纳秒数值,范围[0,999_999_999]
public int getNano()

举个简单的例子:

public class LocalTimeMain {

public static void main(String[] args) throws Exception {
LocalTime localTime = LocalTime.now();
System.out.println(localTime);
localTime = LocalTime.of(23, 59);
System.out.println(localTime);
localTime = LocalTime.MAX;
System.out.println(String.format("Hour:%d,minute:%d,second:%d,nano:%d", localTime.getHour(),
localTime.getMinute(), localTime.getSecond(), localTime.getNano()));
}
}
// 某个时刻下的输出结果
00:46:08.845848800
23:59
Hour:23,minute:59,second:59,nano:999999999

LocalDateTime

java.time.LocalDateTime实际上就是LocalDateLocalTime的结合版本,代表ISO-8601日历系统中不包含时区(LocalDateTime不存储时区信息,但是可以使用时区ID构造LocalDateTime实例)的日期时间表示,例如2007-12-03T10:15:30。LocalDateTime是一个不可变的时间对象,也就是只能表示日期时间,通常的表示格式为年-月日 时:分:秒,也可以包含一个纳秒属性(nano取值范围[0,999999999])。不同的LocalDateTime实例必须通过LocalDateTime#equals()方法比较。LocalDateTime内部持有一个LocalDate实例和一个LocalTime实例。它定义了两个公有的静态常量:

// LocalDateTime能够表示的最小日期时间,即-999999999-01-01T00:00:00
public static final LocalDateTime MIN = LocalDateTime.of(LocalDate.MIN, LocalTime.MIN)

// LocalDateTime能够表示的最大日期时间,即999999999-12-31T23:59:59.999999999
public static final LocalDateTime MAX = LocalDateTime.of(LocalDate.MAX, LocalTime.MAX)

LocalDateTime常用的静态工厂方法如下:

// 基于当前日期时间、时区ID、时钟创建LocalDateTime实例
public static LocalDateTime now()
public static LocalDateTime now(ZoneId zone)
public static LocalDateTime now(Clock clock)

// 基于年月(枚举)日时分秒纳秒创建LocalDateTime实例
public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute)
public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute, int second)
public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond)
public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute)
public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second)
public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond)

// 基于LocalDate和LocalTime实例创建LocalDateTime实例
public static LocalDateTime of(LocalDate date, LocalTime time)

// 基于Instant实例和时区ID实例创建LocalDateTime实例
public static LocalDateTime ofInstant(Instant instant, ZoneId zone)

// 基于新纪元偏移秒数、纳秒数和时间偏移量创建LocalDateTime实例
public static LocalDateTime ofEpochSecond(long epochSecond, int nanoOfSecond, ZoneOffset offset)

LocalDateTime的实例方法和前面介绍过的类差不多,这里不做详细展开,举个简单的使用例子:

public class LocalDateTimeMain {

public static void main(String[] args) throws Exception {
LocalDateTime localDateTime = LocalDateTime.now(ZoneId.systemDefault());
System.out.println(localDateTime);
localDateTime = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
System.out.println(localDateTime);
localDateTime = localDateTime.plus(1, ChronoUnit.YEARS);
System.out.println(localDateTime);
}
}
// 某个时刻的输出如下
2019-01-01T17:43:48.260517400
2019-01-01T17:43:48.260517400
2020-01-01T17:43:48.260517400

OffsetTime

java.time.OffsetTime表示ISO-8601日历系统中带有基于UTC/Greenwich时间偏移量的时间,例如10:15:30+01:00。OffsetTime也是一个不可变的时间对象,通常表示格式为时:分:秒-时间偏移量,当然它也可以包含一个纳秒属性(nano取值范围[0,999999999])。相比LocalTime,它多存储了一个时区时间偏移量(zone offset)属性。OffsetTime的公有静态实例如下:

// 代表00:00:00+18:00
public static final OffsetTime MIN = LocalTime.MIN.atOffset(ZoneOffset.MAX)

// 代表23:59:59.999999999-18:00
public static final OffsetTime MAX = LocalTime.MAX.atOffset(ZoneOffset.MIN)

OffsetTime的常用工厂方法如下:

// 基于当前时间、时区ID、时钟创建OffsetTime实例
public static OffsetTime now()
public static OffsetTime now(ZoneId zone)
public static OffsetTime now(Clock clock)

// 基于LocalTime实例和时间偏移量创建OffsetTime实例
public static OffsetTime of(LocalTime time, ZoneOffset offset)

// 基于时分秒纳秒和时间偏移量创建OffsetTime实例
public static OffsetTime of(int hour, int minute, int second, int nanoOfSecond, ZoneOffset offset)

// 基于Instant实例和时区ID实例创建OffsetTime实例
public static OffsetTime ofInstant(Instant instant, ZoneId zone)

举个简单的使用例子:

public class OffsetTimeMain {

public static void main(String[] args) throws Exception {
OffsetTime offsetTime = OffsetTime.now();
System.out.println(offsetTime);
offsetTime = OffsetTime.ofInstant(Instant.now(), ZoneId.systemDefault());
System.out.println(offsetTime);
offsetTime = OffsetTime.of(LocalTime.now(), ZoneOffset.UTC);
System.out.println(offsetTime);
}
}
//某个时刻下的输出结果如下
18:08:26.263710800+08:00
18:08:26.264713600+08:00
18:08:26.264713600Z

OffsetDateTime

java.time.OffsetDateTime表示ISO-8601日历系统中带有基于UTC/Greenwich时间偏移量的日期时间,例如2007-12-03T10:15:30+01:00。OffsetDateTime也是一个不可变的日期时间对象,通常表示格式为年-月-日 时:分:秒-时间偏移量,当然它也可以包含一个纳秒属性(nano取值范围[0,999999999])。相比LocalDateTime,它多存储了一个时区时间偏移量(zone offset)属性。OffsetDateTime提供的公有静态实例常量如下:

// OffsetDateTime能表示的最小的日期时间-999999999-01-01T00:00:00+18:00
public static final OffsetDateTime MIN = LocalDateTime.MIN.atOffset(ZoneOffset.MAX)

// OffsetDateTime能表示的最大的日期时间999999999-12-31T23:59:59.999999999-18:00
public static final OffsetDateTime MAX = LocalDateTime.MAX.atOffset(ZoneOffset.MIN)

OffsetDateTime的常用静态工厂方法如下:

// 基于当前的日期时间、时区ID、时钟创建OffsetDateTime实例
public static OffsetDateTime now()
public static OffsetDateTime now(ZoneId zone)
public static OffsetDateTime now(Clock clock)

// 基于LocalDate实例、LocalTime实例和时间偏移量创建OffsetDateTime实例
public static OffsetDateTime of(LocalDate date, LocalTime time, ZoneOffset offset)

// 基于LocalDateTime实例和时间偏移量创建OffsetDateTime实例
public static OffsetDateTime of(LocalDateTime dateTime, ZoneOffset offset)

// 基于年月日时分秒纳秒和时间偏移量创建OffsetDateTime实例
public static OffsetDateTime of(
int year, int month, int dayOfMonth,
int hour, int minute, int second, int nanoOfSecond, ZoneOffset offset)

// 基于Instant实例和时区ID实例创建OffsetDateTime实例
public static OffsetDateTime ofInstant(Instant instant, ZoneId zone)

举个简单的使用例子:

public class OffsetDateTimeMain {

public static void main(String[] args) throws Exception {
OffsetDateTime offsetDateTime = OffsetDateTime.now();
System.out.println(offsetDateTime);
offsetDateTime = OffsetDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
System.out.println(offsetDateTime);
offsetDateTime = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.ofHours(8));
System.out.println(offsetDateTime);
}
}
// 某个时刻的输出如下
2019-01-01T20:38:03.388846400+08:00
2019-01-01T20:38:03.388846400+08:00
2019-01-01T20:38:03.388846400+08:00

ZonedDateTime

java.time.ZonedDateTime应该是JSR-310中最复杂但是最全面的日期时间类(它的API文档中注释也是最多的,从这点也可以看出它的复杂性)。ZonedDateTime可以简单理解为LocalDateTime,时区ID和一个可处理的ZoneOffset三者的共同实现,或者更简单理解为日期时间、时间偏移量、区域时区等时区规则的多重实现。ZonedDateTime也是一个不可变的日期时间对象,常用的格式为:年-月-日 时:分:秒-时区偏移量-区域,例如2007-12-03T10:15:30+01:00 Europe/Paris。除了包含所有的日期时间属性之外,ZonedDateTime还包含一个纳秒属性(nano取值范围[0,999999999])。ZonedDateTime的常用静态工厂方法如下:

// 根据当前的日期时间、时区ID和时钟创建ZonedDateTime实例
public static ZonedDateTime now()
public static ZonedDateTime now(ZoneId zone)
public static ZonedDateTime now(Clock clock)

// 基于LocalDate实例、LocalTime实例和时区ID创建ZonedDateTime实例
public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone)

// 基于LocalDateTime实例和时区ID创建ZonedDateTime实例
public static ZonedDateTime of(LocalDateTime localDateTime, ZoneId zone)

// 基于年月日时分秒纳秒和时区ID创建ZonedDateTime实例
public static ZonedDateTime of(
int year, int month, int dayOfMonth,
int hour, int minute, int second, int nanoOfSecond, ZoneId zone)

// 基于LocalDateTime实例、时区ID和候选偏好的时间偏移量创建ZonedDateTime实例
public static ZonedDateTime ofLocal(LocalDateTime localDateTime, ZoneId zone, ZoneOffset preferredOffset)

举个简单的使用例子:

public class ZonedDateTimeMain {

public static void main(String[] args) throws Exception {
ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println(zonedDateTime);
zonedDateTime = ZonedDateTime.of(LocalDateTime.now(), ZoneId.systemDefault());
System.out.println(zonedDateTime);
zonedDateTime = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
System.out.println(zonedDateTime);
}
}
// 某个时刻的执行结果
2019-01-01T21:00:03.193242200+08:00[Asia/Shanghai]
2019-01-01T21:00:03.193242200+08:00[Asia/Shanghai]
2019-01-01T21:00:03.193242200+08:00[Asia/Shanghai]

其他

Year

java.time.Year基于ISO-8601日期系统下表示年份,支持的范围是[-999_999_999,999_999_999]。举个简单的例子:

public class YearMain {

public static void main(String[] args) throws Exception{
Year year = Year.now();
System.out.println(year);
System.out.println(year.isLeap());
year = Year.of(2016);
System.out.println(year);
System.out.println(year.isLeap());
}
}
// 输出结果
2019
false
2016
true

Month

java.time.Month是一个枚举,代表ISO-8601日期系统中的月份。枚举的成员一共有12个,就是JANUARY到DECEMBER一共12个月份的英文大写表示。举个简单的使用例子:

public class MonthMain {

public static void main(String[] args) throws Exception {
Month month = Month.of(12);
System.out.println(month);
month = Month.JANUARY;
System.out.println(month);
}
}
// 输出结果
DECEMBER
JANUARY

DayOfWeek

java.time.DayOfWeek是一个枚举,表示一个星期中具体是星期几。枚举成员一共有7个,就是从MONDAY到SUNDAY一共7个指代具体星期几的英文大写表示。举个简单的使用例子:

public class DayOfWeekMain {

public static void main(String[] args) throws Exception{
DayOfWeek dayOfWeek = DayOfWeek.of(1);
System.out.println(dayOfWeek);
dayOfWeek = DayOfWeek.SUNDAY;
System.out.println(dayOfWeek);
}
}
// 输出结果
MONDAY
SUNDAY

MonthDay

java.time.MonthDay代表月份和对应月份一共存在的天数,内部维护着整型的属性month和整型的属性day。举个例子:

public class MonthDayMain {

public static void main(String[] args) throws Exception {
MonthDay monthDay = MonthDay.now();
System.out.println(monthDay);
monthDay = MonthDay.of(2, 29);
System.out.println(monthDay);
}
}
//某个时刻的输出结果
--01-01
--02-29

MonthDay通过静态工厂方法构建实例的时候会判断月份或者天数是否超过实际的限制,如果超限会抛异常。

YearMonth

java.time.YearMonth代表年份和月份,内部维护着整型的属性month和整型的属性month。举个例子:

public class YearMonthMain {

public static void main(String[] args) throws Exception{
YearMonth yearMonth = YearMonth.now();
System.out.println(yearMonth);
yearMonth = YearMonth.of(2019, 12);
System.out.println(yearMonth);
}
}
// 某个时刻的输出
2019-01
2019-12

类型转换

这里主要总结一下JSR-310的日期时间类之间的转换以及JSR-310的日期时间类和已经存在的旧Java日期时间类之间的转换关系。

Instant和其他日期时间类互转

如果有注意到上面介绍日期时间类的时候会发现每个类的工厂方法都包含ofInstant()方法,也就是Instant实例可以转化为其他日期时间类实例,这里总结一下:

public class InstantConvertTo {

public static void main(String[] args) throws Exception {
Instant instant = Instant.now();
ZoneId zoneId = ZoneId.systemDefault();
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zoneId);
LocalDate localDate = LocalDate.ofInstant(instant, zoneId);
LocalTime localTime = LocalTime.ofInstant(instant, zoneId);
OffsetTime offsetTime = OffsetTime.ofInstant(instant, zoneId);
OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(instant,zoneId);
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, zoneId);
}
}

其实很好理解,即使在旧的Java日期时间API中,长整型的时间戳毫秒也可以通过各种日期时间类的构造或者静态工厂方法创建对应的实例。值得注意的是,只有同时包含日期和时间的类才能转换为Instant实例,这一点也很好理解,只包含时间或者只包含日期的类转换成瞬时时间会丢失部分时间值。这里举个简单例子:

public class InstantConvertFrom {

public static void main(String[] args) throws Exception {
// 这里只以LocalDateTime为例,其他类似
LocalDateTime localDateTime = LocalDateTime.now();
Instant instant = localDateTime.toInstant(ZoneOffset.UTC);
// 或者
instant = Instant.ofEpochMilli(localDateTime.toEpochSecond(ZoneOffset.UTC) * 1000);
}
}

JSR-310日期时间类之间互相转换

日期时间类本身就包含日期和时间的维度,一般它们直接保存时间类实例作为成员属性,所以转换也十分方便:

public class DateTimeToTime {

public static void main(String[] args) throws Exception{
LocalDateTime localDateTime = LocalDateTime.now();
LocalDate localDate = localDateTime.toLocalDate();
LocalTime localTime = localDateTime.toLocalTime();
}
}

日期类不包含时间部分,所以日期类转换为日期时间类的时候,时间部分会取最小,例如:

public class DateToDateTime {

public static void main(String[] args) throws Exception{
LocalDate localDate = LocalDate.now();
System.out.println(localDate);
LocalDateTime localDateTime = localDate.atStartOfDay();
System.out.println(localDateTime);
ZonedDateTime zonedDateTime = localDate.atStartOfDay(ZoneId.systemDefault());
System.out.println(zonedDateTime);
}
}
// 某个时刻的输出如下
2019-01-01
2019-01-01T00:00
2019-01-01T00:00+08:00[Asia/Shanghai]

带有时区ID(时间偏移量或者地区)的类型可以轻易转变为不带有时区ID的类型,如果要反过来,则需要添加对应的时区ID属性,例如:

public class ZoneIdDateTimeMain {

public static void main(String[] args) throws Exception {
ZonedDateTime zonedDateTime = ZonedDateTime.of(LocalDateTime.now(), ZoneId.systemDefault());
System.out.println(zonedDateTime);
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
LocalDate localDate = zonedDateTime.toLocalDate();
LocalTime localTime = zonedDateTime.toLocalTime();
zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.systemDefault());

OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.UTC);
localDateTime = offsetDateTime.toLocalDateTime();
localDate = offsetDateTime.toLocalDate();
localTime = offsetDateTime.toLocalTime();
offsetDateTime = OffsetDateTime.of(localDateTime, ZoneOffset.UTC);
}
}

JSR-310中的类和旧的日期时间相关类之间的转换

java.sql.Timestampjava.time.LocalDateTime之间的转换:

public class TimestampLocalDateTime {

public static void main(String[] args) throws Exception {
LocalDateTime localDateTime = LocalDateTime.now();
Timestamp timestamp = Timestamp.valueOf(localDateTime);
LocalDateTime ldt = timestamp.toLocalDateTime();
}
}

java.sql.Datejava.time.LocalDate之间的转换:

public class DateLocalDate {

public static void main(String[] args) throws Exception {
Date date = new Date(2018, 1, 1);
LocalDate localDate = date.toLocalDate();
date = new Date(localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth());
}
}

只要是能使用毫秒表示的旧的日期时间类,都可以和java.time.Instant相互转换,例如:

public class ToInstant {

public static void main(String[] args) throws Exception{
Timestamp timestamp = new Timestamp(System.currentTimeMillis());
Instant instant = timestamp.toInstant();
java.util.Date date = new Date(System.currentTimeMillis());
instant = date.toInstant();
timestamp = new Timestamp(instant.toEpochMilli());
date = new Date(instant.toEpochMilli());
}
}

日期时间API之间的关系

LocalDateTime尽管可以使用ZoneId构造实例,但是它只能表示本地日期时间,LocalDateTime转换到Instant或者OffsetDatetime都需要添加ZoneOffset用于指定时区的偏移量。原则上,InstantOffsetDatetimeZonedDateTime都可以表示时间线上任意的一个时间点,OffsetDatetime的计算规则只包含了时区的偏移量ZoneOffset,而ZonedDateTime的计算规则包括了时区的偏移量ZoneOffset和基于区域表示的偏移量ZoneRegion,因此ZonedDateTime可以表示涵盖夏令时Daylight Saving Time(DST)等日期时间表示方式。换言之,OffsetDatetime已经可以满足大多数场景下的日期时间表示。

小结

JSR-310的新时间日期类库的设计相比已经存在的旧的日期时间类库来说,个人认为有以下的优点:

  • 线程安全。
  • 类的职责更加分明,时间、日期、日期时间需要使用明确的类去表示。
  • API封装更加合理,使得易用性提高。

不过会存在一些问题,最明显的是已有的旧类库存在兼容性问题,例如JDBC模块里面处理日期时间需要进行新的日期时间类和java.sql.Timestamp进行转换的问题,不过转换成本并不高。

(本文完 c-3-d e-a-20181230 r-a-20200302)