<script type="text/javascript"> google_ad_client = "pub-8800625213955058"; /* 336x280, 创建于 07-11-21 */ google_ad_slot = "0989131976"; google_ad_width = 336; google_ad_height = 280; // </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
探索 Java 中的 Date, Calendar, TimeZone 和Timestamp 对象
宋晟 (shengsong@hotmail.com)
2001 年 11 月
如何计时,自古以来就是一个有着各种答案的问题。中国人从习惯使用农历计时,到选择便于与世界沟通的公元计时。在生活中这两者又一起使用,这已经融入到文化中。在计算机世界里,计时也是一个丰富多采的话题。各个系统各有一套方法,例如 2000 年引起全世界关注的"千年虫",就是因为计时系统设计上的限制而产生的。那么 Java 作为一种通用编程语言和运行平台,它的计时系统是如何设计的,我们在应用程序的开发中又如何驾驭它呢?对象的状态总是要被储存起来的,那么我们又要注意哪些情况呢?
1 最常用的对象(概念最易被混逍的对象) Date
在 Java 中,如何获得系统当前的时间?恐怕一个合格的 Java 程序员可以列举出至少下面这种:
Date date = new Date();
// 打印出具体的年,月,日,小时,分钟,秒钟 以及时区
System.out.println(date);
看起来 Date 这个对象包含了相当丰富的时间信息。那么我们能不能用它来构件任何一个指定的时间呢?很多的应用程序是需要用户输入一个特定的时间。
// 我们能不能用下面的代码构件出 2001/8/8 8:8
import java.io.*;
import java.util.*;
public class WhatIsDate
{
public static void main(String[] args) {
Date date = new Date(2001, 8, 8, 8, 8, 8);
System.out.println(date);
}
}
|
Java 的编译器竟然报如下信息 (Sun JDK1.3, Windows 2000 中文下)
注意:
WhatIsDate.java 使用或覆盖一个不鼓励使用的API。
注意:
使用-deprecation重新编译,以得到详细信息。!
|
我们使用 javac -deprecation WhatIsDate.java, 得到如下信息:
WhatIsDate.java:7: 警告:
在java.util.Date中的Date(int,int,int,int,int,int)已经不提倡使用
Date date = new Date(2001, 8, 8, 8, 8, 8);
^
1 warning
|
那么 Date 对象究竟是为了满足哪个需求呢?看来它不是用来实现基于年/月/日 小时:分钟 的时间表述。我们查看 Java 的文档,我们看到有 getTime() 方法,它返回的竟然是一个 long 值。
System.out.println(date.getTime());
文档进一步又告诉我们这个值代表了当前系统的时间离1970/1/1 0:0 的毫秒差,而且是在 GMT 时区下(也被称为 EPOC)。如果我们指定的时间是在此之前的,那它将返回一个负数值。
这个发现让我们对 Date 对象有了一个全新的认识-Date 存放的是与 EPOC 的偏差值。换而言之我们也可通过 long 类型来表示时间?对了,这个猜想是得到了 Java 的支持:
// 第二种获得当前时间的方法
long dateInMilliSeconds = System.currentTimeMillis();
// 这时候打印出的只是一串数字而已
System.out.println(dateInMilliSeconds);
|
对程序执行效率敏感的程序员可以发现这个方法只是生成一个 Java 的原始类型 (primitive type) long, 不需要实例化一个对象。因此如果我们对时间的处理只是在内部进行时,可以用 long 来代替 Date 对象。
最典型的应用就是在一段代码开始和结束时,分别获得系统当前的时间,然后计算出代码执行所需的时间(微秒级)。
long start = System.currentTimeMillis();
// 代码段
System.out.println("需要 " (System.currentTimeMillis()-start) " 微秒");
|
那么当我们要把这个 long 值已更为友好的表现形式显示处理的时候,我们可以用它来构造 Date 对象:
Date date = new Date(dateInMilliSeconds);
System.out.println(date);
我们看到了在 Java 中对时间最为基本的表示,有通过对EPOC 的偏差值进行处理。Date 对象是对它的一个对象的封装。我们同时也看到了,在现时世界中我们对时间的描述通常是通过"某年某月某日某时某分"来定义的。Date 的显示(实际上是 toString() 方法)描述了这些信息,但 Java 并不建议我们用这种方式直接来构件 Date 对象。因此我们需要找出哪个对象可以实现这个需求。这就是我们下面就要讲述的 Calendar 对象的功能。
在我们进一步研究 Calendar 之前,请记住 Date 只是一个对 long 值(基于 GMT 时区)的对象封装。它所表现出来的年/月/日 小时:分钟 时区的时间表述,只是它的 toString() 方法所提供的。千万不要为这个假象所迷惑。
2 强大的 Calendar
首先请记住 Calendar 只是一个抽象类, 也就是说你无法直接获得它的一个实例,换而言之你可以提供一个自己开发的 Calendar 对象。
那究竟什么是一个 Calendar 呢?中文的翻译就是日历,那我们立刻可以想到我们生活中有阳(公)历、阴(农)历之分。它们的区别在哪呢?
比如有:
月份的定义 - 阳`(公)历 一年12 个月,每个月的天数各不同;阴(农)历,每个月固定28天
每周的第一天 - 阳(公)历 星期日是第一天;阴(农)历,星期一是第一天
实际上,在历史上有着许多种纪元的方法。它们的差异实在太大了,比如说一个人的生日是"八月八日" 那么一种可能是阳(公)历的八月八日,但也可以是阴(农)历的日期。所以为了计时的统一,必需指定一个日历的选择。那现在最为普及和通用的日历就是 "Gregorian Calendar"。也就是我们在讲述年份时常用 "公元几几年"。Calendar 抽象类定义了足够的方法,让我们能够表述日历的规则。Java 本身提供了对 "Gregorian Calendar" 规则的实现。我们从 Calendar.getInstance() 中所获得的实例就是一个 "GreogrianCalendar" 对象(与您通过 new GregorianCalendar() 获得的结果一致)。
下面的代码可以证明这一点:
import java.io.*;
import java.util.*;
public class WhatIsCalendar
{
public static void main(String[] args) {
Calendar calendar = Calendar.getInstance();
if (calendar instanceof GregorianCalendar)
System.out.println("It is an instance of GregorianCalendar");
}
}
|
Calendar 在 Java 中是一个抽象类(Abstract Class),GregorianCalendar 是它的一个具体实现。
我们也可以自己的 Calendar 实现类,然后将它作为 Calendar 对象返回(面向对象的特性)。在 IBM alphaWorks 上,IBM 的开发人员实现了多种日历(http://www.alphaworks.ibm.com/tech/calendars)。同样在 Internet 上,也有对中国农历的实现。本文对如何扩展 Calendar 不作讨论,大家可以通过察看上述 Calendar 的源码来学习。
Calendar 与 Date 的转换非常简单:
Calendar calendar = Calendar.getInstance();
// 从一个 Calendar 对象中获取 Date 对象
Date date = calendar.getTime();
// 将 Date 对象反应到一个 Calendar 对象中,
// Calendar/GregorianCalendar 没有构造函数可以接受 Date 对象
// 所以我们必需先获得一个实例,然后设置 Date 对象
calendar.setTime(date);
|
Calendar 对象在使用时,有一些值得注意的事项:
1. Calendar 的 set() 方法
set(int field, int value) - 是用来设置"年/月/日/小时/分钟/秒/微秒"等值
field 的定义在 Calendar 中
set(int year, int month, int day, int hour, int minute, int second) 但没有
set(int year, int month, int day, int hour, int minute, int second, int millisecond) 前面 set(int,int,int,int,int,int) 方法不会自动将 MilliSecond 清为 0。
另外,月份的起始值为0而不是1,所以要设置八月时,我们用7而不是8。
calendar.set(Calendar.MONTH, 7);
我们通常需要在程序逻辑中将它清为 0,否则可能会出现下面的情况:
import java.io.*;
import java.util.*;
public class WhatIsCalendarWrite
{
public static void main(String[] args) throws Exception{
ObjectOutputStream out =
new ObjectOutputStream(
new FileOutputStream("calendar.out"));
Calendar cal1 = Calendar.getInstance();
cal1.set(2000, 7, 1, 0, 0, 0);
out.writeObject(cal1);
Calendar cal2 = Calendar.getInstance();
cal2.set(2000, 7, 1, 0, 0, 0);
cal2.set(Calendar.MILLISECOND, 0);
out.writeObject(cal2);
out.close();
}
}
|
我们将 Calendar 保存到文件中
import java.io.*;
import java.util.*;
public class WhatIsCalendarRead
{
public static void main(String[] args) throws Exception{
ObjectInputStream in =
new ObjectInputStream(
new FileInputStream("calendar.out"));
Calendar cal2 = (Calendar)in.readObject();
Calendar cal1 = Calendar.getInstance();
cal1.set(2000, 7, 1, 0, 0, 0);
if (cal1.equals(cal2))
System.out.println("Equals");
else
System.out.println("NotEqual");
System.out.println("Old calendar " cal2.getTime().getTime());
System.out.println("New calendar " cal1.getTime().getTime());
cal1.set(Calendar.MILLISECOND, 0);
cal2 = (Calendar)in.readObject();
if (cal1.equals(cal2))
System.out.println("Equals");
else
System.out.println("NotEqual");
System.out.println("Processed Old calendar " cal2.getTime().getTime());
System.out.println("Processed New calendar " cal1.getTime().getTime());
}
}
|
然后再另外一个程序中取回来(模拟对数据库的存储),但是执行的结果是:
NotEqual
Old calendar 965113200422 <------------ 最后三位的MilliSecond与当前时间有关
New calendar 965113200059 <-----------/
Equals
Processed Old calendar 965113200000
Processed New calendar 965113200000
|
另外我们要注意的一点是,Calendar 为了性能原因对 set() 方法采取延缓计算的方法。在 JavaDoc 中有下面的例子来说明这个问题:
Calendar cal1 = Calendar.getInstance();
cal1.set(2000, 7, 31, 0, 0 , 0); //2000-8-31
cal1.set(Calendar.MONTH, Calendar.SEPTEMBER); //应该是 2000-9-31,也就是 2000-10-1
cal1.set(Calendar.DAY_OF_MONTH, 30); //如果 Calendar 转化到 2000-10-1,那么现在的结果就该是 2000-10-30
System.out.println(cal1.getTime()); //输出的是2000-9-30,说明 Calendar 不是马上就刷新其内部的记录
|
在 Calendar 的方法中,get() 和 add() 会让 Calendar 立刻刷新。Set() 的这个特性会给我们的开发带来一些意想不到的结果。我们后面会看到这个问题。
2. Calendar 对象的容错性,Lenient 设置
我们知道特定的月份有不同的日期,当一个用户给出错误的日期时,Calendar 如何处理的呢?
import java.io.*;
import java.util.*;
public class WhatIsCalendar
{
public static void main(String[] args) throws Exception{
Calendar cal1 = Calendar.getInstance();
cal1.set(2000, 1, 32, 0, 0, 0);
System.out.println(cal1.getTime());
cal1.setLenient(false);
cal1.set(2000, 1, 32, 0, 0, 0);
System.out.println(cal1.getTime());
}
}
|
它的执行结果是:
Tue Feb 01 00:00:00 PST 2000
Exception in thread "main" java.lang.IllegalArgumentException
at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:1368)
at java.util.Calendar.updateTime(Calendar.java:1508)
at java.util.Calendar.getTimeInMillis(Calendar.java:890)
at java.util.Calendar.getTime(Calendar.java:871)
at WhatIsCalendar.main(WhatIsCalendar.java:12)
|
当我们设置该 Calendar 为 Lenient false 时,它会依据特定的月份检查出错误的赋值。
3. 不稳定的 Calendar
我们知道 Calendar 是可以被 serialize 的,但是我们要注意下面的问题
import java.io.*;
import java.util.*;
public class UnstableCalendar implements Serializable
{
public static void main(String[] args) throws Exception{
Calendar cal1 = Calendar.getInstance();
cal1.set(2000, 7, 1, 0, 0 , 0);
cal1.set(Calendar.MILLISECOND, 0);
ObjectOutputStream out =
new ObjectOutputStream(
new FileOutputStream("newCalendar.out"));
out.writeObject(cal1);
out.close();
ObjectInputStream in =
new ObjectInputStream(
new FileInputStream("newCalendar.out"));
Calendar cal2 = (Calendar)in.readObject();
cal2.set(Calendar.MILLISECOND, 0);
System.out.println(cal2.getTime());
}
}
|
运行的结果竟然是: Thu Jan 01 00:00:00 PST 1970
它被复原到 EPOC 的起始点,我们称该 Calendar 是处于不稳定状态。这个问题的根本原因是 Java 在 serialize GregorianCalendar 时没有保存所有的信息,所以当它被恢复到内存中,又缺少足够的信息时,Calendar 会被恢复到 EPOCH 的起始值。Calendar 对象由两部分构成:字段和相对于 EPOC 的微秒时间差。字段信息是由微秒时间差计算出的,而 set() 方法不会强制 Calendar 重新计算字段。这样字段值就不对了。
下面的代码可以解决这个问题:
import java.io.*;
import java.util.*;
public class StableCalendar implements Serializable
{
public static void main(String[] args) throws Exception{
Calendar cal1 = Calendar.getInstance();
cal1.set(2000, 7, 1, 0, 0 , 0);
cal1.set(Calendar.MILLISECOND, 0);
ObjectOutputStream out =
new ObjectOutputStream(
new FileOutputStream("newCalendar.out"));
out.writeObject(cal1);
out.close();
ObjectInputStream in =
new ObjectInputStream(
new FileInputStream("newCalendar.out"));
Calendar cal2 = (Calendar)in.readObject();
cal2.get(Calendar.MILLISECOND); //先调用 get(),强制 Calendar 刷新
cal2.set(Calendar.MILLISECOND, 0); //再设值
System.out.println(cal2.getTime());
}
}
|
运行的结果是: Tue Aug 01 00:00:00 PDT 2000
这个问题主要会影响到在 EJB 编程中,参数对象中包含 Calendar 时。经过 Serialize/Deserialize 后,直接操作 Calendar 会产生不稳定的情况。
4. add() 与 roll() 的区别
add() 的功能非常强大,add 可以对 Calendar 的字段进行计算。如果需要减去值,那么使用负数值就可以了,如 add(field, -value)。
add() 有两条规则:
当被修改的字段超出它可以的范围时,那么比它大的字段会自动修正。如:
Calendar cal1 = Calendar.getInstance();
cal1.set(2000, 7, 31, 0, 0 , 0); //2000-8-31
cal1.add(Calendar.MONTH, 1); //2000-9-31 => 2000-10-1, 对吗?
System.out.println(cal1.getTime()); //结果是 2000-9-30
另一个规则是,如果比它小的字段是不可变的(由 Calendar 的实现类决定),那么该小字段会修正到变化最小的值。
以上面的例子,9-31 就会变成 9-30,因为变化最小。
Roll() 的规则只有一条:
当被修改的字段超出它可以的范围时,那么比它大的字段不会被修正。 如:
Calendar cal1 = Calendar.getInstance();
cal1.set(1999, 5, 6, 0, 0, 0); //1999-6-6, 周日
cal1.roll(Calendar.WEEK_OF_MONTH, -1); //1999-6-1, 周二
cal1.set(1999, 5, 6, 0, 0, 0); //1999-6-6, 周日
cal1.add(Calendar.WEEK_OF_MONTH, -1); //1999-5-30, 周日
WEEK_OF_MONTH 比 MONTH 字段小,所以 roll 不能修正 MONTH 字段。
|
由于 Calendar 对象可以将一个时间转化成我们所需要的不同字段信息,可以帮助我们的开发任务。