|
SimpleDateFormat、Date和Calendar这三个类现在很多地方已经不再推荐使用了,JDK8可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat
很多地方都会提到SimpleDateFormat线程不安全,这确实是一个非常严重的问题,但是这几个类的问题或者说设计缺陷不止于此: Date的名字有误导性,它表达的语义其实是一个时刻而不是一个日期; 很多地方隐式地将时区变换为系统默认时区,比如SimpleDateFormat的parse()方法; 月份从0开始(0表示1月),而日期从1开始(1表示1号),很容易混淆。
这次我就遇到了隐式变换时区的问题
先看一段代码
SimpleDateFormat df1 = new SimpleDateFormat("yyyy-MM-dd hh:MM:ss");
SimpleDateFormat df2 = new SimpleDateFormat("yyyy-MM-dd hh:MM:ss");
SimpleDateFormat df3 = new SimpleDateFormat("yyyy-MM-dd hh:MM:ss");
TimeZone timezone1 = TimeZone.getTimeZone("GMT");
TimeZone timezone2 = TimeZone.getTimeZone("GMT+7");
TimeZone timezone3 = TimeZone.getTimeZone("GMT+8");
df1.setTimeZone(timezone1);
df2.setTimeZone(timezone2);
df3.setTimeZone(timezone3);
Date date1 = df1.parse("2012-01-01 23:00:00 +0800");
Date date2 = df2.parse("2012-01-01 23:00:00 +0800");
Date date3 = df3.parse("2012-01-01 23:00:00 +0800");
System.out.println(date1);
System.out.println(date2);
System.out.println(date3);
System.out.println(df1.format(date1));
System.out.println(df2.format(date2));
System.out.println(df3.format(date3));
输出结果
MON JAN 02 07:00:00 CST 2012
MON JAN 02 00:00:00 CST 2012
SUN JAN 01 23:00:00 CST 2012
2012-01-01 11:00:00
2012-01-01 11:00:00
2012-01-01 11:00:00
有没有一头雾水,为什么输入东八区23点,转换成格林尼治时间(GMT)就变成了次日7点?按说是GMT时间加上8个小时才是东八区时间呀,东八区23点应该是GMT15点呀。还有,为什么使用format()方法转换成字符串再输出,又变成了11点?
首先这段代码有些问题,第一个问题是Java日期和时间格式中,hh代表的是12小时制,24小时制应该用HH,所以前面输出的11点其实是晚上11点; 使用格式pattern“yyyy-MM-dd HH:MM:ss”再试一下:
输出结果
MON JAN 02 07:00:00 CST 2012
MON JAN 02 00:00:00 CST 2012
SUN JAN 01 23:00:00 CST 2012
2012-01-01 23:00:00
2012-01-01 23:00:00
2012-01-01 23:00:00
第二个问题是字符串中代表东八区的“+0800”其实并没有被处理,需要在格式pattern后面加上Z代表时区; 使用格式pattern"yyyy-MM-dd HH:MM:ss Z"再试一下:
输出结果
SUN JAN 01 23:00:00 CST 2012
SUN JAN 01 23:00:00 CST 2012
SUN JAN 01 23:00:00 CST 2012
2012-01-01 15:00:00 +0000
2012-01-01 22:00:00 +0700
2012-01-01 23:00:00 +0800
似乎只有最后三行输出解释得通: 东八区23点,相当于GMT15,相当于东七区22点。
先上结论:SimpleDateFormat的parse()会自动地将时间转换为系统默认时区的时间,在我这里,就是东八区时间,所以在前两次的输出中,前三行的时间都是东八区的时间
格式pattern为“yyyy-MM-dd HH:MM:ss”的情况:
此时相当于df.parse("2012-01-01 23:00:00"),日期字符串中没有给出时区,这时df.setTimeZone(timezone)语句所设置的时区生效,相当于分别输入了GMT的23点、东七区的23点和东八区的23点,转换为东八区时间后,分别为次日7点、次日0点和23点; 那为什么后三行输出全部是23点呢,这是由于东八区次日7点转换为GMT时间、东八区次日0点转换为东七区时间和东八区23点转换为和东八区时间,正好都是23点。
格式pattern为“yyyy-MM-dd HH:MM:ss Z”的情况:
对于前三行: 由于日期字符串中给出了时区东八区,这时df.setTimeZone(timezone)语句不生效了,三个东八区23点全部转换为东八区23点,原样输出,我们可以验证这一点 输入东七区23点:
Date date1 = df1.parse("2012-01-01 23:00:00 +0700");
转换为东八区就是次日0点,输出结果和预测一致:
MON JAN 02 00:00:00 CST 2012
对于后三行: 三个东八区23点分别转换为GMT、东七区和东八区时间,结果就是15点、22点和23点。
这种隐式的系统默认时区的转换,真的让人摸不着头脑。 留意到上面输出里的CST了吗,CST的意思是Central Standard Time,即美国中部标准时间,带有CST的输出对应System.out.println(date),调用了Date的toString()方法,查看源码可以发现这样一段:
TimeZone zi = date.getZone();
if (zi != null) {
sb.append(zi.getDisplayName(date.isDaylightTime(), TimeZone.SHORT, Locale.US));
} else {
sb.append("GMT");
}
只要时区不为空,就在后面加上CST,否则加上GMT,不是美国就是格林尼治,还真是粗暴啊,看到其中的isDaylightTime()方法好像还会判断夏令时(因为美国是使用夏冬双时间制的),如果是夏令时,或许还会显示CDT,也就是Central Daylight Time美国中部夏令时
接下来列出一些parse()方法的源码,表面上述结论确实是源码的行为: 主要行为有两个:
- 日期字符串中给定时区对
setTimeZone()方法的覆盖; parse()方法将时间隐式转换为系统默认时区的时间。
第一点 parse(String source)方法 parse(String source)第363行调用parse(String source, ParsePosition pos)方法(SimpleDateFormat实现类) parse(String source, ParsePosition pos)第1514行调用subParse()方法 subParse()第2099行调用subParseNumericZone()方法 如果日期字符串指定了时区,时区就会以偏移量的毫秒值(即偏离GMT时间的毫秒值,如东八区就是+86060*1000=+28800000)的形式储存在CalendarBuilder对象中:
subParseNumericZone()第1791行calb.set()将CalendarBuilder对象的field属性(int数组)的第15个元素设置为8,第33个元素设置为偏移量的毫秒值。 如果日期字符串没有指定时区,第15个元素和第33个元素均为0。
第1532行calb.establish(calendar).getTime() 这里的calendar是SimpleDateFormat对象的属性,可以发现在这个属性已经初始化,其zone属性就是通过setTimeZone()方法指定的时区 这里调用CalendarBuilder的establish()方法建立calendar,然后再调用Calendar的getTime()。
establish()第114行cal.clear()先将cal的field属性的所有元素置零 第120行cal.set()将CalendarBuilder对象calb中的所有属性设置到Calendar对象cal(也就是SimpleDateFormat对象的calendar属性)的中,其中就包括偏移量的毫秒值,这个值储存在cal的field属性中,field是一个int数组,偏移量的毫秒值放在第15个元素中,15用常量ZONE_OFFSET表示。如果日期字符串没有指定时区,那么偏移量的毫秒值为0。
getTime()调用了getTimeMills()方法 getTimeMills()调用了updateTime()方法 updateTime()调用了computeTime()方法(GregorianCalendar实现类) computeTime()方法第2787行注释提到:
We use TimeZone object, unless the user has explicitly set the ZONE_OFFSET or DST_OFFSET; then we use those fields.
我们用TimeZone对象,除非用户显式设置了ZONE_OFFSET或者是DST_OFFSET
这说明ZONE_OFFSET会覆盖TimeZone对象,这里的ZONE_OFFSET就是先前提到的field[15],TimeZone对象就是zone属性。 覆盖具体表现为:到底是ZONE_OFFSET还是TimeZone对象覆盖了zoneOffsets属性的第0个元素,zoneOffsets[0]储存的就是偏移量的毫秒值,这就是日期字符串中给定时区对setTimeZone()方法的覆盖。
zoneOffsets[0]在第2813行会与millis做减法运算,而millis是给定字面时间(即不考虑时区,在这里是23点)转换成当地时区(也就是系统默认时区)时间的毫秒值(这里就是parse()对时间的隐式时区变换) 这个运算等价于将setTimeZone()或日期字符串指定时区的时间转换成系统默认时区的时间。
第二点在parse()方法的注释上也有提到:
This parsing operation uses the calendar to produce a Date. As a result, the calendar’s date-time fields and the TimeZone value may have been overwritten, depending on subclass implementations. Any TimeZone value that has previously been set by a call to setTimeZone may need to be restored for further operations.
该parse操作使用calendar生成一个Date。calendar的date-time字段和TimeZone值可能会被覆盖,这取决于子类的实现。先前通过调用setTimeZone所设置的TimeZone值可能需要恢复,以进行更多操作。
|