将字符串转换为“Instant”

58
我尝试使用Java 8或者一个工具包将字符串中的日期时间转换为Instant实例。

例如,

String requestTime = "04:30 PM, Sat 5/12/2018";
to
Instant reqInstant should result in 2018-05-12T20:30:00.000Z

reqString在America/Toronto时区。

这是我尝试的方法。

 String strReqDelTime = "04:30 PM, Sat 5/12/2018";
 Date date = new SimpleDateFormat("hh:mm a, EEE MM/dd/yyyy").parse(requestTime);
 Instant reqInstant = date.toInstant();

上述代码的结果为"2018-05-12T23:30:00Z"

我该怎么做?


你可以尝试调用 sdf.setTimeZone("你的时区")。 - Danila Zharenkov
@DanilaZharenkov 我试过了,但它给出的是“2018-05-12T16:30:00Z”。 - masterfly
抱歉,我编辑了我的问题。我期望Instant的结果为2018-05-12T20:30:00.000Z,对应于美国/多伦多时区中的“04:30 PM, Sat 5/12/2018”。 - masterfly
@YCF_L 这会导致异常 java.time.format.DateTimeParseException: 无法解析文本“04:30 PM,Sat 5/12/2018”,索引为14。 - masterfly
4
不需要混合使用现代类(Instant)和存在问题的传统类(DateSimpleDateFormat)。只需使用java.time类;它们完全取代了旧类。 - Basil Bourque
显示剩余3条评论
5个回答

115

简而言之

  • 修复未填充月份和日期的格式模式。
  • 仅使用java.time类,不要使用旧版类。

假设的例子:

LocalDateTime.parse(                   // Parse as an indeterminate `LocalDate`, devoid of time zone or offset-from-UTC. NOT a moment, NOT a point on the timeline.
    "04:30 PM, Sat 5/12/2018" ,        // This input uses a poor choice of format. Whenever possible, use standard ISO 8601 formats when exchanging date-time values as text. Conveniently, the java.time classes use the standard formats by default when parsing/generating strings.
    DateTimeFormatter.ofPattern("hh:mm a, EEE M/d/uuuu", Locale.US)  // Use single-character `M` & `d` when the number lacks a leading padded zero for single-digit values.
)                                      // Returns a `LocalDateTime` object.
.atZone(                               // Apply a zone to that unzoned `LocalDateTime`, giving it meaning, determining a point on the timeline.
    ZoneId.of("America/Toronto")       // Always specify a proper time zone with `Contintent/Region` format, never a 3-4 letter pseudo-zone such as `PST`, `CST`, or `IST`.
)                                      // Returns a `ZonedDateTime`. `toString` → 2018-05-12T16:30-04:00[America/Toronto].
.toInstant()                           // Extract a `Instant` object, always in UTC by definition.
.toString()                            // Generate a String in standard ISO 8601 format representing the value within this `Instant` object. Note that this string is *generated*, not *contained*.

2018-05-12T20:30:00Z

使用单个数字格式化模式

您在格式化模式中使用了MM,以表示任何一位数字(1到9月)将出现一个前导零。

但是您的输入缺少这个填充的前导零,所以请使用单个M

对于我期望的每月天数,使用d而不是dd

仅使用java.time

您正在使用有问题的、过时的日期时间类(DateSimpleDateFormat),这些类已经被java.time类取代多年了。新类完全取代了旧类。没有必要混合遗留和现代。

LocalDateTime

解析为LocalDateTime,因为您的输入字符串缺少任何时区偏移量指示器。这个值不是一个瞬间,也不是时间线上的一个点。它只是一个大约26-27小时范围内可能的瞬间集合。

String input = "04:30 PM, Sat 5/12/2018";
DateTimeFormatter f = DateTimeFormatter.ofPattern("hh:mm a, EEE M/d/uuuu", Locale.US);  // Specify locale to determine human language and cultural norms used in translating that input string.
LocalDateTime ldt = LocalDateTime.parse(input, f);
ldt.toString(): 2018-05-12T16:30

ZonedDateTime

如果您确定输入的时间是使用加拿大多伦多地区人们使用的墙上时钟表示的时刻,请应用ZoneId以获取ZonedDateTime对象。

分配时区赋予了未带时区的LocalDateTime含义。现在我们有了一个时刻,即时间线上的一个点。

ZoneId z = ZoneId.of("America/Toronto");
ZonedDateTime zdt = ldt.atZone(z);  // Give meaning to that `LocalDateTime` by assigning the context of a particular time zone. Now we have a moment, a point on the timeline.

zdt.toString():2018-05-12T16:30-04:00[美洲/多伦多]

Instant

要将该时刻视为UTC时间,请提取Instant。同一时刻,不同的挂钟时间。

Instant instant = zdt.toInstant();

instant.toString():2018-05-12T20:30:00Z

关于java.time

java.time框架内置于Java 8及更高版本中。这些类替代了旧的legacy日期时间类,如java.util.DateCalendarSimpleDateFormat

Joda-Time项目现已进入maintenance mode,建议迁移到java.time类。

要了解更多信息,请参见Oracle教程。在Stack Overflow上搜索许多示例和说明。规范为JSR 310

您可以直接使用与JDBC 4.2或更高版本兼容的JDBC驱动程序,直接将java.time对象与数据库进行交换。无需使用字符串或java.sql.*类。

我们从哪里获取java.time类?

ThreeTen-Extra项目扩展了java.time的附加类。该项目是java.time可能未来添加内容的试验场。您可能会在这里找到一些有用的类,例如Interval, YearWeek, YearQuarter, 以及more


