JSR 310 Java Date与Time API

新旧 API 的更迭

旧的 Java API 主要包括java.util.Datejava.util.Calendar 两个包的内容。这两个包的时间类型是可变的。如 Date 的实例可以通过 setYear 来产生变化。

JSR 310 中包括的日期类型主要有:

  1. 计算机时间:Instant,对应 java.util.Date,它代表了一个确定的时间点,即相对于标准Java纪元(1970年1月1日)的偏移量;但与java.util.Date类不同的是其精确到了纳秒级别。
  2. 人类时间:对应于人类自身的观念,比如LocalDate和LocalTime。他们代表了一般的时区概念,要么是日期(不包含时间),要么是时间(不包含日期),类似于java.sql的表示方式。此外,还有一个MonthDay,它可以存储某人的生日(不包含年份)。每个类都在内部存储正确的数据而不是像java.util.Date那样利用午夜12点来区分日期,利用1970-01-01来表示时间。这些类型的实例是 immutable 的,而且只能通过工厂方法创建。

时区(比如Europe/Paris与America/New_York)和距UTC的偏移量(比如+01:00与-08:00)之间的差别

偏移量仅仅是 UTC (基础偏移起点)和本地时间之间的差值。

而时区是一个具名的规则集合,描述了偏移量改如何随着时间的变化而变化。比如说,时区会描述一个特定的区域(比如纽约)在给定的一个时刻具有某个偏移量,之后具有另一个 偏移量(在本地时间线上创建一个间隙或是重叠,如春秋时制的变换)。换言之,时区是比偏移量更复杂的规则集合。

有三个级别的类支持这些概念:

  1. LocalDateTime 无需使用偏移量和时区就能表示时间。
  2. OffsetDateTime 额外地指定了偏移量。
  3. ZonedDateTime 则增加了时区规则。

过去,很多应用都喜欢使用时区,但他们真正需要的其实只是偏移量而已(使用偏移量更简单、更快且不易出错)。所以我们更加应该使用偏移量。XML Schema规范就是一个典型,它只支持偏移量而不支持时区。JSR 310可以明确表示出这些差别。

时区规则会随着时间的推移而不断发生变化。就在千禧年之前,一些国家将时区由国际日界线之后改为之前;此外,夏时制也在不断变化。

其他附属类型

可以使用Duration界定任意两个Instant之间的范围。

现在有一些具体的概念来表示YearMonth和MonthDay,在适当的时候应该使用这两个类。还有一个Period类来表示任意的时间周期,如“两年、3个月、7天、4小时、50分钟”等。

核心日历是ISOChronology,默认情况下使用它来映射时间,就像目前Java API中的GregorianCalendar一样。然而,我们对其他一些年代也提供了支持,如CopticChronology和ThaiBuddhistChronology。

API 举例

操作日期的时候,都会在方法名上让人看出合理的行为模式,模仿自 Joda API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.journaldev.java8.time;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
import java.time.temporal.TemporalAdjusters;

public class DateAPIUtilities {

public static void main(String[] args) {

LocalDate today = LocalDate.now();

//Get the Year, check if it's leap year
System.out.println("Year "+today.getYear()+" is Leap Year? "+today.isLeapYear());

//Compare two LocalDate for before and after
System.out.println("Today is before 01/01/2015? "+today.isBefore(LocalDate.of(2015,1,1)));

//Create LocalDateTime from LocalDate
System.out.println("Current Time="+today.atTime(LocalTime.now()));

//plus and minus operations
System.out.println("10 days after today will be "+today.plusDays(10));
System.out.println("3 weeks after today will be "+today.plusWeeks(3));
System.out.println("20 months after today will be "+today.plusMonths(20));

System.out.println("10 days before today will be "+today.minusDays(10));
System.out.println("3 weeks before today will be "+today.minusWeeks(3));
System.out.println("20 months before today will be "+today.minusMonths(20));

//Temporal adjusters for adjusting the dates
System.out.println("First date of this month= "+today.with(TemporalAdjusters.firstDayOfMonth()));
LocalDate lastDayOfYear = today.with(TemporalAdjusters.lastDayOfYear());
System.out.println("Last date of this year= "+lastDayOfYear);

Period period = today.until(lastDayOfYear);
System.out.println("Period Format= "+period);
System.out.println("Months remaining in the year= "+period.getMonths());
}
}

