如何使用Java处理日历时区?

96

我有一个时间戳的值来自我的应用程序。用户可以处于任何给定的本地时区。

由于此日期用于假定所提供时间始终为GMT的WebService,因此我需要将用户的参数从例如(EST)转换为(GMT)。这是关键:用户对他的TZ毫不知情。他输入他想要发送到WS的创建日期,所以我需要:

用户输入:5/1/2008 6:12 PM (EST)
WS的参数需要:5/1/2008 6:12 PM (GMT)

我知道TimeStamps默认情况下应该始终在GMT中,但是当发送参数时,即使我从TS(应该在GMT中)创建了我的日历,小时数也总是错误的,除非用户处于GMT中。我错过了什么?

Timestamp issuedDate = (Timestamp) getACPValue(inputs_, "issuedDate");
Calendar issueDate = convertTimestampToJavaCalendar(issuedDate);
...
private static java.util.Calendar convertTimestampToJavaCalendar(Timestamp ts_) {
  java.util.Calendar cal = java.util.Calendar.getInstance(
      GMT_TIMEZONE, EN_US_LOCALE);
  cal.setTimeInMillis(ts_.getTime());
  return cal;
}

使用之前的代码,这是我得到的结果(简化格式以便易读):

[2008年5月1日晚上11:12]


2
你为什么只改变时区而不将日期/时间一起转换呢? - Spencer Kormos
1
将一个在一个时区的Java日期转换为另一个时区的日期确实很麻烦。例如,将EDT下午5点转换为PDT下午5点。 - mtyson
9个回答

62
public static Calendar convertToGmt(Calendar cal) {

    Date date = cal.getTime();
    TimeZone tz = cal.getTimeZone();

    log.debug("input calendar has date [" + date + "]");

    //Returns the number of milliseconds since January 1, 1970, 00:00:00 GMT 
    long msFromEpochGmt = date.getTime();

    //gives you the current offset in ms from GMT at the current date
    int offsetFromUTC = tz.getOffset(msFromEpochGmt);
    log.debug("offset is " + offsetFromUTC);

    //create a new calendar in GMT timezone, set to this date and add the offset
    Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.setTime(date);
    gmtCal.add(Calendar.MILLISECOND, offsetFromUTC);

    log.debug("Created GMT cal with date [" + gmtCal.getTime() + "]");

    return gmtCal;
}

如果我传入当前时间(来自Calendar.getInstance())的话,下面是输出结果:

DEBUG - 输入日历日期为 [Thu Oct 23 12:09:05 EDT 2008]
DEBUG - 偏移量为 -14400000
DEBUG - 创建了一个 GMT 日历,其日期为 [Thu Oct 23 08:09:05 EDT 2008]

12:09:05 GMT 是 8:09:05 EDT。

这里令人困惑的部分在于Calendar.getTime()会以你当前的时区返回一个Date对象,同时也没有任何方法可以修改日历的时区并且使底层日期也跟着变化。根据你的Web服务所需的参数类型不同,你可能只需要使用从纪元开始算起的毫秒数。


11
你应该减去offsetFromUTC而不是加上它,以便进行正确的转换。以你的例子为例,如果12:09 GMT是8:09 EDT(这是正确的),如果用户输入"12:09 EDT",算法应该输出"16:09 GMT",我认为如此。 - Dzinx

29
感谢大家的回应。经过进一步调查,我找到了正确的答案。正如Skip Head所提到的那样,我从应用程序中获取的TimeStamped被调整为用户的时区。因此,如果用户输入下午6:12(东部标准时间),我会得到下午2:12(格林威治标准时间)。我需要的是一种撤消转换的方法,以便用户输入的时间是我发送到WebServer请求的时间。以下是我实现这一目标的方法:
// Get TimeZone of user
TimeZone currentTimeZone = sc_.getTimeZone();
Calendar currentDt = new GregorianCalendar(currentTimeZone, EN_US_LOCALE);
// Get the Offset from GMT taking DST into account
int gmtOffset = currentTimeZone.getOffset(
    currentDt.get(Calendar.ERA), 
    currentDt.get(Calendar.YEAR), 
    currentDt.get(Calendar.MONTH), 
    currentDt.get(Calendar.DAY_OF_MONTH), 
    currentDt.get(Calendar.DAY_OF_WEEK), 
    currentDt.get(Calendar.MILLISECOND));
