Java - 来自 Calendar.set(HOUR_OF_DAY) 的意外结果

Java - Unexpected result from Calendar.set(HOUR_OF_DAY)

JVM 版本为 1.7。时区为 GMT+3,偏移 180 分钟。 1500411600000对应7/19/2017, 12:00:00 AM(我验证过这个online)。

我正在执行以下代码来调整 Date 实例的时间:

final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
Date date = new Date(1500411600000L);
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 23);
calendar.set(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 59);
calendar.set(Calendar.MILLISECOND, 999);
date = calendar.getTime();

我希望 date 变成 7/19/2017, 11:59:59 PM 但我得到的不是这个 7/19/2017, 2:59:59 AM。 3 小时时差 - 与我的时区与 UTC/GMT 的时差完全一样,所以我想这里发生了一些未被注意到的转换。

你能帮我找到与时区无关的代码来调整日期时间吗?

您正在使用 Calendar.getInstance(TimeZone.getTimeZone("UTC")),但您必须使用您所在的时区。正如您所述 GMT+3

请参阅此处解释有关日期和时区问题的线程。

How to set time zone of a java.util.Date?

Date 对象将具有正确的调整时间,但在显示时,输出将使用您当地的时区。您可以使用以下代码强制设置 JVM 的时区,但这可能会对代码的其他部分产生意想不到的后果。

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

在理想情况下,您可以使用 Java 8 日期 类 或 Joda 时间库 类,它们都提供了一些简单的日期操作方法。

Java 8 date classes

使用clear。对我来说,这似乎是一个历史 "bug",一个时区日历,其中 setTime 不会改变时区。

final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
Date date = new Date(1500411600000L);
calendar.clear(); // To reset _all_ fields, incl. the time zone offset ZONE_OFFSET.
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 23);
calendar.set(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 59);
calendar.set(Calendar.MILLISECOND, 999);
date = calendar.getTime();

当然,这可能是切换到新 java 时间的正确论据 API。

问题比我的问题中描述的要严重。它源于对用户时区的不正确管理 Date/Time。在我的应用程序中,时间戳在用户的时区中发送,然后在服务器的时区中评估日期,但没有考虑时区差异。我试图解决这个问题并遇到了问题中描述的问题。

我听取了 @ThomasEdwin 的建议使用 Joda Time,很高兴分享这个解决方案:

long userTimezoneOffset = 180;  // it's a parameter submitted by client app
Date date = new Date(1500411600000L);  // it's another parameter submitted by client app

final DateTimeZone zone = DateTimeZone.forOffsetMillis((int) TimeUnit.MINUTES.toMillis(userTimezoneOffset));
final DateTimeZone serverZone = DateTimeZone.getDefault();

MutableDateTime dateTime = new MutableDateTime(date, zone);
dateTime.setHourOfDay(23);
dateTime.setMinuteOfHour(59);
dateTime.setSecondOfMinute(59);
dateTime.setMillisOfSecond(999);
dateTime.setZoneRetainFields(serverZone);
date = dateTime.toDate();
// now date.toString() returns expected result

我还发现 -Duser.timezone JVM 参数在调试此问题时非常有用。请在此处查看支持的 timezone IDs.

列表

您是正确的,在偏移量 UTC+3 处,您的毫秒值 1500411600000 对应于 2017 年 7 月 19 日午夜(一天的开始)。在其他偏移量下,它对应于 7 月 18 日或 19 日的其他时间。

java.time

假设您在自己的时区度过午夜并非巧合,该值实际上应该代表日期,而不是时间,我建议您使用 LocalDate from [=16] =] 来表示它:

    ZoneId yourTimeZone = ZoneId.of("Europe/Riga");
    LocalDate date = Instant.ofEpochMilli(1500411600000L)
            .atZone(yourTimeZone)
            .toLocalDate();
    System.out.println(date);

这会打印预期的

2017-07-19

请替换您正确的时区以防它恰好不是 Europe/Riga,或者使用 ZoneOffset 代替:.atOffset(ZoneOffset.ofHoursMinutes(3, 0))(其他行相同) .

我怀疑你并不真的想要一天结束,即使在你的问题中你试图设置它。如果是为了判断某个时间点是否在当天结束之前,则将其与第二天的开始进行比较,并要求严格在之前。这为您省去了看起来奇怪的分钟、秒和秒的小数部分的麻烦。

    ZonedDateTime startOfNextDay = date.plusDays(1).atStartOfDay(yourTimeZone);

java.time 于 2014 年问世,替代了 Java 1.0 和 1.1 中设计糟糕的日期和时间 类 以及 Joda-Time,从中获得了很多灵感画。我热烈推荐你使用它。

你在问题中尝试了什么

我相信当用 java.time:

表达时,您的问题代码也更清晰
    OffsetDateTime endOfDay = Instant.ofEpochMilli(1500411600000L)
            .atOffset(ZoneOffset.UTC)
            .with(LocalTime.MAX);
    System.out.println(endOfDay);

这会打印

2017-07-18T23:59:59.999999999Z

(7 月 18 日在 UTC 一天结束时;Z 在结束时表示 UTC)。除了小数位数,这也是你得到的结果。您可能被 Date 实例打印成类似 Wed Jul 19 02:59:59 EEST 2017 (时区缩写取决于您的 JVM 时区设置)这一事实所愚弄。 Date.toString() 获取 JVM 的时区设置,并将日期时间转换为该时区,仅用于生成的字符串; Date 实例本身没有被修改,只保留时间线上的一个点,没有时区。

问题:我的 Java 版本可以使用 java.time 吗?

是的,你可以。你只需要至少使用 Java 6.

  • 在 Java 8 及更高版本中内置了新的 API。
  • 在Java中6和7得到the ThreeTen Backport, the backport of the new classes (ThreeTen for JSR 310)。
  • 在 Android 上,使用 ThreeTen Backport 的 Android 版本。它叫做 ThreeTenABP,在 .
  • 中有详细的解释

要学习使用 java.time,请参阅 the Oracle tutorial 或在网上查找其他资源。