新式的 DateTimeFormatter,使用字符串来初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.journaldev.java8.time;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class DateParseFormatExample {

public static void main(String[] args) {

//Format examples
LocalDate date = LocalDate.now();
//default format
System.out.println("Default format of LocalDate="+date);
//specific format
System.out.println(date.format(DateTimeFormatter.ofPattern("d::MMM::uuuu")));
System.out.println(date.format(DateTimeFormatter.BASIC_ISO_DATE));

LocalDateTime dateTime = LocalDateTime.now();
//default format
System.out.println("Default format of LocalDateTime="+dateTime);
//specific format
System.out.println(dateTime.format(DateTimeFormatter.ofPattern("d::MMM::uuuu HH::mm::ss")));
System.out.println(dateTime.format(DateTimeFormatter.BASIC_ISO_DATE));

Instant timestamp = Instant.now();
//default format
System.out.println("Default format of Instant="+timestamp);

//Parse examples
LocalDateTime dt = LocalDateTime.parse("27::Apr::2014 21::39::48",
DateTimeFormatter.ofPattern("d::MMM::uuuu HH::mm::ss"));
System.out.println("Default format after parsing = "+dt);
}

}

与旧 API 的兼容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.journaldev.java8.time;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

public class DateAPILegacySupport {

public static void main(String[] args) {

//Date to Instant
Instant timestamp = new Date().toInstant();
//Now we can convert Instant to LocalDateTime or other similar classes
LocalDateTime date = LocalDateTime.ofInstant(timestamp,
ZoneId.of(ZoneId.SHORT_IDS.get("PST")));
System.out.println("Date = "+date);

//Calendar to Instant
Instant time = Calendar.getInstance().toInstant();
System.out.println(time);
//TimeZone to ZoneId
ZoneId defaultZone = TimeZone.getDefault().toZoneId();
System.out.println(defaultZone);

//ZonedDateTime from specific Calendar
ZonedDateTime gregorianCalendarDateTime = new GregorianCalendar().toZonedDateTime();
System.out.println(gregorianCalendarDateTime);

//Date API to Legacy classes
Date dt = Date.from(Instant.now());
System.out.println(dt);

TimeZone tz = TimeZone.getTimeZone(defaultZone);
System.out.println(tz);

GregorianCalendar gc = GregorianCalendar.from(gregorianCalendarDateTime);
System.out.println(gc);

}

}

DateTimeFormatter

DateTimeFormatter 是线程安全的,而 SimpleDateFormat 不是。

它的基础用法是:

1
2
3
LocalDate date = LocalDate.now();
String text = date.format(formatter);
LocalDate parsedDate = LocalDate.parse(text, formatter);

常见的模式有:

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’

其中 date 和 time 之间的 T 是 ISO 8601 标准。

其他缩写字母的含义见此文档:https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html

以上内容主要参考:

  1. 《JSR 310 Java Date与Time API》
  2. 《Java8 日期/时间(Date Time)API指南》

MySQL 中的时间类型(Date and Time Types)

MySQL 共有5种类型来表示时间值(temporal values),包括 DATE,TIME,DATETIME,TIMESTAMP 和 YEAR。所有的时间类型都有一个 valid range,当你插入 invalid value 的时候,有个 zero value 会被使用

MySQL 的时间 API 有几个通用的设计考量:

  1. 从 MySQL 中获取数据,总是会获取到标准化数据(严出 );但对于同一种特定类型,它可以接受并试图解释多种格式的输入。
  2. 对于日期,MySQL 倾向于使用年月日的顺序(’98-09-04’),而不是其他顺序。
  3. 只有两位数字(two-digit)的年份可能存在二义性,MySQL 的转义规则是:
    1. Year values in the range 70-99 are converted to 1970-1999.
    2. Year values in the range 00-69 are converted to 2000-2069.
  4. 如果日期被用在一个数字上下文,MySQL 会自动帮转化为数字。反之亦然。
  5. 改变 SQL MODE,可以让 MySQL 接受一些看起来 invalid 的日期。有些日期是用户故意输错的,MySQL 只检查月份是不是在1-12,而日期是不是在1-31里。
  6. MySQL 允许在月和日里存储零值(zero value)。这可以拿来存储不知道确切日期的生日(类似 null 之于 integer)。要 disable 掉这种行为,启动 NO_ZERO_IN_DATE MODE。’0000-00-00’ 就是这样一个 dummy date。使用这种日期的话,相关的日期函数计算就不能得到正确的结果。
  7. 零值在 ODBC 等连接器里面,通常会被转化为 NULL,因为这些连接器不知道怎么处理这些零值。
  8. 零值表
