如何在Java中转换UTC和本地时区

10

我对Java中的时区很好奇。我想从设备中获取UTC时间的毫秒数,并将其发送到服务器。服务器在向用户显示时间时会将其转换为本地时区。我的系统时区为澳大利亚/悉尼(UTC + 11:00),当我测试时区时,得到了以下结果:

int year = 2014;
int month = 0;
int date = 14;
int hourOfDay = 11;
int minute = 12;
int second = 0;
Calendar c1 = Calendar.getInstance();
c1.set(year, month, date, hourOfDay, minute, second);
    
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss z");
System.out.println(sdf.format(c1.getTime()));
    
Calendar c2 = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
c2.set(year, month, date, hourOfDay, minute, second);
System.out.println(sdf.format(c2.getTime()));

输出:

14/01/2014 11:12:00 EST
14/01/2014 22:12:00 EST

我认为应该在c2中使用13/01/2014 00:12:00,因为UTC时间比我的时间晚11个小时。难道日历不按照我期望的方式工作吗?

希望你能提供帮助。

编辑

添加z以显示时区。这让我更加困惑,因为Mac说它的时区是(AEDT)澳大利亚东部夏令时,但Java是EST。无论如何,结果仍然不同,因为EST是UTC-5小时。


这个 new Date(c1.getTimeInMillis())) 是多余的。使用 c1.getTime() 即可。 - Sotirios Delimanolis
同时在你的格式末尾添加一个 Z 以查看你所在的时区。 - Sotirios Delimanolis
请注意,像java.util.Datejava.util.Calendarjava.text.SimpleDateFormat这样的日期时间类现在已经成为遗留系统,被内置于Java 8及更高版本中的java.time类所取代。请参阅Oracle的教程 - Basil Bourque
4个回答

13

更新:本回答已过时,Joda-Time库现在已被Java 8及更高版本内置的java.time框架取代。请参见这个新回答

三字代码

应避免使用3或4字母的时区代码,如ESTIST,它们既不是标准的也不是唯一的。

使用正确的时区名称,通常是形如Continent/CityOrRegion的名称,例如America/MontrealAsia/Kolkata

Joda-Time

java.util.Date/Calendar类非常糟糕,请避免使用它们。使用Joda-Time或Java 8中由JSR 310定义并受Joda-Time启发的java.time.*

注意下面所示的Joda-Time代码要简单得多且更为明显。Joda-Time甚至知道如何计数——1月是1而不是0!

时区

在Joda-Time中,DateTime实例知道自己的时区。

澳大利亚悉尼标准时间比UTC/GMT快10小时,夏令时比UTC/GMT快11个小时,并适用于问题所指定的日期。

提示:不要这样思考……

UTC时间比我的时间晚11小时

应该这样思考……

悉尼夏令时比UTC/GMT快11个小时。

如果以UTC/GMT为基准进行日期和时间工作,将变得更加容易且不容易出错。仅在用户界面中显示本地化日期时间,因此仅在显示时转换成本地时间。全球思维,本地展示。您的用户和服务器可以轻松移动到其他时区,因此忘记您自己的时区。始终指定一个时区,不要假设或依赖默认值。

示例代码

以下是使用Joda-Time 2.3和Java 8的示例代码。

// Better to specify a time zone explicitly than rely on default.
// Use time zone names, not 3-letter codes. 
// This list is not quite up-to-date (read page for details): http://joda-time.sourceforge.net/timezones.html
DateTimeZone timeZone = DateTimeZone.forID("Australia/Sydney");
DateTime dateTime = new DateTime(2014, 1, 14, 11, 12, 0, timeZone);
DateTime dateTimeUtc = dateTime.toDateTime(DateTimeZone.UTC); // Built-in constant for UTC (no time zone offset).

在控制台输出...

System.out.println("dateTime: " + dateTime);
System.out.println("dateTimeUtc: " + dateTimeUtc);

当运行一个Python脚本时,如果没有指定解释器,则Linux和macOS操作系统默认使用的是系统自带的Python 2版本。此外,Python 2将在2020年正式停止维护,因此建议尽快升级到Python 3。

dateTime: 2014-01-14T11:12:00.000+11:00
dateTime in UTC: 2014-01-14T00:12:00.000Z

