Making Sense of Java's Dates
by Philipp K. Janert, Ph.D. translated by humx
06/04/2003
简介
在计算机程序中精确的处理日期是困难的。不仅有显而易见的(英语: January, 法语: Janvier, 德语: Januar, 等)国际化需求, 而且得考虑不同的日期系统(并非所有的文化都用基督耶稣的生日作为纪年的开始)。如有高精度或非常大规模的时间需要被处理, 就有额外的方面需要被注意,比如闰秒或时间系统的变化。(公历(阳历, 格里高利历法)在西方被普遍接受是在1582年,但并非所有的国家在同一天接受!)
尽管有关于闰秒, 时区, 夏令时, 阴历的问题, 度量时间却是一个非常简单的概念: 时间的进行是线性的很容易被忽略。一旦时间轴的区域被定义, 任何时间点被从起点时间的流逝就可以确定。这和地理位置或当地时区是独立的 – 对任意指定的时间点, 对任意地区, 从起点的过程是相同的(忽略相对论的矫正)。
可当我们试图根据某些日历解释这一时间点的时候困难来了, 比如, 根据月, 日, 或者年来表示它。在这一步上地理信息变得相关: 在时间上的同一个点对应不同的天的某一时间, 依赖于区域 (比如: 时区)。基于解释日期的修正经常是必要的(今天一个月以后是哪一天?) 并且增加了额外的困难: 上溢和下溢(12月15号的后一个月是下一年), 且不明确(1月30号后的一个月是哪一天?).
在最初的JDK 1.0, 一个时间点, 通过把它解释为java.util.Date类, 它被计算在一起来表示. 虽然相对容易处理, 但它并不支持国际化; 从JDK 1.1.4 或JDK 1.1.5, 多样的负责处理日期的职责被分配到以下类中:
|
java.util.Date |
代表一个时间点. |
|
abstract java.util.Calendar java.util.GregorianCalendar extends java.util.Calendar |
解释和处理Date. |
|
abstract java.util.TimeZone java.util.SimpleTimeZone extends java.util.TimeZone |
代表一个任意的从格林威治的偏移量, 也包含了适用于夏令时(daylight savings rules)的信息. |
|
abstract java.text.DateFormat extends java.text.Format java.text.SimpleDateFormat extends java.text.DateFormat |
变形到格式良好的, 可打印的String, 反之亦然. |
|
java.text.DateFormatSymbols |
月份, 星期等的翻译, 作为从Locale取得信息的一种替代选择. |
|
java.sql.Date extends java.util.Date java.sql.Time extends java.util.Date java.sql.Timestamp extends java.util.Date |
代表时间点, 同时为了在sql语句中使用也包含适当的格式. |
注意: DateFormat 和相关的类在java.text.*包. 所有的java.sql.*包中日期处理相关类继承了java.util.Date类. 所有的其它类在java.util.*包中.
这些"新"类来自三个分离的继承层次, 其顶层类(Calendar, TimeZone, and DateFormat)是抽象的. 针对每一个抽象类, Java标准类库提供了一个具体的实现.
java.util.Date
类java.util.Date代表一个时间点. 在许多应用中, 此种抽象被称为"TimeStamp." 在标准的Java类库实现中, 这个时间点代表Unix纪元January 1, 1970, 00:00:00 GMT开始的毫秒数. 因而概念上来说, 这个类是long的简单封装.
根据此种解释, 类中仅有的没有过期的(除了那些毫秒数的get和set方法)是那些排序方法.
这个类依靠System.currentTimeMillis() 来取得当前的时间点. 因此它的准确度和精度由System的实现和它所调用底层(本质是操作系统)决定.
|
The java.util.Date API
在最初的 Date类使用中名字和约定引起了无尽的混淆. 然而用0-11计算月, 从1900计算年的决定模仿了C标准类库的习惯, 调用函数 getTime()返回起始于Unix纪元的毫秒数和 getDate()返回星期的决定显然是Java类设计者自己的. |
java.util.Calendar
语义
Calendar代表一个时间点(一个"Date"), 用以在特定的区域和时区适当的解释器. 每一个Calendar 实例有一个包含了自纪元开始的代表时间点的long变量.
这意味着Calendar 不是一个(无状态) 变换者或解释器, 也不是一个修改dates的工厂. 它不支持如下方式:
Month Interpreter.getMonth(inputDate) or
Date Factory.addMonth(inputDate)
Instead, Calendar实例必须被初始化到特定的Date. 此Calendar实例可以被修改或查询interpreted属性.
奇怪的是, 此类的instances 总是被初始化为当前时间. 获得一个初始化为任意Date的Calendar 实例是不可能的—API强制程序员通过一系列的在实例上的方法调用, 比如setTime(date)来显式的设置日期.
访问Interpreted 字段和类常量
Calendar类遵从一不常用的方式来访问interpreted date实例的单个字段. 而不是提供一些dedicated属性 getters和setters方法(比如getMonth()), 它仅提供了一个, 使用一个标示作为参数来获取请求的属性的方法:
int get(Calendar.MONTH) 等等.
注意这个函数总是返回一个int!
这些字段的标示符被定义为Calendar类的public static final变量. (这些identifiers是raw的整数, 没有被封装为一个枚举抽象.)
除了对应这些字段标示(键值), Calendar 类定义了许多附加的public static final 变量来保存这些字段的值. 因此, 为测试某一特定date (由Calendar 的实例calendar表示) 是否在一年的第一个月, 会有像如下的代码:
if (calendar.get(Calendar.MONTH) == Calendar.JANUARY) {...}
注意月份被叫做 JANUARY, FEBRUARY, 等等, 不管location(相对更中性的名字比如: MONTH_1, MONTH_2, 等等). 也有一个字段UNDECIMBER, 被一些(非公历(阳历, 格里高利历法))日历使用, 代表一年的第十三个月.
不幸的是, 键值和值既没有通过名字也没分组成嵌套的inerface来区分.
处理
Calendar提供了三种办法来修改当前实例代表的日期: set(), add(), 和roll(). set()方法简单的设置特定的字段为期望的值. add() 和 roll() 的不同在于它们处理over- and underflows: add() 传递变更到"较小"或"较大"的字段, 而roll()不影响其它字段. 比如, 当给代表12月15号的Calendar实例增加一个月, 当add()使用年会增加, 但使用roll()不会发生任何变化. 为每一种case对应一个函数的决定的动机是, 它们可能在GUI中不同的使用情形.
由于Calendar的实现的方式, 它包含冗余的数据: 所有的字段都可以从给定的时区和纪元开始的毫秒数计算出来,反之亦然. 这个类为这些操作分别定义了抽象方法computeFields()和