来源:Gitbook
作者:寒月
写在前面
日常的工作中,我们也许会遇到 “判断 Unix 时间是否为 0 点” 的问题。我们也许会构建以下代码:
boolean at0Clock(long unixTimeStamp) {
return unixTimeStamp % 86400 == 0;
}
我们做一个单元测试(使用 junit
单元测试框架):
@Test void testAt0Clock() { Calendar calendar = Calendar.getInstance(); // // 设置日期与时间 2017-11-11T00:00:00 // calendar.set(Calendar.YEAR, 2017); calendar.set(Calendar.MONTH, 10); // November calendar.set(Calendar.DATE, 11); calendar.set(Calendar.HOUR, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); Assert.assertEquals(true, Main.at0Clock(calendar.getTimeInMillis() / 1000)); }
感兴趣的读者可以试一试,单元测试是否通过。无论通过与否,也请尝试执行以下代码:
void printTimeZone() { TimeZone timeZone = Calendar.getInstance().getTimeZone(); System.out.println(timeZone.getID() + " " + timeZone.getDisplayName()); }
作者位于上海,单元测试 testAt0Clock()
不通过,执行代码 printTimeZone()
的输出:
Asia/Shanghai 中国标准时间
关于单元测试 testAt0Clock()
不通过的原因,我们有必要先了解若干基础的概念:“Unix 时间”、“时区”。
Unix 时间、时区
Unix 时间
Wikipedia 中关于 Unix 时间 定义为:
Unix time is a system for describing a point in time, defined as the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970, minus the number of leap seconds that have taken place since then.
根据定义:Unix 时间表示自 1970-01-01T00:00:00Z(Z 即表示 UTC 时间,请参阅 ISO 8601)所经过的秒数,并且移除期间的 “闰秒”(例如:1998-12-31T23:59:60Z)。Unix 时间体系中,每天固定 86400 秒。
Java 中的 Unix 时间
Java 确保:每天 24 小时、每小时 60 分、每分钟 60 秒。
Java 中获取 “当前” 时间的方法,其底层实现,全部由 java.lang.System.currentTimeMillis()
提供自 UTC 1970-01-01T00:00:00 的毫秒数。java.lang.System.currentTimeMillis()
作为 native 方法,其实现与 JVM 所在的机器相关(通常使用 NTP 协议保持更新)。
本文中,我们将 java.lang.System.currentTimeMillis()
的返回值,称为毫秒级精度的 “Unix 时间”。
时区
时区作为地理概念,表示 “遵守统一时间标准的一个地区”。Wikipedia 中关于 时区 定义为:
A time zone is a region of the globe that observes a uniform standard time for legal, commercial, and social purposes.
使用与 UTC 的偏移来表示时区,例如:中国所在时区为 UTC+08:00(又称为 Chinese Standard Time,即 “中国标准时间”;tz 时区数据库 中,表示为 Asia/Shanghai)
“偏移”,即为表示与 UTC 的时间差。例如:1970-01-01T00:00:00Z == 1970-01-01T00:08:00+08:00(+08:00 表示位于 UTC+08:00 时区)。
关于 boolean at0Clock(long unixTimeStamp)
完成 “Unix 时间” 和 “时区” 的阅读,相信读者对于 boolean at0Clock(long unixTimeStamp)
的单元测试不通过的原因,已经有了自己的答案。
Calendar
的实现:首先,根据年、月、日、时、分、秒计算获得自 1970-01-01 00:00:00 经过的秒数;然后,减去当前时区的 UTC 偏移,得到 Unix 时间。因此,对于运行于 “中国标准时间” 的系统,正确的 >boolean at0Clock(long unixTimeStamp)
实现:
boolean at0Clock(long unixTimeStamp) { return (unixTimeStamp + 8 * 3600) % 86400 == 0; }
Java 8 以前的方案
承载日期与时间的类型
java.util.Date
使用 java.util.Date
表示日期与时间,其承载了毫秒级精度的 Unix 时间,除此之外的功能(包括:承载 “年、月、日、时、分、秒” 字段,格式化,字符串解析),均标记为 @Deprecated
。
java.util.Date
实现了 Comparable
接口,并提供 boolean before(Date)
/ boolean after(Date)
方法。
特别说明:java.util.Date
的 String toString()
方法,使用系统的时区。
void printDate(java.util.Date date) { // // date 表示日期与时间,输出: // 1. Unix 时间(毫秒) // 2. EEE MMM dd HH:mm:ss zzz yyyy 格式输出 // System.out.println(String.format("%d - %s", date.getTime(), date.toString())); } void usingDate() { java.util.Date now = new java.util.Date(); // 当前时间 printDate(now); java.util.Date date = new java.util.Date(1511193600 * 1000L); // Unix 时间:1511193600 * 1000(毫秒) printDate(date); // 输出:1511193600000 - Tue Nov 21 00:00:00 CST 2017 date.setTime(0 * 1000L); // 设置时间,Unix 时间:0 * 1000(毫秒) printDate(date); // 输出:0 - Thu Jan 01 08:00:00 CST 1970 }
此外,Java 提供了 java.sql.Date
、java.sql.Time
、java.sql.Timestamp
,作为 java.util.Date
的子类,用于 JDBC 中,分别代表 SQL 的 DATE
、TIME
、TIMESTAMP
类型,其中 java.sql.Timestamp
的精度是纳秒。
java.util.Calendar
使用抽象类 java.util.Calendar
表示日期与时间,java.util.GregorianCalendar
是其常见的非抽象子类。
相比较于 java.util.Date
,java.util.Calendar
除了承载毫秒级的 Unix 时间,还承载了时区信息(默认使用系统的时区),并且,提供了诸多接口:“年、月、日、时、分、秒” 字段的获取与设置,时区设置,日期与时间的偏移调整。
java.util.Calendar
实现了 Comparable
接口,并提供:boolean before(Object)
/ boolean after(Object)
方法。
void printCalendar(Calendar calendar) { // // date 表示时间和日期,输出: // 1. Unix 时间(毫秒) // 2. 格式化输出 yyyy-MM-dd HH:mm:ss // System.out.println(String.format("%d -> %04d-%02d-%02d %02d:%02d:%02d", calendar.getTime().getTime(), calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DATE), calendar.get(Calendar.HOUR), calendar.get(Calendar.MINUTE), calendar.get(Calendar.SECOND))); } void usingCalendar() { Calendar calendar = Calendar.getInstance(); // 当前时间 printCalendar(calendar); calendar.setTime(new Date(1511193600 * 1000L + 12)); // Unix 时间:1511193600 * 1000 + 12(毫秒) printCalendar(calendar); // 输出:1511193600012 -> 2017-11-21 00:00:00 // // 设置日期与时间 2017-10-31T00:00:00 // calendar.set(2017, Calendar.OCTOBER, 31, 0, 0, 0); printCalendar(calendar); // 输出:1509379200012 -> 2017-10-31 00:00:00 // // 设置时区 // calendar.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo")); printDate(calendar.getTime()); // 输出:1509379200012 - Tue Oct 31 00:00:00 CST 2017 printCalendar(calendar); // 输出:1509379200012 -> 2017-10-31 01:00:00 calendar.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); printDate(calendar.getTime()); // 1509379200012 - Tue Oct 31 00:00:00 CST 2017 calendar.set(Calendar.SECOND, 0); printDate(calendar.getTime()); // 1509382800012 - Tue Oct 31 01:00:00 CST 2017 printCalendar(calendar); // 1509382800012 -> 2017-10-31 01:00:00 // // 设置 & 调整 // calendar.add(Calendar.MINUTE, 60); printCalendar(calendar); // 1509382800012 -> 2017-10-31 02:00:00 calendar.roll(Calendar.DATE, 1); printCalendar(calendar); // 1506794400012 -> 2017-10-01 02:00:00 }
格式化输出 & 字符串解析
通过抽象类 java.text.DateFormat
及其非抽象子类 java.text.SimpleDateFormat
,能够快速地进行日期与时间的格式化输出和字符串解析。关于日期与时间字段(年、月、日、时、分、秒、时区)格式化 & 解析的模式,请参阅 Java doc。
void formatAndParse() throws ParseException { DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss XXX"); // XXX 即为 “时区” // // 格式化 // Calendar calendar = Calendar.getInstance(); calendar.setTime(new Date(0)); System.out.println(dateFormat.format(calendar.getTime())); // 输出:1970-01-01T08:00:00 +08:00 // // 解析 // String raw = "2017-11-30T00:00:00 +00:00"; Date date = dateFormat.parse(raw); System.out.println(dateFormat.format(date)); // 输出:2017-11-30T08:00:00 +08:00 }
可能存在的问题
概念定义:
-
java.util.Date
仅承载 Unix 时间,与名称 “Date” 不相符 - 未区分 “Unix 时间” 与 “可理解的日期与时间”
- 缺少不携带时区信息的 “本地时间” 模型
- 未区分 “时区” & “UTC 偏移”
- 无 “时间段” 模型
编程模型:
- 承载日期与时间的类型,全部 “mutable”
-
java.util.Calendar
:月份表达范围 0-11 -
java.text.DateFormat
:非线程安全
更多的内容,请参考:Java 8 日期与时间方案(JSR 310)提案者的访谈。
Java 8 的方案 (JSR 310)
承载日期与时间的类型
承载日期与时间的类型主要包括:java.time.Instant
、java.time.LocalDate
/ java.time.LocalTime
/ java.time.LocalDateTime
、java.time.ZonedDateTime
,全部是 “immutable” 类型。
Instant
java.time.Instant
承载纳秒级精度的 Unix 时间,其 String toString()
方法基于 ISO-8601 进行格式化。Instant
不承载时区信息。
Instant now = Instant.now(); System.out.println(now); Instant instant = Instant.ofEpochSecond(1511193600L); System.out.println(instant); // 输出:2017-11-20T16:00:00Z instant = Instant.ofEpochMilli(15111936001206L); System.out.println(instant); // 输出:2448-11-16T16:00:01.206Z instant = Instant.ofEpochSecond(1511193600L, 276); System.out.println(instant); // 输出:2017-11-20T16:00:00.000000276Z
LocalDate
/ LocalTime
/ LocalDateTime
java.time.LocalDate
用于表示 “本地日期”,无 “时间”。LocalDate
不承载时区信息。
LocalDate now = LocalDate.now(); // 使用系统所在时区,由当前的 Unix 时间构建 System.out.println(now); LocalDate date = LocalDate.of(2017, 11, 25); System.out.println(date); // 输出:2017-11-25 date = LocalDate.of(2017, Month.DECEMBER, 20); System.out.println(date); // 输出:2017-11-20
java.time.LocalTime
用于表示 “本地时间”,无 “日期”。LocalTime
不承载时区信息。
LocalTime now = LocalTime.now(); // 使用系统所在时区,由当前的 Unix 时间构建 System.out.println(now); LocalTime time = LocalTime.of(11, 20); System.out.println(time); // 输出:11:20 time = LocalTime.of(13, 20, 12); System.out.println(time); // 输出:13:20:12 time = LocalTime.of(13, 20, 12, 1); System.out.println(time); // 输出:13:20:12.000000001
java.time.LocalDateTime
用于表示 “本地日期与时间”。LocalDateTime
不承载时区信息。
LocalDate
实例与 LocalTime
实例能够共同构建 LocalDateTime
实例,由 LocalDateTime
实例能够获取 LocalDate
实例与 LocalTime
实例。
LocalDateTime now = LocalDateTime.now(); // 使用系统所在时区,由当前的 Unix 时间构建 System.out.println(now); LocalDateTime dateTime = LocalDateTime.of(2017,11,20,13,47); System.out.println(dateTime); // 输出:2017-11-20T13:47 dateTime = LocalDateTime.of(2017,11,20,13,47, 20); System.out.println(dateTime); // 输出:2017-11-20T13:47:20 dateTime = LocalDateTime.of(2017,11,20,13,47, 20, 1); System.out.println(dateTime); // 输出:2017-11-20T13:47:20.000000001 // // LocalDate & LocalTime -> LocalDateTime // LocalDate date = LocalDate.of(2017, 11, 20); LocalTime time = LocalTime.of(16, 32, 25, 6); dateTime = date.atTime(time); System.out.println(dateTime); // 输出:2017-11-20T16:32:25.000000006 dateTime = date.atTime(17,11,21); System.out.println(dateTime); // 输出:2017-11-20T17:11:21 dateTime = time.atDate(date); System.out.println(dateTime); // 输出:2017-11-20T16:32:25.000000006 // // LocalDateTime -> LocalDate & LocalTime // dateTime = LocalDateTime.of(2017,11,20,13,47, 20, 1); date = dateTime.toLocalDate(); System.out.println(date); time = dateTime.toLocalTime(); System.out.println(time);
由于 LocalDateTime 不承载时区信息,因此,其不能与 Instant 相互转换,必须提供时区信息。
LocalDateTime localDateTime = LocalDateTime.of(2017,11,25,13,6,7); System.out.println(localDateTime); // 输出:2017-11-30T14:56:07 Instant instant = localDateTime.toInstant(ZoneOffset.of("+08:00")); System.out.println(instant); // 输出:2017-11-25T05:06:07Z LocalDateTime localDateTimeFromInstant = instant.atZone(ZoneId.of("Asia/Tokyo")).toLocalDateTime(); System.out.println(localDateTimeFromInstant); // 输出:2017-11-25T14:06:07 LocalDateTime localDateTime = LocalDateTime.of(2017,11,30,14,56,7); System.out.println(localDateTime); // 输出:2017-11-30T14:56:07
ZonedDateTime
java.time.ZonedDateTime
用于表示位于特定 “时区” 的 “日期与时间”,其 “时区” 信息使用 tz 时区数据库 表示(明确与 UTC 偏移区分)。
ZonedDateTime
的时区调整方法非常明确,区分:保持 “Unix 时间” 固定进行时区调整和保持 “本地日期与时间” 固定进行时区调整。 (相比较于 java.util.Calendar
,更加清晰)
LocalDateTime localDateTime = LocalDateTime.of(2017, 11, 11,14,57,21); System.out.println(localDateTime); // 输出:2017-11-11T14:57:21 ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("Asia/Shanghai")); System.out.println(zonedDateTime); // 输出:2017-11-11T14:57:21+08:00[Asia/Shanghai] System.out.println(zonedDateTime.toInstant().getEpochSecond()); zonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo")); System.out.println(zonedDateTime); // 输出:2017-11-11T15:57:21+09:00[Asia/Tokyo] System.out.println(zonedDateTime.toInstant().getEpochSecond()); zonedDateTime = zonedDateTime.withZoneSameLocal(ZoneId.of("Asia/Shanghai")); System.out.println(zonedDateTime); // 输出:2017-11-11T15:57:21+09:00[Asia/Tokyo] System.out.println(zonedDateTime.toInstant().getEpochSecond());
java.time.OffsetDateTime
、java.time.OffsetTime
使用相对于 UTC 的偏移承载 “时区” 信息,本文中不予展开阐述。
承载时间段的类型
承载时间段的类型主要包括:java.time.Period
、java.time.Duration
,全部是 “immutable” 类型。
Period
/ Duration
java.time.Period
承载基于日期的时间段,例如:1 年 1 月 1 日。
Period period = Period.of(1, 1, 1); LocalDate date = LocalDate.of(2017,11,12); System.out.println(date); // 输出:2017-11-12 date = date.plus(period); System.out.println(date); // 输出:2018-12-13 LocalDate beginDate = LocalDate.of(2017, 11,12); LocalDate endDate = LocalDate.of(2019, 12,11); period = Period.between(beginDate, endDate); System.out.println(period.getYears() + " years, " + period.getMonths() + " months, " + period.getDays() + " days"); // 2 years, 0 months, 29 days
java.time.Duration
承载基于秒的时间段(提供纳秒级精度)。
Duration duration = Duration.ofSeconds(86401); LocalDateTime dateTime = LocalDateTime.of(2017,11,12,0,0,0); System.out.println(dateTime); // 2017-11-12T00:00 dateTime = dateTime.minus(duration); System.out.println(dateTime); // 2017-11-10T23:59:59 LocalDateTime beginDateTime = LocalDateTime.of(2017, 11,12,0,0,0,12); LocalDateTime endDateTime = LocalDateTime.of(2017, 11,12,0,1,1); duration = Duration.between(beginDateTime, endDateTime); System.out.println(duration.getSeconds() + " seconds, " + duration.getNano() + " nanos"); // 60 seconds, 999999988 nanos
核心接口
Temporal
、TemporalAccessor
、TemporalAdjuster
、TemporalAmount
类图所示,java.time.temporal 提供的接口:
-
TemporalField
:日期与时间 “字段”,例如:2017-11-18 中的 18 “天” -
TemporalUnit
:时间 “单位”,例如:1 年 13 天的 13 “天” -
TemporalAccessor
:“时间相关” 对象的 “只读” 接口 -
Temporal
:“时间相关” 对象的 “读写” 接口,继承自TemporalAccessor
-
TemporalAdjuster
:Temporal
类型对象 “设置 & 调整” 的函数式接口 -
TemporalAmount
:时间段
java.time 提供的类:
-
Instant
、LocalDate
、LocalTime
、LocalDateTime
、ZonedDateTime
:实现Temporal
与TemporalAdjuster
接口 -
Duration
、Period
:实现TemporalAmount
接口
//
// 调整日期与时间:通过 <code>Temporal</code> 接口
//
Duration duration = Duration.of(88, ChronoUnit.SECONDS);
localDateTime = localDateTime.plus(duration);
System.out.println(localDateTime); // 输出:2017-11-30T14:57:35
//
// 调整日期与时间:通过 <code>Temporal</code> 接口
//
LocalDateTime nextDay = localDateTime.with(temporal -> temporal.plus(1, ChronoUnit.DAYS));
System.out.println(nextDay); // 输出:2017-12-01T14:57:35
//
// 调整日期与时间:通过 <code>TemporalAdjuster</code> 接口
//
localDateTime = (LocalDateTime) nextDay.adjustInto(localDateTime);
System.out.println(localDateTime); // 输出:2017-12-01T14:57:35
//
// 获取日期与时间字段值:通过 <code>TemporalAccessor</code> 接口
//
System.out.println("nextDay = " + nextDay.get(ChronoField.YEAR) + " 年 " + nextDay.get(ChronoField.MONTH_OF_YEAR) + " 月 " + nextDay.get(ChronoField.DAY_OF_MONTH) + " 日");
// 输出:nextDay = 2017 年 12 月 1 日
格式化输出 & 字符串解析
java.time.format.DateTimeFormatter
能够进行 TemporalAccessor
类型(包括:LocalDate
、LocalTime
、LocalDateTime
、ZonedDateTime
)的格式化输出。同时,LocalDate
、LocalTime
、LocalDateTime
、ZonedDateTime
提供了静态的 parse 方法,能够进行字符串解析。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy 年 MM 月 dd 日"); ZonedDateTime zonedDateTime = Instant.ofEpochSecond(1511463192L).atZone(ZoneId.of("Asia/Shanghai")); System.out.println(formatter.format(zonedDateTime)); // 输出:2017 年 11 月 24 日 LocalDate localDate = LocalDate.parse("2017 年 11 月 25 日", formatter); System.out.println(localDate); // 输出:2017-11-25
LocalDate
、LocalTime
、LocalDateTime
、ZonedDateTime
允许基于类型的默认格式进行格式化输出和字符串解析。
类型 | 默认格式示例 |
Instant |
2017-11-23T10:15:30.00Z |
LocalDate |
2017-11-23 |
LocalTime |
10:15:30 |
LocalDateTime |
2017-11-23T10:15:30 |
ZonedDateTime |
2017-11-23T10:15:30+01:00[Asia/Shanghai] |
写在结束
通过本文的内容,期望读者能够对于日期与时间相关的概念能有基础的理解,也期望读者能够掌握 Java 提供的涉及日期与时间的类及接口。