bigywater.tech
Java · 429

时区与 Locale 测试

时区和 Locale 让同一份测试在不同机器上给出不同结果。这篇在真实 JVM 里验证:默认值何时读取、缓存的 Clock、夏令时的缺口与重叠、不同 JDK 版本与本地化数据的输出差别,以及测试应该怎样固定它们。

TİTLE
Locale.setDefault(tr-TR) 之后 "title".toUpperCase() 的结果;显式传 Locale.ROOT 不受影响
23 / 25 小时
2026-03-08 与 2026-11-01 在 America/New_York 的一天长度
1 / 7
JDK 17 与 JDK 21 本地化输出不同的行数(JDK 21 与 27 是 0 / 7)

默认 Locale 的读取时机

实测 · 本机执行输出
  启动时 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

默认时区、缓存的 Clock 与夏令时

实测 · 本机执行输出
  默认时区 = 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 版本与本地化数据

实测 · 本机执行输出
  [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

分步演示

时区与 Locale
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 17.0.20、21.0.12、27;用 -Duser.language / -Duser.country / -Duser.timezone 与 -Djava.locale.providers=COMPAT 启动;输出里的 Unicode 码点转义是为了显示不可见字符
    • 没有验证:Linux 容器里的 TZ 环境变量与 /etc/localtime 的读取、JUnit 5 的 @DefaultLocale / @DefaultTimeZone(junit-pioneer)扩展、tzdata 版本差异(同一个 JDK 更新 tzdata 之后的历史偏移)、Locale.setDefault 对 ForkJoin 线程与其它线程的并发影响、java.util.logging 与日志框架的时间格式
    京ICP备2026067502号-1京公网安备11010502063487号