理解 Java 时间, 日期

关于时间,在JavaDoc中谈论比较多文字的是UTC、UT、GMT、TimeZone等

下面是科学的对它们的简单解释。 
UTC:科学纪年,时间采自原子时钟。在每过一两年会有一个跳秒,在某个跳点,一分钟有61秒
UT: GMT格林威治时间的科学学名,取自天文学观测。GMT 是标准的“民间”名称;UT 是相同标准的“科学”名称

但java中, GMT的意思是有所不同的。 文中会介绍它们的意义。

UTC
如果我们要回答 "when" 而不是 “30秒之后”, 我们需要一种方法能够表示任何一个时间点。 我们可以通过使用时间刻度 -- 就像尺子 -- 达到我们的目的。在一把尺子的范围内, 它有零刻度, 正刻度(可能也有负刻度)。对于时间, 我们可以使用相同的方法。 我们任意定义一个时间为 “0”。 通过这个零点, 我们可以表示将来的任意时间点, 当然也可以表示过去的时间点。 这就是 UTC (Coordinated Universal Time)所以做的功能。  通过它, 我们能给每一秒命名。例如 "2010-1-13 16:34:58.000 UTC". UTC总是增长的, 它从不会返回。 有时候,一分钟并不总是60秒, 可能为59秒或者61秒。 我们把这些秒叫做跳秒, 他们用于
纠正时间。

通过操作系统表示时间

在IT系统里, 有时间, 我们需要准确的表示一个时间点。 比如
  
  •    文件上次更改的时间
  •    启动一个设备
  •    什么时候发送的邮件

  
     这些时间点, 都有一个事件的特征。 如果两个时间是在同一时刻发生的。 不管一个是发生在上海, 另一个发生在伦敦。 它们都是发生在同一时刻的。与时区没有关系。 我们经常使用操作系统时间去表示。操作系统的时间戳是一个从1970-1-1 UTC 零点到现在的整型数字, Java中的java.util.Date 类有效的封装了这个时间戳。它跟时区是没有关系的。
注意:Date 的 toString 方法会用JVM所在系统的时区把时间打印出来。

时区 -- Time Zones


您肯定听说过时区, 但您真正理解时区的意义吗? 把他们理解为一种测量时间的单位。就像测量长度的单位。 1米的长度是1000毫米。不管你说成1米或者1000毫米,长度是不会变化的。对于时间也是一样。 一个确切的时间点不会因为在不用的时区下而变化。只是表达不同而已。所有时区的母亲是 Greenwich Mean Time (GMT).  GMT是UTC使用的时区。 所有时区的偏移量, 都是通过与GMT的偏移来表示的。 往东的时区有正的偏移量, 那么往西的话就是负的偏移量了。同样我们还可以跟长度比较。 米是所有长度单位的母亲。
1mm = 1/1000m, 1km=1000m.
转为时区的话, 也是同样的道理:
18:00GM = 18:00+00:00 = 19:00+01:00 = 17:00-01:00

Java 中java.util.TimeZone能够表示两个内容。

  • A time zone
  • A time zone database of a location


       时区不是通过天文学家划分的, 而是政治家。这些划分会变化。 这就是为什么许多城市保留一个历史时区数据库。  许多国家在一年中会跟改时区两次: 夏令时(daylight savings time) 只是一个time zone.   许多关于时间的应用中, 通过TimeZone.getTimeZone("Europe/Paris")使用a location database。 这样可以解决
夏令时中的很多问题。 当然了, 应用也可以简单的使用确切地时区去获得TimeZone: TimeZone.getTimeZone("GMT+04:30").

        在java.util.Calendar和java.text.DateFormat正确的使用 time zone是对应用中时区的安全问题至关重要的。应用的时间国际化, 不仅仅是支持不同的LocaleS. 同样需要支持不同的TimeZoneS. 所有, 应用中Locale依赖的,  同样也是TimeZone依赖的。需要注意的一个地方是TimeZone.getTimeZone()不会因为不认识传入的时区名字而抛出Exception, 而仅仅是返回GMT.如果一个时区的名字来自于不安全的地方,比如用户输入界面, 那么我们需要验证时区名字是否正确。

       下面我们来研究Java API 中的时间类.

       java.util.Date, java.sql.Date 和java.sql.Timestamp 的关系


      通过前面的介绍,我们知道java.util.Date是对UTC时间的一个封装。来看看它的构造器
  public Date() {
        this(System.currentTimeMillis());
    }

     其实, 就是一个long型数据的封装。
     而sql.Date和sql,Timestamp 是对util.Date的包装 (继承)。与sql中的date和timestamp一致, 可以直接插入数据库。
     这里说下Timestamp. 此类型由 java.util.Date 和单独的毫微秒值组成。只有整数秒才会存储在 java.util.Date 组件中。小数秒(毫微秒)是独立存在的。传递不是 java.sql.Timestamp 实例的对象时,Timestamp.equals(Object) 方法永远不会返回 true,因为日期的毫微秒组件是未知的。因此,相对于 java.util.Date.equals(Object) 方法而言,Timestamp.equals(Object) 方法是不对称的。此外,hashcode 方法使用底层 java.util.Date 实现并因此在其计算中不包括毫微秒。
