Java Date&Time Tutorial

易用的Date&Time API对开发者至关重要。而在Java8问世以前,Java的Date&Time机制不仅称不上易用,甚至可以说给开发者挖了很多坑。本文将会详细介绍Java的Date&Time机制。

1. 背景

时间是什么?

我不是想讨论物理学或者哲学上的时间,而是我们日常生活中的时间。举个例子:现在中午12点,这意味着什么?这意味地球在自转,这是今天我和太阳距离最近的时候。再举个例子:今天是春分日(3月的某一天),这又意味着什么?这意味着地球在公转,太阳照到北回归线了,大概是一年中我和太阳最近的一天。

所以,通常讨论时间,是在讨论地球的公转和自转,转到了一个和太阳怎样的位置。因为地球是圆的,所以时间会不一样。于是就有了年月日,时分秒,时区的概念。

GMT和UTC

由于时间不一样,所以大家都认为该有个参照,只要记住任意两个地方和参照点的时差,就能算出这两个地方的时差。比如我知道上海比这个参照点早了三个小时,伊拉克比这个参照点晚了三个小时,我就可以计算出上海和伊拉克时差为六个小时。

那么这儿参照点选在哪里呢?标准化组织决定啦, Greenwich(位于英国伦敦),由你来当这个参照点。当然,实际上是因为地理原因才选的Greenwich作为参照。于是就有了GMT,全称为Greenwich Mean Time,中文叫格林威治平均时间。GMT表示的就是Greenwich当地的时间。

GMT是根据地球公转和自转来确定的,可是随着自然科学的发展,人们发现,地球这转的速度不均匀啊,有时候多一秒(+1s),有时候少一秒(-1s)。于是就有了UTC,全称Coordinated Universal Time,中文叫协调世界时。UTC是根据原子衰变时间确定的,准确的很,据说几亿年误差都不到一秒。不过通常情况下,UTC和GMT之间的差距很小。

在计算机领域,为了确定统一时间,建立了一个“大纪元”的概念,也就是epoch,这个时间是1970-01-01 00:00:00 UTC。

2. System.currentTimeMillis()函数

有了上面的背景,就很容易理解System.currentTimeMillis()了,这个API会返回一个long型的值,代表距离epoch过去的毫秒数。不用担心已经过去了这么多毫秒,返回值会不会不够用,long型能够表示的数字很大很大。。。

需要注意的是,假如两台电脑的时钟都是精确的,一台在上海,显示的时间是晚上11:00;一台在伦敦,显示的时间是下午3:00,那么在这两个电脑上同时调用:System.currentTimeMillis(),返回时值理论上应该是相同的(事实上这个函数依赖操作系统,所以可能会有微小误差)。

3. 最早的是比较烂的java.util.Date

虽然System.currentTimeMillis()的返回值可以在理论上表示时间,这个数值太大,而且没有年月日时分秒的概念。因此,jdk最初使用java.util.Date类表示日常生活中的时间。简要地说,Date类内部保存了现在到1970.1.1.00.00.00 UTC之间的毫秒数,并且把这个毫秒数换算成年月日时分秒供开发者使用。使用默认构造函数就会创建一个表示当前日期和时间的对象:

Date date = new Date();

Date类实在是太烂了,看下面的例子:

import java.util.Date;
 
public class DateSucks {
 
    public static void main(String[] args) {
        Date date = new Date(12, 12, 12);
        System.out.println(date);
    }
}

你猜这段代码的输出是什么?答案是:

Sun Jan 12 00:00:00 IST 1913

这个输出有很多槽点:

  1. 上面的每个12表示啥意思?年月日还是月日年还是其他东西?
  2. 月份索引从0开始,所以十二月份的索引是11。
  3. 超过索引后会从一月份继续开始,12就成了一月份。
  4. 年份是从1900年开始,所以 1900 + 12 + 1 == 1913,输出为1913年
  5. Date类不仅表示了日期,还表示了时间
  6. 为什么输出还有时区呢?我并没有要打印时区啊。。事实上调用了JVM的默认时区。

总的来说,Date这个类责任不清晰,既表示日期,还表示时间,打印时还会打印出时区。这真的是个很糟糕的设计。

另外,为了格式化日期,需要利用java.text.SimpleDateFormat类,这个设计也不好,将Date放在了java.util包下,却将SimpleDateFormat放在java.text包下,显得很混乱。

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

String dateString = format.format( new Date()   );
Date   date       = format.parse ( "2009-12-31" );    

4. 后来又有了更烂的java.util.Calendar

因为java.util.Date类设计太烂,所以在jdk1.1中,引入了Calendar类作为替代,与Date类相比,这个类有两个变化:

  1. 在对象内部保存了时区信息。
  2. 增加了对日期和时间的加加减减操作。

使用getInstance()方法就可以获得一个带有本地时区的Calendar:

Calendar rightNow = Calendar.getInstance();

世界上有多种历法,所以Calendar只是一个抽象类,具体的历法由具体子类实现。世界上使用最多的(包括中国)就是格里高利历法,jdk中也只有这一个实现,java.util.GregorianCalendar。

