NOTE: I really hope that you are way past needing these upgrade notes. In 2026, Java 25 is already an LTS (long-Term support) version. But there are always a few hidden legacy systems around to cause problems…
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!!
More Version Changes
The COMPAT locale provider was removed in Java 23.
The COMPAT provider (also known as JRE) was the legacy locale data used in JDK releases up to Java 8. Starting with Java 9, the default was switched to the more modern CLDR (Common Locale Data Repository) provider, while COMPAT remained available as an option to allow for migration.
Version Summary
- Java 8 and earlier:
COMPATorJREwas the default locale provider. - Java 9 to Java 22: The default changed to
CLDR, butCOMPAT/JREcould still be used by setting the system propertyjava.locale.providers=COMPAT,CLDR(orJRE,CLDR). - Java 21: A warning was issued that the
COMPATlocale provider would be removed in a future release. - Java 23: The legacy locale data was completely removed. Specifying
JREorCOMPATin the java.locale.providers system property is ignored and a warning message is issued. We should now rather useSPI(Service Provide Interface) instead ofCOMPAT. We can still includeCLDRas well (comma separated), as in:
java -Djava.locale.providers=SPI,CLDR MyApplication
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.