鉴于 Timestamp 类和上述 java.util.Date 类之间的不同,建议代码一般不要将 Timestamp 值视为 java.util.Date 的实例。Timestamp 和 java.util.Date 之间的继承关系实际上指的是实现继承,而不是类型继承。

       由于Date其实是个long型数字,  但人们往往需要日历型去表示时间。 当然,Calendar为我们封装了很多方便的方法。 其实Date也封装了获得日期的方法, 但已被废弃不用了。 这时,我们可以通过DateFormat来format  Date.
         下面的例子, 用不同国家的默认格式显示时间。
                Date date = new Date();
		Locale localeEN = Locale.US;
		Locale localeCH = Locale.CHINA;

		// Get a date time formatter for display in China.
		DateFormat fullDateFormatCH = DateFormat.getDateTimeInstance(
				DateFormat.FULL, DateFormat.FULL, localeCH);

		// Get a date time formatter for display in the U.S.
		DateFormat fullDateFormatEN = DateFormat.getDateTimeInstance(
				DateFormat.FULL, DateFormat.FULL, localeEN);
		
		System.out.println("Locale: " + localeCH.getDisplayName());
		System.out.println(fullDateFormatCH.format(date));
		System.out.println("Locale: " + localeEN.getDisplayName());
		System.out.println(fullDateFormatEN.format(date));

输出的结果是:
Locale: Chinese (China)
2011年1月13日 星期四 下午05时54分23秒 CST
Locale: English (United States)
Thursday, January 13, 2011 5:54:23 PM CST
         
         由于使用默认TimeZone, 我们跟美国日期一样。 下面,我们加上不同TimeZone, 来看看美国和GMT时区的这个时候日期是多少。
Date date = new Date();
		Locale localeEN = Locale.US;
		Locale localeCH = Locale.CHINA;
		Locale locale = Locale.ENGLISH;

		// Get a date time formatter for display in China.
		DateFormat fullDateFormatCH = DateFormat.getDateTimeInstance(
				DateFormat.FULL, DateFormat.FULL, localeCH);

		// Get a date time formatter for display in the U.S.
		DateFormat fullDateFormatEN = DateFormat.getDateTimeInstance(
				DateFormat.FULL, DateFormat.FULL, localeEN);
		
		DateFormat fullDateFormat = DateFormat.getDateTimeInstance(
				DateFormat.FULL, DateFormat.FULL, locale);
		
		TimeZone timeZoneChina = TimeZone.getDefault();
		TimeZone timeZoneGMT = TimeZone.getTimeZone("GMT");
		TimeZone timeZoneUS = TimeZone.getTimeZone("America/Whitehorse");

		fullDateFormatCH.setTimeZone(timeZoneChina);
		fullDateFormatEN.setTimeZone(timeZoneUS);
		fullDateFormat.setTimeZone(timeZoneGMT);
		
		System.out.println("  " + timeZoneChina.useDaylightTime() );
		System.out.println(" China " + fullDateFormatCH.format(date));
		System.out.println(" US " + fullDateFormatEN.format(date));
		System.out.println(" GMT " + fullDateFormat.format(date));

输出的结果是:
China     2011年1月13日 星期四 下午06时14分35秒 CST
US          Thursday, January 13, 2011 2:14:35 AM PST
GMT       Thursday, January 13, 2011 10:14:35 AM GMT

          把String转化为日期, 这在GUI或者XML相关传递时间中经常见到。 我们可以使用DateFormat的一个默认实现SimpleDateFormat来完成我们的需求。 例如:
	       DateFormat indfm = new SimpleDateFormat("MM/dd/yyyy HH'h'mm");
		indfm.setTimeZone(TimeZone.getTimeZone("America/Whitehorse"));
		Date purchaseDate = indfm.parse("12/31/2007 20h15");

		DateFormat outdfm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		outdfm.setTimeZone(TimeZone.getTimeZone("GMT"));
		System.out.println ( outdfm.format(purchaseDate) +" GMT");

