JAVA如何正确地处理时间时区
⽇期和时间在程序中应⽤⼴泛,每种程序开发语⾔都⾃带处理⽇期和时间的相关函数,很多开发者把⽇期和时间存⼊数据库中,但是,⼀旦涉及到跨时区的⽇期和时间的处理时,⼤多数开发者根本就不明⽩如何正确地处理⽇期和时间。
⾸先,我们来看⼤部分的程序都是这么创建当前时间并存⼊数据库的:
Date date = new Date();
store2db(date);
这么做的问题在于,数据库的DateTime类型没有时区(time zone)信息,因此,存⼊的是本地时间,并且丢掉了时区信息。如果你把数据库服务器的时区改了,或者把应⽤服务器的时区改了,读出来的⽇期和时间就是错误的。如果以Timestamp类型存储,各数据库的实现也不相同,有的进⾏了内部时区⾃动转换,⽽且,存储的时间不超过2037年。
如果应⽤服务器的时区和数据库服务器的时区不⼀致,你⽆法确定数据库驱动程序会不会⾃动帮你转换。
⼤多数开发者遇到这个问题不是去探索正确的解决⽅法,⽽是⾃作聪明地在存⼊数据库之前先来个“调整”,⽐如把当前时间减掉8⼩时,在显⽰的时候遇到不正确的时间时,⼜来个“调整”,以“负负得正”的⽅式来掩盖错误。在遇到夏令时的时区时,还需要写更复杂的代码来调整⼩时。
正确的做法是先理解时间和时区的概念。
时区的概念
之所以有时区的概念是因为住在地球上不同地⽅的⼈看到太阳升起的时间是不⼀样的。我们假设北京⼈民在早上8:00看到了太阳刚刚升起,⽽此刻欧洲⼈民还在夜⾥,他们还需要再过7个⼩时才能看到太阳升起,所以,此刻欧洲⼈民的⼿表上显⽰的是凌晨1:00。如果你强迫他们⽤北京时间那他们每天看到⽇出的时间就是下午3点。
也就是说,东8区的北京⼈民的⼿表显⽰的8:00和东1区欧洲⼈民⼿表显⽰的1:00是相同的时刻:
"2014-10-14 08:00 +8:00" = "2014-10-14 01:00 +1:00"
这就是本地时间的概念。
但是,在计算机中,如果⽤本地时间来存储⽇期和时间,在遇到时区转换的问题上,即便你⾮常清楚地知道如何转换,也⾮常⿇烦,尤其是矫情的美国⼈还在采⽤夏令时。
所以我们需要引⼊“绝对时间”的概念。绝对时间不需要年⽉⽇,⽽是以秒来计时。当前时间是指从⼀个基准时间(1970-1-1 00:00:00 +0:00),到现在的秒数,⽤⼀个整数表⽰。
当我们⽤绝对时间表⽰⽇期和时间时,⽆论服务器在哪个时区,任意时刻,他们⽣成的时间值都是相等的。所有编程语⾔都提供了⽅法来⽣成这个时间戳,Java和JavaScript输出以毫秒计算的Long型整数,Python等输出标准的Unix时间戳,以秒计算的Float型浮点数,这两者转换只存在1000倍的关系。
实际上,操作系统内部的计时器也是这个标准的时间戳,只有在显⽰给⽤户的时候,才转换为字符串格式的本地时间。
正确的存储⽅式
基于“数据的存储和显⽰相分离”的设计原则,我们只要把表⽰绝对时间的时间戳(⽆论是Long型还是Float)存⼊数据库,在显⽰的时候根据⽤户设置的时区格式化为正确的字符串。
数据的存储和显⽰相分离是⾮常基本的设计原则,却常常被⼤多数开发⼈员忽略。举个例⼦,在Excel中编写⼀个表格,表格的数据可视为数据的存储格式,你可以把表格的数据以柱状图或饼图表⽰出来,这些不同的图表是数据的不同显⽰格式,存储数据的时候,我们应该存储表格数据,绝不应该存储柱状图等图⽚信息。
HTML和CSS也是数据的存储和显⽰相分离的设计思想。
所以,数据库存储时间和⽇期时,只需要把Long或者Float表⽰的时间戳存到BIGINT或REAL类型的列中,完全不⽤管数据库⾃⼰提供
的DATETIME或TIMESTAMP,也不⽤担⼼应⽤服务器和数据库服务器的时区设置问题,遇到Oracle数据库你不必去理会with
timezone和with local timezone到底有啥区别。
读取时间时,读到的是⼀个Long或Float,只需要按照⽤户的时区格式化为字符串就能正确地显⽰出来:
// Java:
long t = System.currentTimeMillis();
System.out.println("long = " + t);
// current time zone:
SimpleDateFormat sdf_default = new SimpleDateFormat("yyyy-MM-dd HH:mm");
System.out.println(sdf_default.format(t));
// +8:00 time zone:
SimpleDateFormat sdf_8 = new SimpleDateFormat("yyyy-MM-dd HH:mm");
sdf_8.TimeZone("GMT+8:00"));
System.out.println("GMT+8:00 = " + sdf_8.format(t));
// +7:00 time zone:
SimpleDateFormat sdf_7 = new SimpleDateFormat("yyyy-MM-dd HH:mm");
sdf_7.TimeZone("GMT+7:00"));
System.out.println("GMT+7:00 = " + sdf_7.format(t));
// -9:00 time zone:
SimpleDateFormat sdf_la = new SimpleDateFormat("yyyy-MM-dd HH:mm");
sdf_la.TimeZone("America/Los_Angeles"));
System.out.println("America/Los_Angeles = " + sdf_la.format(t));
输出:
long = 1413230086802
2014-10-1403:54
GMT+8:00 = 2014-10-1403:54
GMT+7:00 = 2014-10-1402:54
America/Los_Angeles = 2014-10-1312:54
基于绝对时间戳的时间存储,从根本上就没有时区的问题。时区只是⼀个显⽰问题。额外获得的好处还包括:
两个时间的⽐较就是数值的⽐较,根本不涉及时区问题,极其简单;
时间的筛选也是两个数值之间筛选,写出SQL就是between(?, ?);
显⽰时间时,把Long或Float传到页⾯,⽆论⽤服务端脚本还是⽤JavaScript都能简单⽽正确地显⽰时间。
你唯⼀需要编写的两个辅助函数就是String->Long和Long->String。String->Long的作⽤是把⽤户输⼊的时间字符串按照⽤户指定时区转换成Long存进数据库。
unix时间戳转换日期格式唯⼀的缺点是数据库查询你看到的不是时间字符串,⽽是类似1413266801750之类的数字。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论