计算机程序的思惟逻辑 (32) - 剖析日期和时间

本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html

本节和下节,咱们讨论在Java中如何进行日期和时间相关的操做。java

日期和时间是一个比较复杂的概念,Java API中对它的支持不是特别好,有一个第三方的类库反而特别受欢迎,这个类库是Joda-Time,Java 1.8受Joda-Time影响,从新设计了日期和时间API,新增了一个包java.time。算法

虽然以前的设计有一些不足,但Java API依然是被大量使用的,本节介绍Java 1.8以前API中对日期和时间的支持,下节介绍Joda-Time,Java 1.8中的新API与Joda-Time比较相似,暂时就不介绍了。编程

关于日期和时间,有一些基本概念,咱们先来看下。设计模式

基本概念

时区

咱们都知道,同一时刻,世界上各个地区的时间多是不同的,具体时间与时区有关,一共有24个时区,英国格林尼治是0时区,北京是东八区,也就是说格林尼治凌晨1点,北京是早上9点。0时区的时间也称为GMT+0时间,GMT是格林尼治标准时间,北京的时间就是GMT+8:00。数组

时刻和Epoch Time (纪元时)

全部计算机系统内部都用一个整数表示时刻,这个整数是距离格林尼治标准时间1970年1月1日0时0分0秒的毫秒数。为何要用这个时间呢?更多的是历史缘由,本文就不介绍了。安全

格林尼治标准时间1970年1月1日0时0分0秒也被称为Epoch Time (纪元时)。bash

这个整数表示的是一个时刻,与时区无关,世界上各个地方都是同一个时刻,但各个地区对这个时刻的解读,如年月日时分秒,多是不同的。微信

如何表示1970年之前的时间呢?使用负数。多线程

年历

咱们都知道,中国有公历和农历之分,公历和农历都是年历,不一样的年历,一年有多少月,每个月有多少天,甚至一天有多少小时,这些可能都是不同的。

好比,公历有闰年,闰年2月是29天,而其余年份则是28天,其余月份,有的是30天,有的是31天。农历有闰月,好比闰7月,一年就会有两个7月,一共13个月。

公历是世界上普遍采用的年历,除了公历,还有其余一些年历,好比日本也有本身的年历。Java API的设计思想是支持国际化的,支持多种年历,但实际中没有直接支持中国的农历,本文主要讨论公历。

简单总结下,时刻是一个绝对时间,对时刻的解读,如年月日周时分秒等,则是相对的,与年历和时区相关。

Java日期和时间API

Java API中关于日期和时间,有三个主要的类:

  • Date:表示时刻,即绝对时间,与年月日无关。
  • Calendar:表示年历,Calendar是一个抽象类,其中表示公历的子类是GregorianCalendar
  • DateFormat:表示格式化,可以将日期和时间与字符串进行相互转换,DateFormat也是一个抽象类,其中最经常使用的子类是SimpleDateFormat。

还有两个相关的类:

  • TimeZone: 表示时区
  • Locale: 表示国家和语言

下面,咱们来看这些类。

Date

Date是Java API中最先引入的关于日期的类,一开始,Date也承载了关于年历的角色,但因为不能支持国际化,其中的不少方法都已通过时了,被标记为了@Deprecated,再也不建议使用。

Date表示时刻,内部主要是一个long类型的值,以下所示:

private transient long fastTime;
复制代码

fastTime表示距离纪元时的毫秒数,此处,关于transient关键字,咱们暂时忽略。

Date有两个构造方法:

public Date(long date) {
    fastTime = date;
}

public Date() {
    this(System.currentTimeMillis());
}
复制代码

第一个构造方法,就是根据传入的毫秒数进行初始化,第二个构造方法是默认构造方法,它根据System.currentTimeMillis()的返回值进行初始化。System.currentTimeMillis()是一个经常使用的方法,它返回当前时刻距离纪元时的毫秒数。

Date中的大部分方法都已通过时了,其中没有过期的主要方法有:

返回毫秒数

public long getTime() 复制代码

判断与其余Date是否相同

