如何设置java.util.Date的时区?

287

我已经从一个String中解析了一个java.util.Date对象,但是它将本地时区设置为date对象的时区。

String中没有指定时区,我想设置date对象的特定时区。

我该如何做到这一点?


8
虽然并非回答你的问题,但我在这里看到多次提到后开始使用 Joda Time。它似乎比标准 API 更合理,并且可以轻松地完成这种操作。 - clstrfsck
3
现在使用 java.time 类而不是 Joda-Time。Joda-Time 项目现已进入维护模式,该团队建议迁移到 java.time 类。请参见Oracle的教程 - Basil Bourque
12个回答

366
使用DateFormat。例如,
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
Date date = isoFormat.parse("2010-05-23T09:01:02");

7
如果日期是通过Calendar类创建的,你可以为Calendar设置时区。 - Jackie
37
@lwpro2 这个声明有误导性;你可以为 Calendar 对象设置时区,但是使用 getTime() 方法从中获取 Date 对象会返回具有主机计算机时区的 Date 对象。 - BrDaHa
2
@BrDaHa 是正确的。在调用 getTime() 之前,您需要使用 TimeZone.setDefault() 将新的日期对象设置为所需的时区。在 JDK 1.8 中,Calendar.getTime() 调用 return new Date(getTimeInMillis()); - jpllosa
1
@Woodchuck 是绝对正确的..... - Chris W.
1
@Woodchuck完全正确..... - undefined
显示剩余2条评论

226

请注意,java.util.Date对象本身不包含任何时区信息 - 您无法在Date对象上设置时区。 Date对象唯一包含的是自"时代"以来的毫秒数-1970年1月1日00:00:00 UTC。

如ZZ Coder所示,您可以在DateFormat对象上设置时区,以告诉它您要显示日期和时间的时区。


98
在进行谷歌搜索、实验和尝试后,我意识到这是答案的一个精确且有用的补充,并值得强调:Date 包含毫秒值。如果您查看源代码,几乎只有一个名为fastTimelong字段。实际上,Date.toString()使用Calendar来解释这个毫秒时间。因此,打印出一个Date会让它 看起来 有一个(默认)时区,从而引起人们对如何设置时区的理解上的疑问。 - David Carboni
4
Date对象中包含时区信息,但可能无法更改。 - Jackie
3
@Iwpro2,Jesper声称(我也同意)Date对象不存储时区信息。如果你声称java.util.Date内存储了时区信息,请提供参考文献。 - Jim
8
@Jim,这里是java.util.Date的源代码。它包含一个名为fastTime的unix纪元时间和一个名为BaseCalendar.Date cdate的日期,如果定义了它就会用它代替fastTime。后者包含时区信息。我理解这意味着Date实例可以包含时区信息,但也可能不包含。 - eis
2
@Jim 我并没有说你可以设置它,我只是说它包含这个信息。我也没看到任何用户设置的方法。我认为楼主是正确的,它似乎总是用户/系统默认时区。 - eis
显示剩余5条评论

157

简述

...从字符串中解析出来...没有指定时区...我想设置一个特定的时区。

LocalDateTime.parse( "2018-01-23T01:23:45.123456789" )  // Parse string, lacking an offset-from-UTC and lacking a time zone, as a `LocalDateTime`.
    .atZone( ZoneId.of( "Africa/Tunis" ) )              // Assign the time zone for which you are certain this date-time was intended. Instantiates a `ZonedDateTime` object.

j.u.Date 没有时区

正如其他正确的答案所述,java.util.Date 没有时区。它代表 UTC/GMT(没有时区偏移)。这非常令人困惑,因为它的 toString 方法在生成字符串表示时应用了 JVM 的默认时区。

避免使用 j.u.Date

出于这个原因和许多其他原因,您应该避免使用内置的 java.util.Date & .Calendar & java.text.SimpleDateFormat。它们是出了名的麻烦。

相反,请使用随 Java 8 捆绑的 java.time 包

java.time