数据类型 零值
DATE ‘0000-00-00’
TIME ‘00:00:00’
DATETIME ‘0000-00-00 00:00:00’
TIMESTAMP ‘0000-00-00 00:00:00’
YEAR 0000
  1. 时间值通常有两种字面量表达方法,用引号标准起来的字符串,或者纯数字:’2015-07-21’, ‘20150721’ 和 20150721 都是合理的日期。松散的字面量定义见此链接,注意下面的评论,有些断句符本身必须和语言 collation 一起使用。。

取值范围

DATE 类型没有时间部分,通用格式为 ‘YYYY-MM-DD’,合理的取值范围为’1000-01-01’ 至 ‘9999-12-31’。

DATETIME 类型既有日期部分,又有时间部分。合理的取值范围为’1000-01-01’ 至 ‘9999-12-31’。这类似 Java 8 的 LocalDatetime,是属于人的形式,不需要理解时区和偏移。

TIMESTAMP 类型既有日期部分,又有时间部分。合理的取值范围为 ‘1970-01-01 00:00:01’ UTC 至 ‘2038-01-19 03:14:07’ UTC。也就是说, TIMESTAMP 这个类型类似 Unix Epoch timestamp,时间范围又窄,基础偏移量又基于 UTC。

这两种类型的字面量里都是没有 T 的。

DATETIME 和 TIMESTAMP 可以包含最多到微秒六位精度的小数部分。

如果加入了小数部分:

DATETIME 的取值范围变为 ‘1000-01-01 00:00:00.000000’ 到 ‘9999-12-31 23:59:59.999999’。

TIMESTAMP 的取值范围变为 ‘1970-01-01 00:00:01.000000’ 到 ‘2038-01-19 03:14:07.999999’。

DATETIME 和 TIMESTAMP 的自动初始化和更新行为

可以从初始值和其他 column 被修改时自动修改值两个维度来设置类型:

1
2
3
4
CREATE TABLE t1 (
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
dt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

但总体来讲不是很好用。还是把缺省初始值设置为某个时间戳比较简单。

具体文档在《Automatic Initialization and Updating for TIMESTAMP and DATETIME》

DATETIME 和 TIMESTAMP 的其他区别

  1. TIMESTAMP 消耗的存储空间更小,因为它表达的时间范围更小-所以这导致未来的时间问题。美团很多人不在乎这一点,愿意用,我自己不太愿意用,使用 DATETIME 大方一点也没错。
  2. TIMESTAMP 会隐藏一些时区的自动转换,输出结果会受服务器时区配置(locale)影响。而与时区无关的 DATETIME 反而可以让时区的转换显式被业务控制。
  3. DATETIME 在低版本里不能使用函数生成缺省值(因为这个 post)。要谨慎地使用字面量作为时间类型列的缺省值,如果选错了,会导致插入出错(用 null 也很危险):
1
2
3
4
5
6
7
8
9
CREATE TABLE `tbl_test_datetime_column`
(
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增物理主键,单表唯一',
`test_datetime` DATETIME NOT NULL DEFAULT '1970-01-01' COMMENT '创建时间',
`foo` text COMMENT 'foo',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='测试时间表';

INSERT INTO `test_db`.`tbl_test_datetime_column` (`foo`) VALUES ('2');

时区的影响

TIMESTAMP 类型在被存储时,会被转往 UTC 时区(此处 UTC 被当做时区来用),在取值的时候,又会被取成当前时区的时间-所以 TIMESTAMP 可以适应服务器、连接的时区变更,DATETIME不行。

而 MySQL 的时区设置,是针对每一个连接可以单独设置的。这也就意味着,如果时区不是一个常量,一个 TIMESTAMP 被存进去一个值,取出来是另一个值。这个可能存在的问题,是大家都喜欢用 datetime 的原因。时区相关的具体行为,可以通过 time_zone 这个环境变量来修改。

矫正到零值

对于不合理的赋值,MySQL 可能把它赋值为各种类型的零值,还可能产生若干警告。

精度定义

语法是 type_name(fsp)。type_name 可以是 TIME, DATETIME, 或者 TIMESTAMP,而 fsp 是小数秒精度。

一个例子:

1
CREATE TABLE t1 (t TIME(3), dt DATETIME(6));

如果定义里面不存在秒数,存值的时候可能会被抹去,比值的时候可能失败

日期和时间类型的转换

这两种类型相互转换有点类似移位。能补零则补零,否则会变成全零值。

具体内容见《Conversion Between Date and Time Types》