易用的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
这个输出有很多槽点:
- 上面的每个12表示啥意思?年月日还是月日年还是其他东西?
- 月份索引从0开始,所以十二月份的索引是11。
- 超过索引后会从一月份继续开始,12就成了一月份。
- 年份是从1900年开始,所以 1900 + 12 + 1 == 1913,输出为1913年
- Date类不仅表示了日期,还表示了时间
- 为什么输出还有时区呢?我并没有要打印时区啊。。事实上调用了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类相比,这个类有两个变化:
- 在对象内部保存了时区信息。
- 增加了对日期和时间的加加减减操作。
使用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类的设计更烂。槽点如下:
- Date类之所以烂是因为功能太多,但是Calendar不但没有对Date进行分拆,而是又往里面塞了时区这个域,于是,一个Calendar完成了本来应该三个类的功能:日期,时间和时区。
- 并且,Calendar是可变的,也就是每次我想对日期做一些加加减减,我要首先备个份再做,不然原来的日期就丢失了。
- 除此之外,月份的索引仍然是从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)。最终形成的几个核心类责任清晰,都是不可变类,给开发提供了便利,希望新开发的程序可以充分利用。
参考: