滴答、滴答时间在流逝--现在是何时?(二)
前面我们介绍了 Java 中的 Date 和Calendar对象。接下去我们将讨论一些我们平时可能不太注意的一些问题。
时区,我们知道地球是有自转的。因此地球上不同地区的人民为了更方便的表达时间,通常都是以自己所处地区的昼夜变化来定义
24 小时的(我们有时直接使用 12
个小时再加上日夜来表达时间)。通常地区所处的政治实体会定义对时间描述的标准,这通常用时区的概念来表达。因此,我们有了一个基准时区,它所处的时间被称为"格林威治标准平时"(Greenwich
Mediation
Time)。其他地区的计时往往是相对于这个时间的一个小时整数倍数偏差(offset)。从地理上而言,地球就有
24
个时区。但地理上区域的定义往往与政治实体的区域定义不一致,所以在实际的生活中我们所讨论的时区是基于政治实体的区域界定。另外,由于"夏"令时(Daylight
Saving
Time)的引入,人们会人为地调整时区的定义,以至于一个地区在一年中的不同时间相对于
GMT 的偏差是不同的。
那对开发人员而言这会带来何种影响呢?比如我们有一个内部订单系统,在时区A
的部门A通过该系统提交了一个订单要求在某月某日完成(信息存放在一个共享的数据库中)。在时区B
的部门B也通过系统看到了这个订单要求,在做订单安排时可就要对交货日期有一个准确的解释了。对部门A而言,订单的时间基准是时区
A,如果计算机系统没有能够将这个隐含的信息准确地传给在时区 B
的部门,那么就会产生时间描述的歧义了(同一套订单系统分别安装在两个客户环境中,它们如果忽略所处时区的定义或者有着不同的缺省定义就会产生问题)。因此,在
Java 中对时间的内部表述都是基于 GMT
的偏差。这是一个系统设定的隐含条件。
幸运的是中国在行政上对全国各地都定义一个统一的时区,也就是我们通常所说的"北京时间"。而美国大陆本土有
5
个主要时区(Arizona州单独一个时区),再加夏威夷时区和阿拉斯加时区。国土面积不大的澳大利亚竟有
5
个时区。但是如果我们所开发的应用是要考虑支持多个时区共享使用时,我们需要对
Java
的时区要有一个了解。例如,如何将一个时区下定义的时间转换到另一个时区来显示,等等问题。
Java 中定义了TimeZone
对象,它和Calendar一样是个抽象类。Java的SimpleTimeZone对象则提供了真正的实现。
首先我们来看一个简单的需求,将当前时间转换到另一时区的时间:
例如我们将 2001 年 8 月 1 日 0 时 0 分,当前时区下的时间转换成在
GMT 下同一时刻的时间
import java.io.*;
import java.util.*;
public class TimeZoneConversion
{
public static void main(String[] args) throws Exception{
// 这也是获得当前时间的一种方式
Calendar cal1 = Calendar.getInstance();
cal1.set(2001, 7, 1, 0, 0, 0);
// 显示当前的时间
System.out.println(cal1.getTime());
// 显示该 Calendar 的时区
System.out.println(cal1.getTimeZone().getID());
cal1.setTimeZone(TimeZone.getTimeZone("GMT"));
// 似乎这样就可以获得在 GMT 下的时间
System.out.println(cal1.getTime());
System.out.println(cal1.getTimeZone().getID());
}
}
但是结果却是没有改变,看来 Calendar 是基于保存下来的 millisecond
值,而不是根据 TimeZone
的更改而变化的,下面的程序说明了这一点:
import java.io.*;
import java.util.*;
public class TimeZoneConversion
{
public static void main(String[] args) throws Exception{
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
Calendar cal1 = Calendar.getInstance();
cal1.set(2001, 7, 1, 0, 0, 0);
System.out.println(cal1.getTime());
// 在当前的时区下,我们指定了时间是 8 月,显示 7
System.out.println(cal1.get(Calendar.MONTH));
// 在当前的时区下,我们指定了时间是 1 日,显示 1
System.out.println(cal1.get(Calendar.DATE));
// 在当前的时区下,我们指定了时间是 0 时,显示 0
System.out.println(cal1.get(Calendar.HOUR_OF_DAY));
System.out.println(cal1.getTimeZone().getID());
cal1.setTimeZone(TimeZone.getTimeZone("GMT"));
System.out.println(cal1.getTime());
System.out.println(cal1.getTimeZone().getID());
// 由于实际保存的时间没有修改,在 GMT 下,由于时差,显示 6 (7月)
System.out.println(cal1.get(Calendar.MONTH));
// 在 GMT 下,由于时差,显示 31
System.out.println(cal1.get(Calendar.DATE));
// 由于更改了时区,在 GMT 下时间就会变成 16,8个小时时差
System.out.println(cal1.get(Calendar.HOUR_OF_DAY));
}
}
那如果我们是要得到 GMT 下的 2001/8/1 00:00,如何得到呢?
只需要加上一行代码
cal1.set(Calendar.MILLISECOND, 0); // 或者任何值
import java.io.*;
import java.util.*;
public class TimeZoneConversion
{
public static void main(String[] args) throws Exception{
Calendar cal1 = Calendar.getInstance();
cal1.set(2001, 7, 1, 0, 0, 0);
System.out.println(cal1.getTime());
System.out.println(cal1.get(Calendar.MONTH));
System.out.println(cal1.get(Calendar.DATE));
System.out.println(cal1.get(Calendar.HOUR_OF_DAY));
System.out.println(cal1.getTimeZone().getID());
cal1.setTimeZone(TimeZone.getTimeZone("GMT"));
// 如果取消下面代码的注释,结果就和前面的一样
//int save = cal1.get(Calendar.MILLISECOND);
cal1.set(Calendar.MILLISECOND, 0);
System.out.println(cal1.getTime());
System.out.println(cal1.getTimeZone().getID());
System.out.println(cal1.get(Calendar.MONTH));
System.out.println(cal1.get(Calendar.DATE));
System.out.println(cal1.get(Calendar.HOUR_OF_DAY));
}
}
这里要注意的是,如何在修改完Calendar的TimeZone与第一次调用set()方法间,调用了get()/add()等方法,那么
Calendar将锁定其所保存的millisecond值。大家可以自己执行上面的程序来了解这个区别。由于很少有应用程序精确到
MILLISECOND级别,所以我们将其置为0的作法是可以接受的。
然后我们来看另一个非常重要的问题,如何将时间保存到数据库中:
不同的数据库对时间的表示是不同的,这个问题对于开发支持多个数据库的开发商而言,需要额外地注意。我们将在
Timestamp中来介绍这个问题及解决方案。但是它们的一个共性是,它们都忽略TimeZone。当我们将
2001/8/1 00:00 CST保存到数据库时,它的结果就是:"2001-08-01
00:00:00.0"。那么就有一个隐含的问题存在了,如果我们有两个客户一个在中国,将这个时间写入到数据库中;另一个客户在GMT时区下,将这个数据取出,它会得到什么结果呢?
Calendar cal1 = Calendar.getInstance();
cal1.set(2001, 7, 1, 0, 0, 0);
Timestamp ts = new Timestamp(cal1.getTime().getTime());
System.out.println(ts.getTime());
// 获得数据库连接
Connetion conn = …;
Statement stmt = conn.createStatement();
// 执行SQL 语句"update EMPLOYEE set STARTTIME='
2001-08-01 00:00:00.0' where ID=1" stmt.executeUpdate
("update EMPLOYEE set STARTTIME='"+ts+"' where ID=1");
// 我们动态更改JVM的TimeZone来模拟另一客户在不同TimeZone下的访问
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
ResultSet rs = stmt.executeQuery
("select STARTTIME from EMPLOYEE where ID=1");
if (rs.next())
Timestamp ts = rs.getTimestamp(1);
// 结果竟然与前面的不一致
System.out.println(ts.getTime());
也就是说,当JDBC将时间保存到数据库中时,它根据当前JVM的时区将时间传递给数据库保存。当该时间在另一个时区下取出的时候,以该时区还原。而这个
还原不是根据其所表示的MilliSecond来进行的。这个问题对要支持多时区客户端同时访问数据库的同一时间而言是致命的问题。根本的解决方法是,
JDBC程序对数据库中的时间有一个公认的TimeZone。在JVM中我们用GMT时区下的MilliSecond,来同一了对时间的表述。我们可以将
同样的方法拓展到数据库中的保存。我们需要在JDBC上加一层我们自己的API,在这一层中我们将Date/Timestamp先转换成GMT下的表示进
行保存,在取回时基于GMT再转换回来。由于我们对数据库的操作还是要通过低层的JDBC进行,所以我们还要欺骗它(因为它总是使用当前JVM的时区进行
转换的)。
如:2001-08-01 00:00 CST,对应GMT下的时间应为 2001-07-31 16:00
GMT JDBC 保存时该时间时,我们应该要在数据库中得到"2001-07-31
16:00",这样我们在取回时就得到正确的时间表述了。
public static Calendar toGMT(Calendar cal) {
// 也可以用 Calendar cal1 = Calendar.getInstance();
Calendar cal1 = (Calendar)cal.clone();
// 先保存原来的时区
TimeZone tzSave = cal.getTimeZone();
// 将时区转换到GMT下
cal.setTimeZone(TimeZone.getTimeZone("GMT"));
// 将GMT下的年月日等信息再填回到当前时区下的Calendar
cal1.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH)
, cal.get(Calendar.DATE), cal.get(Calendar.HOUR_OF_DAY)
, cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND));
cal1.set(Calendar.MILLISECOND, cal.get(Calendar.MILLISECOND));
// 恢复原来的时区
cal.setTimeZone(tzSave);
// 锁定MilliSecond
cal.getTime();
return cal1;
}
如何从GMT恢复回来的程序就留给大家做个练习,在下一篇中再给出。
在新的JDBC2.0规范中在getTimestamp()和setTimestamp()上加了一个参数,TimeZone。JDBC
Driver应该根据它,将时间转换成GMT下的时间,然后再转回来。但是目前还没有一个厂商的JDBC
Driver支持它。