IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> SimpleDateFormat.parse()方法中的时区设置缺陷 -> 正文阅读

[Java知识库]SimpleDateFormat.parse()方法中的时区设置缺陷

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");  // 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点。

先上结论:SimpleDateFormatparse()会自动地将时间转换为系统默认时区的时间,在我这里,就是东八区时间,所以在前两次的输出中,前三行的时间都是东八区的时间

格式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),调用了DatetoString()方法,查看源码可以发现这样一段:

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()方法的源码,表面上述结论确实是源码的行为:
主要行为有两个:

  1. 日期字符串中给定时区对setTimeZone()方法的覆盖;
  2. 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()
这里的calendarSimpleDateFormat对象的属性,可以发现在这个属性已经初始化,其zone属性就是通过setTimeZone()方法指定的时区
这里调用CalendarBuilderestablish()方法建立calendar,然后再调用CalendargetTime()

establish()第114行cal.clear()先将calfield属性的所有元素置零
第120行cal.set()将CalendarBuilder对象calb中的所有属性设置到Calendar对象cal(也就是SimpleDateFormat对象的calendar属性)的中,其中就包括偏移量的毫秒值,这个值储存在calfield属性中,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值可能需要恢复,以进行更多操作。

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-04-30 08:33:07  更:2022-04-30 08:34:35 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 1:50:14-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码