Skip to content

Java 8 日期和时间、PostgreSQL 日期和时间

1.1 Java 8中的日期和时间

1.1.1 背景

在Java 8之前,Java中处理日期和时间的方式有很多,包括使用java.util.Datejava.util.Calendarjava.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:01

1.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

PeriodDuration都是表示时间段,但两者表示的时间段尺度不同:

  • Period:基于年月日表示时间段;
  • Duration:基于秒和纳秒表示时间段;
java
Period period = Period.of(1, 1, 1);  // 1年1个月1天
Duration duration = Duration.ofSeconds(1, 1);  // 1秒1纳秒

PeriodDuration可以用来衡量两个日期之间的时间差:

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);  // 367

1.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:27

DateTimeFormatter也可以用于从字符串中解析时间:

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 中,timetimestampinterval 数据类型可以接受一个可选的精度值 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 zonetime [(p)] with time zonetime 单独使用时等同于 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 类型输入值的例子:

  1. 仅日期和时间

    sql
    '1999-01-08 04:05:06'

    这是一个没有时区信息的 timestamp 值,表示1999年1月8日的4点5分6秒。

  2. 日期、时间和时区

    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 会默默忽略任何时区指示。这意味着,结果值是根据输入值中的日期/时间字段派生的,而不会根据时区进行调整。

例如:

  1. 不带时区的时间戳

    sql
    SELECT '2004-10-19 10:23:54'::TIMESTAMP;

    在这个例子中,PostgreSQL 将字符串 '2004-10-19 10:23:54' 视为不带时区的时间戳,结果值将是 2004-10-19 10:23:54,不考虑任何时区信息。

  2. 带时区的时间戳

    sql
    SELECT '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/Shanghai
sql
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:54

1.2.2.4 特殊值

PostgreSQL支持在日期/时间中使用特殊值表示时间,注意:这些特殊值应该使用单引号扩起来。除了 infinity-infinity 之外,其他的特殊值都是符号简写,当它们被读取时会被转换为普通的日期/时间值。特别是 now 和相关的字符串,一旦被读取,就会被转换为一个特定的时间值。这意味着每次查询时,now 都会返回查询执行时的确切时间。

特殊值可转换的日期/时间类型说明
epochdate, timestamp1970-01-01 00:00:00+00
nowdate, time, timestamp当前时间
todaydate, timestamp今天零点(时间为00:00:00)
yesterdaydate, timestamp昨天零点(时间为00:00:00)
tomorrowdate, timestamp明天零点(时间为00:00:00)
allballstime00:00:00.00 UTC
infinitydate, timestamp, interval晚于所有时间
-infinitydate, 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()表示当前时间

![image-20241212184021111](./assets/java 8日期和时间、PostgreSQL日期和时间/image-20241212184021111.png)

1.2.3 Date/Time的输出

PostgreSQL 允许你设置日期/时间类型的输出格式为以下四种风格之一:ISO 8601、SQL(Ingres)、传统的 POSTGRES(Unix 日期格式)或德国风格。默认是 ISO 格式。

![image-20241212185007131](./assets/java 8日期和时间、PostgreSQL日期和时间/image-20241212185007131.png)

我们可以设置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中的日期时间对照

![image-20241209203633144](./assets/java 8日期和时间、PostgreSQL日期和时间/image-20241209203633144.png)

注意

不支持ZonedDateTime , InstantOffsetTime / TIME WITH TIME ZONE

这解释为什么MyBatisZonedDateTimeTypeHandler,但是解析时仍然报错。

注意

OffsetDateTime的时区是UTC。因为数据库就是以UTC时区存储的时间。

例如,使用MyBatis查询数据库,通过日志可以看到:

![image-20241212190439048](./assets/java 8日期和时间、PostgreSQL日期和时间/image-20241212190439048.png)

参考资料

[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