Java 8与Java 10+中使用德国本地化的SimpleDateFormat的区别

51

我有一段代码和一个测试用例,它们都在一个遗留应用程序中,可以总结如下:

@Test
public void testParseDate() throws ParseException {
    String toParse = "Mo Aug 18 11:25:26 MESZ +0200 2014";
    String pattern = "EEE MMM dd HH:mm:ss z Z yyyy";

    DateFormat dateFormatter = new SimpleDateFormat(pattern, Locale.GERMANY);
    Date date = dateFormatter.parse(toParse);

    //skipped assumptions
}

这个测试在Java 8及以下版本中通过。但是在Java 10及以上版本中,会导致一个java.text.ParseException: Unparseable date: "Mo Aug 18 11:25:26 MESZ +0200 2014"的错误。

记录一下: 除了de_DE,异常也会被抛出到其他地区设置为 de_CHde_ATde_LU

我知道日期格式化在JDK 9中进行了更改(JEP 252)。然而,我认为这是一项破坏向后兼容性的颠覆性变化。摘自:

在JDK 9中,默认启用Unicode Consortium的Common Locale Data Repository(CLDR)数据,因此您可以使用标准语言环境数据而无需采取进一步的操作。

在JDK 8中,虽然将CLDR语言环境数据捆绑在JRE中,但默认情况下不启用。

使用语言环境敏感服务(如日期、时间和数字格式化)的代码可能会产生与CLDR语言环境数据不同的结果。

在日期中添加星期几的缩写Mo.可以弥补这一点,从而使测试通过。然而,这对于旧数据(以序列化形式如XML)并不是一个真正的解决方案。

根据这个stackoverflow帖子,似乎这种行为是故意的德语区域设置,可以通过指定java.locale.providersCOMPAT模式来减轻。然而,我不喜欢依赖某些系统属性值的想法,因为它可能:

  1. 在JDK的下一个版本中更改。
  2. 在不同的环境中被遗忘。

我的问题是:

  • 如何在不重新编写/修改现有序列化数据或添加/更改系统属性(如java.locale.providers)的情况下,保持遗留代码与这个特定日期格式的向后兼容性,这可能会在不同的环境(应用程序服务器、独立的jar等)中被遗忘?


8
您可以在Java中设置系统属性:System.setProperty("java.locale.providers", "COMPAT,CLDR");。这将防止在任何环境中遗忘它。当然,这仍不能保证适用于Java 11及以后的版本。您可能需要考虑一个将所有旧日期时间数据转换为ISO 8601(这似乎相当具有未来性)的项目: - Ole V.V.
2
可以将EEE更改为EE,但这可能会对其他语言环境产生不良影响。而且你可能想要一些宽容性,比如Mo和Mon。 - Joop Eggen
2
@JoopEggen EE MMM dd HH:mm:ss z Z yyyy 不起作用。它会导致 java.text.ParseException: Unparseable date: "Mo Aug 18 11:25:26 MESZ +0200 2014" - rzo1
2
@rzo,我很困惑为什么你遇到了兼容性问题却拒绝使用Oracle明确提供的兼容性解决方案java.locale.providersCOMPATjava.util.spi.LocaleServiceProvider API? - Basil Bourque
1
必须 在命令行中设置 java.locale.providers 属性。根据 Javadocs 所述: "本地化敏感服务的搜索顺序可以通过使用 "java.locale.providers" 系统属性进行配置。该系统属性声明了用户首选的按逗号分隔的查找本地化敏感服务的顺序。它只在 Java 运行时启动时读取,因此稍后对 System.setProperty() 的调用不会影响顺序。" - Joep Weijers
显示剩余14条评论
4个回答

22
我不是说这是一个好的解决方案,但它似乎是一种可行的方法。
    Map<Long, String> dayOfWeekTexts = Map.of(1L, "Mo", 2L, "Di", 
            3L, "Mi", 4L, "Do", 5L, "Fr", 6L, "Sa", 7L, "So");
    Map<Long, String> monthTexts = Map.ofEntries(Map.entry(1L, "Jan"), 
            Map.entry(2L, "Feb"), Map.entry(3L, "Mär"), Map.entry(4L, "Apr"),
            Map.entry(5L, "Mai"), Map.entry(6L, "Jun"), Map.entry(7L, "Jul"),
            Map.entry(8L, "Aug"), Map.entry(9L, "Sep"), Map.entry(10L, "Okt"),
            Map.entry(11L, "Nov"), Map.entry(12L, "Dez"));

    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendText(ChronoField.DAY_OF_WEEK, dayOfWeekTexts)
            .appendLiteral(' ')
            .appendText(ChronoField.MONTH_OF_YEAR, monthTexts)
            .appendPattern(" dd HH:mm:ss z Z yyyy")
            .toFormatter(Locale.GERMANY);

    String toParse = "Mo Aug 18 11:25:26 MESZ +0200 2014";
    OffsetDateTime odt = OffsetDateTime.parse(toParse, formatter);
    System.out.println(odt);
    ZonedDateTime zdt = ZonedDateTime.parse(toParse, formatter);
    System.out.println(zdt);

以下是在我的Oracle JDK 10.0.1上运行的输出:

2014-08-18T11:25:26+02:00
2014-08-18T11:25:26+02:00[Europe/Berlin]

再说一遍,可能不存在好的解决方案。

java.time 是现代 Java 日期和时间 API,可让我们为格式化和解析都指定字段所使用的文本。因此,我利用它来指定星期几和月份的缩写(不带点),这些缩写是旧的 COMPAT 或 JRE 区域设置数据中使用的。我使用了 Java 9 的 Map.ofMap.ofEntries 来构建我们需要的映射。如果这也要在 Java 8 中运行,您必须找到其他方法来填充这两个映射,我相信您能做到。

如果您确实需要一个老式的 java.util.Date(可能在传统代码库中),请按以下方式进行转换:

    Date date = Date.from(odt.toInstant());
    System.out.println("As legacy Date: " + date);

以我的时区为准(欧洲/哥本哈根),输出:

As legacy Date: Mon Aug 18 11:25:26 CEST 2014

策略建议

如果我是您,我会考虑这样做:

  1. 等待。从Java内部设置相关系统属性:System.setProperty("java.locale.providers", "COMPAT,CLDR"); 以便在任何环境中都不会忘记。COMPAT语言环境数据已经存在自Java 1.0以来(至少非常接近),因此许多现有代码都依赖它(不仅仅是您的代码)。名称在Java 9中从JRE更改为COMPAT。对我来说,这听起来像是一项计划,在相当长的时间内保留数据。根据早期访问文档,它仍将可用于Java 11(下一个“长期支持”Java版本),并且没有弃用警告之类的内容。如果将来的某个Java版本中删除了它,您可能很快就能发现该问题,并在升级之前解决它。
  2. 使用上述的解决方案
  3. 使用Basil Bourque链接提供的语言环境服务提供程序接口。毫无疑问,如果COMPAT数据将来可能被删除,这是一个很好的解决方案。您甚至可以将COMPAT语言环境数据复制到自己的文件中,以便他们不能从您那里拿走,但在这样做之前请检查是否存在版权问题。我最后提到此解决方案的原因是因为您表示不满意必须在可能运行程序的每个环境中设置系统属性。据我所知,通过语言环境服务提供程序接口使用自己的语言环境数据仍需要您设置相同的系统属性(只是值不同)。

3
“Mar” 更应该写作 “Mär”(有时甚至写作“Mrz”)。 - Meno Hochschild
1
非常感谢,@Meno。你可以相信我也可以不相信,但在阅读你的评论之前我已经纠正了(并且还删除了“请检查我的德语拼写”;尽管如此,你仍然可以这样做,因为我们永远不知道是否还有更多错误)。 - Ole V.V.

3

需要说明的是:SimpleDateFormat 是一种旧的日期格式化方式,顺便提一下,它不是线程安全的。自从 Java 8 开始,有新的包叫做 java.timejava.time.format,你应该使用这些来处理日期。对于你的目的,你应该使用类 DateTimeFormatter


这是事实,但这是一个庞大的遗留代码库。然而,问题是关于在使用相同的遗留类时Java 8和Java 10之间格式差异的区别。 - rzo1
1
是的,我理解了这一点,也明白您在处理遗留代码和向后兼容性方面所遇到的问题。我只是想提一下,以防万一这可能是一个选项。 - Michael Gantman
这似乎是我情况的一个选项。谢谢提醒。 - mondjunge
DateTimeFormatter有相同的问题。今天使用Java 19.0.2进行了测试。 - ChrLipp

0
Java 8中的格式化值为Fr Juni 15 00:20:21 MESZ +0900 2018,但现在变成了Fr. Juni 15 00:20:21 MESZ +0900 2018。EEE包括在内。这是兼容性问题,旧版本的代码无法在新版本中运行并不重要(抱歉翻译器)。如果日期字符串是您自己的,请为新版本用户添加点号。或者让用户使用Java 8来使用您的软件。
使用substring方法可以使软件变慢,但也是一个好方法。
    String toParse = "Mo Aug 18 11:25:26 MESZ +0200 2014";
    String str = toParse.substring(0, 2) + "." + toParse.substring(2);
    String pattern = "EEE MMM dd HH:mm:ss z Z yyyy";

    DateFormat dateFormatter = new SimpleDateFormat(pattern, Locale.GERMANY);
    System.out.println(dateFormatter.format(System.currentTimeMillis()));
    Date date = dateFormatter.parse(str);

非常抱歉我的英语不好。


0

这里有一个可行但丑陋的解决方法。它很丑陋,因为你必须重新定义所有单词在自己的映射中,但你仍然拥有高效和灵活的默认解析器的所有优点。

String dateString = "Mi Mai 09 09:17:24 2018";

Map<Long, String> dayOfWeekTexts =
    Map.of(1L, "Mo", 2L, "Di", 3L, "Mi", 4L, "Do", 5L, "Fr", 6L, "Sa", 7L, "So");
Map<Long, String> monthTexts =
    Map.ofEntries(
        Map.entry(1L, "Jan"),
        Map.entry(2L, "Feb"),
        Map.entry(3L, "Mär"),
        Map.entry(4L, "Apr"),
        Map.entry(5L, "Mai"),
        Map.entry(6L, "Jun"),
        Map.entry(7L, "Jul"),
        Map.entry(8L, "Aug"),
        Map.entry(9L, "Sep"),
        Map.entry(10L, "Okt"),
        Map.entry(11L, "Nov"),
        Map.entry(12L, "Dez"));

DateTimeFormatter dtf =
    new DateTimeFormatterBuilder()
        .appendText(ChronoField.DAY_OF_WEEK, dayOfWeekTexts)
        .appendLiteral(' ')
        .appendText(ChronoField.MONTH_OF_YEAR, monthTexts)
        .appendPattern(" dd HH:mm:ss yyyy")
        .toFormatter(Locale.GERMAN);

LocalDateTime dateTime = LocalDateTime.parse(dateString, dtf);

这只是从https://dev59.com/j1UL5IYBdhLWcg3waHRw#50412644稍微修改过的答案。


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