// convert to hours
gmtOffset = gmtOffset / (60*60*1000);
System.out.println("Current User's TimeZone: " + currentTimeZone.getID());
System.out.println("Current Offset from GMT (in hrs):" + gmtOffset);
// Get TS from User Input
Timestamp issuedDate = (Timestamp) getACPValue(inputs_, "issuedDate");
System.out.println("TS from ACP: " + issuedDate);
// Set TS into Calendar
Calendar issueDate = convertTimestampToJavaCalendar(issuedDate);
// Adjust for GMT (note the offset negation)
issueDate.add(Calendar.HOUR_OF_DAY, -gmtOffset);
System.out.println("Calendar Date converted from TS using GMT and US_EN Locale: "
    + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
    .format(issueDate.getTime()));

代码的输出为:(用户输入为5/1/2008 6:12PM (EST))

当前用户时区:东部标准时间
与GMT的当前偏移量(小时):-4(正常情况下为-5,除非进行了DST调整)
从ACP获取的时间戳:2008-05-01 14:12:00.0
使用GMT和US_EN语言环境将时间戳转换为日历日期:5/1/08 6:12 PM (GMT)


21
你说日期在Web服务中使用,所以我假设它在某个时候被序列化为字符串。
如果是这样的话,你应该查看DateFormat类的setTimeZone方法。这会决定打印时间戳时使用的时区。
一个简单的例子:
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));

Calendar cal = Calendar.getInstance();
String timestamp = formatter.format(cal.getTime());

4
没意识到SDF有自己的时区,所以很奇怪为什么改变日历的时区似乎没有起作用! - Chris.Jenkins
TimeZone.getTimeZone("UTC") 不是有效的,因为 UTC 不在 AvailableIDs() 中...天知道为什么。 - childno͡.de
TimeZone.getTimeZone("UTC") 在我的机器上可用。它是否依赖于JVM? - CodeClimber
2
setTimeZone() 方法是个绝妙的想法。你帮我省去了很多麻烦,非常感谢! - Bogdan Zurac
1
正是我需要的!您可以在格式化程序中设置服务器时区,然后将其转换为日历格式时就不用担心任何问题了。这绝对是最好的方法!对于getTimeZone,您可能需要使用格式Ex:"GMT-4:00"来表示ETD。 - Michael Kern

12
你可以使用Joda Time来解决这个问题:
Date utcDate = new Date(timezoneFrom.convertLocalToUTC(date.getTime(), false));
Date localDate = new Date(timezoneTo.convertUTCToLocal(utcDate.getTime()));

Java 8:

LocalDateTime localDateTime = LocalDateTime.parse("2007-12-03T10:15:30");
ZonedDateTime fromDateTime = localDateTime.atZone(
    ZoneId.of("America/Toronto"));
ZonedDateTime toDateTime = fromDateTime.withZoneSameInstant(
    ZoneId.of("Canada/Newfoundland"));

请注意,如果您正在使用不带附加依赖项和额外配置的Hibernate 4,则需要小心。但是,对于第三个版本来说,这是最快且最容易使用的方法。 - Aubergine

8

看起来你的时间戳被设置为原始系统的时区。

这已经过时,但它应该能够工作:

cal.setTimeInMillis(ts_.getTime() - ts_.getTimezoneOffset());

非废弃的方式是使用。
Calendar.get(Calendar.ZONE_OFFSET) + Calendar.get(Calendar.DST_OFFSET)) / (60 * 1000)

但这需要在客户端完成,因为该系统知道它所在的时区。


7

将一个时区转换为另一个时区的方法(可能有效 :) )。

/**
 * Adapt calendar to client time zone.
 * @param calendar - adapting calendar
 * @param timeZone - client time zone
 * @return adapt calendar to client time zone
 */
public static Calendar convertCalendar(final Calendar calendar, final TimeZone timeZone) {
    Calendar ret = new GregorianCalendar(timeZone);
    ret.setTimeInMillis(calendar.getTimeInMillis() +
            timeZone.getOffset(calendar.getTimeInMillis()) -
            TimeZone.getDefault().getOffset(calendar.getTimeInMillis()));
    ret.getTime();
    return ret;
}

