Appearance
Java 8 日期和时间、PostgreSQL 日期和时间
1.1 Java 8中的日期和时间
1.1.1 背景
在Java 8之前,Java中处理日期和时间的方式有很多,包括使用java.util.Date、java.util.Calendar、java.text.SimpleDateFormat等类,但是这些类存在一些问题,比如非线程安全、API设计糟糕、不支持时区。
1.1.2 LocalDate、LocalTime、LocalDateTime
这三个类代表本地时间,不涉及时区。以LocalDateTime为例,演示如何获取实例:
java
LocalDateTime localDateTime1 = LocalDateTime.now(); // 获取当前时间
LocalDateTime localDateTime2 = LocalDateTime.of(2000, 1, 1, 0, 0, 0); // 通过指定年月日时分秒获取时间
LocalDateTime localDateTime3 = LocalDateTime.parse("2000-01-01T12:00:00"); // 通过解析字符串获取时间LocalTime表示时间,有三个常量:
java
System.out.println(LocalTime.MIN); // 00:00
System.out.println(LocalTime.NOON); // 12:00
System.out.println(LocalTime.MAX); // 23:59:59.999999999这可以用于进行时间范围查询。
1.1.3 Instant
Instant表示的是时间轴上的一个点,是一个瞬间,时间轴的起点是1970-01-01 00:00:00, 我们可以通过以下方法获取实例:
java
Instant instant1 = Instant.now(); // 获取当前时间点
Instant instant2 = Instant.ofEpochSecond(1); // 通过指定晚于起点多少秒来表示时间点
Instant instant3 = Instant.ofEpochSecond(-1); // 也可以指定负数,表示早于起点多少秒,来表示时间点
Instant instant4 = Instant.ofEpochMilli(1); // 通过指定毫秒来表示时间点
Instant instant5 = Instant.ofEpochSecond(1, 1); // 通过指定秒和纳秒来表示时间点1s = 1000ms,1s = 1,000,000,000ns
1秒 = 1000毫秒,1秒 = 十亿 纳秒
1.1.4 ZoneId、ZoneOffset
ZoneId表示时区,我们可以通过如下方式来找出所有的时区:
java
Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(System.out::println);txt
Asia/Aden
America/Cuiaba
Etc/GMT+9
Etc/GMT+8
Africa/Nairobi
America/Marigot
Asia/Aqtau
... 共603个,省略有了上面的基础,我们就可以使用如下方法来创建实例:
java
ZoneId zoneId = ZoneId.of("Asia/Shanghai"); // 北京时间ZoneOffset用于创建相对于UTC/格林威治时间的偏移量,比时区精度更高。我们可以使用以下的方法创建实例:
java
ZoneOffset zoneOffset1 = ZoneOffset.of("+2"); // +02:00
ZoneOffset zoneOffset2 = ZoneOffset.ofHours(1); // +01:00
ZoneOffset zoneOffset3 = ZoneOffset.ofHoursMinutes(-1, -10); // -01:10 注意:符号需相同
ZoneOffset zoneOffset4 = ZoneOffset.ofHoursMinutesSeconds(3, 0, 30); // +03:00:30
ZoneOffset zoneOffset5 = ZoneOffset.ofTotalSeconds(-1); // -00:00:011.1.5 ZonedDateTime、OffsetDateTime
ZonedDateTime表示某个时区的时间,与之对应的是ZoneId。我们可以通过如下方式获取某个时间点在某个时区的表示:
java
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(
Instant.ofEpochSecond(1),
ZoneId.of("Asia/Shanghai")
);如果我们直接输出该时间,结果为:
txt
1970-01-01T08:00:01+08:00[Asia/Shanghai]Instant.ofEpochSecond(1)在时间轴上指定的时间是1970-01-01T00:00:01,注意,这是UTC时间。但是,我们指定的时区是Asia/Shanghai,该时区相对于UTC时间早了8小时,所以显示的是1970-01-01T08:00:01。
OffsetDateTime表示相对于UTC时间的偏移时间,与之对应的是ZoneOffset,我们可以通过如下方式获取实例:
java
OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(
Instant.ofEpochSecond(1),
ZoneOffset.ofHoursMinutesSeconds(1,1,1)
);结果如下:
txt
1970-01-01T01:01:02+01:01:01解释和ZonedDateTime类似,只是OffsetDateTime想比于ZonedDateTime可以更精细地控制偏移量。
1.1.6 Period、Duration、ChronoUnit
Period和Duration都是表示时间段,但两者表示的时间段尺度不同:
Period:基于年月日表示时间段;Duration:基于秒和纳秒表示时间段;
java
Period period = Period.of(1, 1, 1); // 1年1个月1天
Duration duration = Duration.ofSeconds(1, 1); // 1秒1纳秒Period和Duration可以用来衡量两个日期之间的时间差:
java
Period period1 = Period.between(
LocalDate.of(2000, 1, 1),
LocalDate.of(2010, 12, 10)
);
System.out.println(period1); // P10Y11M9D
Duration duration1 = Duration.between(
LocalDateTime.of(2000, 1, 1, 1, 1, 1),
LocalDateTime.of(2001, 1, 2, 1, 12, 16)
);
System.out.println(duration2); // PT8808H11M15S我们也可以使用ChronoUnit来度量时间差:
java
long days = ChronoUnit.DAYS.between(
LocalDateTime.of(2000, 1, 1, 1, 1, 1),
LocalDateTime.of(2001, 1, 2, 1, 12, 16)
);
System.out.println(days); // 3671.1.7 DateTimeFormatter
DateTimeFormatter用于格式化输出日期时间,例如:
java
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.of(2000, 1, 1, 12, 13, 27);
System.out.println(localDateTime); // 直接输出:2000-01-01T12:13:27
System.out.println(localDateTime.format(dateTimeFormatter)); // 格式化输出:2000-01-01 12:13:27DateTimeFormatter也可以用于从字符串中解析时间:
java
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
LocalDate localDate = LocalDate.parse("2000年12月11日",dateTimeFormatter);1.2 PostgreSQL中的日期和时间
1.2.1 Date/Time类型
在PostgreSQL中,支持如下Date/Time类型:
| 类型 | 描述 |
|---|---|
| timestamp [ (p) ] [ without time zone ] | 不带时区的日期时间类型 |
| timestamp [ (p) ] with time zone | 带时区的日期时间类型 |
| date | 日期类型(没有时间) |
| time [ (p) ] [ without time zone ] | 不带时区的时间类型(没有日期) |
| time [ (p) ] with time zone | 带时区的时间类型(没有日期) |
| interval [ fields ] [ (p) ] | 时间间隔 |
注意
timestamp 是timestamp without time zone的简写
timestamptz 是 timestamp with time zone的简写
time 是 time without time zone的简写
在 PostgreSQL 中,time、timestamp 和 interval 数据类型可以接受一个可选的精度值 p,该值指定在秒字段中保留的小数位数。默认情况下,精度没有明确的限制。允许的 p 值范围是从 0 到 6。
由于interval用处较少,后面无涉及。
1.2.2 Date/Time的输入
PostgreSQL 在输入日期和时间时,几乎可以接受任何合理的格式,包括 ISO 8601、SQL兼容格式、传统的 POSTGRES 格式等。对于某些格式,日期输入中的日、月、年的顺序是模糊的,因此 PostgreSQL 支持指定这些字段的预期顺序。设置 DateStyle 参数为 MDY 可以选择月-日-年的解释,设置为 DMY 可以选择日-月-年的解释,或者设置为 YMD 可以选择年-月-日的解释。我们可以使用如下命令显示设定的日期顺序:
sql
show DateStyle;记住,任何日期或时间的字面量输入都需要用单引号括起来,就像文本字符串一样。例如:
sql
SELECT '1999-01-01 00:01:01'::TIMESTAMP;
SELECT '1/8/1999'::DATE;
SELECT '20:01:19'::TIME;
SELECT '20:01:19.123456'::TIME(3);1.2.2.1 Date 输入
Date类型支持的部分输入形式如下:
| 例子 | 说明 |
|---|---|
| 1999-01-08 | 年-月-日,推荐格式 |
| 19990108 | 年月日 |
| 1/8/1999 | 在MDY模式下,表示1月8号;在DMY模式下,表示8月1号; |
| 1/18/1999 | 在MDY模式下,表示1月18号;在其他模式下,错误; |
1.2.2.2 Time 输入
在 PostgreSQL 中,有两种与时区相关的时间类型:time [(p)] without time zone 和 time [(p)] with time zone。time 单独使用时等同于 time without time zone。
对于这些类型,有效的输入包括一天中的时间,后面可以跟一个可选的时区。如果为 time without time zone 类型指定了时区,它将被默默地忽略。你也可以指定一个日期,但它也会被忽略,除非你使用的时区名称涉及夏令时规则,比如 America/New_York。
时间类型支持的部分输入形式如下:
| 例子 | 说明 |
|---|---|
| 04:05:06.123456 | 完整的时间类型 |
| 04:05:06 | 时:分:秒 |
| 04:05 | 时:分 |
| 040506 | 时分秒 |
| 04:05:06.789-8 | 带偏移时间(时区)的时间类型 |
| 04:05:06 PST | 带时区缩写的时间类型 |
| 2003-04-12 04:05:06 America/New_York | 带完整时区名称的时间类型 |
注意,在最后一个例子中,日期是必要的:
sql
SELECT '2023-03-15 14:30:00 America/New_York'::time with time zone;
-- 下面的语句会报错
SELECT '14:30:00 America/New_York'::time with time zone;
-- ERROR: invalid input syntax for type time with time zone: "14:30:00 America/New_York"因为 America/New_York 时区有夏令时规则,需要日期来确定是使用标准时间还是夏令时。指定的日期将帮助 PostgreSQL 确定正确的时区偏移量。
时区的输入形式如下:
| 例子 | 说明 |
|---|---|
| PST | 时区缩写 |
| America/New_York | 时区完整名称 |
| -8:00:00 | 相对于UTC的时间偏移量 |
1.2.2.3 TimeStamp 输入
对于 PostgreSQL 中的 timestamp 类型,有效的输入格式包括日期和时间的连接,后面可以跟一个可选的时区,再后面可以跟一个可选的公元(AD)或公元前(BC)标识。以下是一些有效的 timestamp 类型输入值的例子:
仅日期和时间:
sql'1999-01-08 04:05:06'这是一个没有时区信息的
timestamp值,表示1999年1月8日的4点5分6秒。日期、时间和时区:
sql'1999-01-08 04:05:06 -8:00'这是一个带时区信息的
timestamp值,表示1999年1月8日在UTC -8时区的4点5分6秒。
在这两个例子中,日期和时间部分是必须提供的,而时区和公元/公元前标识是可选的。
如果不指定时区,默认情况下,不带时区的时间戳(timestamp without time zone)会使用服务器的时区设置,而带时区的时间戳(timestamp with time zone)会存储指定的时区偏移量。
PostgreSQL 在确定字面量字符串的类型时,从不检查字符串的内容,因此会将字面量都视为“不带时区的时间戳(timestamp without time zone)”。为了确保字面量被视为“带时区的时间戳(timestamp with time zone)”,需要给它指定正确的显式类型:
sql
'2004-10-19 10:23:54+02'::TIMESTAMP WITH TIME ZONE在被确定为“不带时区的时间戳”的字面量中,PostgreSQL 会默默忽略任何时区指示。这意味着,结果值是根据输入值中的日期/时间字段派生的,而不会根据时区进行调整。
例如:
不带时区的时间戳:
sqlSELECT '2004-10-19 10:23:54'::TIMESTAMP;在这个例子中,PostgreSQL 将字符串
'2004-10-19 10:23:54'视为不带时区的时间戳,结果值将是2004-10-19 10:23:54,不考虑任何时区信息。带时区的时间戳:
sqlSELECT '2004-10-19 10:23:54+02'::TIMESTAMP WITH TIME ZONE;在这个例子中,PostgreSQL 将字符串
'2004-10-19 10:23:54+02'视为带时区的时间戳,结果值将是2004-10-19 10:23:54,并且会根据时区偏移量+02进行调整。
小结:
- PostgreSQL 不会根据字面量字符串的内容来判断其类型。
- 如果需要将字面量视为带时区的时间戳,必须使用显式类型声明。
- 对于不带时区的时间戳,任何时区指示都会被忽略,结果值仅基于输入的日期和时间字段。
在PostgreSQL 中,带时区的时间戳总是转换为UTC时间存储。如果输入不含时间戳,那么 PostgreSQL 将会使用设置的TimeZone解析到UTC时间进行存储。我们可以使用如下命令查看TimeZone配置:
sql
show TimeZone;
-- Asia/Shanghaisql
SELECT '2004-10-19 10:23:54+02'::TIMESTAMP WITH TIME ZONE;
-- 2004-10-19 16:23:54+08在上面的例子中,字面量表示相对于UTC时间偏移+2小时的时间2004-10-19 10:23:54,那么转换为UTC时间就是2004-10-19 08:23:54,再将其转换为TimeZone所代表的时区时间,结果就是2004-10-19 16:23:54。
如果我们想更改结果的显示时区,可以使用AT TIME ZONE指令:
sql
SELECT '2004-10-19 10:23:54+02'::TIMESTAMP WITH TIME ZONE AT TIME ZONE 'UTC';
-- 2004-10-19 08:23:54
SELECT '2004-10-19 10:23:54+02'::TIMESTAMP WITH TIME ZONE AT TIME ZONE '-2';
-- 2004-10-19 10:23:54
SELECT '2004-10-19 10:23:54+02'::TIMESTAMP WITH TIME ZONE AT TIME ZONE '+2';
-- 2004-10-19 06:23:541.2.2.4 特殊值
PostgreSQL支持在日期/时间中使用特殊值表示时间,注意:这些特殊值应该使用单引号扩起来。除了 infinity 和 -infinity 之外,其他的特殊值都是符号简写,当它们被读取时会被转换为普通的日期/时间值。特别是 now 和相关的字符串,一旦被读取,就会被转换为一个特定的时间值。这意味着每次查询时,now 都会返回查询执行时的确切时间。
| 特殊值 | 可转换的日期/时间类型 | 说明 |
|---|---|---|
| epoch | date, timestamp | 1970-01-01 00:00:00+00 |
| now | date, time, timestamp | 当前时间 |
| today | date, timestamp | 今天零点(时间为00:00:00) |
| yesterday | date, timestamp | 昨天零点(时间为00:00:00) |
| tomorrow | date, timestamp | 明天零点(时间为00:00:00) |
| allballs | time | 00:00:00.00 UTC |
| infinity | date, timestamp, interval | 晚于所有时间 |
| -infinity | date, timestamp, interval | 早于所有时间 |
sql
SELECT
'now' :: TIMESTAMP AS now,
'epoch' :: TIMESTAMP AS epoch,
'today' :: TIMESTAMP AS today,
'tomorrow' :: TIMESTAMP AS tomorrow,
'yesterday' :: TIMESTAMP AS yesterday,
'2000-01-01 allballs' :: TIMESTAMP AS allballs,
'infinity' :: TIMESTAMP AS infinity,
'-infinity' :: TIMESTAMP AS "-infinity",
'infinity' :: TIMESTAMP > now() AS "infinity-greater-than-any-time",
'-infinity' :: TIMESTAMP < now() AS "-infinity-less-than-any-time";
-- 使用了函数now()表示当前时间
1.2.3 Date/Time的输出
PostgreSQL 允许你设置日期/时间类型的输出格式为以下四种风格之一:ISO 8601、SQL(Ingres)、传统的 POSTGRES(Unix 日期格式)或德国风格。默认是 ISO 格式。

我们可以设置DateStyle来更改输出格式,例如:
sql
SET DateStyle='Postgres,YMD';
SELECT '2004-10-19 10:23:54+02' :: TIMESTAMP WITH TIME ZONE ;
-- Tue Oct 19 16:23:54 2004 CST注意:SET DateStyle命令有效范围只是会话session范围。
1.3 Java SE 8和 PostgreSQL中的日期时间对照

注意
不支持ZonedDateTime , Instant 和 OffsetTime / TIME WITH TIME ZONE
这解释为什么MyBatis有ZonedDateTimeTypeHandler,但是解析时仍然报错。
注意
OffsetDateTime的时区是UTC。因为数据库就是以UTC时区存储的时间。
例如,使用MyBatis查询数据库,通过日志可以看到:

参考资料
[1] https://www.baeldung.com/java-8-date-time-intro
[2] https://docs.oracle.com/javase/tutorial/datetime/index.html
[3] https://jdbc.postgresql.org/documentation/query/#using-java-8-date-and-time-classes
[4] https://www.postgresql.org/docs/current/datatype-datetime.html