public boolean equals(Object obj) 复制代码

主要就是比较内部的毫秒数是否相同。

与其余Date进行比较

public int compareTo(Date anotherDate) 复制代码

Date实现了Comparable接口,比较也是比较内部的毫秒数,若是当前Date的毫秒数小于参数中的,返回-1,相同返回0,不然返回1。

除了compareTo,还有另外两个方法,与给定日期比较,判断是否在给定日期以前或以后,内部比较的也是毫秒数。

public boolean before(Date when) public boolean after(Date when) 复制代码

哈希值

public int hashCode() 复制代码

哈希值算法与Long相似。

TimeZone

TimeZone表示时区,它是一个抽象类,有静态方法用于获取其实例。

获取当前的默认时区,代码为:

TimeZone tz = TimeZone.getDefault();
System.out.println(tz.getID());
复制代码

获取默认时区,并输出其ID,在个人电脑上,输出为:

Asia/Shanghai
复制代码

默认时区是在哪里设置的呢,能够更改吗?Java中有一个系统属性,user.timezone,保存的就是默认时区,系统属性能够经过System.getProperty得到,以下所示:

System.out.println(System.getProperty("user.timezone"));
复制代码

在个人电脑上,输出为:

Asia/Shanghai
复制代码

系统属性能够在Java启动的时候传入参数进行更改,如

java -Duser.timezone=Asia/Shanghai xxxx
复制代码

TimeZone也有静态方法,能够得到任意给定时区的实例,好比:

获取美国东部时区

TimeZone tz = TimeZone.getTimeZone("US/Eastern");
复制代码

ID除了能够是名称外,还能够是GMT形式表示的时区,如:

TimeZone tz = TimeZone.getTimeZone("GMT+08:00");
复制代码

国家和语言Locale

Locale表示国家和语言,它有两个主要参数,一个是国家,另外一个是语言,每一个参数都有一个代码,不过国家并非必须的。

好比说,中国的大陆代码是CN,台湾地区的代码是TW,美国的代码是US,中文语言的代码是zh,英文是en。

Locale类中定义了一些静态变量,表示常见的Locale,好比:

  • Locale.US:表示美国英语
  • Locale.ENGLISH:表示全部英语
  • Locale.TAIWAN:表示台湾中文
  • Locale.CHINESE:表示全部中文
  • Locale.SIMPLIFIED_CHINESE:表示大陆中文

与TimeZone相似,Locale也有静态方法获取默认值,如:

Locale locale = Locale.getDefault();
System.out.println(locale.toString());
复制代码

在个人电脑上,输出为:

zh_CN
复制代码

Calendar

Calendar类是日期和时间操做中的主要类,它表示与TimeZone和Locale相关的日历信息,能够进行各类相关的运算。

咱们先来看下它的内部组成。

内部组成

与Date相似,Calendar内部也有一个表示时刻的毫秒数,定义为:

protected long  time;
复制代码

除此以外,Calendar内部还有一个数组,表示日历中各个字段的值,定义为:

protected int   fields[];
复制代码

这个数组的长度为17,保存一个日期中各个字段的值,都有哪些字段呢?Calendar类中定义了一些静态变量,表示这些字段,主要有:

  • Calendar.YEAR:表示年
  • Calendar.MONTH:表示月,一月份是0,Calendar一样定义了表示各个月份的静态变量,如Calendar.JULY表示7月。
  • Calendar.DAY_OF_MONTH:表示日,每个月的第一天是1。
  • Calendar.HOUR_OF_DAY:表示小时,从0到23。
  • Calendar.MINUTE:表示分钟,0到59。
  • Calendar.SECOND:表示秒,0到59。
  • Calendar.MILLISECOND:表示毫秒,0到999。
  • Calendar.DAY_OF_WEEK:表示星期几,周日是1,周一是2,周六是7,Calenar一样定义了表示各个星期的静态变量,如Calendar.SUNDAY表示周日。

获取Calendar实例

Calendar是抽象类,不能直接建立对象,它提供了四个静态方法,能够获取Calendar实例,分别为:

