Java SimpleDataFormat解析JDK 11和12与JDK 14

Java SimpleDataFormat解析JDK 11和12与JDK 14,java,Java,我有一个用于JDK 11和JDK 12的测试: @Test public void billingDateFormat() throws Exception { final SimpleDateFormat billingDateFormatter = new SimpleDateFormat("MMM d, yyyy"); final LocalDate x = DateTimeUtils.javaUtilDateToLocalDate(billingDa

我有一个用于JDK 11和JDK 12的测试:

@Test
public void billingDateFormat() throws Exception {

    final SimpleDateFormat billingDateFormatter = new SimpleDateFormat("MMM d, yyyy");
    final LocalDate x = DateTimeUtils.javaUtilDateToLocalDate(billingDateFormatter.parse("Dec 1, 2014"));
    assertEquals(LocalDate.of(2014, 12, 1), x);
}
然而,当我升级到JDK 14时,它说它无法解析2014年12月1日

该测试不再有效,还是JDK14上的一个bug

配合

$ java -version
openjdk version "12.0.2" 2019-07-16
OpenJDK Runtime Environment Zulu12.3+11-CA (build 12.0.2+3)
OpenJDK 64-Bit Server VM Zulu12.3+11-CA (build 12.0.2+3, mixed mode, sharing)
它在上失败了

openjdk version "14.0.1" 2020-04-14
OpenJDK Runtime Environment (build 14.0.1+7)
OpenJDK 64-Bit Server VM (build 14.0.1+7, mixed mode, sharing)
Locale对于JDK 12和JDK 14都是en_CA的,我会使用a,它能够采用特定的语言来解析以特定语言给出的月份名称,这可能是a或不可解析日期时间的原因

您正在使用,因此也应该使用该包的格式化选项

指定一个区域设置,以确定翻译月份名称Dec时使用的人类语言和文化规范

tl;博士 您提到但未记录的异常可能是由于隐式依赖于JVM的当前默认语言环境。 JVM当前的默认时区在运行时会发生变化,可能会将预期日期推迟一天。 运行billingDateFormatter.getTimeZone以查看正在使用的区域。 看作为证据。 你正在使用糟糕的日期时间类。仅使用java.time类,如中所示。 提示:不要以本地化格式以文本形式交换日期时间值。仅使用标准格式。 解析时指定区域设置 我已经尝试在我自己的机器上用Java14.0.1运行您的解析代码。我在Java12.0.1下。两者都成功了

所以我怀疑你的问题和语言环境有关。输入字符串包含月份的英文名称,并以某种方式缩写。要正确翻译月份名称,必须指定人类语言。要正确解释缩写,必须指定一组文化规范。语言和规范都包含在Locale对象中

您的代码没有指定区域设置。因此JVM的当前默认语言环境将被隐式应用。在运行时,默认值可能会有所不同。如果您的开发机器的JVM默认为Locale.US,那么您的代码将成功运行。如果在运行时,部署计算机的JVM默认为Locale.JAPAN,那么代码将失败,无法将Dec转换为一年中的第一个月

要查看JVM的当前默认区域设置,请运行:

Locale.getDefault()
但请记住,默认值随时可能发生变化。JVM中任何应用程序的任何线程中的任何代码都可以将默认值更改为另一个区域设置对象。这样做会立即影响所有线程中所有应用程序中的所有其他代码。所以我建议你永远不要依赖默认值。明确指定所需/预期的语言环境

直接的解决方案是在SimpleDataFormat对象上指定区域设置

但更大的解决方案是永远不要使用SimpleDataFormat

你还有另一个问题:时区

日期因时区而异 要知道,在任何一个特定的时刻,全球各地的日期都因时区而异。在任何时刻,日本东京都可能是“明天”,而美国俄亥俄州托莱多仍然是“昨天”

SimpleDataFormat.parse应用指定的时区 使用过时的遗留类SimpleDataFormat,以及隐式使用JVM的当前默认时区,可能会在运行时产生不同的结果

您正在解析一个字符串,该字符串只表示日期,没有一天中的时间,也没有时区。然而,您调用的方法返回一个java.util.Date对象。Date类的名称有误,它代表一个时刻,UTC时间轴上的一个点。换句话说,日期是一个日期,其时间以UTC为单位。所以,方钉,圆孔

SimpleDateFormat类试图打破这个方形的限制,但这是一个不明智的尝试,它正在应用一个默认时区,根据该时区确定该日期在该时区的第一个时刻

您的代码忽略了时区这一关键问题。然而,时区仍在发挥作用。但时区是在幕后暗中应用的

为了展示这一点,让我们运行与您类似的代码。我们将明确指定UTC时区,偏移量为零小时分秒

转储Java版本,因为您认为这是与版本相关的问题

System.out.println( Runtime.version() );
定义所需的格式化程序。我们在这里使用SimpleDataFormat是因为您坚持,但在实际工作中,我永远不会使用这个类。这门课是它的现代替代品

SimpleDateFormat billingDateFormatter = new SimpleDateFormat( "MMM d, yyyy" );
我们将明确设置时区。我怀疑这就是问题的原因:您正在隐式地分配JVM的当前默认时区,而不是分配一个。默认值在运行时会有所不同,因此您会看到不同的行为

billingDateFormatter.setTimeZone( TimeZone.getTimeZone( "Etc/UTC" ) );
验证该时区的分配。遗留日期时间案例中的许多设计缺陷之一是,当您传递一个无法识别的区域名称时,TimeZone.getTimeZone会自动失败。所以你必须再检查一遍。再一次,我永远不会在实际工作中使用这个类,而是使用它的替换项ZoneId和ZoneOffset

解析输入字符串。如果输入无效,我们捕获抛出的异常

java.util.Date d = null;
try
{
    d = billingDateFormatter.parse( "Dec 1, 2014" );
}
catch ( ParseException e )
{
    e.printStackTrace();
}
将java.util.Date对象转换为其现代替代对象Instant。两个cla SSE代表UTC中的一个时刻。在此基础上,我们分配一个偏移量,以获得更灵活的OffsetDateTime对象,该对象表示UTC任何偏移量中的一个时刻。从那里,我们只提取日期部分作为LocalDate,而忽略时间部分和偏移部分

LocalDate ld = d.toInstant().atOffset( ZoneOffset.UTC ).toLocalDate();
我们指定预期的日期结果

LocalDate expected = LocalDate.of( 2014 , Month.DECEMBER , 1 );
我们比较两个LocalDate对象,看看是否满足了我们的期望

boolean metExpectations = ld.equals( expected );
转储到控制台

System.out.println( "d = " + d );
System.out.println( "ld = " + ld );
System.out.println( "expected = " + expected );
System.out.println( "metExpectations = " + metExpectations );
跑步的时候。这里我们看到Java14中的结果。参见Java12中的相同内容

14.0.1+7

sun.util.calendar.ZoneInfo[id=Etc/UTC,offset=0,dststaving=0,useDaylight=false,transitions=0,lastRule=null]

d=2014年11月30日星期日太平洋标准时间16:00:00

ld=2014-12-01

预计=2014年12月1日

MeteExpections=true

但现在让我们将指定的时区改为远东的时区,远远领先UTC。我们使用亚洲/东京代替Etc/UTC作为时区名称。请注意,结果日期是11月30日,而不是12月1日。还要注意java.util.Date对象如何表示与上面看到的不同的时刻

billingDateFormatter.setTimeZone( TimeZone.getTimeZone( "Asia/Tokyo" ) );
结果:

14.0.1+7

sun.util.calendar.ZoneInfo[id=Asia/Tokyo,offset=3240000,dststavings=0,useDaylight=false,transitions=10,lastRule=null]

d=2014年11月30日星期日太平洋标准时间07:00:00

ld=2014-11-30

预计=2014年12月1日

metexpections=false

结论 JVM当前的默认区域设置不需要一年中最后一个月的英文缩写名称。 JVM当前的默认时区在运行时会发生变化,给您一个日期,与您可能期望的日期相差一天。 可悲的是,所有这些都是浪费时间,因为你的问题没有意义。您不应该使用任何遗留日期时间类,如SimpleDataFormat、date和Calendar。仅使用java.time包名称中的日期时间类。time框架是业界领先的,由了解复杂的日期时间处理的人精心设计,由

要查看现代解决方案,请查看

关于java.time 该框架内置于Java8及更高版本中。这些类取代了麻烦的旧日期时间类,例如,&

要了解更多信息,请参阅。并搜索堆栈溢出以获得许多示例和解释。规格是

该项目现已启动,建议迁移到类

您可以直接与数据库交换java.time对象。使用兼容的或更高版本。不需要字符串,也不需要java.sql.*类。Hibernate5和JPA2.2支持java.time

从哪里获得java.time类

、和更高版本-标准Java API的一部分,带有捆绑实现。 Java9添加了一些次要功能和修复。 和 大多数java.time功能都在中向后移植到Java6和Java7。 更高版本的Android捆绑包实现了java.time类。
对于早期的Android Java 11或14和Java.util.SimpleDataFormat?为什么?为什么不使用java.time.format.DateTimeFormatter呢?您真的应该放弃旧的API,因为您可以访问JDK 14中的。您使用的日期时间类现在已经过时了,多年前被JSR 310中定义的现代java.time类所取代。保留LocalDate,但停止使用SimpleDateFormat。会出现什么错误/异常?我希望它在JDK14中运行-当我运行新的SimpleDataFormammd,yyyy.parseDec 12014时没有错误,这是Java 12 JVM的默认语言环境?您的Java14JVM呢?如果您不知道,请使用System.out.printlnLocale.getDefault;。不回答我的问题question@ArchimedesTrajano实际上,这个答案确实解决了一个更大的问题:使用糟糕的遗留类。几年前,Sun、Oracle和JCP社区。你也应该这样。有关代码中问题的详细信息,请参阅。或者通过采用这里显示的代码来节省自己的悲伤和时间。
LocalDate expected = LocalDate.of( 2014 , Month.DECEMBER , 1 );
boolean metExpectations = ld.equals( expected );
System.out.println( "d = " + d );
System.out.println( "ld = " + ld );
System.out.println( "expected = " + expected );
System.out.println( "metExpectations = " + metExpectations );
billingDateFormatter.setTimeZone( TimeZone.getTimeZone( "Asia/Tokyo" ) );