时区和日历的混乱结果

4

最近我一直在处理时区转换的问题,对结果感到非常惊讶。基本上,我想将一个日期从一个时区转换为另一个时区。以下是代码,转换工作正常,但我在调试时观察到的是,除非我调用Calendar#get(Calendar.FIELD),否则日期不会被转换。

    private static void convertTimeZone(String date, String time, TimeZone fromTimezone, TimeZone toTimeZone){
        Calendar cal = Calendar.getInstance(fromTimezone);
        String[] dateSplit = null;
        String[] timeSplit = null;
        if(time !=null){
            timeSplit = time.split(":");
        }
        if(date!=null){
            dateSplit = date.split("/");
        }
        if(dateSplit !=null){
            cal.set(Calendar.DATE, Integer.parseInt(dateSplit[0]));
            cal.set(Calendar.MONTH, Integer.parseInt(dateSplit[1])-1);
            cal.set(Calendar.YEAR, Integer.parseInt(dateSplit[2]));
        }
        if(timeSplit !=null){
            cal.set(Calendar.HOUR_OF_DAY, Integer.parseInt(timeSplit[0]));
            cal.set(Calendar.MINUTE, Integer.parseInt(timeSplit[1]));

        }
//      System.out.println("Time in " + fromTimezone.getDisplayName() + " : " + cal.get(Calendar.DATE) +"/"+ (cal.get(Calendar.MONTH)+1)+"/"+ cal.get(Calendar.YEAR) +" " + ((cal.get(Calendar.HOUR_OF_DAY)<10) ? ("0"+cal.get(Calendar.HOUR_OF_DAY) ): (cal.get(Calendar.HOUR_OF_DAY)))
//              +":" + (cal.get(Calendar.MINUTE)<10 ? "0"+cal.get(Calendar.MINUTE) : cal.get(Calendar.MINUTE)) );
        cal.setTimeZone(toTimeZone);
        System.out.println("Time in " + toTimeZone.getDisplayName() + " : " + cal.get(Calendar.DATE) +"/"+ (cal.get(Calendar.MONTH)+1)+"/"+ cal.get(Calendar.YEAR) +" " + ((cal.get(Calendar.HOUR_OF_DAY)<10) ? ("0"+cal.get(Calendar.HOUR_OF_DAY) ): (cal.get(Calendar.HOUR_OF_DAY)))
                +":" + (cal.get(Calendar.MINUTE)<10 ? "0"+cal.get(Calendar.MINUTE) : cal.get(Calendar.MINUTE)) );
}

public static void main(String[] args) throws ParseException {

        convertTimeZone("23/04/2013", "23:00", TimeZone.getTimeZone("EST5EDT"), TimeZone.getTimeZone("GB"));
    }

期望输出:格林威治平均时间: 2013年4月24日04:00

当我注释掉sysout 1时得到的输出:格林威治平均时间:2013年4月23日23:00

如果我取消注释sysout1,我将得到期望的有效输出。

感谢您的帮助。


第一个 SOUT 之后调用了 cal.setTimeZone(),也许这与此有关? - 0x6C38
@MrD 起始日期在时区EST5EDT,即在第一个sysout之前。我需要将此日期转换为GMT。因此,在第一个sysout之后,我设置了时区(GB)。 - PermGenError
@MrD 正如我在问题中所说的那样,当我取消注释第一个 Sysout 时,转换就可以正常工作。 :) - PermGenError
尝试取消注释第一个 SysOut 并将 cal.setTimeZone() 放在两者之前,看一下输出是什么。 - 0x6C38
5个回答

3
给定日期的内部表示直到真正需要时才会被评估,也就是说,直到您尝试通过那些获取器访问它时才会被评估。然而,解析日期的最佳方法是通过SimpleDateFormat。

编辑(添加以总结下面的评论并更好地澄清我的答案)。

日历的工作方式更加高效:它不会在每次调用 setter 时重新计算所有内容,而是等待您调用 getter。

日历应主要用于日期计算(请参见 add()roll()),但您正在使用它进行解析和格式化:这些任务最好使用 SimpleDateFormat 完成,这就是我说您对日历的使用不够优雅的原因。

请参考此示例:

private static void convertTimeZone(String date, String time,
            TimeZone fromTimezone, TimeZone toTimeZone) throws ParseException {
    SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy HH:mm");
    df.setTimeZone(fromTimezone);
    Date d = df.parse(date + " " + time);
    df.setTimeZone(toTimeZone);
    System.out.println("Time in " + toTimeZone.getDisplayName() + " : " +
                       df.format(d));
}

