Java 日期和时间

主要有三个主要的类:Date, Calendar, DateFormat

  • Date: 表示时刻,绝对时间,与年月日无关

  • Calendar: 表示年历,是一个抽象类,表示公历的子类是 GregorianCalendar

  • DateFormat: 表示格式化,将日期和时间与字符串相互转换,也是抽象类,最常用的子类是 SimpleDateFormat

因此常用的实例类有 Date, GregorianCalendar, SimpleDateFormat

还有两个相关的类:

  • TimeZone: 表示时区(抽象类)

  • Locale: 表示地区和语言

Date

是最早关于日期的类,一开始也有关于年历的功能,但由于不支持国际化被标记为@Deprecated,因此 Date 现在只用其绝对时间功能。

内部主要是一个long类型的值:fastTime,表示距离 Epoch Time (1970 年 1 月 1 日 0 时 0 分 0 秒)的毫秒数


private transient long fastTime;

构造方法:


public Date(long date) {

fastTime = date;

}



public Date() {

this(System.currentTimeMillis())

}

Date 中大部分方法已经过时了,没过时的方法有这些:


public long getTime() // 返回毫秒数

public boolean equals(Object obj) // 比较内部的毫秒数是否相同

public int compareTo(Date anotherDate) // 比较日期 -1 0 1

public boolean before(Date when) // 是否之前?

public boolean after(Date when) // 是否之后?

public int hashCode() // 哈希值计算方法与 Long 类似

TimeZone

TimeZone 也是抽象类,静态方法获取其实例


TimeZone tz = TimeZone.getDefault();

TimeZone tz_us = TimeZone.getTimeZone("US/Eastern");



System.out.prinlnt(tz.getID()); // 输出 Asia/Shanghai

默认时区是在 Java 的系统属性user.timezone中获得的,可以通过System.getProperty获得:


System.getProperty("user.timezone")

系统属性可以在 Java 启动时传入参数进行更改:


java -D user.timezone=Asia/Shanghai ...

Locale

Locale 表示国家(或地区)和语言,两个参数(国家,语言),其中国家不是必须的,语言是必须的

中国大陆是 CN,台湾地区是 TW,美国是 US

中文是 zh, 英语是 en

Locale 常见的静态变量:

  • Locale.US: 美国英语

  • Locale.ENGLISH: 所有英语

  • Locale.TAIWAN: 台湾所用的中文

  • Locale.SIMIPLIFIED_CHINESE: 中国内地的中文

  • Locale.CHINESE: 所有中文


Locale locale = Locale.getDefault();

System.out.println(locale.toString()); // 输出 zh_CN

Calendar

Calendar 类是日期和时间操作的主要类(抽象类),它表示与Time-Zone和Locale相关的日历信息,可以进行各种相关的运算。

与Date类似,Calendar内部也有一个表示时刻的毫秒数


protected long time;

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


protected int fields[]; // 长度为17, 保存日期中各字段的值

fields 数组保存的字段有:

  • Calendar.YEAR:表示年。

  • Calendar.MONTH:表示月,1月是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 实例的静态方法:


public static Calendar getInstance() // 其实还是需要 timezone 和 locale



public static Calendar getInstance(Timzone zone, Locale alocale)

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)); // 24 小时, HOUR 是 12 小时

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));

内部,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, int second)

public void set(int field, int value)

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


// amount为正数表示增加,负数表示减少。

public void add(int field, int 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的这些方法中一个比较方便和强大的地方在于,它能够自动调整相关的字段。比如,我们知道2月最多有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);

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

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

除了add方法,Calendar还有一个类似的方法 roll, 与add方法的区别是,roll方法不影响时间范围更大的字段值。


Calendar calendar = Calendar.getInstance();

calendar.set(Calendar.HOUR_OF_DAY, 13);

calendar.set(Calendar.MINUTE, 59);

calendar.add(Calendar.MINUTE, 3); // 14:02

calendar.add(Calendar.MINUTE, 3); // 13:02

Calendar可以方便地转换为Date或毫秒数,方法是:


public final Date getTime()

public long getTimeInMillis()

Calendar 直接也有可以比较:equals, compareTo, after, before

