时间像是一个箭头,可以很容易的设置开始点,然后向前数或者向后数。但是为什处理时间这么难呢,因为你得让人看的懂啊。比方说,我告诉你“我们将于1523793600开一次会”,你是觉得自己理解能力差还是会觉得我不正常?
历史
在Java 1.0中,设计者提供了Date接口处理时间,但是事实证明这是一个“naïve”的设计,1.1之后大部分的方法都被废弃了。
Java 1.1引入了Calendar方法,结果也不咋地,他的实例是mutable的,而且无法处理闰秒(Leap seconds)这样的问题。
于是在Java 8中,对处理时间的接口进行了第三次设计,一次“charm”的设计。引入了java.time API。本文主要介绍java.time的基本概念和使用方式。
时间的定义
在英语中(学编程咋能不学英语 ;-<),时间有两个概念Date和Time,Date指的是日期如2019年11月4日,而Time指的是一天内的“时间”,如上午10:46分。
开始,人类主要是通过地球的自转来确定时间,将地自转一圈的时间等分为86,400份,定为一秒。然而,地球会抖动,是的,抖动。想象一下周末你用洗衣机甩干衣服,由于滚筒里的衣服放的不均匀,在滚筒滚动的时候,就会发生抖动。不要说你家的高级西门子洗衣机几乎没有抖动,他那么贵是有原因的。所以说根据地球自转计算出来的秒并不准确。好在,人类发现了一种化学元素,铯原子-133,根据这种原子的一种内在不变的特性,可以精确的定义秒的长度。一些介绍相对论等前沿物理学(如《时间简史》)的科普书中经常提到“原子钟”,全名就是铯原子种。
这样,时间准了。但是地球还是会抖动啊,铯原子钟计算出来的86,400秒,也就是“1天”,仍然需要和地球的自转同步,因此也就有了闰秒。通常情况下,计算机通过“smoothing”的方式将“一天”和地球的自转同步,计算机上的“一天”在闰秒发生之前被智能的稍稍减慢或者加快,使得“一天”恒定是86,400秒。
以上内容可看可不看!
Java中“时间点”的定义
在java中,一个Instant
代表时间线上的一点。时间线的起点被称为“epoch”(新纪元),被设置为子午线上(位于伦敦的格林尼治皇家天文台)的1970年1月1日的午夜,这和UNIX/POSIX时间中的约定是一致的。从这个时间开始,每天的时间定为86,400秒,精确到纳秒(nanosecond 十亿分之一秒)。Instant
可以回溯到10亿年前(Instant.MIN
),(地球的年龄是45年,不够用啊。。。)最大的时间,是10年后的12月31日(Instant.MAX
)。
static方法Instant.now()
返回当前的时间点。可以用equals
和compareTo
对Instant
进行比较,所以你可以将Instant
当作timestamp使用。
也可以用Duration.between
得到两个Instant
的间隔。
Instant start = Instant.now();
runAlgorithm();
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
long millis = timeElapsed.toMillis();
Duration
是两个Instant
之间的间隔量,可以将其转换为人类熟悉的形式:
toNanos, toMillis, toSeconds, toMinutes, toHours, or toDays
如果需要精确到纳秒,就需要考虑溢出问题。一个long型变量可以保存将尽300年的纳米秒。如果相差过大,可以用Duration来做:
boolean overTenTimesFaster = timeElapsed.toNanos() * 10 < timeElapsed2.toNanos();
Instant
和Duration
是immutable。
Local Date
上一节主要讲了绝对时间,现在我们来看人类读的懂的时间。在java API中,有两中人类读的懂的时间,local date/time
和zoned date/time
。Local date/time
仅包含date和time信息,而没有zone信息,因此也就无法同一个精确的Instant
时间关联起来,例如June 14,1903 (lambda的发明者Alonzo Church的生日)。与之相反,zoned date/time,July 16,1969, 09:32:00 EDT(阿波罗11号的发射日),可以精确的代表时间线上的一个Instant
时间。
在一些存在夏令时(提出者是本杰明·富兰克林,你没看错,我也没写错,就是那个被印在100美元钞票上的神人)的国家和地区(例如现在的美帝和多年前的中国,哈,暴漏了我的年龄),没有必要使用zoned date/time。因为如果你将和女朋友约会的时间定为早上10:00,那么你不能因为夏令时来到,就晚到一个小时(你可以试试)。由此,API的设计者建议,在不需要使用zonde 时间的情况下,就不要乱用。如生日,节日,开会时间,最好还是用local date/time。
好,就让我们看看如何构造和使用lcoal date/time。Java API提供了LocalDate
类,这是一个描述date(日期)的对象,顾名思义,就是由年、月、日构成,可以用static方法now()
或者of()
创建一个实例:
LocalDate today = LocalDate.now(); // Today’s date
LocalDate alonzosBirthday = LocalDate.of(1903, 6, 14);
alonzosBirthday = LocalDate.of(1903, Month.JUNE, 14);
// Uses the Month enumeration
在UNIX和java.util.Date中,月份是从0开始的,年是从1900年开始数的,为啥?你去问C程序猿!与之相比,LocalDate
正常了一些,月份从1开始算,而且还提供了Month enumeration。
比方说,如果你想知道程序员日(一年的第256天,多么自恋的程序员,除了他们,谁想知道这个问题。。。),
LocalDate programmersDay = LocalDate.of(2014, 1, 1).plusDays(255);
// September 13, but in a leap year it would be September 12
主要到Instant
之间的间隔是一个Duration
,而LocalDate
之间的间隔是一个Period
,代表逝去的年、月、日:
birthday.plus(Period.ofYears(1))
birthday.plusYears(1)
作用相同。
birthday.plus(Period.ofDays(365))
在非闰年的的时候也行
要想知道两个local date的间隔:
independenceDay.until(christmas)
java8和java9还提供了更多的工具类,想了解更多的话可以参照官方文档。
Local Time
Java API提供了LocalTime
类,这是一个描述time(时间,入15:30:00)的对象,可以用static方法now()
或者of()
创建一个实例:
LocalTime rightNow = LocalTime.now();
LocalTime bedtime = LocalTime.of(22, 30); // or LocalTime.of(22, 30, 0)
还有一个LocalDateTime
类,用来代表date和time,主要用来在一个固定的time zone下存储时间点。
Zoned 时间
上帝说“世界还不够复杂”,于是就有了时区。这是一个百分百的人工产物。在一个理性的世界中,我们都得服从格林尼治时间,一些人在“凌晨”两点吃午饭,还有一些人在“夜里”22:00吃,你为啥知道到饭点了?因为你饿了。想想吧,我天朝上国幅员辽阔,跨越4个时区。。。而其他地方的人也不好过,他们的时间考虑了时区,但是每当跨时区的时候,都要重调时间,这很麻烦,更烦的是,还要考虑夏/冬令时!
然而,这就是人生,总得微笑着活下去。
在Java中,每个时区都有一个ID
ZonedDateTime apollo11launch = ZonedDateTime.of(1969, 7, 16, 9, 32, 0, 0,
ZoneId.of("America/New_York"));
// 1969-07-16T09:32-04:00[America/New_York]
ZonedDateTime和Instant也可以相互转换:
instant.atZone(ZoneId.of("UTC"))
apollo11launch.toInstant
格式化
DateTimeFormatter类提供了3种formatter打印date/time
- 预定义的标准formatter,主要方便机器读取。
String formatted = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(apollo11launch);
// 1969-07-16T09:32:00-04:00"
主要是从一些常量中选择需要的格式。
- Locale-specific formatter
方便人类阅读,主要有四种形式:human short,medium,long,full。
通过调用static方法创建:ofLocalizedDate, ofLocalizedTime, and ofLocalizedDateTime。
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
String formatted = formatter.format(apollo11launch);
// July 16, 1969 9:32:00 AM EDT
上述方法使用系统默认的locale信息,还可以指定特殊的locale信息:
formatted = formatter.withLocale(Locale.FRENCH).format(apollo11launch);
// 16 juillet 1969 09:32:00 EDT
- 自定义的pattern
formatter = DateTimeFormatter.ofPattern("E yyyy-MM-dd HH:mm");
同遗留代码交互
java.time API 还提供了一些方法,一遍java 8之后的代码同之前的代码进行交互。可以查看相关文档。
【1】本文大部分内容来自于《Core Java,Volume II —— Advanced Features, 11th Edition》by Cay S. Horstmann,Prentice Hall 2019年出版,第六章,‘The Date and Time API’。