0%

Java中日期时间相关核心类

一、日期时间基本概念

对于日期时间,首先作以下几点基本说明:

  • 日期时间涉及到“年,月,日,时,分,秒,毫秒,时区”8个部分,当且仅当“年,月,日,时,分,秒,毫秒,时区”8个部分都明确时,才唯一可确定一个日期时间
  • 只有在明确“时区”的前提下,谈论“年,月,日,时,分,秒,毫秒”才有意义,即:脱离“时区”,谈论“年,月,日,时,分,秒,毫秒”毫无意义
  • 两个日期时间等价当且仅当这两个日期时间与某个基准日期时间的毫秒差值相等,一般选取“1970-01-01 00:00:00.000 GMT+00:00”作为基准日期时间,一个日期时间与该基准日期时间的毫秒差值称为“基准偏移量”。比如“2018-12-10 07:30:20.500 GMT+05:00”和“2018-12-10 08:30:20.500 GMT+06:00”这两个日期时间等价,它们与“1970-01-01 00:00:00.000 GMT+00:00”基准日期时间的毫秒差值都为“1544409020500”,即“基准偏移量”都为“1544409020500”。日期时间的时区转换是一个等价转换,内部基于“基准偏移量”进行计算

二、Java中日期时间相关核心类

Java中日期时间相关核心类有4个:TimeZone,Date,SimpleDateFormat,Calendar。
通过实际使用可发现“JDK1.8前的日期时间相关类设计极度糟糕”的论断是真的(比如“Calendar类的使用”,“从日期时间字符串获取时区信息”等),因此,推荐使用Joda-Time第三方库或者JDK1.8后的日期时间相关类。

2.1、TimeZone

2.1.1、基本介绍

表示时区对象。

2.1.2、常用方法

1、getTimeZone(String ID)
获取时区对象,推荐以时区的“IANA别名”或者“自定义ID”表示“ID”,而不要以“英文别名缩略”表示,原因参见时区
时区的“IANA别名”可通过TimeZone.getAvailableIDs()方法获取,也可通过网络查询获取。
“自定义ID”的格式描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
CustomID:
GMT Sign Hours : Minutes
GMT Sign Hours Minutes
GMT Sign Hours
Sign:
one of + -
Hours:
Digit
Digit Digit
Minutes:
Digit Digit
Digit:
one of 0 1 2 3 4 5 6 7 8 9

“自定义ID”规范化后的格式描述如下:

1
2
3
4
5
6
7
8
9
10
NormalizedCustomID:
GMT Sign TwoDigitHours : Minutes
Sign:
one of + -
TwoDigitHours:
Digit Digit
Minutes:
Digit Digit
Digit:
one of 0 1 2 3 4 5 6 7 8 9

2.2、Date

2.2.1、基本介绍

Java日期时间体系核心对象,时区固定为TimeZone.getDefaultRef()方法值所表示时区,即“操作系统所配置使用时区”。

2.2.2、常用方法

1、Date()
创建一个表示当前日期时间的Date对象。
2、Date(long date)
创建一个Date对象,该Date对象所表示日期时间的“基准偏移量”为date
3、getTime()
返回该Date对象所表示日期时间的“基准偏移量”。
4、setTime(long time)
设置该Date对象所表示日期时间的“基准偏移量”为time

2.3、SimpleDateFormat

2.3.1、基本介绍

格式化Date对象到日期时间字符串,从日期时间字符串解析获取Date对象。

备注:
通过传入Locale对象参数,设定日期时间字符串的语系环境。
示意代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class Main {

public static void main(String[] args) {
Date date = new Date();

SimpleDateFormat formatter1 = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss.SSS ZZZ", Locale.ENGLISH);
System.out.println(formatter1.format(date));

SimpleDateFormat formatter2 = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss.SSS ZZZ", Locale.CHINA);
System.out.println(formatter2.format(date));
}

}

运行结果如下:

1
2
2018-Aug-22 22:23:24.086 +0800
2018-八月-22 22:23:24.086 +0800

2.3.2、常用方法

1、format(Date date)
格式化Date对象到日期时间字符串,涉及到“年,月,日,时,分,秒,毫秒,时区”。
2、parse(String source)
解析日期时间字符串获取Date对象,涉及到“年,月,日,时,分,秒,毫秒,时区”。
3、setTimeZone(TimeZone zone)
格式化时,设置目标时区;解析时,设置源时区。

