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.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<String> 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<String> createCountryCodeSet() {
return LocaleISOData.computeISO3166_1Alpha3Countries();
}
},
PART1_ALPHA3,
/**
* PART3 is used to represent the ISO3166-3 four letter country codes.
*/
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);
}
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<Object, Locale> {
private static final ReferencedKeyMap<Object, Locale> LOCALE_CACHE
= ReferencedKeyMap.create(true, ReferencedKeyMap.concurrentHashMapSupplier());
private static final Function<Object, Locale> LOCALE_CREATOR = new LocaleCache();
public static Locale cache(Object key) {
return LOCALE_CACHE.computeIfAbsent(key, LOCALE_CREATOR);
private static final Supplier<ReferencedKeyMap<Object, Locale>> LOCALE_CACHE =
StableValue.supplier(new Supplier<>() {
@Override
public ReferencedKeyMap<Object, Locale> get() {
return ReferencedKeyMap.create(true, ReferencedKeyMap.concurrentHashMapSupplier());
}
});
private static final Function<Object, Locale> 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<String> 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));
}
List<String>subtags = 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<String> 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

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.
*
* 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<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.
*/
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<String> computeISO3166_1Alpha3Countries() {
private static Set<String> 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() {}
}

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.
*
* 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<ReferencedKeySet<BaseLocale>> CACHE =
StableValue.supplier(new Supplier<>() {
@Override
public ReferencedKeySet<BaseLocale> 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<BaseLocale> 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