输出的结果为:
2008-01-01 04:15:00 GMT

下图是常用的日期和时间模式:
理解 Java 时间, 日期_第1张图片
模式字母的意义:
理解 Java 时间, 日期_第2张图片
这里介绍个取得正确TimeZone的简单技巧。 我们往往知道一个国家的时区, 但不知道这个时区的名字。 这时,我们可以简单的遍历下这个时区的所有名字来获得合法的名字。

//+8 * 60 * 60 * 1000  或者 -7 * 60 * 60 * 1000
String[] ids = TimeZone.getAvailableIDs(0 * 60 * 60 * 1000);
		if (ids.length == 0)
			System.exit(0);

		for (int i = 0; i < ids.length; i++) {
			System.out.println("ids: " + ids[i]);
		}


java.util.Calendar

这是语言环境敏感类,包含了TimeZone 信息. Calendar 提供了一个类方法 getInstance,以获得此类型的一个通用的对象。Calendar 的 getInstance 方法返回一个 Calendar 对象,其日历字段已由当前日期和时间初始化:

     Calendar rightNow = Calendar.getInstance();
Calendar 对象能够生成为特定语言和日历风格实现日期-时间格式化所需的所有日历字段值,例如,日语-格里高里历,日语-传统日历。Calendar 定义了某些日历字段返回值的范围,以及这些值的含义。例如,对于所有日历,日历系统第一个月的值是 MONTH == JANUARY。其他值是由具体子类(例如 ERA)定义的。

      认识下日历
1.
Calendar now = Calendar.getInstance();
		TimeZone timeZoneUS = TimeZone.getTimeZone("America/Whitehorse");
		Calendar us_now =  new GregorianCalendar(timeZoneUS);
		System.err.println(" China timestamp: " + now.getTimeInMillis());
		System.err.println(" U timestamp: " + us_now.getTimeInMillis());
		
		System.err.println(" China hour: " + now.get(Calendar.HOUR_OF_DAY));
		System.err.println(" US Hour: " + us_now.get(Calendar.HOUR_OF_DAY));

输出结果是:
China timestamp: 1294915267581
U timestamp: 1294915267581
China hour: 18
US Hour: 2

Calendar.getTimeInMillis() 方法,返回的是UTC时间戳, 所有, 不管处于哪个时区, 这个时间戳是相同的。
但是, Calendar.get(int field) 方法, 可返回当地时区的时间表示。 不用时区的话, 返回的值肯定是不同的。 我们此时是下午6点, 美国是凌晨2点。

那么,日期中的这些年,月,日, 小时,分钟,秒, 毫秒是怎样计算出来的。 因为TimeZone拥有偏移量 (包含夏令时的偏移量), 那么, UTC跟不同用时区的偏移量进行计算, 就能得出不同时期的日历表示。 在Calendar 类中, 通过方法 computeFields来完成。我们来简单看下Calendar类的一个默认实现GregorianCalendar的 computeFields 方法长什么样:
 /**
     * Converts the time value (millisecond offset from the <a
     * href="Calendar.html#Epoch">Epoch</a>) to calendar field values.
     * The time is <em>not</em>
     * recomputed first; to recompute the time, then the fields, call the
     * <code>complete</code> method.
     *
     * @see Calendar#complete
     */
    protected void computeFields() {
	int mask = 0;
	if (isPartiallyNormalized()) {
	    // Determine which calendar fields need to be computed.
	    mask = getSetStateFields();
	    int fieldMask = ~mask & ALL_FIELDS;
	    // We have to call computTime in case calsys == null in
	    // order to set calsys and cdate. (6263644)
	    if (fieldMask != 0 || calsys == null) {
		mask |= computeFields(fieldMask,
				      mask & (ZONE_OFFSET_MASK|DST_OFFSET_MASK));
		assert mask == ALL_FIELDS;
	    }
	} else {
	    mask = ALL_FIELDS;
	    computeFields(mask, 0);
	}
	// After computing all the fields, set the field state to `COMPUTED'.
	setFieldsComputed(mask);
    }