public static Calendar getInstance() public static Calendar getInstance(Locale aLocale) public static Calendar getInstance(TimeZone zone) public static Calendar getInstance(TimeZone zone, Locale aLocale) 复制代码

最终调用的方法都是须要TimeZone和Locale的,若是没有,则会使用上面介绍的默认值。getInstance方法会根据TimeZone和Locale建立对应的Calendar子类对象,在中文系统中,子类通常是表示公历的GregorianCalendar。

getInstance方法封装了Calendar对象建立的细节,TimeZone和Locale不一样,具体的子类可能不一样,但都是Calendar,这种隐藏对象建立细节的方式,是计算机程序中一种常见的设计模式,它有一个名字,叫工厂方法,getInstance就是一个工厂方法,它生产对象。

获取日历信息

与new Date()相似,新建立的Calendar对象表示的也是当前时间,与Date不一样的是,Calendar对象能够方便的获取年月日等日历信息。

来看代码,输出当前时间的各类信息:

Calendar calendar = Calendar.getInstance();
System.out.println("year: "+calendar.get(Calendar.YEAR));
System.out.println("month: "+calendar.get(Calendar.MONTH));
System.out.println("day: "+calendar.get(Calendar.DAY_OF_MONTH));
System.out.println("hour: "+calendar.get(Calendar.HOUR_OF_DAY));
System.out.println("minute: "+calendar.get(Calendar.MINUTE));
System.out.println("second: "+calendar.get(Calendar.SECOND));
System.out.println("millisecond: " +calendar.get(Calendar.MILLISECOND));
System.out.println("day_of_week: " + calendar.get(Calendar.DAY_OF_WEEK));
复制代码

具体输出与执行时的时间和默认的TimeZone以及Locale有关,在写做时,个人电脑上的输出为:

year: 2016
month: 7
day: 14
hour: 13
minute: 55
second: 51
millisecond: 564
day_of_week: 2
复制代码

内部,Calendar会将表示时刻的毫秒数,按照TimeZone和Locale对应的年历,计算各个日历字段的值,存放在fields数组中,Calendar.get方法获取的就是fields数组中对应字段的值。

设置和修改时间

Calendar支持根据Date或毫秒数设置时间:

public final void setTime(Date date) public void setTimeInMillis(long millis) 复制代码

也支持根据年月日等日历字段设置时间:

public final void set(int year, int month, int date) public final void set(int year, int month, int date, int hourOfDay, int minute) public final void set(int year, int month, int date, int hourOfDay, int minute, int second) public void set(int field, int value) 复制代码

除了直接设置,Calendar支持根据字段增长和减小时间:

public void add(int field, int amount) 复制代码

amount为正数表示增长,负数表示减小。

好比说,若是想设置Calendar为次日的下午2点15,代码能够为:

Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 14);
calendar.set(Calendar.MINUTE, 15);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
复制代码

Calendar的这些方法中一个比较方便和强大的地方在于,它可以自动调整相关的字段。

好比说,咱们知道二月份最多有29天,若是当前时间为1月30号,对Calendar.MONTH字段加1,即增长一月,Calendar不是简单的只对月字段加1,那样日期是2月30号,是无效的,Calendar会自动调整为2月最后一天,即2月28或29。

再好比,设置的值能够超出其字段最大范围,Calendar会自动更新其余字段,如:

Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR_OF_DAY, 48);
calendar.add(Calendar.MINUTE, -120);
复制代码

至关于增长了46小时。

内部,根据字段设置或修改时间时,Calendar会更新fields数组对应字段的值,但通常不会当即更新其余相关字段或内部的毫秒数的值,不过在获取时间或字段值的时候,Calendar会从新计算并更新相关字段。

简单总结下,Calenar作了一项很是繁琐的工做,根据TimeZone和Locale,在绝对时间毫秒数和日历字段之间自动进行转换,且对不一样日历字段的修改进行自动同步更新。

除了add,Calendar还有一个相似的方法:

public void roll(int field, int amount) 复制代码

