This is the mail archive of the
java-patches@gcc.gnu.org
mailing list for the Java project.
Patch: FYI: java.util.ResourceBundle
- From: Bryce McKinlay <mckinlay at redhat dot com>
- To: java-patches at gcc dot gnu dot org
- Date: Fri, 09 Jul 2004 22:13:22 -0400
- Subject: Patch: FYI: java.util.ResourceBundle
This patch implements a new caching strategy for ResourceBundle and also
simplifies the lookup code substantially.
The previous version used SoftReferences in its cache - unfortunately
these are not fully implemented in libgcj and the references would be
deleted during each GC cycle, resulting in poor lookup performance since
the same ResourceBundles would be loaded over and over. I don't think
using SoftReferences is a significant win here - although a resource
bundle may contain many localized strings, I would imagine that the
typical usage model is for those strings to stay in use throughout the
lifetime of the application. Perhaps this decision could be revisted
later, when we have a fully implemented SoftReference.
The old version also created a StringBuffer and did String manipulation
in getBundle() even in the case where the bundle was cached. There is
now no allocations or string manipulation required for getBundle() in
the case where the bundle is found in the cache.
These improvements double libgcj's performance for the following test
case, from ~35s to ~17s. This test case results in a lot of calls to
Calendar.getInstance() which in turn uses ResourceBundle.getBundle().
I'm checking this in. I will also merge into classpath if nobody beats
me to it.
Regards
Bryce
import java.util.Date;
public class DateTester {
static final int N = 250000;
public static String[] s = new String[N];
public static void main(String[] args)
{
long start = System.currentTimeMillis();
for (int i=0; i<N; i++)
{
s[i] = new Date().toString();
if (i % 5000 == 0)
System.out.print(".");
}
System.out.println((System.currentTimeMillis() - start) + "ms");
}
}
2004-07-09 Bryce McKinlay <mckinlay@redhat.com>
* java/util/ResourceBundle.java (bundleCache): Renamed from
resourceBundleCache. Update comments.
(getObject): Don't catch MissingResourceException.
(getBundle(String)): Remove 'final'. Use system classloader if
getCallingClassLoader returned null.
(getBundle(String, Locale)): Likewise.
(BundleKey): New private class. HashMap key for bundle cache lookup.
(lookupKey): New. Singleton instance of BundleKey.
(nullEntry): New. Cache entry to represent failed lookups.
(getBundle(String, Locale, ClassLoader)): Re-written to use new
caching strategy, no-allocation lookup, and new tryBundle methods.
(tryBundle(String, ClassLoader)): New. Load a locale-qualified bundle
name using given classloader.
(tryBundle(String, Locale, ClassLoader, boolean): New. Qualify
baseName for given Locale and attempt to load bundle.
Index: ResourceBundle.java
===================================================================
RCS file: /cvs/gcc/gcc/libjava/java/util/ResourceBundle.java,v
retrieving revision 1.25
diff -u -r1.25 ResourceBundle.java
--- ResourceBundle.java 5 May 2004 06:52:56 -0000 1.25
+++ ResourceBundle.java 10 Jul 2004 01:37:42 -0000
@@ -105,12 +105,9 @@
private static native ClassLoader getCallingClassLoader();
/**
- * The resource bundle cache. This is a two-level hash map: The key
- * is the class loader, the value is a new HashMap. The key of this
- * second hash map is the localized name, the value is a soft
- * references to the resource bundle.
+ * The resource bundle cache.
*/
- private static Map resourceBundleCache;
+ private static Map bundleCache;
/**
* The last default Locale we saw. If this ever changes then we have to
@@ -172,15 +169,11 @@
public final Object getObject(String key)
{
for (ResourceBundle bundle = this; bundle != null; bundle = bundle.parent)
- try
- {
- Object o = bundle.handleGetObject(key);
- if (o != null)
- return o;
- }
- catch (MissingResourceException ex)
- {
- }
+ {
+ Object o = bundle.handleGetObject(key);
+ if (o != null)
+ return o;
+ }
throw new MissingResourceException("Key not found", getClass().getName(),
key);
@@ -220,10 +213,12 @@
* @throws MissingResourceException if the resource bundle can't be found
* @throws NullPointerException if baseName is null
*/
- public static final ResourceBundle getBundle(String baseName)
+ public static ResourceBundle getBundle(String baseName)
{
- return getBundle(baseName, Locale.getDefault(),
- getCallingClassLoader());
+ ClassLoader cl = getCallingClassLoader();
+ if (cl == null)
+ cl = ClassLoader.getSystemClassLoader();
+ return getBundle(baseName, Locale.getDefault(), cl);
}
/**
@@ -238,11 +233,74 @@
* @throws MissingResourceException if the resource bundle can't be found
* @throws NullPointerException if baseName or locale is null
*/
- public static final ResourceBundle getBundle(String baseName,
- Locale locale)
+ public static ResourceBundle getBundle(String baseName, Locale locale)
{
- return getBundle(baseName, locale, getCallingClassLoader());
- }
+ ClassLoader cl = getCallingClassLoader();
+ if (cl == null)
+ cl = ClassLoader.getSystemClassLoader();
+ return getBundle(baseName, locale, cl);
+ }
+
+ /** Cache key for the ResourceBundle cache. Resource bundles are keyed
+ by the combination of bundle name, locale, and class loader. */
+ private static class BundleKey implements Cloneable
+ {
+ String baseName;
+ Locale locale;
+ ClassLoader classLoader;
+ int hashcode;
+
+ BundleKey() {}
+
+ BundleKey(String s, Locale l, ClassLoader cl)
+ {
+ set(s, l, cl);
+ }
+
+ void set(String s, Locale l, ClassLoader cl)
+ {
+ baseName = s;
+ locale = l;
+ classLoader = cl;
+ hashcode = baseName.hashCode() ^ locale.hashCode() ^
+ classLoader.hashCode();
+ }
+
+ public int hashCode()
+ {
+ return hashcode;
+ }
+
+ public boolean equals(Object o)
+ {
+ if (! (o instanceof BundleKey))
+ return false;
+ BundleKey key = (BundleKey) o;
+ return hashcode == key.hashcode &&
+ baseName.equals(key.baseName) &&
+ locale.equals(key.locale) &&
+ classLoader.equals(key.classLoader);
+ }
+
+ public Object clone()
+ {
+ Object clone = null;
+ try
+ {
+ clone = super.clone();
+ }
+ catch (CloneNotSupportedException x) {}
+
+ return clone;
+ }
+ }
+
+ /** A cache lookup key. This avoids having to a new one for every
+ * getBundle() call. */
+ private static BundleKey lookupKey = new BundleKey();
+
+ /** Singleton cache entry to represent previous failed lookups. */
+ private static Object nullEntry = new Object();
/**
* Get the appropriate ResourceBundle for the given locale. The following
@@ -320,81 +378,59 @@
*/
// This method is synchronized so that the cache is properly
// handled.
- public static final synchronized ResourceBundle getBundle
+ public static synchronized ResourceBundle getBundle
(String baseName, Locale locale, ClassLoader classLoader)
{
- // This implementation searches the bundle in the reverse direction
- // and builds the parent chain on the fly.
+ // If the default locale changed since the last time we were called,
+ // all cache entries are invalidated.
Locale defaultLocale = Locale.getDefault();
if (defaultLocale != lastDefaultLocale)
{
- resourceBundleCache = new HashMap();
+ bundleCache = new HashMap();
lastDefaultLocale = defaultLocale;
}
- HashMap cache = (HashMap) resourceBundleCache.get(classLoader);
- StringBuffer sb = new StringBuffer(60);
- sb.append(baseName).append('_').append(locale);
- String name = sb.toString();
- if (cache == null)
+ // This will throw NullPointerException if any arguments are null.
+ lookupKey.set(baseName, locale, classLoader);
+
+ Object obj = bundleCache.get(lookupKey);
+ ResourceBundle rb = null;
+ if (obj instanceof ResourceBundle)
{
- cache = new HashMap();
- resourceBundleCache.put(classLoader, cache);
+ return (ResourceBundle) obj;
}
- else if (cache.containsKey(name))
+ else if (obj == nullEntry)
{
- Reference ref = (Reference) cache.get(name);
- // If REF is null, that means that we added a `null' value to
- // the hash map. That means we failed to find the bundle
- // previously, and we cached that fact. The JDK does this, so
- // it must be ok.
- if (ref == null)
- throw new MissingResourceException("Bundle " + baseName
- + " not found",
- baseName, "");
- else
- {
- ResourceBundle rb = (ResourceBundle) ref.get();
- if (rb != null)
- {
- // RB should already have the right parent, except if
- // something very strange happened.
- return rb;
- }
- // If RB is null, then we previously found it but it was
- // collected. So we try again.
- }
+ // Lookup has failed previously. Fall through.
}
-
- // It is ok if this returns null. We aren't required to have the
- // base bundle.
- ResourceBundle baseBundle = tryBundle(baseName, emptyLocale,
- classLoader, null, cache);
-
- // Now use our locale, followed by the default locale. We only
- // need to try the default locale if our locale is different, and
- // if our locale failed to yield a result other than the base
- // bundle.
- ResourceBundle bundle = tryLocalBundle(baseName, locale,
- classLoader, baseBundle, cache);
- if (bundle == baseBundle && !locale.equals(defaultLocale))
+ else
{
- bundle = tryLocalBundle(baseName, defaultLocale,
- classLoader, baseBundle, cache);
- // We need to record that the argument locale maps to the
- // bundle we just found. If we didn't find a bundle, record
- // that instead.
- if (bundle == null)
- cache.put(name, null);
+ // First, look for a bundle for the specified locale. We don't want
+ // the base bundle this time.
+ boolean wantBase = locale.equals(defaultLocale);
+ ResourceBundle bundle = tryBundle(baseName, locale, classLoader,
+ wantBase);
+
+ // Try the default locale if neccessary.
+ if (bundle == null && !locale.equals(defaultLocale))
+ bundle = tryBundle(baseName, defaultLocale, classLoader, true);
+
+ BundleKey key = (BundleKey) lookupKey.clone();
+ if (bundle == null)
+ {
+ // Cache the fact that this lookup has previously failed.
+ bundleCache.put(key, nullEntry);
+ }
else
- cache.put(name, new SoftReference(bundle));
+ {
+ // Cache the result and return it.
+ bundleCache.put(key, bundle);
+ return bundle;
+ }
}
- if (bundle == null)
- throw new MissingResourceException("Bundle " + baseName + " not found",
- baseName, "");
-
- return bundle;
+ throw new MissingResourceException("Bundle " + baseName + " not found",
+ baseName, "");
}
/**
@@ -423,44 +459,13 @@
* Tries to load a class or a property file with the specified name.
*
* @param localizedName the name
- * @param locale the locale, that must be used exactly
* @param classloader the classloader
- * @param bundle the backup (parent) bundle
* @return the resource bundle if it was loaded, otherwise the backup
*/
- private static final ResourceBundle tryBundle(String localizedName,
- Locale locale,
- ClassLoader classloader,
- ResourceBundle bundle,
- HashMap cache)
+ private static ResourceBundle tryBundle(String localizedName,
+ ClassLoader classloader)
{
- // First look into the cache.
- if (cache.containsKey(localizedName))
- {
- Reference ref = (Reference) cache.get(localizedName);
- // If REF is null, that means that we added a `null' value to
- // the hash map. That means we failed to find the bundle
- // previously, and we cached that fact. The JDK does this, so
- // it must be ok.
- if (ref == null)
- return null;
- else
- {
- ResourceBundle rb = (ResourceBundle) ref.get();
- if (rb != null)
- {
- // RB should already have the right parent, except if
- // something very strange happened.
- return rb;
- }
- // If RB is null, then we previously found it but it was
- // collected. So we try again.
- }
- }
-
- // foundBundle holds exact matches for the localizedName resource
- // bundle, which may later be cached.
- ResourceBundle foundBundle = null;
+ ResourceBundle bundle = null;
try
{
Class rbClass;
@@ -468,48 +473,36 @@
rbClass = Class.forName(localizedName);
else
rbClass = classloader.loadClass(localizedName);
- foundBundle = (ResourceBundle) rbClass.newInstance();
- foundBundle.parent = bundle;
- foundBundle.locale = locale;
- }
- catch (Exception ex)
- {
- // ignore them all
- foundBundle = null;
+ bundle = (ResourceBundle) rbClass.newInstance();
}
- if (foundBundle == null)
+ catch (IllegalAccessException ex) {}
+ catch (InstantiationException ex) {}
+ catch (ClassNotFoundException ex) {}
+
+ if (bundle == null)
{
try
{
InputStream is;
- final String resourceName
+ String resourceName
= localizedName.replace('.', '/') + ".properties";
if (classloader == null)
is = ClassLoader.getSystemResourceAsStream(resourceName);
else
is = classloader.getResourceAsStream(resourceName);
if (is != null)
- {
- foundBundle = new PropertyResourceBundle(is);
- foundBundle.parent = bundle;
- foundBundle.locale = locale;
- }
+ bundle = new PropertyResourceBundle(is);
}
catch (IOException ex)
{
+ MissingResourceException mre = new MissingResourceException
+ ("Failed to load bundle", localizedName, "");
+ mre.initCause(ex);
+ throw mre;
}
}
- // Put the result into the hash table. If we didn't find anything
- // here, we record our parent bundle. If we record `null' that means
- // nothing, not even the base, was found.
- if (foundBundle == null)
- foundBundle = bundle;
- if (foundBundle == null)
- cache.put(localizedName, null);
- else
- cache.put(localizedName, new SoftReference(foundBundle));
- return foundBundle;
+ return bundle;
}
/**
@@ -520,47 +513,71 @@
* @param locale the locale
* @param classloader the classloader
* @param bundle the backup (parent) bundle
+ * @param wantBase whether a resource bundle made only from the base name
+ * (with no locale information attached) should be returned.
* @return the resource bundle if it was loaded, otherwise the backup
*/
- private static final ResourceBundle tryLocalBundle(String baseName,
- Locale locale,
- ClassLoader classloader,
- ResourceBundle bundle,
- HashMap cache)
- {
- final String language = locale.getLanguage();
- final String country = locale.getCountry();
- final String variant = locale.getVariant();
+ private static ResourceBundle tryBundle(String baseName, Locale locale,
+ ClassLoader classLoader,
+ boolean wantBase)
+ {
+ String language = locale.getLanguage();
+ String country = locale.getCountry();
+ String variant = locale.getVariant();
+
+ int baseLen = baseName.length();
+
+ // Build up a StringBuffer containing the complete bundle name, fully
+ // qualified by locale.
+ StringBuffer sb = new StringBuffer(baseLen + variant.length() + 7);
- StringBuffer sb = new StringBuffer(60);
sb.append(baseName);
- sb.append('_');
-
+
if (language.length() > 0)
{
+ sb.append('_');
sb.append(language);
- bundle = tryBundle(sb.toString(), new Locale(language),
- classloader, bundle, cache);
- }
- // If LANGUAGE was empty, we still need to try the other
- // components, and the `_' is required.
- sb.append('_');
-
- if (country.length() > 0)
- {
- sb.append(country);
- bundle = tryBundle(sb.toString(), new Locale(language, country),
- classloader, bundle, cache);
+
+ if (country.length() > 0)
+ {
+ sb.append('_');
+ sb.append(country);
+
+ if (variant.length() > 0)
+ {
+ sb.append('_');
+ sb.append(variant);
+ }
+ }
}
- sb.append('_');
- if (variant.length() > 0)
+ // Now try to load bundles, starting with the most specialized name.
+ // Build up the parent chain as we go.
+ String bundleName = sb.toString();
+ ResourceBundle first = null; // The most specialized bundle.
+ ResourceBundle last = null; // The least specialized bundle.
+
+ while (true)
{
- sb.append(variant);
- bundle = tryBundle(sb.toString(), locale,
- classloader, bundle, cache);
+ ResourceBundle foundBundle = tryBundle(bundleName, classLoader);
+ if (foundBundle != null)
+ {
+ if (first == null)
+ first = foundBundle;
+ if (last != null)
+ last.parent = foundBundle;
+ foundBundle.locale = locale;
+ last = foundBundle;
+ }
+ int idx = bundleName.lastIndexOf('_');
+ // Try the non-localized base name only if we already have a
+ // localized child bundle, or wantBase is true.
+ if (idx > baseLen || (idx == baseLen && (first != null || wantBase)))
+ bundleName = bundleName.substring(0, idx);
+ else
+ break;
}
-
- return bundle;
+
+ return first;
}
}