我已经重新实现了你的方法,仅使用SimpleDateFormat。我的方法更小,没有分割逻辑(它隐藏在parse()中),输出也以更简单的方式处理。此外,日期格式以紧凑且标准的方式表达,可以使用ResourceBundle轻松地国际化。另外请注意,时区转换只是一个格式化任务:解析后的日期的内部表示不会改变。

1
你能描述一下为什么不行吗?(链接已省略) - 0x6C38
为了更好的效率:不要每次调用setter时重新计算所有东西,而是等到调用getter时再计算。你使用Calendar的方式不够优雅,我建议你使用SimpleDateFormat。 - Pino
据我所知,SimpleDateFormat用于格式化日期。我需要进行时区转换。您能具体说明为什么这种方法不够优雅吗? - PermGenError
1
说实话,我认为这应该算作一个 bug,setTimeZone() 不会重新计算字段,它只是将 areAllFieldsSet 设置为 false,这太荒谬了,因为 .get 不会检查这个。只需查看我发布的链接即可。 - 0x6C38
@MrD 谢谢,是的,你可能是对的。我目前正在自己检查源代码 :) - PermGenError
显示剩余3条评论

2
答案在setTimeZone()的注释中有部分解释

考虑以下调用顺序:cal.setTimeZone(EST); cal.set(HOUR, 1); cal.setTimeZone(PST)。 cal是以东部标准时间1点还是太平洋标准时间1点?答案是太平洋标准时间。更一般地说,对setTimeZone()的调用会影响到它之前和之后的set()调用,直到下一次complete()的调用。

换句话说,以下顺序
  1. 设置日历时间
  2. 更改时区
被解释为“在这个新时区使用此时间”,而以下顺序
  1. 设置日历时间
  2. 获取时间(或某些部分)
  3. 更改时区
将被解释为“在旧时区使用此时间,然后更改它”。
因此,在您的情况下,为了获得所希望的行为,您需要在更改时区之前调用get()或任何其他在Calendar内部调用complete()的方法。

0

你是否限制使用TimeZone和Calendar?如果不是的话,我建议使用优秀的库JodaTime,它使处理时区变得更加容易。

那么你的示例将如下所示:

public static void convertTimeZoneJoda(String date, String time, DateTimeZone fromTimezone, DateTimeZone toTimeZone) {
    DateTimeFormatter dtf = DateTimeFormat.forPattern("dd/MM/yyyyHH:mm").withZone(fromTimezone);
    DateTime dt = dtf.parseDateTime(date+time).withZone(toTimeZone);

    System.out.println("Time in " + toTimeZone.getID() + " : " + dt.toString());
}

public static void main(String[] args) {
    convertTimeZoneJoda("23/04/2013", "23:00", DateTimeZone.forID("EST5EDT"), DateTimeZone.forID("GMT"));
}

JodaTime 还允许 TimeZone 和 DateTimeZone 之间方便的转换。

不幸的是,我只能使用TimeZone和Calendar。正如您所看到的,这是一个简单的时区转换。因此,基本的Calendar和TimeZone API应该可以胜任。虽然我不知道我做错了什么 :) - PermGenError

0

补充@Pino的回答,get()不返回更新时间的原因是因为setTimeZone()只是简单地设置areAllFieldsSet为false,而不更新字段,这是无用的,因为get()不检查它。如果你问我,这是SUN公司编写的非常糟糕的代码。这里是Calendar的相关代码:

setTimeZone()

    public void  setTimeZone(TimeZone value){
        zone = value;
        sharedZone = false;
        areAllFieldsSet = areFieldsSet = false;

    }

get():

protected final int internalGet(int field){
    return fields[field];
}

0

这段代码对我来说可以将时间转换为UTC:

使用UTC时区创建日历对象
``` Calendar utcTime = Calendar.getInstance(TimeZone.getTimeZone("GMT")); ```
将"任何时区"的日历对象设置为UTC日历对象中的时间点
``` utcTime.setTimeInMillis(myCalendarObjectInSomeOtherTimeZone.getTimeInMillis()); ```
现在,`utcTime`将包含与已转换为UTC的`myCalendarObjectInSomeOtherTimeZone`相同的时间点。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接