Calendar calendar = new GregorianCalendar();

int year       = calendar.get(Calendar.YEAR);
int month      = calendar.get(Calendar.MONTH); 
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); // Jan = 0, not 1 int hour       = calendar.get(Calendar.HOUR);       
int minute     = calendar.get(Calendar.MINUTE);
int second     = calendar.get(Calendar.SECOND);
int millisecond= calendar.get(Calendar.MILLISECOND);

对Calendar中各个域操作主要有两种方法:set和add:

Calendar calendar = new GregorianCalendar();

calendar.set(Calendar.YEAR, 2009);
calendar.set(Calendar.MONTH, 11); // 11 = december calendar.set(Calendar.DAY_OF_MONTH, 31); // new years eve 
calendar.add(Calendar.DAY_OF_MONTH, 1);

java.util.TimeZone类下定义了世界上的时区,Calendar内有一个域会存储时区,通过改变这个域,就可以进行时间转换:

TimeZone timeZone = TimeZone.getTimeZone("America/Los_Angeles");

Calendar calendar = new GregorianCalendar();
System.out.println("hour = " + calendar.get(Calendar.HOUR_OF_DAY));

calendar.setTimeZone(timeZone);
System.out.println("hour = " + calendar.get(Calendar.HOUR_OF_DAY));

输出为:

hour = 20
hour = 11

不幸的是,Calendar类比Date类的设计更烂。槽点如下:

  1. Date类之所以烂是因为功能太多,但是Calendar不但没有对Date进行分拆,而是又往里面塞了时区这个域,于是,一个Calendar完成了本来应该三个类的功能:日期,时间和时区。
  2. 并且,Calendar是可变的,也就是每次我想对日期做一些加加减减,我要首先备个份再做,不然原来的日期就丢失了。
  3. 除此之外,月份的索引仍然是从0开始,一不小心就要踩坑;每个周的第一天是周日,也是一个大坑。

总而言之,Calendar的设计犯了责任不清和可变两大错误。很多开发者使用第三方库JodaTime,放弃Calendar和Date。不幸的是,一直到jdk1.7中,这两个类都是官方的Date&Time API。

5. 千呼万唤始出来:Java8中的java.time包

Java8开启了新的篇章,除了引入函数式编程,在jdk中新加入了java.time包以及一些子包,从而引入新的Date&Time机制。

其中,有四个核心类,

  • java.time.LocalDate(只表示日期,不包含时区信息)
  • java.time.LocalTime(只表示时间,不包含时区信息)
  • java.time.LocalDateTime (大概相当于java.util.Date,不包含时区信息,而且可以进行加减操作)。
  • java.time.ZonedDateTime(大概相当于java.util.Calendar)

简要地说:日期,时间和时区终于区分开来了。

另外,还增加了一些新类,都在java.time包或者其子包下,这样就显得很清晰。下面分别来看一下。

java.time.Instant

有两个域,一个表示距离epoch的秒数,一个表示纳秒数:

Instant now = Instant.now();

now.getEpochSecond()
now.getNano()

Instant later = now.plusSeconds(3);
Instant earlier = now.minusSeconds(3);

java.time.Duration

表示两个Instant之间的距离:

Instant first = Instant.now();
Instant second = Instant.now();

Duration duration = Duration.between(first, second);

java.time.LocalDate

//两种构造方法 LocalDate localDate = LocalDate.now();
LocalDate localDate2 = LocalDate.of(2015, 12, 31);

//get操作 int   year       = localDate.getYear();
Month month      = localDate.getMonth();
int   dayOfMonth = localDate.getDayOfMonth()

//加加减减操作 LocalDate localDate1 = localDate.plusYears(3);
LocalDate localDate2 = localDate.minusYears(3);

java.time.LocalTime

也是两种构造:

LocalTime localTime = LocalTime.now();
LocalTime localTime2 = LocalTime.of(21, 30, 59, 11001);

java.time.LocalDateTime

可以认为是LocalDate和LocalTime的组合。

java.time.ZonedDateTime

比LocalDateTime多了一个ZonedId域:

ZoneId zoneId = ZoneId.of("UTC+1");

ZonedDateTime zoned = ZonedDateTime.of(2015, 11, 30, 23, 45, 59, 1234, zoneId);

java.time.format.DateTimeFormatter

提供了一些已有的格式:

DateTimeFormatter formatter = DateTimeFormatter.BASIC_ISO_DATE;

String formattedDate = formatter.format(LocalDate.now());
System.out.println(formattedDate);

输出
20150703

6. 总结

jdk的Date&Time机制经历了三个阶段:java.util.Date(jdk1.0),java.util.Calendar(jdk1.1)和java.time包(jdk1.8)。最终形成的几个核心类责任清晰,都是不可变类,给开发提供了便利,希望新开发的程序可以充分利用。

参考:

  1. java8 doc
  2. jenkov blog
  3. Let’s master Java 8 Date Time API
    原文作者:孙兴斌
    原文地址: https://zhuanlan.zhihu.com/p/24884070
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