java.time类可以用三种方式表示时间线上的时刻:

  • UTC(Instant
  • 带有偏移量(使用ZoneOffsetOffsetDateTime
  • 带有时区(使用ZoneIdZonedDateTime

Instant

java.time中,基本构建块是Instant,它是UTC上的时间线上的一个时刻。在大多数业务逻辑中使用Instant对象。

Instant instant = Instant.now();

OffsetDateTime

应用偏移量以调整到某个地区的本地时间

应用ZoneOffset以获取OffsetDateTime

ZoneOffset zoneOffset = ZoneOffset.of( "-04:00" );
OffsetDateTime odt = OffsetDateTime.ofInstant( instant , zoneOffset );

ZonedDateTime

更好的方法是应用时区,一个偏移量加上处理异常情况的规则,例如夏令时(DST)

ZoneId应用到Instant上,以获取ZonedDateTime。始终指定正确的时区名称。不要使用3-4个缩写,例如ESTIST,它们既不唯一也不标准化。

ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );

LocalDateTime

如果输入的字符串缺乏时区或偏移量指示符,则解析为LocalDateTime

如果您确定所需的时区,请分配一个ZoneId以生成ZonedDateTime。请参见上面“tl;dr”部分中的代码示例。

格式化字符串

调用这三个类中的任何一个上的toString方法,以生成表示标准ISO 8601格式的日期时间值的字符串。 ZonedDateTime类通过在括号中附加时区名称来扩展标准格式。

String outputInstant = instant.toString(); // Ex: 2011-12-03T10:15:30Z
String outputOdt = odt.toString(); // Ex: 2007-12-03T10:15:30+01:00
String outputZdt = zdt.toString(); // Ex: 2007-12-03T10:15:30+01:00[Europe/Paris]

对于其他格式,请使用DateTimeFormatter类。通常最好让该类使用用户预期的人类语言和文化规范生成本地化格式。或者您可以指定特定的格式。

Table of all date-time types in Java, both modern and legacy


关于java.time

java.time 框架是内置于Java 8及更高版本中的。这些类取代了令人头疼的老式legacy日期时间类,例如 java.util.DateCalendarSimpleDateFormat

要了解更多,请参阅Oracle教程。并在Stack Overflow上搜索许多示例和解释。规范是JSR 310

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

您可以直接使用与JDBC 4.2或更高版本兼容的JDBC驱动程序,无需字符串,也无需java.sql.*类,即可直接与数据库交换java.time对象。Hibernate 5和JPA 2.2支持java.time
在哪里获取java.time类?

Joda-Time

虽然Joda-Time仍在积极维护,但其制造商告诉我们尽快迁移到java.time。我保留这个部分作为参考,但建议使用上面的java.time部分。

Joda-Time中,日期时间对象(DateTime)确实知道其分配的时区。这意味着与UTC的偏移量以及该时区的夏令时(DST)和其他异常规则和历史。

String input = "2014-01-02T03:04:05";
DateTimeZone timeZone = DateTimeZone.forID( "Asia/Kolkata" );
DateTime dateTimeIndia = new DateTime( input, timeZone );
DateTime dateTimeUtcGmt = dateTimeIndia.withZone( DateTimeZone.UTC );

调用toString方法生成一个符合ISO 8601格式的字符串。
String output = dateTimeIndia.toString();

Joda-Time还提供了生成各种其他字符串格式的丰富功能。
如果需要,您可以将Joda-Time DateTime转换为java.util.Date。
Java.util.Date date = dateTimeIndia.toDate();

在StackOverflow上搜索“joda date”,可以找到更多的例子,有些非常详细。


实际上,在java.util.Date中有一个嵌入的时区,用于一些内部功能(请参见此答案的评论)。但是,这个内部时区不作为属性公开,并且不能设置。这个内部时区不是用于生成日期时间值字符串表示形式的toString方法所使用的时区;相反,JVM当前的默认时区是即时应用的。因此,简而言之,我们经常说“j.u.Date没有时区”。令人困惑吗?是的。这是避免使用这些老旧类的又一个原因。


3
“j.u.Date中没有时区”是错误的。如果设置了时区信息,j.u.Date中存储有时区信息,并保存在BaseCalendar.Date cdate属性中。可以在这里查看源代码。除非更改JVM的默认时区(通过调用TimeZone.setDefault(TimeZone.getTimeZone("NEW_TIME_ZONE"))),否则无法设置j.u.Date对象的时区。因此,存在时区偏移量,可以通过调用已弃用的方法j.u.Date.getTimezoneOffset()来获取偏移量。 - Thai Bui
5
@blquythai 没错,你做了功课。就像我一样,之前也看过那个源代码。在其中确实有一个时区。但是在所有实际应用中,那个时区都被忽略了。java.util.Date可以在没有任何时区的情况下工作,实际上处于UTC时间,同时忽略了那个隐藏的时区。除了应用JVM当前默认的时区来处理toString方法外,又一次忽略了那个隐藏的时区。因此为了简洁起见,我们说java.util.Date没有时区。就像艺术一样,它是告诉真相的谎言。 - Basil Bourque
1
关于调用TimeZone.setDefault,您不会设置java.util.Date对象的时区--Date对象仍然忽略其嵌入式时区,在UTC中有效地运行。您将影响Date的toString方法。设置默认值会更改JVM的默认时区,该时区通常设置为主机操作系统的时区。不建议进行此调用,因为它会影响在该JVM中运行的所有应用程序的所有线程中的所有代码,并且在执行时动态进行。由于粗鲁和危险,只有在最后一种情况下才应考虑进行此调用。 - Basil Bourque
5
那个时区经常被使用(在equalshashcodegetTime等方法中使用)。如果您查看equals方法,它会调用getTime()方法,该方法又调用getTimeImpl()方法,如果cdate属性未被规范化,则调用normalize()方法。在normalize()方法中,最后一个if语句会重新计算自 1/1/70 起的毫秒数,如果cdate的时区与当前JVM环境的时区不同,则基于其存储的时区信息进行计算。(请参考sun.util.calendar.AbstractCalendar getCalendarDate(long millis, CalendarDate date) - Thai Bui
3
根据你的建议,我一段时间前添加了一个关于嵌入式区域的后记。 - Basil Bourque
显示剩余5条评论

83

你还可以在JVM级别设置时区

Date date1 = new Date();
System.out.println(date1);

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
// or pass in a command line arg: -Duser.timezone="UTC"

Date date2 = new Date();
System.out.println(date2);

输出:

Thu Sep 05 10:11:12 EDT 2013
Thu Sep 05 14:11:12 UTC 2013

1
这对我有所帮助。在 SDF 中设置时区没有任何区别。 - vishnu viswanath
我认为这更可靠。 (+1) - foobar
28
注意:调用 TimeZone.setDefault 是一种相当极端的操作,它会影响整个 JVM,影响所有其他对象和线程。有关详细信息,请参见 此答案,包括如果您正在使用 SecurityManager 运行时甚至更多的复杂情况。更令人复杂的是:这种行为在各个版本的 Java 中都有所改变,如此问题中所讨论的那样。 - Basil Bourque
这会在此语句之后生成的所有线程中设置一个共同的时区,对吗? - Jaydev
1
这个回答比被采纳的更好。 - user2587965
@francogrex 我不太同意。设置默认时区可能会干扰程序中的任何时间操作以及在JVM中运行的其他程序,这是不鼓励的。使用DateTimeZone这两个设计不良且过时的类也是如此。 - Ole V.V.

12

如果你必须只使用标准JDK类,你可以使用以下方法:

/**
 * Converts the given <code>date</code> from the <code>fromTimeZone</code> to the
 * <code>toTimeZone</code>.  Since java.util.Date has does not really store time zome
 * information, this actually converts the date to the date that it would be in the
 * other time zone.
 * @param date
 * @param fromTimeZone
 * @param toTimeZone
 * @return
 */
public static Date convertTimeZone(Date date, TimeZone fromTimeZone, TimeZone toTimeZone)
{
    long fromTimeZoneOffset = getTimeZoneUTCAndDSTOffset(date, fromTimeZone);
    long toTimeZoneOffset = getTimeZoneUTCAndDSTOffset(date, toTimeZone);

    return new Date(date.getTime() + (toTimeZoneOffset - fromTimeZoneOffset));
}

/**
 * Calculates the offset of the <code>timeZone</code> from UTC, factoring in any
 * additional offset due to the time zone being in daylight savings time as of
 * the given <code>date</code>.
 * @param date
 * @param timeZone
 * @return
 */
private static long getTimeZoneUTCAndDSTOffset(Date date, TimeZone timeZone)
{
    long timeZoneDSTOffset = 0;
    if(timeZone.inDaylightTime(date))
    {
        timeZoneDSTOffset = timeZone.getDSTSavings();
    }

    return timeZone.getRawOffset() + timeZoneDSTOffset;
}

本文翻译参考了这篇帖子


4
时区偏移并非总是恒定不变的。实际上,它们可能因地缘政治原因而改变。TimeZone.getDSTSavings()未考虑此情况,始终返回当前偏移量。这意味着,如果您处理的是一个历史日期,并且所涉及的时区在该日期以后更改了偏移量,则可能会得到错误的转换结果。请注意在进行时区转换时进行必要的检查。 - andrerobot

6

java.util.Calendar是使用JDK类处理时区的常见方式。 Apache Commons提供了一些其他的选择/实用工具,可能会有所帮助。 Edit Spong的说明让我想起了我听说过Joda-Time非常好(尽管我自己没有使用过)。


对于Joda Time我给出+1的评价。虽然它没有提供任何标准Java API所不能实现的额外功能(至少在我发现的情况下是这样的——如果有人能够证明我错了,我也很乐意接受),但Joda Time确实使一些任务变得更加容易。 - Joshua Hutchison
2
@JoshuaHutchison Joda-Time拥有大量的附加功能。例如:使用类PeriodDurationInterval表示时间间隔。这些时间间隔包括比较方法,如containsabutsoverlapgap。而PeriodFormatterBuilder可以构建描述性短语,如“15年8个月”。 - Basil Bourque

1
将日期转换为字符串,并使用SimpleDateFormat进行操作。
    SimpleDateFormat readFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
    readFormat.setTimeZone(TimeZone.getTimeZone("GMT" + timezoneOffset));
    String dateStr = readFormat.format(date);
    SimpleDateFormat writeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
    Date date = writeFormat.parse(dateStr);

1
在Stack Overflow上,答案应该有一些讨论或解释。这个网站的目的是成为一个不仅仅是代码片段库的地方。 - Basil Bourque
感谢@Basil Bourque的评论。我已经编辑了答案。 - avisper

1
这段代码对我正在开发的应用程序很有帮助:

This code was helpful in an app I'm working on:

    Instant date = null;
    Date sdf = null;
    String formatTemplate = "EEE MMM dd yyyy HH:mm:ss";
    try {
        SimpleDateFormat isoFormat = new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss");
        isoFormat.setTimeZone(TimeZone.getTimeZone(ZoneId.of("US/Pacific")));
        sdf = isoFormat.parse(timeAtWhichToMakeAvailable);
        date = sdf.toInstant();

    } catch (Exception e) {
        System.out.println("did not parse: " + timeAtWhichToMakeAvailable);
    }

    LOGGER.info("timeAtWhichToMakeAvailable: " + timeAtWhichToMakeAvailable);
    LOGGER.info("sdf: " + sdf);
    LOGGER.info("parsed to: " + date);

1
在这里,您可以获得像“2020-03-11T20:16:17”这样的日期,并返回“11/Mar/2020 - 20:16”。
 private String transformLocalDateTimeBrazillianUTC(String dateJson) throws  ParseException {
    String localDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss";
    SimpleDateFormat formatInput = new SimpleDateFormat(localDateTimeFormat);

    //Here is will set the time zone
    formatInput.setTimeZone(TimeZone.getTimeZone("UTC-03"));

    String brazilianFormat = "dd/MMM/yyyy - HH:mm";
    SimpleDateFormat formatOutput = new SimpleDateFormat(brazilianFormat);
    Date date = formatInput.parse(dateJson);
    return formatOutput.format(date);
}

TimeZone.getTimeZone("UTC-03") 返回 GMT。我怀疑这不是你想要的。这不是你的问题,而是 TimeZone 类的问题。我建议我们不要使用 DateSimpleDateFormatTimeZone,而是使用 Java.time,正如 Basil Bourque 的答案 中所解释的那样。 - Ole V.V.
在我的电脑上,你的代码输出了 11/mar./2020 - 21:16。请注意,时间是 21:16,这是错误的。我建议使用 LocalDateTime.parse("2020-03-11T20:16:17").atZone(ZoneId.of("America/Bahia"))。它将输出 2020-03-11T20:16:17-03:00[America/Bahia]。当然,你可以按照自己的方式进行格式化。为此,请使用 DateTimeFormatter - Ole V.V.

0
package org.example;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

public class time {
   public static void main(String[] args) {
       SimpleDateFormat sdf=new SimpleDateFormat("yyyy/MM/dd HH:mm");
       sdf.setTimeZone(TimeZone.getTimeZone("Asia/Jakarta"));
       Date date=new Date();
       sdf.format(date);
       System.out.println(sdf.format(date));
   }
}

1
感谢您想要做出贡献。我认为这并没有添加任何已经在被接受的答案和其他答案中提到的内容。此外,我强烈建议您不要使用 SimpleDateFormatDate。这些类设计得很差,而且已经过时了,特别是前者非常麻烦。相反,使用来自 java.time,现代 Java 日期和时间 APIZonedDateTimeZoneIdDateTimeFormatter - Ole V.V.
1
谢谢回复,我会查看 ZonedDateTime、ZoneId 和 DateTimeFormatter。 - akhesh

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