Locale Problems Upgrading from Java 8

How to fix Java local problems when upgrading from Java 8. Map of world with pictures of paper currency overlaid on each country

Maybe you’ve got a system that’s running perfectly on Java 8, but you need or want to move to Java 11.

Often when you migrate from an older Java version to a later version, you bump your head on some migration problems. In this tip, I’m going to focus on one problem that jumped out and hit me a while ago: the locale problem.

A word about Java versions

With the ever increasing Java version numbers, you’ll be forgiven for getting confused as to which version you should use. Should you chase version numbers and use the latest and greatest version? Or should you be more conservative and stay with a tried and true version?

My advice: play around with any version you want, but only use an LTS (long term support) version in production! Currently Java 8, 11 and 17 are LTS versions. There’s an easy to read roadmap at https://adoptium.net/support/.

A Java locale problem

I’m assuming you’ve been using the printf() and format() methods from java.io.PrintStream and the String.format() method to easily create neatly formatted output strings. Both methods use formatting strings to define the output of any parameters used.

Let’s look at one conversion specifier in particular: the %f conversion. This converts a floating point parameter value to a String. Easy enough. We can use this in combination with additional flags, and width and precision numbers to format the floating number value to your heart’s content (or at least to your client’s requirements). One of the additional flags is the comma , which inserts locale-specific grouping separators. This includes the thousands separator (a comma here in South Africa), and the decimal separator (the period/dot/full-stop).

Text formatting using your system locale

Let’s look at a few examples of text formatting that use your system locale as the default setting.

import java.text.NumberFormat;

public class LocaleTest {

    public static void main(String args[]) {

        // Printing out the country
        System.out.printf("Country is %s%n", System.getProperty("user.country"));

        final double value = 10000.0;

        // This formats the value using the default locale's 
        // decimal separator and thousands separator.
        System.out.printf("%,.2f%n" , value);

        // This creates a String using the default locale's 
        // decimal separator and thousands separator.
        String str = String.format("%,.2f" , value);
        System.out.println(str);

        // This formats the value using the local currency symbol, 
        // decimal separator and thousands separator
        String currency = NumberFormat.getCurrencyInstance().format(value);
        System.out.println(currency);
    }
} // end of class

Locale results in Java 8

Compiling and running this class with Java 8 results in:

Country is ZA
10,000.00
10,000.00
R 10,000.00

The decimal separator here is the decimal point, as we would expect (my locale is set up as South Africa). The thousands separator is the comma, which is also what we expect.

Locale results in Java 11

However, if we compile and run using Java 11, the output is different:

Country is ZA
10 000,00
10 000,00
R10 000,00

Instead of a decimal point, a comma is printed. Instead of a comma to separate thousands, a space is used.

What went wrong? With Java 11, Java now loads locales differently. Most notably, CLDR (the Unicode Common Locale Data Repository archive) is now loaded before COMPAT which was the Java 8 (and previous) behaviour. This leads to incorrect and/or unexpected behaviour as we’ve just seen.

The fix for Java locale problems

Fortunately fixing the problem is easy. Passing the option -Djava.locale.providers=COMPAT to the VM when running your application will make the Java 11 locale loader to behave in the same way as the earlier versions.

Using the flag -Djava.locale.providers=COMPAT when running the LocaleTest application will force the JVM 11 locale loader to behave like the JVM 8 locale loader, and we will get the expected output:

Country is ZA
10,000.00
10,000.00
R 10,000.00

A better fix for Java locale problems

A possibly better fix would be to specify the desired locale in the printf() call, as follows:

System.out.printf("%nUsing Locale.ENGLISH%n");
System.out.printf(Locale.ENGLISH, "%f%n", value);
System.out.printf(Locale.ENGLISH, "%.2f%n", value);
System.out.printf(Locale.ENGLISH, "%,.2f%n", value);

This works correctly in all versions without needing to specify the java.locale.providers property. However this requires you to go through all your code and change every occurrence of printf() and format(). Aarrghh!!

Want a detailed explanation?

The blog post at [https://www.oracle.com/technetwork/java/javase/documentation/java11locales-5069639.html#providers]
(https://www.oracle.com/technetwork/java/javase/documentation/java11locales-5069639.html#providers) details the loading behaviour for JDK 11, and how it differs from the previous versions. Importantly, CLDR now is before COMPAT (compatible with previous versions of the JDK). And yes, there is a spelling mistake in the URL!

To override this setting, we can use the java.locale.providers system setting. This is a comma-separated list of locale data providers, where the possible values are CLDR, COMPAT, SPI ,HOST and JRE. JRE is a deprecated synonym for COMPAT, so it shouldn’t be used now.

The JDK 11 default value is CLDR,COMPAT,HOST,JRE. We would probably prefer to have COMPAT and HOST before CLDR, i.e. COMPAT,HOST,CLDR. This will use the built-in locales first (giving the same behaviour as JDK 8), then the host locales, then the class loader locales.

Locale problems sorted!

Hopefully this clears up one of the potential headaches when moving from Java 8.

Leave a Comment

Your email address will not be published. Required fields are marked *

Code like a Java Guru!

Thank You

We're Excited!

Thank you for completing the form. We're excited that you have chosen to contact us about training. We will process the information as soon as we can, and we will do our best to contact you within 1 working day. (Please note that our offices are closed over weekends and public holidays.)

Don't Worry

Our privacy policy ensures your data is safe: Incus Data does not sell or otherwise distribute email addresses. We will not divulge your personal information to anyone unless specifically authorised by you.

If you need any further information, please contact us on tel: (27) 12-666-2020 or email info@incusdata.com

How can we help you?

Let us contact you about your training requirements. Just fill in a few details, and we’ll get right back to you.

Your Java tip is on its way!

Check that incusdata.com is an approved sender, so that your Java tips don’t land up in the spam folder.

Our privacy policy means your data is safe. You can unsubscribe from these tips at any time.