From cd9b1bc820540184c79dd1957edc7ad4e8e469dc Mon Sep 17 00:00:00 2001 From: Justin Lu Date: Mon, 9 Jun 2025 20:49:33 +0000 Subject: [PATCH] 8358426: Improve lazy computation in Locale Reviewed-by: naoto, liach --- .../share/classes/java/util/Locale.java | 180 +++++------------- .../classes/java/util/LocaleISOData.java | 91 ++++++++- .../classes/sun/util/locale/BaseLocale.java | 18 +- 3 files changed, 148 insertions(+), 141 deletions(-) diff --git a/src/java.base/share/classes/java/util/Locale.java b/src/java.base/share/classes/java/util/Locale.java index 993495ff5ab..9059d196861 100644 --- a/src/java.base/share/classes/java/util/Locale.java +++ b/src/java.base/share/classes/java/util/Locale.java @@ -48,8 +48,8 @@ import java.io.Serializable; import java.text.NumberFormat; import java.text.MessageFormat; import java.text.ParsePosition; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.function.Supplier; import java.util.spi.LocaleNameProvider; import java.util.stream.Stream; @@ -733,58 +733,24 @@ public final class Locale implements Cloneable, Serializable { * @see #getISOCountries(Locale.IsoCountryCode) * @since 9 */ - public static enum IsoCountryCode { + public enum IsoCountryCode { /** * PART1_ALPHA2 is used to represent the ISO3166-1 alpha-2 two letter * country codes. */ - PART1_ALPHA2 { - @Override - Set createCountryCodeSet() { - return Set.of(Locale.getISOCountries()); - } - }, + PART1_ALPHA2, /** * * PART1_ALPHA3 is used to represent the ISO3166-1 alpha-3 three letter * country codes. */ - PART1_ALPHA3 { - @Override - Set createCountryCodeSet() { - return LocaleISOData.computeISO3166_1Alpha3Countries(); - } - }, + PART1_ALPHA3, /** * PART3 is used to represent the ISO3166-3 four letter country codes. */ - PART3 { - @Override - Set createCountryCodeSet() { - return Set.of(LocaleISOData.ISO3166_3); - } - }; - - /** - * Concrete implementation of this method attempts to compute value - * for iso3166CodesMap for each IsoCountryCode type key. - */ - abstract Set createCountryCodeSet(); - - /** - * Map to hold country codes for each ISO3166 part. - */ - private static final Map> iso3166CodesMap = new ConcurrentHashMap<>(); - - /** - * This method is called from Locale class to retrieve country code set - * for getISOCountries(type) - */ - static Set retrieveISOCountryCodes(IsoCountryCode type) { - return iso3166CodesMap.computeIfAbsent(type, IsoCountryCode::createCountryCodeSet); - } + PART3 } /** @@ -1004,30 +970,28 @@ public final class Locale implements Cloneable, Serializable { return getInstance(baseloc, extensions); } - static Locale getInstance(BaseLocale baseloc, LocaleExtensions extensions) { if (extensions == null) { Locale locale = CONSTANT_LOCALES.get(baseloc); if (locale != null) { return locale; } - return LocaleCache.cache(baseloc); + return LOCALE_CACHE.get().computeIfAbsent(baseloc, LOCALE_CREATOR); } else { LocaleKey key = new LocaleKey(baseloc, extensions); - return LocaleCache.cache(key); + return LOCALE_CACHE.get().computeIfAbsent(key, LOCALE_CREATOR); } } - private static final class LocaleCache implements Function { - private static final ReferencedKeyMap LOCALE_CACHE - = ReferencedKeyMap.create(true, ReferencedKeyMap.concurrentHashMapSupplier()); - - private static final Function LOCALE_CREATOR = new LocaleCache(); - - public static Locale cache(Object key) { - return LOCALE_CACHE.computeIfAbsent(key, LOCALE_CREATOR); - } + private static final Supplier> LOCALE_CACHE = + StableValue.supplier(new Supplier<>() { + @Override + public ReferencedKeyMap get() { + return ReferencedKeyMap.create(true, ReferencedKeyMap.concurrentHashMapSupplier()); + } + }); + private static final Function LOCALE_CREATOR = new Function<>() { @Override public Locale apply(Object key) { if (key instanceof BaseLocale base) { @@ -1036,7 +1000,7 @@ public final class Locale implements Cloneable, Serializable { LocaleKey lk = (LocaleKey)key; return new Locale(lk.base, lk.exts); } - } + }; private static final class LocaleKey { @@ -1301,12 +1265,8 @@ public final class Locale implements Cloneable, Serializable { * @return An array of ISO 3166 two-letter country codes. */ public static String[] getISOCountries() { - if (isoCountries == null) { - isoCountries = getISO2Table(LocaleISOData.isoCountryTable); - } - String[] result = new String[isoCountries.length]; - System.arraycopy(isoCountries, 0, result, 0, isoCountries.length); - return result; + String[] countries = LocaleISOData.ISO_3166_1_ALPHA2.get(); + return Arrays.copyOf(countries, countries.length); } /** @@ -1319,7 +1279,11 @@ public final class Locale implements Cloneable, Serializable { */ public static Set getISOCountries(IsoCountryCode type) { Objects.requireNonNull(type); - return IsoCountryCode.retrieveISOCountryCodes(type); + return switch (type) { + case PART1_ALPHA2 -> Set.of(LocaleISOData.ISO_3166_1_ALPHA2.get()); + case PART1_ALPHA3 -> LocaleISOData.ISO_3166_1_ALPHA3.get(); + case PART3 -> LocaleISOData.ISO_3166_3.get(); + }; } /** @@ -1339,22 +1303,8 @@ public final class Locale implements Cloneable, Serializable { * @return An array of ISO 639 two-letter language codes. */ public static String[] getISOLanguages() { - String[] languages = Locale.isoLanguages; - if (languages == null) { - Locale.isoLanguages = languages = getISO2Table(LocaleISOData.isoLanguageTable); - } - String[] result = new String[languages.length]; - System.arraycopy(languages, 0, result, 0, languages.length); - return result; - } - - private static String[] getISO2Table(String table) { - int len = table.length() / 5; - String[] isoTable = new String[len]; - for (int i = 0, j = 0; i < len; i++, j += 5) { - isoTable[i] = table.substring(j, j + 2); - } - return isoTable; + String[] languages = LocaleISOData.ISO_639.get(); + return Arrays.copyOf(languages, languages.length); } /** @@ -1683,61 +1633,54 @@ public final class Locale implements Cloneable, Serializable { * @since 1.7 */ public String toLanguageTag() { - String lTag = this.languageTag; - if (lTag != null) { - return lTag; - } + return languageTag.get(); + } + private String computeLanguageTag() { LanguageTag tag = LanguageTag.parseLocale(baseLocale, localeExtensions); - StringBuilder buf = new StringBuilder(); + StringBuilder bldr = new StringBuilder(); String subtag = tag.language(); if (!subtag.isEmpty()) { - buf.append(LanguageTag.canonicalizeLanguage(subtag)); + bldr.append(LanguageTag.canonicalizeLanguage(subtag)); } subtag = tag.script(); if (!subtag.isEmpty()) { - buf.append(LanguageTag.SEP); - buf.append(LanguageTag.canonicalizeScript(subtag)); + bldr.append(LanguageTag.SEP); + bldr.append(LanguageTag.canonicalizeScript(subtag)); } subtag = tag.region(); if (!subtag.isEmpty()) { - buf.append(LanguageTag.SEP); - buf.append(LanguageTag.canonicalizeRegion(subtag)); + bldr.append(LanguageTag.SEP); + bldr.append(LanguageTag.canonicalizeRegion(subtag)); } Listsubtags = tag.variants(); for (String s : subtags) { - buf.append(LanguageTag.SEP); + bldr.append(LanguageTag.SEP); // preserve casing - buf.append(s); + bldr.append(s); } subtags = tag.extensions(); for (String s : subtags) { - buf.append(LanguageTag.SEP); - buf.append(LanguageTag.canonicalizeExtension(s)); + bldr.append(LanguageTag.SEP); + bldr.append(LanguageTag.canonicalizeExtension(s)); } subtag = tag.privateuse(); if (!subtag.isEmpty()) { - if (buf.length() > 0) { - buf.append(LanguageTag.SEP); + if (bldr.length() > 0) { + bldr.append(LanguageTag.SEP); } - buf.append(LanguageTag.PRIVATEUSE).append(LanguageTag.SEP); + bldr.append(LanguageTag.PRIVATEUSE).append(LanguageTag.SEP); // preserve casing - buf.append(subtag); + bldr.append(subtag); } - String langTag = buf.toString(); - synchronized (this) { - if (this.languageTag == null) { - this.languageTag = langTag; - } - } - return langTag; + return bldr.toString(); } /** @@ -1961,7 +1904,7 @@ public final class Locale implements Cloneable, Serializable { return lang; } - String language3 = getISO3Code(lang, LocaleISOData.isoLanguageTable); + String language3 = LocaleISOData.getISO3LangCode(lang); if (language3 == null) { throw new MissingResourceException("Couldn't find 3-letter language code for " + lang, "FormatData_" + toString(), "ShortLanguage"); @@ -1983,7 +1926,7 @@ public final class Locale implements Cloneable, Serializable { * three-letter country abbreviation is not available for this locale. */ public String getISO3Country() throws MissingResourceException { - String country3 = getISO3Code(baseLocale.getRegion(), LocaleISOData.isoCountryTable); + String country3 = LocaleISOData.getISO3CtryCode(baseLocale.getRegion()); if (country3 == null) { throw new MissingResourceException("Couldn't find 3-letter country code for " + baseLocale.getRegion(), "FormatData_" + toString(), "ShortCountry"); @@ -1991,27 +1934,6 @@ public final class Locale implements Cloneable, Serializable { return country3; } - private static String getISO3Code(String iso2Code, String table) { - int codeLength = iso2Code.length(); - if (codeLength == 0) { - return ""; - } - - int tableLength = table.length(); - int index = tableLength; - if (codeLength == 2) { - char c1 = iso2Code.charAt(0); - char c2 = iso2Code.charAt(1); - for (index = 0; index < tableLength; index += 5) { - if (table.charAt(index) == c1 - && table.charAt(index + 1) == c2) { - break; - } - } - } - return index < tableLength ? table.substring(index + 2, index + 5) : null; - } - /** * Returns a name for the locale's language that is appropriate for display to the * user. @@ -2393,7 +2315,13 @@ public final class Locale implements Cloneable, Serializable { private static volatile Locale defaultDisplayLocale; private static volatile Locale defaultFormatLocale; - private transient volatile String languageTag; + private final transient Supplier languageTag = + StableValue.supplier(new Supplier<>() { + @Override + public String get() { + return computeLanguageTag(); + } + }); /** * Return an array of the display names of the variant. @@ -2587,10 +2515,6 @@ public final class Locale implements Cloneable, Serializable { baseLocale.getRegion(), baseLocale.getVariant(), localeExtensions); } - private static volatile String[] isoLanguages; - - private static volatile String[] isoCountries; - private static String convertOldISOCodes(String language) { // we accept both the old and the new ISO codes for the languages whose ISO // codes have changed, but we always store the NEW code, unless the property diff --git a/src/java.base/share/classes/java/util/LocaleISOData.java b/src/java.base/share/classes/java/util/LocaleISOData.java index c2090d3be19..29e0b28be01 100644 --- a/src/java.base/share/classes/java/util/LocaleISOData.java +++ b/src/java.base/share/classes/java/util/LocaleISOData.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,11 +25,47 @@ package java.util; +import java.util.function.Supplier; + +// Methods and suppliers for producing ISO 639/3166 resources used by Locale. class LocaleISOData { + + static final Supplier ISO_639 = + StableValue.supplier(new Supplier<>() { + @Override + public String[] get() { + return getISO2Table(isoLanguageTable); + } + }); + + static final Supplier ISO_3166_1_ALPHA2 = + StableValue.supplier(new Supplier<>() { + @Override + public String[] get() { + return getISO2Table(isoCountryTable); + } + }); + + static final Supplier> ISO_3166_1_ALPHA3 = + StableValue.supplier(new Supplier<>() { + @Override + public Set get() { + return computeISO3166_1Alpha3Countries(); + } + }); + + static final Supplier> ISO_3166_3 = + StableValue.supplier(new Supplier<>() { + @Override + public Set get() { + return Set.of(ISO3166_3); + } + }); + /** * The 2- and 3-letter ISO 639 language codes. */ - static final String isoLanguageTable = + private static final String isoLanguageTable = "aa" + "aar" // Afar + "ab" + "abk" // Abkhazian + "ae" + "ave" // Avestan @@ -223,7 +259,7 @@ class LocaleISOData { /** * The 2- and 3-letter ISO 3166 country codes. */ - static final String isoCountryTable = + private static final String isoCountryTable = "AD" + "AND" // Andorra, Principality of + "AE" + "ARE" // United Arab Emirates + "AF" + "AFG" // Afghanistan @@ -480,18 +516,60 @@ class LocaleISOData { /** * Array to hold country codes for ISO3166-3. */ - static final String[] ISO3166_3 = { + private static final String[] ISO3166_3 = { "AIDJ", "ANHH", "BQAQ", "BUMM", "BYAA", "CSHH", "CSXX", "CTKI", "DDDE", "DYBJ", "FQHH", "FXFR", "GEHH", "HVBF", "JTUM", "MIUM", "NHVU", "NQAQ", "NTHH", "PCHH", "PUUM", "PZPA", "RHZW", "SKIN", "SUHH", "TPTL", "VDVN", "WKUM", "YDYE", "YUCS", "ZRCD" }; + static String getISO3LangCode(String language) { + return getISO3Code(language, isoLanguageTable); + } + + static String getISO3CtryCode(String country) { + return getISO3Code(country, isoCountryTable); + } + + private static String getISO3Code(String iso2Code, String table) { + int codeLength = iso2Code.length(); + if (codeLength == 0) { + return ""; + } + + int tableLength = table.length(); + int index = tableLength; + if (codeLength == 2) { + char c1 = iso2Code.charAt(0); + char c2 = iso2Code.charAt(1); + for (index = 0; index < tableLength; index += 5) { + if (table.charAt(index) == c1 + && table.charAt(index + 1) == c2) { + break; + } + } + } + return index < tableLength ? table.substring(index + 2, index + 5) : null; + } + + /** + * This method computes an array of alpha-2 codes from either ISO639 or + * ISO3166. + */ + private static String[] getISO2Table(String table) { + int len = table.length() / 5; + String[] isoTable = new String[len]; + for (int i = 0, j = 0; i < len; i++, j += 5) { + isoTable[i] = table.substring(j, j + 2); + } + return isoTable; + } + /** * This method computes a set of ISO3166-1 alpha-3 country codes from * existing isoCountryTable. */ - static Set computeISO3166_1Alpha3Countries() { + private static Set computeISO3166_1Alpha3Countries() { int tableLength = isoCountryTable.length(); String[] isoTable = new String[tableLength / 5]; for (int i = 0, index = 0; index < tableLength; i++, index += 5) { @@ -500,6 +578,5 @@ class LocaleISOData { return Set.of(isoTable); } - private LocaleISOData() { - } + private LocaleISOData() {} } diff --git a/src/java.base/share/classes/sun/util/locale/BaseLocale.java b/src/java.base/share/classes/sun/util/locale/BaseLocale.java index d1fe8d24b72..529ca1b0c13 100644 --- a/src/java.base/share/classes/sun/util/locale/BaseLocale.java +++ b/src/java.base/share/classes/sun/util/locale/BaseLocale.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -38,6 +38,7 @@ import jdk.internal.util.StaticProperty; import jdk.internal.vm.annotation.Stable; import java.util.StringJoiner; +import java.util.function.Supplier; public final class BaseLocale { @@ -90,6 +91,15 @@ public final class BaseLocale { } } + // Interned BaseLocale cache + private static final Supplier> CACHE = + StableValue.supplier(new Supplier<>() { + @Override + public ReferencedKeySet get() { + return ReferencedKeySet.create(true, ReferencedKeySet.concurrentHashMapSupplier()); + } + }); + public static final String SEP = "_"; private final String language; @@ -164,11 +174,7 @@ public final class BaseLocale { // Obtain the "interned" BaseLocale from the cache. The returned // "interned" instance can subsequently be used by the Locale // instance which guarantees the locale components are properly cased/interned. - class InterningCache { // TODO: StableValue - private static final ReferencedKeySet CACHE = - ReferencedKeySet.create(true, ReferencedKeySet.concurrentHashMapSupplier()); - } - return InterningCache.CACHE.intern(new BaseLocale( + return CACHE.get().intern(new BaseLocale( language.intern(), // guaranteed to be lower-case LocaleUtils.toTitleString(script).intern(), region.intern(), // guaranteed to be upper-case