2.4、Calendar

2.4.1、基本介绍

用来操纵Date对象。
Calendar类是一个设计极度糟糕的类,要想获得预期结果必须严格管控方法是否调用和调用顺序,比如:

  • 在Calendar类内部完成所表示日期时间的计算之前,调用setTimeZone(TimeZone value)方法所设置的时区会参与后续日期时间的计算;否则,所设置的时区不会参与已经完成的日期时间的计算,而仅仅表示等价的时区转换
  • 调用set(int field, int value)方法会令已经完成的日期时间的计算失效,当后续调用getTimeInMillis()getTime()等方法时会重新触发日期时间的计算

关于上述论述的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import java.util.Calendar;
import java.util.TimeZone;

public class Main {

public static void main(String[] args) {
f1();
System.out.println("f1() result above\n");

f2();
System.out.println("f2() result above\n");

f3();
System.out.println("f3() result above\n");
}

public static void f1() {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, 2018);
calendar.set(Calendar.MONTH, 10);
calendar.set(Calendar.DAY_OF_MONTH, 21);
calendar.set(Calendar.HOUR_OF_DAY, 10);
calendar.set(Calendar.MINUTE, 30);
calendar.set(Calendar.SECOND, 20);
calendar.set(Calendar.MILLISECOND, 500);

// 设置的时区参与日期时间的计算
calendar.setTimeZone(TimeZone.getTimeZone("GMT+07:00"));

// 调用getTime()方法,触发日期时间的计算
System.out.println(calendar.getTime());
}

public static void f2() {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, 2018);
calendar.set(Calendar.MONTH, 10);
calendar.set(Calendar.DAY_OF_MONTH, 21);
calendar.set(Calendar.HOUR_OF_DAY, 10);
calendar.set(Calendar.MINUTE, 30);
calendar.set(Calendar.SECOND, 20);
calendar.set(Calendar.MILLISECOND, 500);

// 调用getTime()方法,触发日期时间的计算
System.out.println(calendar.getTime());

// 设置的时区不参与日期时间的计算,只表示转换时区
calendar.setTimeZone(TimeZone.getTimeZone("GMT+07:00"));

// 调用getTime()方法,先前已经完成日期时间的计算,不重复触发计算
System.out.println(calendar.getTime());
}

public static void f3() {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, 2018);
calendar.set(Calendar.MONTH, 10);
calendar.set(Calendar.DAY_OF_MONTH, 21);
calendar.set(Calendar.HOUR_OF_DAY, 10);
calendar.set(Calendar.MINUTE, 30);
calendar.set(Calendar.SECOND, 20);
calendar.set(Calendar.MILLISECOND, 500);

// 调用getTime()方法,触发日期时间的计算
System.out.println(calendar.getTime());

// 调用set(int field, int value)方法,令先前完成的日期时间的计算失效
calendar.set(Calendar.YEAR, 2018);

// 设置的时区参与日期时间的计算
calendar.setTimeZone(TimeZone.getTimeZone("GMT+07:00"));

// 调用getTime()方法,触发日期时间的计算
System.out.println(calendar.getTime());
}
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
Wed Nov 21 11:30:20 CST 2018
f1() result above

Wed Nov 21 10:30:20 CST 2018
Wed Nov 21 10:30:20 CST 2018
f2() result above

Wed Nov 21 10:30:20 CST 2018
Wed Nov 21 11:30:20 CST 2018
f3() result above

因此,为减少使用错误,须拟订Calendar类的使用规范。
首先,关于Calendar类的使用有以下几点描述:

  • 当且仅当Calendar类完成内部计算之后,Calendar类才能确定一个日期时间,准确地来说是确定一个“基准偏移量”
  • 调用setTime(Date date)方法,即时完成内部计算,确定日期时间
  • 调用setTimeInMillis(long millis)方法,即时完成内部计算,确定日期时间
  • 调用一系列set(int field, int value)setTimeZone(TimeZone value)方法,预确定日期时间,等待内部计算被触发完成后,真正确定日期时间
  • getTimeInMillis()getTime()get(int field)getTimeZone()这4个方法获取日期时间相关值。其中,getTimeInMillis()getTime()get(int field)这3个方法会触发内部计算;getTimeZone()方法不会触发内部计算,它获取被设置的时区,默认返回操作系统所配置使用时区,否则跟setTimeZone(TimeZone value)方法设置的时区一致
  • setTimeZone(TimeZone value)方法设置时区,在完成内部计算之前调用,所设置时区参与内部计算;否则,不参与内部计算,只用于等价的时区转换,此时该方法的调用效果只影响这里正在讨论的getTimeZone()get(int field)方法。等价的时区转换不是Calendar类的主线应用场景(通过SimpleDateFormat类的format(Date date)方法完成等价的时区转换任务),因此禁止在完成内部计算之后调用setTimeZone(TimeZone value)方法