与add的区别是,这个方法不影响时间范围更大的字段值。好比说:

Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 13);
calendar.set(Calendar.MINUTE, 59);
calendar.add(Calendar.MINUTE, 3);
复制代码

calendar首先设置为13:59,而后分钟字段加3,执行后的calendar时间为14:02。若是add改成roll,即:

calendar.roll(Calendar.MINUTE, 3);
复制代码

则执行后的calendar时间会变为13:02,在分钟字段上执行roll不会改变小时的值。

转换为Date或毫秒数

Calendar能够方便的转换为Date或毫秒数,方法是:

public final Date getTime() public long getTimeInMillis() 复制代码

Calendar的比较

与Date相似,Calendar之间也能够进行比较,也实现了Comparable接口,相关方法有:

public boolean equals(Object obj) public int compareTo(Calendar anotherCalendar) public boolean after(Object when) public boolean before(Object when) 复制代码

DateFormat

DateFormat类主要在Date和字符串表示之间进行相互转换,它有两个主要的方法:

public final String format(Date date) public Date parse(String source) 复制代码

format将Date转换为字符串,parse将字符串转换为Date。

Date的字符串表示与TimeZone和Locale都是相关的,除此以外,还与两个格式化风格有关,一个是日期的格式化风格,另外一个是时间的格式化风格。

DateFormat定义了四个静态变量,表示四种风格,SHORT、MEDIUM、LONG和FULL,还定义了一个静态变量DEFAULT,表示默认风格,值为MEDIUM,不一样风格输出的信息详细程度不一样。

与Calendar相似,DateFormat也是抽象类,也用工厂模式建立对象,提供了多个静态方法建立DateFormat对象,有三类方法:

public final static DateFormat getDateTimeInstance() public final static DateFormat getDateInstance() public final static DateFormat getTimeInstance() 复制代码

getDateTimeInstance既处理日期也处理时间,getDateInstance只处理日期,getTimeInstance只处理时间,看下面代码:

Calendar calendar = Calendar.getInstance();
//2016-08-15 14:15:20
calendar.set(2016, 07, 15, 14, 15, 20);
System.out.println(DateFormat.getDateTimeInstance()
        .format(calendar.getTime()));
System.out.println(DateFormat.getDateInstance()
        .format(calendar.getTime()));
System.out.println(DateFormat.getTimeInstance()
        .format(calendar.getTime()));
复制代码

输出为:

2016-8-15 14:15:20
2016-8-15
14:15:20
复制代码

每类工厂方法都有两个重载的方法,接受日期和时间风格以及Locale做为参数:

DateFormat getDateTimeInstance(int dateStyle, int timeStyle) DateFormat getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale) 复制代码

好比,看下面代码:

Calendar calendar = Calendar.getInstance();
//2016-08-15 14:15:20
calendar.set(2016, 07, 15, 14, 15, 20);
System.out.println(DateFormat.getDateTimeInstance(
        DateFormat.LONG,DateFormat.SHORT,Locale.CHINESE)
        .format(calendar.getTime()));
复制代码

输出为:

2016年8月15日 下午2:15
复制代码

DateFormat的工厂方法里,咱们没看到TimeZone参数,不过,DateFormat提供了一个setter方法,能够设置TimeZone:

public void setTimeZone(TimeZone zone) 复制代码

DateFormat虽然比较方便,但若是咱们要对字符串格式有更精确的控制,应该使用SimpleDateFormat这个类。

SimpleDateFormat

SimpleDateFormat是DateFormat的子类,相比DateFormat,它的一个主要不一样是,它能够接受一个自定义的模式(pattern)做为参数,这个模式规定了Date的字符串形式。先看个例子:

Calendar calendar = Calendar.getInstance();
//2016-08-15 14:15:20
calendar.set(2016, 07, 15, 14, 15, 20);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 E HH时mm分ss秒");
System.out.println(sdf.format(calendar.getTime()));
复制代码

输出为:

2016年08月15日 星期一 14时15分20秒 
复制代码

