8358426: Improve lazy computation in Locale

Reviewed-by: naoto, liach
This commit is contained in:
Justin Lu 2025-06-09 20:49:33 +00:00
parent fcb68ea22d
commit cd9b1bc820
3 changed files with 148 additions and 141 deletions

View File

@ -48,8 +48,8 @@ import java.io.Serializable;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.text.ParsePosition; import java.text.ParsePosition;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import java.util.spi.LocaleNameProvider; import java.util.spi.LocaleNameProvider;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -733,58 +733,24 @@ public final class Locale implements Cloneable, Serializable {
* @see #getISOCountries(Locale.IsoCountryCode) * @see #getISOCountries(Locale.IsoCountryCode)
* @since 9 * @since 9
*/ */
public static enum IsoCountryCode { public enum IsoCountryCode {
/** /**
* PART1_ALPHA2 is used to represent the ISO3166-1 alpha-2 two letter * PART1_ALPHA2 is used to represent the ISO3166-1 alpha-2 two letter
* country codes. * country codes.
*/ */
PART1_ALPHA2 { PART1_ALPHA2,
@Override
Set<String> createCountryCodeSet() {
return Set.of(Locale.getISOCountries());
}
},
/** /**
* *
* PART1_ALPHA3 is used to represent the ISO3166-1 alpha-3 three letter * PART1_ALPHA3 is used to represent the ISO3166-1 alpha-3 three letter
* country codes. * country codes.
*/ */
PART1_ALPHA3 { PART1_ALPHA3,
@Override
Set<String> createCountryCodeSet() {
return LocaleISOData.computeISO3166_1Alpha3Countries();
}
},
/** /**
* PART3 is used to represent the ISO3166-3 four letter country codes. * PART3 is used to represent the ISO3166-3 four letter country codes.
*/ */
PART3 { PART3
@Override
Set<String> 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<String> createCountryCodeSet();
/**
* Map to hold country codes for each ISO3166 part.
*/
private static final Map<IsoCountryCode, Set<String>> iso3166CodesMap = new ConcurrentHashMap<>();
/**
* This method is called from Locale class to retrieve country code set
* for getISOCountries(type)
*/
static Set<String> retrieveISOCountryCodes(IsoCountryCode type) {
return iso3166CodesMap.computeIfAbsent(type, IsoCountryCode::createCountryCodeSet);
}
} }
/** /**
@ -1004,30 +970,28 @@ public final class Locale implements Cloneable, Serializable {
return getInstance(baseloc, extensions); return getInstance(baseloc, extensions);
} }
static Locale getInstance(BaseLocale baseloc, LocaleExtensions extensions) { static Locale getInstance(BaseLocale baseloc, LocaleExtensions extensions) {
if (extensions == null) { if (extensions == null) {
Locale locale = CONSTANT_LOCALES.get(baseloc); Locale locale = CONSTANT_LOCALES.get(baseloc);
if (locale != null) { if (locale != null) {
return locale; return locale;
} }
return LocaleCache.cache(baseloc); return LOCALE_CACHE.get().computeIfAbsent(baseloc, LOCALE_CREATOR);
} else { } else {
LocaleKey key = new LocaleKey(baseloc, extensions); 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<Object, Locale> { private static final Supplier<ReferencedKeyMap<Object, Locale>> LOCALE_CACHE =
private static final ReferencedKeyMap<Object, Locale> LOCALE_CACHE StableValue.supplier(new Supplier<>() {
= ReferencedKeyMap.create(true, ReferencedKeyMap.concurrentHashMapSupplier()); @Override
public ReferencedKeyMap<Object, Locale> get() {
private static final Function<Object, Locale> LOCALE_CREATOR = new LocaleCache(); return ReferencedKeyMap.create(true, ReferencedKeyMap.concurrentHashMapSupplier());
}
public static Locale cache(Object key) { });
return LOCALE_CACHE.computeIfAbsent(key, LOCALE_CREATOR);
}
private static final Function<Object, Locale> LOCALE_CREATOR = new Function<>() {
@Override @Override
public Locale apply(Object key) { public Locale apply(Object key) {
if (key instanceof BaseLocale base) { if (key instanceof BaseLocale base) {
@ -1036,7 +1000,7 @@ public final class Locale implements Cloneable, Serializable {
LocaleKey lk = (LocaleKey)key; LocaleKey lk = (LocaleKey)key;
return new Locale(lk.base, lk.exts); return new Locale(lk.base, lk.exts);
} }
} };
private static final class LocaleKey { 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. * @return An array of ISO 3166 two-letter country codes.
*/ */
public static String[] getISOCountries() { public static String[] getISOCountries() {
if (isoCountries == null) { String[] countries = LocaleISOData.ISO_3166_1_ALPHA2.get();
isoCountries = getISO2Table(LocaleISOData.isoCountryTable); return Arrays.copyOf(countries, countries.length);
}
String[] result = new String[isoCountries.length];
System.arraycopy(isoCountries, 0, result, 0, isoCountries.length);
return result;
} }
/** /**
@ -1319,7 +1279,11 @@ public final class Locale implements Cloneable, Serializable {
*/ */
public static Set<String> getISOCountries(IsoCountryCode type) { public static Set<String> getISOCountries(IsoCountryCode type) {
Objects.requireNonNull(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. * @return An array of ISO 639 two-letter language codes.
*/ */
public static String[] getISOLanguages() { public static String[] getISOLanguages() {
String[] languages = Locale.isoLanguages; String[] languages = LocaleISOData.ISO_639.get();
if (languages == null) { return Arrays.copyOf(languages, languages.length);
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;
} }
/** /**
@ -1683,61 +1633,54 @@ public final class Locale implements Cloneable, Serializable {
* @since 1.7 * @since 1.7
*/ */
public String toLanguageTag() { public String toLanguageTag() {
String lTag = this.languageTag; return languageTag.get();
if (lTag != null) { }
return lTag;
}
private String computeLanguageTag() {
LanguageTag tag = LanguageTag.parseLocale(baseLocale, localeExtensions); LanguageTag tag = LanguageTag.parseLocale(baseLocale, localeExtensions);
StringBuilder buf = new StringBuilder(); StringBuilder bldr = new StringBuilder();
String subtag = tag.language(); String subtag = tag.language();
if (!subtag.isEmpty()) { if (!subtag.isEmpty()) {
buf.append(LanguageTag.canonicalizeLanguage(subtag)); bldr.append(LanguageTag.canonicalizeLanguage(subtag));
} }
subtag = tag.script(); subtag = tag.script();
if (!subtag.isEmpty()) { if (!subtag.isEmpty()) {
buf.append(LanguageTag.SEP); bldr.append(LanguageTag.SEP);
buf.append(LanguageTag.canonicalizeScript(subtag)); bldr.append(LanguageTag.canonicalizeScript(subtag));
} }
subtag = tag.region(); subtag = tag.region();
if (!subtag.isEmpty()) { if (!subtag.isEmpty()) {
buf.append(LanguageTag.SEP); bldr.append(LanguageTag.SEP);
buf.append(LanguageTag.canonicalizeRegion(subtag)); bldr.append(LanguageTag.canonicalizeRegion(subtag));
} }
List<String>subtags = tag.variants(); List<String>subtags = tag.variants();
for (String s : subtags) { for (String s : subtags) {
buf.append(LanguageTag.SEP); bldr.append(LanguageTag.SEP);
// preserve casing // preserve casing
buf.append(s); bldr.append(s);
} }
subtags = tag.extensions(); subtags = tag.extensions();
for (String s : subtags) { for (String s : subtags) {
buf.append(LanguageTag.SEP); bldr.append(LanguageTag.SEP);
buf.append(LanguageTag.canonicalizeExtension(s)); bldr.append(LanguageTag.canonicalizeExtension(s));
} }
subtag = tag.privateuse(); subtag = tag.privateuse();
if (!subtag.isEmpty()) { if (!subtag.isEmpty()) {
if (buf.length() > 0) { if (bldr.length() > 0) {
buf.append(LanguageTag.SEP); bldr.append(LanguageTag.SEP);
} }
buf.append(LanguageTag.PRIVATEUSE).append(LanguageTag.SEP); bldr.append(LanguageTag.PRIVATEUSE).append(LanguageTag.SEP);
// preserve casing // preserve casing
buf.append(subtag); bldr.append(subtag);
} }
String langTag = buf.toString(); return bldr.toString();
synchronized (this) {
if (this.languageTag == null) {
this.languageTag = langTag;
}
}
return langTag;
} }
/** /**
@ -1961,7 +1904,7 @@ public final class Locale implements Cloneable, Serializable {
return lang; return lang;
} }
String language3 = getISO3Code(lang, LocaleISOData.isoLanguageTable); String language3 = LocaleISOData.getISO3LangCode(lang);
if (language3 == null) { if (language3 == null) {
throw new MissingResourceException("Couldn't find 3-letter language code for " throw new MissingResourceException("Couldn't find 3-letter language code for "
+ lang, "FormatData_" + toString(), "ShortLanguage"); + 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. * three-letter country abbreviation is not available for this locale.
*/ */
public String getISO3Country() throws MissingResourceException { public String getISO3Country() throws MissingResourceException {
String country3 = getISO3Code(baseLocale.getRegion(), LocaleISOData.isoCountryTable); String country3 = LocaleISOData.getISO3CtryCode(baseLocale.getRegion());
if (country3 == null) { if (country3 == null) {
throw new MissingResourceException("Couldn't find 3-letter country code for " throw new MissingResourceException("Couldn't find 3-letter country code for "
+ baseLocale.getRegion(), "FormatData_" + toString(), "ShortCountry"); + baseLocale.getRegion(), "FormatData_" + toString(), "ShortCountry");
@ -1991,27 +1934,6 @@ public final class Locale implements Cloneable, Serializable {
return country3; 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 * Returns a name for the locale's language that is appropriate for display to the
* user. * user.
@ -2393,7 +2315,13 @@ public final class Locale implements Cloneable, Serializable {
private static volatile Locale defaultDisplayLocale; private static volatile Locale defaultDisplayLocale;
private static volatile Locale defaultFormatLocale; private static volatile Locale defaultFormatLocale;
private transient volatile String languageTag; private final transient Supplier<String> languageTag =
StableValue.supplier(new Supplier<>() {
@Override
public String get() {
return computeLanguageTag();
}
});
/** /**
* Return an array of the display names of the variant. * 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); baseLocale.getRegion(), baseLocale.getVariant(), localeExtensions);
} }
private static volatile String[] isoLanguages;
private static volatile String[] isoCountries;
private static String convertOldISOCodes(String language) { private static String convertOldISOCodes(String language) {
// we accept both the old and the new ISO codes for the languages whose ISO // 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 // codes have changed, but we always store the NEW code, unless the property

View File

@ -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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -25,11 +25,47 @@
package java.util; package java.util;
import java.util.function.Supplier;
// Methods and suppliers for producing ISO 639/3166 resources used by Locale.
class LocaleISOData { class LocaleISOData {
static final Supplier<String[]> ISO_639 =
StableValue.supplier(new Supplier<>() {
@Override
public String[] get() {
return getISO2Table(isoLanguageTable);
}
});
static final Supplier<String[]> ISO_3166_1_ALPHA2 =
StableValue.supplier(new Supplier<>() {
@Override
public String[] get() {
return getISO2Table(isoCountryTable);
}
});
static final Supplier<Set<String>> ISO_3166_1_ALPHA3 =
StableValue.supplier(new Supplier<>() {
@Override
public Set<String> get() {
return computeISO3166_1Alpha3Countries();
}
});
static final Supplier<Set<String>> ISO_3166_3 =
StableValue.supplier(new Supplier<>() {
@Override
public Set<String> get() {
return Set.of(ISO3166_3);
}
});
/** /**
* The 2- and 3-letter ISO 639 language codes. * The 2- and 3-letter ISO 639 language codes.
*/ */
static final String isoLanguageTable = private static final String isoLanguageTable =
"aa" + "aar" // Afar "aa" + "aar" // Afar
+ "ab" + "abk" // Abkhazian + "ab" + "abk" // Abkhazian
+ "ae" + "ave" // Avestan + "ae" + "ave" // Avestan
@ -223,7 +259,7 @@ class LocaleISOData {
/** /**
* The 2- and 3-letter ISO 3166 country codes. * The 2- and 3-letter ISO 3166 country codes.
*/ */
static final String isoCountryTable = private static final String isoCountryTable =
"AD" + "AND" // Andorra, Principality of "AD" + "AND" // Andorra, Principality of
+ "AE" + "ARE" // United Arab Emirates + "AE" + "ARE" // United Arab Emirates
+ "AF" + "AFG" // Afghanistan + "AF" + "AFG" // Afghanistan
@ -480,18 +516,60 @@ class LocaleISOData {
/** /**
* Array to hold country codes for ISO3166-3. * 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", "AIDJ", "ANHH", "BQAQ", "BUMM", "BYAA", "CSHH", "CSXX", "CTKI", "DDDE",
"DYBJ", "FQHH", "FXFR", "GEHH", "HVBF", "JTUM", "MIUM", "NHVU", "NQAQ", "DYBJ", "FQHH", "FXFR", "GEHH", "HVBF", "JTUM", "MIUM", "NHVU", "NQAQ",
"NTHH", "PCHH", "PUUM", "PZPA", "RHZW", "SKIN", "SUHH", "TPTL", "VDVN", "NTHH", "PCHH", "PUUM", "PZPA", "RHZW", "SKIN", "SUHH", "TPTL", "VDVN",
"WKUM", "YDYE", "YUCS", "ZRCD" "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 * This method computes a set of ISO3166-1 alpha-3 country codes from
* existing isoCountryTable. * existing isoCountryTable.
*/ */
static Set<String> computeISO3166_1Alpha3Countries() { private static Set<String> computeISO3166_1Alpha3Countries() {
int tableLength = isoCountryTable.length(); int tableLength = isoCountryTable.length();
String[] isoTable = new String[tableLength / 5]; String[] isoTable = new String[tableLength / 5];
for (int i = 0, index = 0; index < tableLength; i++, index += 5) { for (int i = 0, index = 0; index < tableLength; i++, index += 5) {
@ -500,6 +578,5 @@ class LocaleISOData {
return Set.of(isoTable); return Set.of(isoTable);
} }
private LocaleISOData() { private LocaleISOData() {}
}
} }

View File

@ -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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * 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 jdk.internal.vm.annotation.Stable;
import java.util.StringJoiner; import java.util.StringJoiner;
import java.util.function.Supplier;
public final class BaseLocale { public final class BaseLocale {
@ -90,6 +91,15 @@ public final class BaseLocale {
} }
} }
// Interned BaseLocale cache
private static final Supplier<ReferencedKeySet<BaseLocale>> CACHE =
StableValue.supplier(new Supplier<>() {
@Override
public ReferencedKeySet<BaseLocale> get() {
return ReferencedKeySet.create(true, ReferencedKeySet.concurrentHashMapSupplier());
}
});
public static final String SEP = "_"; public static final String SEP = "_";
private final String language; private final String language;
@ -164,11 +174,7 @@ public final class BaseLocale {
// Obtain the "interned" BaseLocale from the cache. The returned // Obtain the "interned" BaseLocale from the cache. The returned
// "interned" instance can subsequently be used by the Locale // "interned" instance can subsequently be used by the Locale
// instance which guarantees the locale components are properly cased/interned. // instance which guarantees the locale components are properly cased/interned.
class InterningCache { // TODO: StableValue return CACHE.get().intern(new BaseLocale(
private static final ReferencedKeySet<BaseLocale> CACHE =
ReferencedKeySet.create(true, ReferencedKeySet.concurrentHashMapSupplier());
}
return InterningCache.CACHE.intern(new BaseLocale(
language.intern(), // guaranteed to be lower-case language.intern(), // guaranteed to be lower-case
LocaleUtils.toTitleString(script).intern(), LocaleUtils.toTitleString(script).intern(),
region.intern(), // guaranteed to be upper-case region.intern(), // guaranteed to be upper-case