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.