11

你可能想在格式化程序上设置时区,而不是日历(或者除了日历之外,它不是100%清楚你想要完成什么!)。用于创建人类表示的时区来自SimpleDateFormat。当您通过调用getTime()将其转换回java.util.Date时,所有“时区”信息都会从Calendar中丢失。

代码:

Calendar c2 = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
c2.set(year, month, date, hourOfDay, minute, second);
System.out.println(sdf.format(c2.getTime()));

输出 14/01/2014 10:12:00 是因为使用了悉尼时区(您的格式化程序所在的时区)中展示的UTC时间上午11点实际上是晚上10点!(在格式中使用HH表示24小时制时间)

这将打印出您可能想要的结果:

SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss z");
System.out.println(sdf.format(c1.getTime()));

sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
System.out.println(sdf.format(c1.getTime()));

“UTC毫秒”的概念是没有意义的。 毫秒数量只是历史上的一个固定点,它没有与之相关联的时区。我们将时区添加到它上面,以将其转换为人类可读的表示形式。

编辑:是的,自从Java诞生以来,在使用“EST”表示(美国)东部时间和(澳大利亚)东部时间的歧义一直存在着问题。


谢谢指点。我已经更改了格式,但结果仍然不符合我的期望。 - sunghun
我本以为当我传入UTC时区参数将EST转换为UTC时,Calendar会返回UTC日期时间,但事实并非如此。 - sunghun
这是大多数人第一次使用时所期望的,没错。 - Affe

2

简而言之

使用现代的java.time类。

ZonedDateTime
.of( 2014 , 1 , 14 , 11 , 12 , 0 , 0 , ZoneId.of( "Australia/Sydney" ) )
.toInstant()
.toEpochMilli() 

1389658320000

Going the other direction.

Instant
.ofEpochMilli( 1_389_658_320_000L )  // .toString(): 2014-01-14T00:12:00Z
.atZone( 
    ZoneId.of( "Australia/Sydney" ) 
)                                    // .toString(): 2014-01-14T11:12+11:00[Australia/Sydney]
.format(
    DateTimeFormatter
    .ofPattern ( 
        "dd/MM/uuuu HH:mm:ss z" , 
        new Locale( "en" , "AU" ) 
    )
)

2014年1月14日11时12分(澳大利亚东部标准时间)。

java.time

您正在使用已经过时多年的糟糕日期时间类,这些类已被 JSR 310 所定义的现代 java.time 类所取代。

我对 Java 中的时区很好奇。

顺便说一下,UTC 偏移量仅是小时-分钟-秒钟的数字。当我们说“UTC”或在字符串末尾放置 Z 时,我们指的是偏移为零小时-分钟-秒钟,即 UTC 本身。

时区则更为复杂。时区是某个特定地区的人们使用的偏移量的过去、现在和未来变化的历史记录。全球各地的政治家有一个奇怪的癖好,喜欢改变其管辖区域的偏移量。

我想从设备获取以毫秒为单位的 UTC 时间并发送到服务器。

对于当前时刻,请使用 Instant。一个 Instant 在内部是自 1970 年第一个 UTC 时刻参考点以来的整秒数,加上纳秒的一小部分。

Instant now = Instant.now() ;  // Capture current moment in UTC.
long millisecondsSinceEpoch = now.toEpochMilli() ;

走另一个方向。
Instant instant = Instant.ofEpochMilli( millisecondsSinceEpoch ) ;

服务器将把它转换为本地时区...
指定用户期望的时区。
如果没有指定时区,JVM会隐式地应用其当前默认的时区。这个默认值可能在运行时任何时刻改变(!),因此你的结果可能会有所不同。最好明确地指定你期望的时区作为参数。如果很重要,请与您的用户确认时区。
大陆/地区的格式指定一个正确的时区名称,例如America/MontrealAfrica/CasablancaPacific/Auckland。永远不要使用2-4个字母的缩写,如ESTIST,因为它们不是真正的时区,没有标准化,甚至不是唯一的(!)。
ZoneId z = ZoneId.of( "America/Montreal" ) ;  
ZonedDateTime zdt = instant.atZone( z ) ;

