这一节我们说说Java语言中的时间和日期。
时间作为一个时空中的维度,每个人时刻都会面对,程序开发中也总是会涉及到这个要素。
Java语言中对于时间日期是如何处理的呢?让我们先谈一些历史,历法,天文和部分地理方面的知识。
一 时间日期小知识
1. 闰年和历法
先看一段小程序,计算一下1583年10月4日后第三天
class DisDate {
public static void main (String[] args) {
Calendar c = Calendar.getInstance();
c.set(1582, 9, 4);
SimpleDateFormat s=new SimpleDateFormat ("yyyy'年'MM'月'dd'日'");
System.out.println(s.format(c.getTime()));
c.add(Calendar.DATE, 3); 计算3天后的时间
System.out.println("3天后的时间是:" + s.format(c.getTime());
}
}
结果是
$ java DisDate
1582年10月04日
3天后的时间是:1582年10月17日
什么,4日后三天是17日?难道穿越了?没错,这就是历史上消失的十天。
我们知道当前我们用的公历是以地球绕太阳旋转一周记为一年,地球自传为一天,从天文学来说,一年为365.2422天左右,所有我们有了闰年,即每4年有2月29日。
在1582年之前,这个历法叫做Julian(朱利安),也翻译成儒略历法,是由公元前著名的凯撒大帝制定的。
四年一闰的方式,每年还会差出0.0078天。这样经过上千年,这个误差就累计放大到了10天。所以在1582年,当时的罗马教皇格里高利13世重新制定了Gregorian calendar(格里高利历法),把当年从10月5日当10月14日的十年人为抹去。
闰年的方式在四年一闰的基础上,加入100年不闰,而400年又一闰的规定。这样同天文学上地球公转周期基本一致了,剩余微小的差异会每隔几年用闰秒的方式补足,这个历法一直沿用至今。
2. 时区和夏令时
我们都知道地球上按照经度划分为不同的时区,中国是在东8区,用世界标准时间表示为UTC+8,世界上不同国家选用适合的时间,有的国家还会有多个时区。
在很多国家,在夏天的一段时期内,会把手表拨快一个小时,可以更多的利用日照而节约能源,这就是夏令时。
我国在20世纪80年代曾经使用过夏令时,后来因为时间的改变给生产生活带来更多的困扰,而一直没有再使用。
Rule PRC 1986 only - May 4 0:00 1:00 D
Rule PRC 1986 1991 - Sep Sun>=11 0:00 0 S
Rule PRC 1987 1991 - Apr Sun>=10 0:00 1:00 D
这是在Joda-time(后文还会介绍)中的我国夏令时定义的规则文本,可以看出从1986年到1991年我国使用过夏令时。
在JDK中,时区和夏令时的时间信息,都存放在tzdb.dat文件中并定期维护,除了每年各国的夏令时时间,还有因为重大活动,比如体育比赛,运动等人为调整的时间。
3. 各国历法和年号
公历是世界各国应用最广泛的历法,然而各国也有特定的历法同时使用,比如我国就有农历,我们的很多传统节日,春节,元宵,端午,重阳等都是农历节日。
农历是阴阳历(而不是阴历),就是同时要考虑月亮绕地球公转(阴)和地球绕太阳公转(阳)。比起公历,考虑月亮,即阴历的成分要多一些。月亮公转一圈是29天半左右,所以农历月份有小月29天,也有大月30天(不会出现31天,28天这样的人为规定)。
至于大月小月,则是用天文学的朔望月时间确定的,所以不会出现凭空消失10天的情况,是古代最精准的历法。
中国的农历历法制定从汉武帝时确定,后一直不断完善,直到民国才改为公历为主要历法。农历在无中气的月份设置闰月,一般每隔2,3年一闰。
不过农历有一个障碍,就是人们极难计算推断出哪个月是大月还是小月,以及闰月加在哪里,这些都是通过天文知识推导出来的,不像公历那样规则简单,小学生也能很快算出某年月日是星期几。
目前在Java中,除了格里高利历法,还有佛历,日本历法等实现。但没有农历历法,可能是的确很难实现吧。
我见过很多计算农历的程序只是内置一个表格,通过查表的方法来计算农历日期。
这里值得一提的是一个计算农历的程序 https://github.com/oyyq99999/ChineseLunarCalendar 是通过天文数据来推算的,然而计算过程很复杂。
年号是始于我国古代封建王朝表示纪年的一种方法,汉武帝首创年号“建元”,后世每位皇帝在位会有一个或者若干个年号,末代皇帝溥仪使用的“宣统”是最后一个官方承认的年号。
民国起废除封建皇权年号,使用民国纪年。中华人民共和国只使用公历。
目前日本国在使用年号,2019年平成退位后会使用新的年号。我国台湾也还沿用民国几年的纪年方法。
Java语言中对于年号也有实现,比如日本近代以来(明治,大正,昭和,明仁,下表为JDK11最新代码,已经给下一个年号留出位置)和民国。
ERA value Era name Since (in Gregorian)
0 N/A N/A
1 Meiji 1868-01-01T00:00:00 local time
2 Taisho 1912-07-30T00:00:00 local time
3 Showa 1926-12-25T00:00:00 local time
4 Heisei 1989-01-08T00:00:00 local time
5 NewEra 2019-05-01T00:00:00 local time
二 Java8之前时间日期的不足
Date类是Java1.0开始就存在的,首先面对的问题是,表示年度要加上1900,也就是说Date方法是从1900年开始计算的。Java1.1加入Calendar类,但同样有很多问题。
比如,
两者的月份都是从0开始计数的,也就是传入的月份1代表的是二月;
它们都不是安全的类,多线程环境下,经常多个线程同时修改同一个引用而产生bug;
还有我认为是最主要的,它们和表示时区的TimeZone,表现历法的各Calendar类,以及需要配合使用打印日期时间文本的DateFormat,设计上是混杂在一起的,编程时经常会非常困惑。
三 Java8引入的DateTime
Java8引入了新的时间日期类,都放置在java.time包中。
1. 基本日期和时间
LocalDate只表示日期,不可变类。
LocalTime只表示时间,不可变类。
LocalDateTime组合了日期和时间,也是不可变类,但不含有时区信息。
2. 时点和间隔
Instant表示的是一个时点,度量单位可以小至纳秒。
Duration表示的是时间间隔,如3.1秒,便于机器处理
Period也表示的是时间间隔,但用间隔多少年多少月多少日表现,便于人们理解。
3. 时区
ZoneId体现了时区,一般是一个标志地理位置,如我国用Asia/Shanghai
ZoneOffSet表示了时区偏移时间,如+8:00,即和UTC格林威治标准时间的偏移值。
ZonedDateTime表示了完整的日期,时间和时区信息
4. 日历系统
放在chrono包下,表示各个日历系统。每种日历系统都有以下接口或类实现:
Chronology表示日历系统
ChronoLocalDate日历系统的日期
Era日历系统中某一个时间段
目前JDK中除了通用的公历之外,还实现了伊斯兰,日本,民国和泰国佛历。
5. 实用工具
format包中有各种日期时间文本格式处理工具
temporal包中有各种对时间,日期进行加减操作,查询等工具。比如,可以组合出明年5月最后一个工作日之前星期五这样的查询。
综合以上,我们可以看出新的日期时间功能不但强大,设计上更是远胜原来的类。而且对于函数式编程来说,也是不二之选。
如今主流的技术,如JDBC,JPA等都已经全面支持新的日期时间类,所以建议大家在编程时,总是要用这些新的类去实现功能。
四 JodaTime
我们必须要提及Jodatime,因为Java8的日期时间源自Jodatime的设计。Java源码中头部版权声明中提及的Stephen Colebourne就是主要其开发者。Jodatime支持早期的Java版本,所以在很多Java三方包中都能看到依赖声明,在Java8之前是事实上的日期时间标准库。
Jodatime中类定义和Java8中的基本是一致的,只是稍有区别。
如Interval和Duration相似,只表示时点之间的区间;Partial只表示时间片段等。
另外日历系统有所区别,以西方为主。同时提供了方法和JDK8中的类进行转换。因为Jodatime在软件维护时经常会遇到,所以还是得比较熟悉。
五 总结
日期时间用到的地方很多,涉及到很多跨学科知识,多阅读其实现代码,也能学到很多有趣的知识点。
个人希望以后有时间和机会,或者国内的开发者有精力能够实现我国农历历法,并提交给OpenJDK,如果能通过合并入OpenJDK中,以后我们可以都用上标准的农历了。
另外对于明清两代的年号,也可以做一个扩展包,读明史追清宫剧也能方便的用程序编写出故事主线时间轴吧。
今天就到这里。留一个思考题,中国农历的节气,在目前日期时间系统中是否存在呢?如果还没有,如果设计比较好呢?