具体的计算还是比较复杂, 有兴趣的朋友可以进一步研究。 这里我想强调的是, 什么时候会调用这个方法。 请参阅下图
理解 Java 时间, 日期_第3张图片

  同一个时间, 使用Calendar来显示不同时区的时间

        比如,通过日历, 来看看美国现在的时间表示是什么。
package util;

public class TimeTest {

	public static Date getDateInTimeZone(Date currentDate, String timeZoneId) {
		TimeZone tz = TimeZone.getTimeZone(timeZoneId);
		Calendar mbCal = new GregorianCalendar(tz);
		
		mbCal.setTimeInMillis(currentDate.getTime());

		Calendar cal = Calendar.getInstance();
		cal.set(Calendar.YEAR, mbCal.get(Calendar.YEAR));
		cal.set(Calendar.MONTH, mbCal.get(Calendar.MONTH));
		cal.set(Calendar.DAY_OF_MONTH, mbCal.get(Calendar.DAY_OF_MONTH));
		cal.set(Calendar.HOUR_OF_DAY, mbCal.get(Calendar.HOUR_OF_DAY));
		cal.set(Calendar.MINUTE, mbCal.get(Calendar.MINUTE));
		cal.set(Calendar.SECOND, mbCal.get(Calendar.SECOND));
		cal.set(Calendar.MILLISECOND, mbCal.get(Calendar.MILLISECOND));

		return cal.getTime();
	}

	public static void main(String[] args)  {

		// Canada/Central
		 String timeZoneId = "Canada/Central";
		 Date now = new Date();
		// System.out.println("Getting Time in the timezone="+timeZoneId);
		 System.out.println("Current Time there="+getDateInTimeZone(now,timeZoneId));
	}

}


输出结果是:
Current Time here =Thu Jan 13 19:10:18 CST 2011
Current Time there=Thu Jan 13 05:10:18 CST 2011

请注意方法Calendar.getTime(). 看看它的源码:
 public final Date getTime() {
        return new Date(getTimeInMillis());
    }

它其实就是UTC时间戳。  前面有提到, Date的toString方法用当地时区格式打印出来。 因为同一时间点, UTC是相同的。 我们要用我们的TimeZone打印出美国的现在时间, 必须改变UTC值。 mbCal的时期(年,月,日,小时,分钟,秒 。。。 )其实已经发生了变化。setTimeInMillis 方法会计算偏移量。 这里我表达的可能不清楚, 如果大家有问题的话, 可以给我留言。

日历的字段操作
日历常用的一个地方, 就是字段的操作了。 可以使用三种方法更改日历字段:set()、add() 和 roll()。下面会具体介绍这三个方法的区别。

set(f, value) 将日历字段 f 更改为 value。此外,它设置了一个内部成员变量,以指示日历字段 f 已经被更改。尽管日历字段 f 是立即更改的,但是直到下次调用 get()、getTime()、getTimeInMillis()、add() 或 roll() 时才会重新计算日历的时间值(以毫秒为单位)。因此,多次调用 set() 不会触发多次不必要的计算。使用 set() 更改日历字段的结果是, 其他日历字段也可能发生更改,这取决于日历字段、日历字段值和日历系统。此外,在重新计算日历字段之后,get(f) 没必要通过调用 set 方法返回 value 集合。具体细节是通过具体的日历类确定的。

示例:假定 GregorianCalendar 最初被设置为 1999 年 8 月 31 日。调用 set(Calendar.MONTH, Calendar.SEPTEMBER) 将该日期设置为 1999 年 9 月 31 日。如果随后调用 getTime(),那么这是解析 1999 年 10 月 1 日的一个暂时内部表示。但是,在调用 getTime() 之前调用 set(Calendar.DAY_OF_MONTH, 30) 会将该日期设置为 1999 年 9 月 30 日,因为在调用 set() 之后没有发生重新计算。

 Calendar cal = Calendar.getInstance();
			
		 System.out.println("Current Timezone="+cal.getTimeZone().getDisplayName());
			
		 cal.set(Calendar.MONTH, Calendar.DECEMBER);
		 cal.set(Calendar.DAY_OF_MONTH, 31);
			
		 System.out.println("Calendar raw = " + cal.getTime());
			
		 cal.set(Calendar.MONTH, Calendar.FEBRUARY);	
		 System.out.println("Calendar after change = 2 " + cal.getTime());


输出结果是
Current Timezone=China Standard Time
Calendar raw = Sat Dec 31 19:26:51 CST 2011
Calendar after change = 2 Thu Mar 03 19:26:51 CST 2011