DateFormat

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


public final String format(Date date)

public Date parse(String source)

Date字符串表示TimeZoneLocale都是相关的,除此之外,还与两个格式化风格有关,一个是日期的格式化风格,另一个是时间的格式化风格, 也就是日期和时间的格式化是可以分开的。

DateFormat定义了4个静态变量,表示4种风格:SHORTMEDIUMLONGFULL;还定义了一个静态变量DEFAULT,表示默认风格,默认风格值为MEDIUM,不同风格输出的信息详细程度不同。

DateFormat也是抽象类,也用工厂方法创建对象,提供了多个静态方法创建DateFormat对象,有三类方法:


public final static DateFormat getDateTimeInstance()

public final static DateFormat getDateInstance()

public final static DateFormat getTimeInstance()

getDateTimeInstance方法既处理日期也处理时间,getDate-Instance方法只处理日期,getTimeInstance方法只处理时间。


Calendar calendar = Calendar.getInstance();

// 注意 calendar 实例 getTime 返回的是 Date

System.out.println(DateFormat.getDateTimeInstance().format(calendar.getTime()));

System.out.println(DateFormat.getDateInstance().format(calendar.getTime()));

System.out.println(DateFormat.getTimeInstance().format(calendar.getTime()));

// 输出为:

// Feb 13, 2018, 11:16:44 PM

// Feb 13, 2018

// 11:16:44 PM

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


DateFormat getDateTimeInstance(int dateStyle, int timeStyle)

// 注意 dateStyle 和 timeStyle 是分开的

DateFormat getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale)

Calendar calendar = Calendar.getInstance();

System.out.println(DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT,Locale.CHINESE).format(calendar.getTime()));



// 输出为:

// 2018年2月13日 下午11:18

DateFormat 的工厂方法里,我们没看到 TimeZone 参数,不过,DateFormat提供了一个setter方法,可以设置 TimeZone


public void setTimeZone(TimeZone zone)

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

SimpleDateFormat

SimpleDateFormatDateFormat 的子类,相比 DateFormat,它的一个主要不同是,它可以接受一个自定义的模式(pattern)作为参数,这个模式规定了 Date 的字符串形式。


Calendar calendar = Calendar.getInstance();

SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 E HH时mm分ss秒");

System.out.println(sdf.format(calendar.getTime()));



// 输出为:

// 2018年02月13日 Tue 23时34分35秒





// pattern 为 yyyy/MM/dd hh:mm:ss a

// 2018/02/13 11:38:29 PM

SimpleDateFormat有个构造方法,可以接受一个pattern作为参数

  • yyyy:表示4位的年。

  • MM:表示月,用两位数表示。

  • dd:表示日,用两位数表示。

  • HH:表示24小时制的小时数,用两位数表示。

  • hh:表示12小时制的小时数,用两位数表示。

  • mm:表示分钟,用两位数表示。

  • ss:表示秒,用两位数表示。

  • SSS: 表示毫秒,用三位数表示。

  • E:表示星期几。

  • a:上午/下午

更多的特殊含义可以参看 SimpleDateFormat 的 API 文档。如果想原样输出英文字符,可以将其用单引号括起来(也算是一种转义)。

字符串转换为 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 PM

局限性

至此,关于Java 8之前的日期和时间相关API的主要内容基本就介绍完了。Date表示时刻,与年月日无关,Calendar 表示日历,与时区和 Locale 相关,可进行各种运算,是日期时间操作的主要类,DateFormat/SimpleDateFormatDate和字符串之间进行相互转换。

一. Date中的过时方法

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


Date date = new Date(2016,8,15);

System.out.println(DateFormat.getDateInstance().format(date));



// 输出为:

// 3916-9-15

Date构造方法中的 year 表示的是与1900年的差,month 是从0开始的。

二. Calendar操作比较烦琐

Calendar API的设计不是很成功,一些简单的操作都需要多次方法调用,写很多代码,比较臃肿。

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

三. DateFormat的线程安全性

DateFormat/SimpleDateFormat不是线程安全的。

Joda-Time 或 Java8 的 API,它们是线程安全的。