6
DateTimestamp对象不考虑时区:它们表示从纪元以来的一定秒数,而不承诺将那个瞬间解释为小时和天。 时区仅在GregorianCalendar(不直接需要此任务)和SimpleDateFormat中起作用,这些对象需要时区偏移量才能在单独字段和日期(或长整型)值之间进行转换。
OP的问题就在他处理的开始处:用户输入具有歧义的小时,并将其解释为本地非GMT时区;此时的值是"6:12 EST",可以轻松地打印成"11.12 GMT"或任何其他时区,但永远不会变为"6.12 GMT"
没有办法使解析"06:12""HH:MM"(默认为本地时区)的SimpleDateFormat缺省为UTC;这对SimpleDateFormat来说过于聪明了。
然而,如果您在输入中显式地添加正确的时区,则可以使任何SimpleDateFormat实例使用正确的时区:只需将固定字符串附加到接收到的(并经过适当验证的)"06:12",以将"06:12 GMT"解析为"HH:MM z"
不需要显式设置GregorianCalendar字段或检索和使用时区和夏令时偏移量。
真正的问题是将默认为本地时区的输入、默认为UTC的输入和确实需要显式时区指示的输入分开。

4

过去对我有用的方法是确定用户时区和 GMT 之间的偏移量(以毫秒为单位)。一旦您有了偏移量,您可以简单地添加/减去(取决于转换方向)以获得任何时区中的适当时间。我通常通过设置 Calendar 对象的毫秒字段来实现这一点,但我确信您可以轻松将其应用于时间戳对象。以下是我用来获取偏移量的代码:

int offset = TimeZone.getTimeZone(timezoneId).getRawOffset();

timezoneId是用户时区的ID(例如EST)。


9
使用原始偏移量时会忽略夏令时。 - Werner Lehmann
1
准确地说,这种方法将在半年内给出错误的结果。这是最糟糕的错误形式。 - Danubian Sailor

2

java.time

现代方法使用取代最早版本的Java捆绑的麻烦的遗留日期时间类的java.time类。 java.sql.Timestamp类是其中之一遗留类,不再需要。相反,使用Instant或其他java.time类直接使用JDBC 4.2及更高版本与数据库交互。 Instant类表示时间线上以UTC为基准的瞬间,分辨率为纳秒(小数部分最多九个数字)。
Instant instant = myResultSet.getObject( … , Instant.class ) ; 

如果您必须与现有的Timestamp进行交互操作,则应立即通过添加到旧类中的新转换方法将其转换为java.time。
Instant instant = myTimestamp.toInstant() ;

为了调整到另一个时区,需要将时区指定为一个 ZoneId 对象。用 continent/region 的格式指定正确的时区名称,例如 America/MontrealAfrica/Casablanca 或者 Pacific/Auckland。绝不要使用像 EST 或者 IST 这样的 3-4 个字母的伪时区,因为它们既不是真正的时区,也没有标准化,甚至不是唯一的!
ZoneId z = ZoneId.of( "America/Montreal" ) ;

申请使用 Instant 以生成一个 ZonedDateTime 对象。
ZonedDateTime zdt = instant.atZone( z ) ;

为了生成一个供用户显示的字符串,可以搜索 Stack Overflow 来找到关于 DateTimeFormatter 的许多讨论和示例。
你的问题实际上是想从用户输入的数据转换成日期时间对象。通常最好将数据输入分为两部分,一部分是日期,另一部分是时间。
LocalDate ld = LocalDate.parse( dateInput , DateTimeFormatter.ofPattern( "M/d/uuuu" , Locale.US ) ) ;
LocalTime lt = LocalTime.parse( timeInput , DateTimeFormatter.ofPattern( "H:m a" , Locale.US ) ) ;

你的问题不够清晰。你想将用户输入的日期和时间解释为UTC时间吗?还是其他时区?
如果你的意思是UTC时间,可以创建一个使用UTC常量ZoneOffset.UTC作为偏移量的OffsetDateTime对象。
OffsetDateTime odt = OffsetDateTime.of( ld , lt , ZoneOffset.UTC ) ;

如果您指的是另一个时区,请与时区对象一起组合使用ZoneId。但是,哪个时区?您可以检测默认时区。或者,如果很关键,您必须确认用户的意图以确保准确性。
ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;

获取一个始终以UTC为定义的简单对象,要提取一个Instant
Instant instant = odt.toInstant() ;

"…或者…"
Instant instant = zdt.toInstant() ; 

发送到您的数据库。
myPreparedStatement.setObject( … , instant ) ;

关于java.time

java.time 框架已经内置于Java 8及更高版本中。这些类取代了老旧的、麻烦的 传统 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

Joda-Time 项目现在处于维护模式,建议迁移到java.time 类。

要了解更多信息,请参阅 Oracle教程。并在Stack Overflow中搜索许多示例和解释。规范是 JSR 310
如何获取java.time类? 该项目扩展了java.time类库,称为ThreeTen-Extra。该项目是未来可能添加到java.time的测试场所。在这里,您可以找到一些有用的类,例如IntervalYearWeekYearQuarter以及更多

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