SimpleDateFormat有个构造方法,能够接受一个pattern做为参数,这里pattern是:

yyyy年MM月dd日 E HH时mm分ss秒
复制代码

pattern中的英文字符a-z和A-Z表示特殊含义,其余字符原样输出,这里:

  • yyyy:表示四位的年
  • MM:表示月,两位数表示
  • dd:表示日,两位数表示
  • HH:表示24小时制的小时数,两位数表示
  • mm:表示分钟,两位数表示
  • ss:表示秒,两位数表示
  • E:表示星期几

这里须要特地提醒一下,hh也表示小时数,但表示的是12小时制的小时数,而a表示的是上午仍是下午,看代码:

Calendar calendar = Calendar.getInstance();
//2016-08-15 14:15:20
calendar.set(2016, 07, 15, 14, 15, 20);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss a");
System.out.println(sdf.format(calendar.getTime()));
复制代码

输出为:

2016/08/15 02:15:20 下午
复制代码

更多的特殊含义能够参看SimpleDateFormat的Java文档。若是想原样输出英文字符,能够用单引号括起来。

除了将Date转换为字符串,SimpleDateFormat也能够方便的将字符转化为Date,看代码:

String str = "2016-08-15 14:15:20.456";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
try {
    Date date = sdf.parse(str);
    SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年M月d h:m:s.S a");
    System.out.println(sdf2.format(date));
} catch (ParseException e) {
    e.printStackTrace();
}
复制代码

输出为:

2016年8月15 2:15:20.456 下午
复制代码

代码将字符串解析为了一个Date对象,而后使用另一个格式进行了输出,这里SSS表示三位的毫秒数。

须要注意的是,parse会抛出一个受检异常(checked exception),异常类型为ParseException,调用者必须进行处理。

局限性

至此,关于Java 1.8以前的日期和时间相关API的主要内容,咱们就介绍的差很少了,这里咱们想强调一下这些API的一些局限性。

Date中的过期方法

Date中的方法参数与常识不符合,过期方法标记容易被人忽略,产生误用。好比说,看以下代码:

Date date = new Date(2016,8,15);
System.out.println(DateFormat.getDateInstance().format(date));
复制代码

想固然的输出为2016-08-15,但其实输出为:

3916-9-15
复制代码

之因此产生这个输出,是由于,Date构造方法中的year表示的是与1900年的差,month是从0开始的。

Calendar操做比较啰嗦臃肿

Calendar API的设计不是很成功,一些简单的操做都须要屡次方法调用,写不少代码,比较啰嗦臃肿。

另外,Calendar难以进行比较复杂的日期操做,好比,计算两个日期之间有多少个月,根据生日计算年龄,计算下个月的第一个周一等。

下一节,咱们会介绍Joda-Time,相比Calendar,Joda-Time要简洁方便的多。

DateFormat的线程安全性

DateFormat/SimpleDateFormat不是线程安全的,关于线程概念,后续文章咱们会详解,这里简单说明一下,多个线程同时使用一个DateFormat实例的时候,会有问题,由于DateFormat内部使用了一个Calendar实例对象,多线程同时调用的时候,这个Calendar实例的状态可能就会紊乱。

解决这个问题大概有如下方案:

  • 每次使用DateFormat都新建一个对象
  • 使用线程同步
  • 使用ThreadLocal
  • 使用Joda-Time,Joda-Time是线程安全的

后续文章咱们再介绍线程同步和ThreadLocal。

小结

本节介绍了Java中(1.8以前)的日期和时间相关API,Date表示时刻,与年月日无关,Calendar表示日历,与时区和Locale相关,可进行各类运算,是日期时间操做的主要类,DateFormat/SimpleDateFormat在Date和字符串之间进行相互转换。

这些API存在着一些不足,操做比较复杂,代码比较臃肿,还有线程安全的问题,实际中一个经常使用的第三方库是Joda-Time,下一节,让咱们一块儿来看下。


未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),深刻浅出,老马和你一块儿探索Java编程及计算机技术的本质。用心原创,保留全部版权。

相关文章
相关标签/搜索