请注意, 结果不是2月31日。因为没有2月31日。 再继续看下面的代码:
Calendar cal = Calendar.getInstance();
			
		 System.out.println("Current Timezone="+cal.getTimeZone().getDisplayName());
			
		 cal.set(Calendar.MONTH, Calendar.DECEMBER);
		 cal.set(Calendar.DAY_OF_MONTH, 31);
			
		 System.out.println("Calendar raw = " + cal.getTime());
			
		 cal.set(Calendar.MONTH, Calendar.FEBRUARY);
		 cal.set(Calendar.DAY_OF_MONTH, 28);
System.out.println("Calendar after change =  " + cal.getTime());

输出的结果是:
Current Timezone=China Standard Time
Calendar raw = Sat Dec 31 19:31:04 CST 2011
Calendar after change = 2 Mon Feb 28 19:31:04 CST 2011

注意: 结果不是3月28号。

add(f, delta) 将 delta 添加到 f 字段中。这等同于调用 set(f, get(f) + delta),但要带以下两个调整:

Add 规则 1。调用后 f 字段的值减去调用前 f 字段的值等于 delta,以字段 f 中发生的任何溢出为模。溢出发生在字段值超出其范围时,结果,下一个更大的字段会递增或递减,并将字段值调整回其范围内。

Add 规则 2。如果期望某一个更小的字段是不变的,但让它等于以前的值是不可能的,因为在字段 f 发生更改之后,或者在出现其他约束之后,比如时区偏移量发生更改,它的最大值和最小值也在发生更改,然后它的值被调整为尽量接近于所期望的值。更小的字段表示一个更小的时间单元。HOUR 是一个比 DAY_OF_MONTH 小的字段。对于不期望是不变字段的更小字段,无需进行任何调整。日历系统会确定期望不变的那些字段。

此外,与 set() 不同,add() 强迫日历系统立即重新计算日历的毫秒数和所有字段。
示例:假定 GregorianCalendar 最初被设置为 1999 年 8 月 31 日。调用 add(Calendar.MONTH, 13) 将日历设置为 2000 年 9 月 30 日。Add 规则 1 将 MONTH 字段设置为 September,因为向 August 添加 13 个月得出的就是下一年的 September。因为在 GregorianCalendar 中,DAY_OF_MONTH 不可能是 9 月 31 日,所以 add 规则 2 将 DAY_OF_MONTH 设置为 30,即最可能的值。尽管它是一个更小的字段,但不能根据规则 2 调整 DAY_OF_WEEK,因为在 GregorianCalendar 中的月份发生变化时,该值也需要发生变化。
 Calendar cal = Calendar.getInstance();	
		 System.out.println("Current Timezone="+cal.getTimeZone().getDisplayName());			
		 cal.set(Calendar.MONTH, Calendar.JANUARY);
		 cal.set(Calendar.DAY_OF_MONTH, 31);
		 System.out.println("Calendar raw = " + cal.getTime());
		 cal.add(Calendar.MONTH, 1);
		 System.out.println("Calendar after change =  " + cal.getTime());
		 cal.add(Calendar.MONTH, 1);
		 System.out.println("Calendar after change =  " + cal.getTime());

结果是:
Current Timezone=China Standard Time
Calendar raw = Mon Jan 31 19:50:23 CST 2011
Calendar after change =  Mon Feb 28 19:50:23 CST 2011
Calendar after change =  Mon Mar 28 19:50:23 CST 2011

roll(f, delta) 将 delta 添加到 f 字段中,但不更改更大的字段。这等同于调用 add(f, delta),但要带以下调整:

Roll 规则。在完成调用后,更大的字段无变化。更大的字段表示一个更大的时间单元。DAY_OF_MONTH 是一个比 HOUR 大的字段。

 Calendar cal = Calendar.getInstance();
			
		 System.out.println("Current Timezone="+cal.getTimeZone().getDisplayName());	
		 cal.set(Calendar.MONTH, Calendar.DECEMBER);
		 cal.set(Calendar.DAY_OF_MONTH, 31);
		 System.out.println("Calendar raw = " + cal.getTime());
		 cal.roll(Calendar.MONTH, 1);
		 System.out.println("Calendar after change =  " + cal.getTime());

结果是:
Current Timezone=China Standard Time
Calendar raw = Sat Dec 31 20:05:25 CST 2011
Calendar after change =  Mon Jan 31 20:05:25 CST 2011

你可能感兴趣的:(理解 Java 时间, 日期)