1
谢谢您提供了简明扼要的解释!!这解决了我的问题。 - masterfly
1
非常详细和好的答案,非常感谢!你能评论一下在 DateTimeFormatter.ofPattern( "hh:mm a, EEE M/d/uuuu" , Locale.US ) 中使用Locale的用法吗?文档说:“语言环境影响格式化和解析的某些方面。例如,ofLocalizedDate提供了一个使用区域设置特定日期格式的格式化程序。”我不太明白它如何影响解析,因为解析是由提供的模式确定的...?更改语言环境如何(隐式地)影响预设解析模式? - Janus Varmarken
2
@JanusVarmarken Locale 确定用于本地化的人类语言和文化规范,例如翻译星期几的名称以及决定逗号与句点、大写、缩写、元素顺序等问题。 - Basil Bourque
LocalDateTime.parse() 存在问题 - 如果您的模式不包含时间,则 parse() 将抛出异常,因此您必须添加两个 try 块,如下所示(伪代码): fun parse(): Instant? = try { LocalDateTime.parse() } catch { try { LocalDate.parse() } catch { null } } - blinker

2
似乎您的计算机(服务器)的时区是美国太平洋夏令时(PDT,GMT-7),但您希望得到美国东部夏令时(EDT,GMT-4)的结果。
Instant.toString()以ISO 8601格式返回UTC(GMT+0)日期时间。 (结尾处的'Z'表示UTC)。
当未指定日期时间字符串的时区时,SimpleDateFormat将其视为计算机的默认时区。而您的输入没有指定时区。
因此,您需要处理输入所在的时区问题。
附注:在我机器上运行时,我的时区为 Eastern DST,您的代码给出了与您期望的完全相同的结果。

1
正确的答案。 - Ole V.V.

0

对于描述,您可以阅读Java中的字符串转日期

String requestTime = "04:30 PM, Sat 5/12/2018 America/Toronto";

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a, EEE M/dd/yyyy z");
ZonedDateTime zonedDateTime = ZonedDateTime.parse(requestTime, formatter);
System.out.println(zonedDateTime.toInstant());

2
在我的Java 17中,在英语环境下运行时它可以正常工作。为了让它在其他语言环境下也能正常工作,我们需要在格式化程序中指定语言环境。无论如何,这比你之前的(现已删除)回答要好得多。 - Ole V.V.

0

你的尝试出了什么问题?

SimpleDateFormat 默认使用系统时区,而你已经提到requestTime是在America/Toronto时区。你不应该依赖默认时区,因为当你的代码在不同时区的机器上运行时,你的应用程序可能会表现出意外的行为。

你应该如何做?

在解析之前将时区设置为America/Toronto。此外,永远不要在没有Locale的情况下使用日期时间格式化/解析API

演示

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class Main {
    public static void main(String[] args) throws ParseException {
        String requestTime = "04:30 PM, Sat 5/12/2018";
        SimpleDateFormat sdf = new SimpleDateFormat("hh:mm a, EEE MM/dd/yyyy", Locale.ENGLISH);
        sdf.setTimeZone(TimeZone.getTimeZone("America/Toronto"));
        Date date = sdf.parse(requestTime);
        Instant reqInstant = date.toInstant();
        System.out.println(reqInstant);
    }
}

输出:

2018-05-12T20:30:00Z

不要使用容易出错的java.util API污染干净的java.time API

Java-8(2014年3月)引入了java.time API,取代了容易出错且过时的java.util及其格式化API SimpleDateFormat。建议停止使用传统的日期时间API并切换到现代日期时间API。您尝试使用传统API解析日期时间字符串,然后使用Date#toInstant切换到现代API,而您可以使用现代API完成所有操作。

如果您正在使用使用java.util.Date的旧代码/库,则应使用Date#toInstant切换到现代API。

使用现代日期时间API的解决方案

将日期时间字符串解析为LocalDateTime,因为它没有时区 → 将获取的LocalDateTime转换为给定时区的ZonedDateTime → 将获取的ZonedDateTime转换为Instant

演示

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class Main {
    public static void main(String[] args) {
        String requestTime = "04:30 PM, Sat 5/12/2018";
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("h:m a, EEE M/d/u", Locale.ENGLISH);

        // Parse the date-time string to LocalDateTime as it does not have time-zone
        LocalDateTime ldt = LocalDateTime.parse(requestTime, dtf);

        // Convert the LocalDateTime into ZonedDateTime of the given time-zone
        ZonedDateTime zdt = ldt.atZone(ZoneId.of("America/Toronto"));

        // Convert the ZonedDateTime into Instant
        Instant instant = zdt.toInstant();
        System.out.println(instant);
    }
}

输出:

2018-05-12T20:30:00Z

请注意,我更喜欢使用DateTimeFormatter代替使用y

使用现代日期时间API的另一种解决方案

您可以使用{{link2:LocalDateTime#toInstant}}将获取的LocalDateTime直接转换为Instant,并提供时区ID。

Instant instant = ldt.toInstant(ZoneId.of("America/Toronto").getRules().getOffset(ldt));

Trail: Date Time了解更多关于现代日期时间 API的内容。


-1

使用Instant.parse(String)适当格式化


5
您的回答可以通过添加更多支持性信息来改进。请[编辑]以添加更多细节,例如引用或文档,以便其他人可以确认您的答案是否正确。您可以在帮助中心中找到有关编写良好答案的更多信息。 - Community
请告诉我们什么是“适当格式化”,这个答案几乎没有用处。 - Captain Chaos

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