Java 常用类型系列(四)日期与时间类型总结
现有问题
java.util.Date
存在的问题:
- 正如类名所表达的,这个类无法表示“日期”(替代方案是
LocalDate
),只能以毫秒的精度表示“时间”(替代方案是LocalDateTime
)。 - 更糟糕的是它的易用性,比如:年份的起始选择是 1900 年,月份的起始从 0 开始。如果要表达 2014 年 3 月 18 日,需要创建以下
Date
实例:Date date = new Date(144,2,18)
- 非线程安全,且所有的日期类都是可变的,这表示在运行期其值可以任意修改,这是 Java 日期类最大的问题之一。
java.util.Date
及 java.sql.Timestamp
API 如下:
Java 8 日期与时间
Java 8 引入了新的 java.time
类库,用于加强对日期与时间的操作,本文主要总结其 API 结构、具体实现和使用方式。
先看下涉及的包结构简介:
Package | Description |
---|---|
java.time | The main API for dates, times, instants, and durations. |
java.time.temporal | Access to date and time using fields and units, and date time adjusters. |
java.time.chrono | Generic API for calendar systems other than the default ISO. |
java.time.zone | Support for time-zones and their rules. |
java.time.format | Provides classes to print and parse dates and times. |
java.time
包的常用类:
1 | Year // 2007 |
java.time.temporal
包的核心接口:
Interface | Description |
---|---|
TemporalAccessor |
框架级别的根接口,定义了对 temporal 对象的只读访问。 |
Temporal |
框架级别的接口,定义了对 temporal 对象的读写访问。实现类有 LocalDate 、OffsetDateTime 、Instant 等等。继承自 TemporalAccessor 。 |
TemporalAmount |
框架级别的接口,定义了一个时间段,例如”6 小时“、”8 天“、”3 个月“。实现类有 Duration 、Period 。可传入 temporal 对象的 plus 或 minus 方法进行时间调整。 |
TemporalUnit |
日期和时间的单元,ChronoUnit 枚举实现了该接口。可传入 temporal 对象的 plus 或 minus 方法进行时间调整。 |
TemporalField |
日期和时间的字段。ChronoField 枚举实现了该接口,可传入 temporal 对象的 get 或 with 方法获取或修改枚举对应的值。 |
TemporalAdjuster |
函数式接口,定义了对 temporal 对象的调整策略。可以使用 TemporalAdjusters 工具类的静态工厂方法生成对象实例,并传入temporal 对象的 with 方法进行时间调整。 |
java.time.temporal
包的核心方法:
历法系统介绍
首先了解几个概念:
Calendar system 历法
https://en.wikipedia.org/wiki/Calendar
历法,或称日历,是用年、月、日等时间单位计算时间的方法。Java 8 提供的历法实现如下:
历法 | Java 8 实现 |
---|---|
格里历(公历),即 Gregorian calendar | java.time.LocalDate |
和历 | java.time.chrono.JapaneseDate |
中华民国历 | java.time.chrono.MinguoDate |
泰国历 | java.time.chrono.ThaiBuddhistDate |
伊斯兰历(回历) | java.time.chrono.HijrahDate |
这些类都实现了 java.time.chrono.ChronoLocalDate
接口,能够对日期进行建模。
1 | // 格里历(公历) |
还有其它一些常见的历法,例如:
- 中国传统历法
- …
Calendar era 纪年
https://en.wikipedia.org/wiki/Calendar_era
https://docs.oracle.com/javase/8/docs/api/java/time/chrono/Era.html
纪年,或称纪元,是指历法中的年份命名体系,例如格里历(公历)所使用的基督纪年(公元),中国农历使用的干支纪年等。世界各地曾存在过各种不同的纪年方法,其中一些至今仍在使用,例如日本现在仍在使用年号纪年。
这里提供了一个常见的纪年对照表,可供参考。
年号是中国历史君主时代帝王纪年所立的名号,缘起于西汉汉武帝时期,后来朝鲜新罗在6世纪、日本在7世纪后期、越南在10世纪都因为中国的影响,开始使用年号;台湾岛的郑氏王朝与台湾民主国、朝鲜半岛大韩帝国与高丽、蒙古国建国初年受到中国影响,都还使用过年号,目前唯一使用年号的是仍保持君主制的日本(日本年号)。
值得一提,中华民国所用的民国纪年、以及朝鲜民主主义人民共和国使用的主体纪年,常被误认为是年号,实际上仅是单纯的纪年历法。
一世一元制,指君主(国王、大君主、可汗、天皇、皇帝)在其在位期间只使用同一个年号,不进行改元的制度。例外:如果君主后来另行称帝或是重祚,会另建新年号,以示区别。
LocalDate、LocalTime
LocalDate
、LocalTime
是人类易读的日期和时间格式,表示一个本地时间点,基于 ISO 8601 历法系统,无时区信息。
ISO 8601 日期和时间表示法
The ISO 8601 calendar system is the modern civil calendar system used today in most of the world. It is equivalent to the proleptic Gregorian calendar system :
In general, ISO 8601 applies to these representations and formats:
- dates, in the Gregorian calendar (including the proleptic Gregorian calendar);
- times, based on the 24-hour timekeeping system, with optional UTC offset; time intervals; and combinations thereof.[2]
The standard does not assign specific meaning to any element of the dates/times represented: the meaning of any element depends on the context of its use.
Dates and times represented cannot use words that do not have a specified numerical meaning within the standard (thus excluding names of years in the Chinese calendar), or that do not use computer characters (excludes images or sounds).[2]
ISO 8601 的日期和时间表示法为:
In representations that adhere to the ISO 8601 interchange standard :
- dates and times are arranged such that the greatest temporal term (typically a year) is placed at the left and each successively lesser term is placed to the right of the previous term.
- Representations must be written in a combination of Arabic numerals and the specific computer characters (such as “-“, “:”, “T”, “W”, “Z”) that are assigned specific meanings within the standard; that is, such commonplace descriptors of dates (or parts of dates) as “January”, “Thursday”, or “New Year’s Day” are not allowed in interchange representations within the standard.
而这几个类都基于 ISO 8601实现的:
https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html
A date without a time-zone in the ISO-8601 calendar system, such as
2007-12-03
.
https://docs.oracle.com/javase/8/docs/api/java/time/LocalTime.html
A time without a time-zone in the ISO-8601 calendar system, such as
10:15:30
.
https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html
A date-time without a time-zone in the ISO-8601 calendar system, such as
2007-12-03T10:15:30
.
底层实现
LocalDate
的底层实现包含不可变的年、月、日:
1 | public final class LocalDate |
LocalTime
的底层实现包含不可变的时、分、秒、纳秒:
1 | public final class LocalTime |
LocalDateTime
的底层实现包含不可变的 LocalDate
和 LocalTime
:
1 | public final class LocalDateTime |
使用方式
方法名 | 是否静态方法 | 描述 |
---|---|---|
of |
是 | 由 Temporal 对象的某个部分创建该对象。 |
now |
是 | 依据系统时钟创建 Temporal 对象。 |
from |
是 | 依据传入的 Temporal 对象创建对象。 |
parse |
是 | 由字符串创建 Temporal 对象。 |
format |
否 | 使用某个指定的 DateTimeFormatter 将 Temporal 对象转换为字符串。 |
atOffset |
否 | 将 Temporal 对象和某个时区偏移相结合。 |
atZone |
否 | 将 Temporal 对象和某个时区相结合。 |
get |
否 | 读取 Temporal 对象的某一部分值。 |
minus |
否 | 创建 Temporal 对象的副本,通过将当前 Temporal 对象的值减去一定的时长创建该副本。 |
plus |
否 | 创建 Temporal 对象的副本,通过将当前 Temporal 对象的值加上一定的时长创建该副本。 |
with |
否 | 以该 Temporal 对象为模板,对某些状态进行修改创建该对象的副本。 |
例子:
创建实例
1 | // 通过静态工厂方法创建实例 |
读取字段信息
读取信息使用 TemporalAccessor
接口,通过传递一个 ChronoField
枚举给 get
方法读取相关信息:
ChronoField
枚举提供的值如下图:
:
1 | // 使用 TemporalAccessor#get(TemporalField),传入 TemporalField 接口的实现类 ChronoField |
创建副本并修改
创建副本使用 Temporal
接口,其提供了一组 with
/plus
/minus
方法:
1 | // 使用 Temporal#with(TemporalField, long),传入 TemporalField 接口的实现类 ChronoField |
如果 with(TemporalField, long)
方法不满足需求,可以使用更灵活的 with(TemporalAdjuster)
方法,配合 TemporalAdjusters
工具类提供的静态工厂方法,方法名非常直观:
1 | // 使用 Temporal#with(TemporalAdjuster) |
如果在 TemporalAdjusters
工具类中没有找到符合要求的预定义静态工厂方法,可以自己实现 TemporalAdjuster
函数式接口,以定制一些更复杂的时间修改操作:
Instant
作为人,我们习惯于以星期几、几号、几点、几分这样的方式理解日期和时间。毫无疑问,这种方式对于计算机而言并不容易理解。从计算机的角度来看,建模时间最自然的格式是表示一个持续时间段上某个点的单一大整型数。这也是新的 java.time.Instant
类对时间建模的方式,它是以 Unix 元年时间(UTC 时区 1970-01-01T00:00:00Z,“Z” 代表 UTC 时区)开始所经历的秒数进行计算。
小数秒精度
参考《国际单位制词头表(Metric prefix)》,小数秒精度(fractional seconds precision)可以分为:
名称词头 | 符号词头 | 中文词头 | 英文 | 科学计数法 | 二进制存储所需位数 | 类型 | 名称示例 (以秒为例) | 符号示例 (以秒为例) |
---|---|---|---|---|---|---|---|---|
milli | m | 毫 | Thousandth | 1×10⁻³ | 2¹⁰ | 小数单位 | millisecond | ms |
micro | μ | 微 | Millionth | 1×10⁻⁶ | 2²⁰ | 小数单位 | microsecond | μs |
nano | n | 纳〔诺〕 | Billionth | 1×10⁻⁹ | 2³⁰ | 小数单位 | nanosecond | ns |
pico | p | 皮〔可〕 | Trillionth | 1×10⁻¹² | 2⁴⁰ | 小数单位 | picosecond | ps |
femto | f | 飞〔母托〕 | Quadrillionth | 1×10⁻¹⁵ | 2⁵⁰ | 小数单位 | femtosecond | fs |
atto | a | 啊〔托〕 | Quintillionth | 1×10⁻¹⁸ | 2⁶⁰ | 小数单位 | attosecond | as |
zepto | z | 仄〔普托〕 | Sextillionth | 1×10⁻²¹ | 2⁷⁰ | 小数单位 | zeptosecond | zs |
yocto | y | 幺〔科托〕 | Septillionth | 1×10⁻²⁴ | 2⁸⁰ | 小数单位 | yoctosecond | ys |
底层实现
从底层实现可见,java.time.Instant
仅包含不可变的秒、纳秒。由此可见,支持的最高存储精度为纳秒(10^-9 秒):
1 | public final class Instant |
可以通过 getter 方法获取这两个属性,例如:
1 | Instant instant = Instant.now(); // 1562497662814 (2019-07-07T11:07:42.814Z) |
使用方式
创建实例
java.time.Instant
提供了一系列静态工厂方法,用于创建实例,例如:
1 | Instant instant = Instant.now(); |
unix_timestamp → java.time.Instant
:
1 | // // 自 1970-01-01T00:00:00Z 之后经过的秒数 |
java.util.Date
→ java.time.Instant
:
1 | Instant instant = new Date().toInstant(); |
java.time.LocalDateTime
→ java.time.ZonedDateTime
→ java.time.Instant
:
1 | ZonedDateTime dt = LocalDateTime.now().atZone(zoneId); |
类型转换
java.time.Instant
→ unix_timestamp:
1 | long seconds = Instant.now().getEpochSecond(); // 秒 |
java.time.Instant
→ java.util.Date
:
1 | Date date = Date.from(Instant.now()); |
读取字段信息
1 | Instant instant = Instant.now(); // 1562497662814 (2019-07-07T11:07:42.814Z) |
但 java.time.Instant
无法处理那些人类非常容易理解的时间单位,例如下述操作将抛出异常:
1 | // java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth |
创建副本并修改
1 | Instant newInstant = instant.with(ChronoField.INSTANT_SECONDS, 30); |
Duration、Period
Duration
和 Period
类用于保存两个 Temporal
对象之间的时间段。
底层实现
Duration
的底层实现仅包含不可变的秒、纳秒:
1 | public final class Duration |
Period
的底层实现包含不可变的年、月、日:
1 | public final class Period |
使用方式
1 | // Duration 类的静态工厂方法 between 仅支持两个 LocalTime 或两个 LocalDateTime、或两个 Instant 对象之间的 duration |
ChronoUnit
枚举提供的值:
日期格式化
DateTimeFormatter
用于日期格式化。和老的 java.util.DateFormat
相比,所有的 DateTimeFormatter
实例都是线程安全的。所以,你能够以单例模式创建格式器实例,并在多个线程间共享。
下面介绍创建格式器的三种方式:
预定义的格式器
创建格式器最简单的方法是通过它预定义的格式器常量,定义如下:
Formatter | Description | Example |
---|---|---|
ofLocalizedDate(dateStyle) |
Formatter with date style from the locale | ‘2011-12-03’ |
ofLocalizedTime(timeStyle) |
Formatter with time style from the locale | ‘10:15:30’ |
ofLocalizedDateTime(dateTimeStyle) |
Formatter with a style for date and time from the locale | ‘3 Jun 2008 11:05:30’ |
ofLocalizedDateTime(dateStyle,timeStyle) |
Formatter with date and time styles from the locale | ‘3 Jun 2008 11:05’ |
BASIC_ISO_DATE |
Basic ISO date | ‘20111203’ |
ISO_LOCAL_DATE |
ISO Local Date | ‘2011-12-03’ |
ISO_OFFSET_DATE |
ISO Date with offset | ‘2011-12-03+01:00’ |
ISO_DATE |
ISO Date with or without offset | ‘2011-12-03+01:00’; ‘2011-12-03’ |
ISO_LOCAL_TIME |
Time without offset | ‘10:15:30’ |
ISO_OFFSET_TIME |
Time with offset | ‘10:15:30+01:00’ |
ISO_TIME |
Time with or without offset | ‘10:15:30+01:00’; ‘10:15:30’ |
ISO_LOCAL_DATE_TIME |
ISO Local Date and Time | ‘2011-12-03T10:15:30’ |
ISO_OFFSET_DATE_TIME |
Date Time with Offset | 2011-12-03T10:15:30+01:00’ |
ISO_ZONED_DATE_TIME |
Zoned Date Time | ‘2011-12-03T10:15:30+01:00[Europe/Paris]’ |
ISO_DATE_TIME |
Date and time with ZoneId | ‘2011-12-03T10:15:30+01:00[Europe/Paris]’ |
ISO_ORDINAL_DATE |
Year and day of year | ‘2012-337’ |
ISO_WEEK_DATE |
Year and Week | 2012-W48-6’ |
ISO_INSTANT |
Date and Time of an Instant | ‘2011-12-03T10:15:30Z’ |
RFC_1123_DATE_TIME |
RFC 1123 / RFC 822 | ‘Tue, 3 Jun 2008 11:05:30 GMT’ |
例子:
1 | // 2021-02-01 |
自定义 Pattern
Patterns for Formatting and Parsing:
Patterns are based on a simple sequence of letters and symbols. A pattern is used to create a Formatter using the
ofPattern(String)
andofPattern(String, Locale)
methods.A formatter created from a pattern can be used as many times as necessary, it is immutable and is thread-safe.
使用静态工厂方法 DateTimeFormatter#ofPattern(String)
创建日期格式器:
1 | // 01/02/2021 |
使用静态工厂方法 DateTimeFormatter#ofPattern(String, Locale)
创建日期格式器:
1 | // 2021 2 1 |
注意:Pattern 的字母数量决定格式,以月份为例:
- Exactly 1 pattern letter will use the minimum number of digits and without padding.
- Exactly 2 pattern letters, the count of digits is used as the width of the output field, with the value zero-padded as necessary.
- Exactly 3 pattern letters will use the
short form
. - Exactly 4 pattern letters will use the
full form
. - Exactly 5 pattern letters will use the
narrow form
.
更灵活的构建器
如果还需要更加细粒度的控制,DateTimeFormatterBuilder
类还提供了更复杂的格式器构建,你可以选择恰当的方法,一步一步地构造自己的格式器:
1 | DateTimeFormatter dtf = new DateTimeFormatterBuilder().appendPattern("dd/MM/yyyy[ [HH][:mm][:ss][.SSS]]") |
另外,DateTimeFormatterBuilder
类还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精确地匹配指定的模式)、填充,以及在格式器中指定可选节。
参考
标准:
- ISO 8601 - DATE AND TIME FORMAT
- RFC 3339 - Date and Time on the Internet: Timestamps
- What’s the difference between ISO 8601 and RFC 3339 Date Formats?
API: