时区和 Locale 让同一份测试在不同机器上给出不同结果。这篇在真实 JVM 里验证:默认值何时读取、缓存的 Clock、夏令时的缺口与重叠、不同 JDK 版本与本地化数据的输出差别,以及测试应该怎样固定它们。
启动时 Locale.getDefault() = en_US,TimeZone.getDefault() = UTC,user.language = en,user.timezone = UTC
System.setProperty(user.language=tr, user.country=TR) 之后:Locale.getDefault() = en_US,"title".toUpperCase() = TITLE(没有变化,默认 Locale 在启动时读取并缓存)
System.setProperty(user.timezone=Asia/Tokyo) 之后:TimeZone.getDefault() = UTC(没有变化)
Locale.setDefault(tr-TR) 之后:Locale.getDefault() = tr_TR,"title".toUpperCase() = TİTLE,String.format("%.2f", 1234.5) = 1234,50,NumberFormat.getInstance().format(1234.5) = 1.234,5(这些都是在调用时读取默认 Locale,立即生效)
只设置 Category.FORMAT 为 de-DE:getDefault(FORMAT) = de_DE,getDefault(DISPLAY) = tr_TR,getDefault() = tr_TR,String.format("%.2f", 1234.5) = 1234,50,Locale.FRANCE.getDisplayCountry() = Fransa
Locale.setDefault(Locale.US) 之后 "title".toUpperCase() = TITLE,String.format("%.2f", 1234.5) = 1234.50
显式传 Locale 就不依赖默认值:"title".toUpperCase(Locale.ROOT) = TITLE,String.format(Locale.ROOT, "%.2f", 1234.5) = 1234.50,NumberFormat.getInstance(Locale.GERMANY).format(1234.5) = 1.234,5 默认时区 = Asia/Tokyo:ZoneId.systemDefault() = Asia/Tokyo,LocalDateTime.ofInstant(fixed, ZoneId.systemDefault()) = 2026-10-06T01:30,new Date(fixed).toString() = Tue Oct 06 01:30:00 JST 2026
默认时区改成 America/Los_Angeles:LocalDateTime.ofInstant(fixed, ZoneId.systemDefault()) = 2026-10-05T09:30;在 UTC 默认时区下创建并缓存的 Clock.systemDefaultZone().getZone() = UTC,用它算出的 LocalDateTime = 2026-10-05T16:30(缓存了旧时区)
类静态字段 Clock CAPTURED 在 JVM 启动时创建(默认时区 UTC),现在默认时区是 America/Los_Angeles,CAPTURED.getZone() = UTC
LocalDateTime.now() 用的是调用时的默认时区,现在是几点:LocalDateTime.now().getHour() = 4 时,LocalDateTime.now(CAPTURED).getHour() = 11 时,LocalDateTime.now(ZoneId.of("Asia/Tokyo")).getHour() = 20 时
夏令时开始(2026-03-08 02:00 → 03:00):ZonedDateTime.of(2026-03-08 02:30 America/New_York) = 2026-03-08T03:30-04:00[America/New_York](不存在的时间被推后 1 小时)
夏令时结束(2026-11-01 02:00 → 01:00):ZonedDateTime.of(2026-11-01 01:30 America/New_York) = 2026-11-01T01:30-04:00[America/New_York](模糊的时间默认取较早的偏移量),withLaterOffsetAtOverlap() = 2026-11-01T01:30-05:00[America/New_York]
跨夏令时的加法:2026-03-07 12:00 plusDays(1) = 2026-03-08T12:00-04:00[America/New_York],plusHours(24) = 2026-03-08T13:00-04:00[America/New_York],Duration.between(d0, d0.plusDays(1)) = 23 小时
一天的长度:2026-03-08 在 America/New_York 有 23 小时,2026-11-01 有 25 小时
SimpleDateFormat 使用默认时区(现在是 America/Los_Angeles):2026-10-05 09:30 PDT;另一个 SimpleDateFormat 在创建之后 TimeZone.setDefault(UTC)(已创建的实例不受影响):
原来那个 sdf 仍然格式化为 2026-10-05 09:30 PDT,新建的 SimpleDateFormat 格式化为 2026-10-05 16:30 UTC [JDK 17]
fr-FR NumberFormat(1234567.89) = 1\u202F234\u202F567,89;货币 = 1\u202F234,50\u00A0\u20AC;日期 MEDIUM = 1 janv. 1970;时间 SHORT = 15:04
de-DE NumberFormat(1234567.89) = 1.234.567,89;货币 = 1.234,50\u00A0\u20AC;日期 MEDIUM = 01.01.1970;时间 SHORT = 15:04
pt-BR NumberFormat(1234567.89) = 1.234.567,89;货币 = R$\u00A01.234,50;日期 MEDIUM = 1 de jan. de 1970;时间 SHORT = 15:04
hi-IN NumberFormat(1234567.89) = 1,234,567.89;货币 = \u20B91,234.50;日期 MEDIUM = 1 \u091C\u0928\u0970 1970;时间 SHORT = 3:04 pm
ar-EG NumberFormat(1234567.89) = \u0661\u066C\u0662\u0663\u0664\u066C\u0665\u0666\u0667\u066B\u0668\u0669;货币 = \u0661\u066C\u0662\u0663\u0664\u066B\u0665\u0660\u00A0\u062C.\u0645.\u200F;日期 MEDIUM = \u0660\u0661\u200F/\u0660\u0661\u200F/\u0661\u0669\u0667\u0660;时间 SHORT = 3:04 \u0645
ja-JP NumberFormat(1234567.89) = 1,234,567.89;货币 = \uFFE51,234;日期 MEDIUM = 1970/01/01;时间 SHORT = 15:04
java.locale.providers = null,Locale.US 的 "Sep" 缩写:Sep,en-GB 的 "Sep" 缩写:Sept
[JDK 21]
fr-FR NumberFormat(1234567.89) = 1\u202F234\u202F567,89;货币 = 1\u202F234,50\u00A0\u20AC;日期 MEDIUM = 1 janv. 1970;时间 SHORT = 15:04
de-DE NumberFormat(1234567.89) = 1.234.567,89;货币 = 1.234,50\u00A0\u20AC;日期 MEDIUM = 01.01.1970;时间 SHORT = 15:04
pt-BR NumberFormat(1234567.89) = 1.234.567,89;货币 = R$\u00A01.234,50;日期 MEDIUM = 1 de jan. de 1970;时间 SHORT = 15:04
hi-IN NumberFormat(1234567.89) = 1,234,567.89;货币 = \u20B91,234.50;日期 MEDIUM = 1 \u091C\u0928\u0970 1970;时间 SHORT = 3:04 pm
ar-EG NumberFormat(1234567.89) = \u0661\u066C\u0662\u0663\u0664\u066C\u0665\u0666\u0667\u066B\u0668\u0669;货币 = \u200F\u0661\u066C\u0662\u0663\u0664\u066B\u0665\u0660\u00A0\u062C.\u0645.\u200F;日期 MEDIUM = \u0660\u0661\u200F/\u0660\u0661\u200F/\u0661\u0669\u0667\u0660;时间 SHORT = 3:04 \u0645
ja-JP NumberFormat(1234567.89) = 1,234,567.89;货币 = \uFFE51,234;日期 MEDIUM = 1970/01/01;时间 SHORT = 15:04
java.locale.providers = null,Locale.US 的 "Sep" 缩写:Sep,en-GB 的 "Sep" 缩写:Sept
[JDK 27]
fr-FR NumberFormat(1234567.89) = 1\u202F234\u202F567,89;货币 = 1\u202F234,50\u00A0\u20AC;日期 MEDIUM = 1 janv. 1970;时间 SHORT = 15:04
de-DE NumberFormat(1234567.89) = 1.234.567,89;货币 = 1.234,50\u00A0\u20AC;日期 MEDIUM = 01.01.1970;时间 SHORT = 15:04
pt-BR NumberFormat(1234567.89) = 1.234.567,89;货币 = R$\u00A01.234,50;日期 MEDIUM = 1 de jan. de 1970;时间 SHORT = 15:04
hi-IN NumberFormat(1234567.89) = 1,234,567.89;货币 = \u20B91,234.50;日期 MEDIUM = 1 \u091C\u0928\u0970 1970;时间 SHORT = 3:04 pm
ar-EG NumberFormat(1234567.89) = \u0661\u066C\u0662\u0663\u0664\u066C\u0665\u0666\u0667\u066B\u0668\u0669;货币 = \u200F\u0661\u066C\u0662\u0663\u0664\u066B\u0665\u0660\u00A0\u062C.\u0645.\u200F;日期 MEDIUM = \u0660\u0661\u200F/\u0660\u0661\u200F/\u0661\u0669\u0667\u0660;时间 SHORT = 3:04 \u0645
ja-JP NumberFormat(1234567.89) = 1,234,567.89;货币 = \uFFE51,234;日期 MEDIUM = 1970/01/01;时间 SHORT = 15:04
java.locale.providers = null,Locale.US 的 "Sep" 缩写:Sep,en-GB 的 "Sep" 缩写:Sept
JDK 17 与 JDK 21 输出不同的行数 1 / 7;JDK 21 与 JDK 27 不同的行数 0 / 7
[JDK 21 + -Djava.locale.providers=COMPAT]
fr-FR NumberFormat(1234567.89) = 1\u00A0234\u00A0567,89;货币 = 1\u00A0234,50 \u20AC;日期 MEDIUM = 1 janv. 1970;时间 SHORT = 15:04
de-DE NumberFormat(1234567.89) = 1.234.567,89;货币 = 1.234,50 \u20AC;日期 MEDIUM = 01.01.1970;时间 SHORT = 15:04
pt-BR NumberFormat(1234567.89) = 1.234.567,89;货币 = R$ 1.234,50;日期 MEDIUM = 01/01/1970;时间 SHORT = 15:04
hi-IN NumberFormat(1234567.89) = \u0967,\u0968\u0969\u096A,\u096B\u096C\u096D.\u096E\u096F;货币 = \u0930\u0942 \u0967,\u0968\u0969\u096A.\u096B\u0966;日期 MEDIUM = \u0967 \u091C\u0928\u0935\u0930\u0940, \u0967\u096F\u096D\u0966;时间 SHORT = 3:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928
ar-EG NumberFormat(1234567.89) = 1,234,567.89;货币 = \u062C.\u0645.\u200F 1,234.5;日期 MEDIUM = 01/01/1970;时间 SHORT = 03:04 \u0645
ja-JP NumberFormat(1234567.89) = 1,234,567.89;货币 = \uFFE51,234;日期 MEDIUM = 1970/01/01;时间 SHORT = 15:04
java.locale.providers = COMPAT,Locale.US 的 "Sep" 缩写:Sep,en-GB 的 "Sep" 缩写:Sep