当它向用户显示时间时,自动本地化为用户的语言和文化。
要进行本地化,请指定:
  • FormatStyle以确定字符串应该有多长或缩写。
  • Locale以确定:
    • 人类语言用于翻译星期几、月份等名称。
    • 文化规范决定缩写、大写、标点符号、分隔符等问题。
示例:
Locale l = Locale.CANADA_FRENCH ;   // Or Locale.US, Locale.JAPAN, etc.
DateTimeFormatter f = 
    DateTimeFormatter
    .ofLocalizedDateTime( FormatStyle.FULL )
    .withLocale( l )
;
String output = zdt.format( f );

我系统中的时区是澳大利亚/悉尼 (UTC + 11:00)。
您服务器的当前默认时区对程序应该没有影响。始终指定所需/预期的时区。坦白说,使各种日期时间方法的时区(和Locale)参数可选是java.time框架中非常少的设计缺陷之一。
提示:通常最好将服务器设置为UTC作为其当前默认时区。
顺便说一下,要清楚时区和语言环境彼此无关。您可能希望使用日语显示在Africa/Tunis时区中看到的某一时刻。
ZoneID zAuSydney = ZoneId.of( "Australia/Sydney" ) ;
ZonedDateTime zdt = instant.atZone( zAuSydney ) ;
String output = zdt.format(
    DateTimeFormatter
    .localizedDateTime( FormatStyle.LONG )
    .withLocale( new Locale( "en" , "AU" ) ;
) ;

int year = 2014; …
请注意,java.time 使用合理的数字编码,不像旧版类。月份是1-12表示一月到十二月,星期几是1-7表示星期一到星期日。
LocalDate ld = LocalDate.of( 2014 , 1 , 14 ) ;
LocalTime lt = LocalTime.of( 11 , 12 ) ;
ZoneId z = ZoneId.of( "Australia/Sydney" ) ;
ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;

zdt.toString() = 2014-01-14T11:12+11:00[澳大利亚/悉尼]
通常最好自动本地化以进行显示,如上所示。但如果您坚持,可以硬编码格式化模式。
Locale locale = new Locale ( "en" , "AU" );
ZoneId z = ZoneId.of ( "Australia/Sydney" );
ZonedDateTime zdt = ZonedDateTime.of ( 2014 , 1 , 14 , 11 , 12 , 0 , 0 , z );

zdt.toString():2014-01-14T11:12+11:00[澳大利亚/悉尼]
请指定您的格式化模式。
DateTimeFormatter f = DateTimeFormatter.ofPattern ( "dd/MM/uuuu HH:mm:ss z" , locale );
String output = zdt.format ( f );

输出 = 2014年1月14日11:12:00 AEDT。您的问题涉及自1970年01月01日T00:00:00Z以来毫秒的计数。因此,请将时区从澳大利亚调整为UTC。相同的瞬间,相同的时间线上的不同挂钟时间。
Instant instant = zdt.toInstant() ;  // Adjust from time zone to UTC.

instant.toString(): 2014-01-14T00:12:00Z
注意 instant 和 zdt 的小时差异。
我认为 c2 可以是 13/01/2014 00:12:00,因为UTC时间比我的时间晚11个小时。
正如您所要求的那样,在悉尼时区上午11点12分后的12分钟和UTC时间午夜12点12分后的12分钟相同,因为在该日期,澳大利亚/悉尼比UTC快11个小时。
计算自纪元以来的毫秒数。
long millisecondsSinceEpoch = instant.toEpochMilli() ;

0
这是你需要查看的输出结果
                final String time="UTC";
                int year = 2014;
                int month = 0;
                int date = 14;
                int hourOfDay = 11;
                int minute = 12;
                int second = 0;
        
               calendar.set(year, month, date, hourOfDay, minute, second);
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSz");   
                  System.out.println(sdf.format(calendar.getTime()));
        
               Calendar calendar1=Calendar.getInstance();
               Date dat= calendar.getTime();
             
                calendar1.set(year,month,date,hourOfDay,minute,second);
                sdf.setTimeZone(TimeZone.getTimeZone(time));
               System.out.println(sdf.format(calendar1.getTime()));

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