根据上述描述,拟订Calendar类的使用规范:

  • 先设置,再获取。禁止获取之后再设置
  • 设置有3条线,分别为:1)setTime(Date date);2)setTimeInMillis(long millis);3)set(int field, int value)setTimeZone(TimeZone value)
  • 获取线为:getTime()getTimeInMillis()get(int field)getTimeZone()

示意图如图1所示。

图1

2.4.2、常用方法

1、getInstance()
获取Calendar实例对象。
2、setTime(Date date)
Calendar内部日期时间对象直接设置为等价于date
3、setTimeInMillis(long millis)
Calendar内部日期时间对象的“基准偏移量”直接设置为millis
4、set(int field, int value)
显式设置“年,月,日,时,分,秒,毫秒”7个部分值。
5、setTimeZone(TimeZone value)
显式设置“时区”值。
6、getTime()
如果未完成内部计算,触发内部计算。获取对应于内部计算后所得日期时间的Date对象。
7、getTimeInMillis()
如果未完成内部计算,触发内部计算。获取对应于内部计算后所得日期时间的“基准偏移量”。
8、get(int field)
如果未完成内部计算,触发内部计算。获取对应于内部计算后所得日期时间的“年,月,日,时,分,秒,毫秒”值。
9、getTimeZone()
不触发内部计算。获取被设置的时区,默认返回操作系统所配置使用时区,否则跟setTimeZone(TimeZone value)方法设置的时区一致。

三、其他

3.1、4个核心类之间的关系示意图

“TimeZone,Date,SimpleDateFormat,Calendar”4个核心类之间的关系示意图如图2。

图2

3.2、对日期时间操作的基本思路

1、操作维度为“时区”
这里所指“对日期时间操作”包括:对某个日期时间进行时区的等价转换。
上述对日期时间操作的基本思路是:由于Date对象默认所使用时区为“操作系统所配置使用时区”,因此如果要进行时区的等价转换,转换的目标对象一般不能是Date对象,而是日期时间字符串,那么很自然可推得这个等价转换是借助于“SimpleDateFormat”类,通过“SimpleDateFormat”类的setTimeZone(TimeZone zone)方法设置转换的目标时区。
示意代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

public class Main {

public static void main(String[] args) {
Date now = new Date();
System.out.println(now);

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
format.setTimeZone(TimeZone.getTimeZone("GMT+07:00"));

System.out.println(format.format(now));
}
}

运行结果如下:

1
2
Wed Aug 29 03:24:16 CST 2018
2018-08-29 02:24:16 +0700

2、操作维度为“日,时,分,秒,毫秒”
这里所指“对日期时间操作”包括:对某个日期时间增加或者减少N日/时/分/秒/毫秒,计算两个日期时间之间间隔的日/时/分/秒/毫秒。
上述对日期时间操作的基本思路是:基于“基准偏移量”,然后再根据“1日=24时=24*60分=24*60*60秒=24*60*60*1000毫秒”的换算公式。
3、操作维度为“年,月”
这里所指“对日期时间操作”包括:对某个日期时间增加或者减少N年/月,计算两个日期时间之间间隔的年/月。
由于年/月不能换算成精确的N日/时/分/秒/毫秒,因此相关操作的实现方案也是不精确的,常见有两种实现方案:

  • 基于“基准偏移量”,然后再采用“1年=365日”和“1月=30日”这两个不精确的换算公式
  • 直接增加或者减少年/月字段或者直接计算年/月字段之间的差值,这是显而易见不精确的,比如“2018-01-31增加1个月后的2018-02-31不存在”,“2018-01-31 23:59:59与2018-02-01 00:00:00之间的月字段差值为1个月而实际只差1秒”等

其实,实际中一般也不多见以“年,月”为操作维度的对日期时间操作。


参考文献: [1]https://docs.oracle.com/javase/7/docs/api/java/util/TimeZone.html [2]https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
您的支持将鼓励我继续分享!