Implement localization using locales and resource bundles. Parse and format messages, dates, times, and numbers, including currency and percentage values.
Locale
Class
MessageFormat
ClassNumberFormat
Class
DateTimeFormatter
ClassIt’s common for software applications to be used by people all around the globe who speak different languages and live in various countries and cultures. To make an application globally accessible and user-friendly, it needs to adapt to the user’s language and cultural norms. This is where localization comes into play.
Localization refers to the process of designing and developing your application in a way that it can be adapted to various locales without requiring engineering changes. Think of it like creating a “world-ready” app.
A locale represents a specific geographical, political, or cultural region. It is made up of a language code, a country code, and optionally, a variant.
Here’s an example of a locale representation:
fr_CA
Notice that:
For example, en
represents English, en_US
represents English as used in the United States, which might differ from en_UK
(English as used in the United Kingdom) in things like spelling (color vs colour), vocabulary (truck vs lorry), or currency ($100 vs £100).
Locale
ClassLocales are represented by the java.util.Locale
class. Basically, this class represents a language and a country although, to be precise, a locale can have the following information:
There are several ways to get or create a Locale
object in Java.
To get the default locale of the Java Virtual Machine (JVM), you use:
Locale locale = Locale.getDefault();
The Locale
class provides several built-in constants for commonly used locales. For example:
Locale usLocale = Locale.US; // English as used in the US
Locale frLocale = Locale.FRANCE; // French as used in France
You can also create a new Locale
object using one of its constructors:
Locale(String language)
Locale(String language, String country)
Locale(String language, String country, String variant)
For example:
Locale locale = new Locale("fr", "CA", "POSIX");
This creates a new Locale
object representing French as used in Canada with the POSIX variant.
The first parameter is the language code, the second is the country code, and the third (optional) is the variant code. Language codes are two or three letter lowercase codes as defined by ISO 639. Country codes are two-letter uppercase codes as defined by ISO 3166. Variants are any arbitrary value used to indicate any kind of variation, not just language variations.
You can also use the forLanguageTag(String)
factory method. This method expects a language code, for example:
Locale german = Locale.forLanguageTag("de");
Additionally, by using Locale.Builder
, you can set the properties you need and build the object at the end, for example:
Locale japan = new Locale.Builder()
.setRegion("JP")
.setLanguage("ja")
.build();
Passing an invalid argument to any of the above constructors and methods will not throw an exception, it will just create an object with invalid options that will make your program behave incorrectly:
Locale badLocale = new Locale("a", "A"); // No error
System.out.println(badLocale); // It prints a_A
The getDefault()
method returns the current value of the default locale for this instance of the Java Virtual Machine (JVM). The JVM sets the default locale during startup based on the host environment. It is used by many locale-sensitive methods if no locale is explicitly specified. However, it can be changed using the setDefault(Locale)
method:
System.out.println(Locale.getDefault()); // Let's say it prints en_GB
Locale.setDefault(new Locale("en", "US"));
System.out.println(Locale.getDefault()); // Now prints en_US
The Locale.Category
enum defines two categories used to differentiate the purposes for which a Locale
might be used: Locale.Category.DISPLAY
and Locale.Category.FORMAT
. These categories help specify which locale settings to apply in different contexts.
DISPLAY
locale to ensure the formats and messages are appropriate for the user’s language and region.FORMAT
locale is used to ensure the data follows the correct localized format.Here’s an example showing how to use these categories:
// Setting the DISPLAY locale
Locale.setDefault(Locale.Category.DISPLAY, Locale.FRANCE);
// Setting the FORMAT locale
Locale.setDefault(Locale.Category.FORMAT, Locale.US);
// Getting the DISPLAY locale
Locale displayLocale = Locale.getDefault(Locale.Category.DISPLAY);
System.out.println("DISPLAY Locale: " + displayLocale);
// Getting the FORMAT locale
Locale formatLocale = Locale.getDefault(Locale.Category.FORMAT);
System.out.println("FORMAT Locale: " + formatLocale);
In this example, the DISPLAY
locale is set to Locale.FRANCE
, meaning that user interface elements will be localized for French users. The FORMAT
locale is set to Locale.US
, meaning that any date or number formatting will follow the conventions used in the United States.
This is the output:
DISPLAY Locale: fr_FR
FORMAT Locale: en_US
Here’s a diagram that summarizes the structure of a locale:
┌──────────────────────────────────────────┐
│ Locale Structure │
│ │
│ ┌───────────────────────────────────┐ │
│ │ Locale │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Language │ │ Country │ │ │
│ │ │ (en) │ │ (US) │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Variant │ │ │
│ │ │ (Optional) │ │ │
│ │ └─────────────┘ │ │
│ └───────────────────────────────────┘ │
│ │
│ Locale Categories: │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ DISPLAY │ │ FORMAT │ │
│ │ │ │ │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ Examples: │
│ - en_US │
│ - fr_FR │
│ - de_DE_EURO │
│ │
└──────────────────────────────────────────┘
Key Points:
- Language is required, country and variant are optional
- Language codes are lowercase, country codes are uppercase
- Variant is used for further distinction (such as dialect)
- DISPLAY category affects how the locale itself is displayed
- FORMAT category affects formatting of dates, numbers, etc.
A resource bundle is a way to organize and access locale-specific data such as messages or labels in your application. Think of it as a collection of key-value pairs, where the keys are the same across all locales but the values are locale-specific.
To support this, we have an abstract class java.util.ResourceBundle
with two subclasses:
java.util.PropertyResourceBundle
: Each locale is represented by a property file. Keys and values are of type String
.
java.util.ListResourceBundle
: Each locale is represented by a subclass of this class that overrides the method Object[][] getContents()
. The returned array represents the keys and values. Keys must be of type String
, but values can be any object.
In Java, resource bundles are typically implemented as property files. A property file is a plain text file that contains key-value pairs separated by an equals sign (=
). For example:
greeting=Hello
farewell=Goodbye
The name of the property file is important. It should be in the format <basename>_<language>_<country>_<variant>.properties
, where:
<basename>
is the name you give to the resource bundle<language>
is the lowercase two-letter ISO-639 language code<country>
is the uppercase two-letter ISO-3166 country code<variant>
is an optional vendor or browser-specific codeFor example, we can have bundles with the following names (assuming we’re working with property files, although it’s the same with classes):
MyBundle.properties
MyBundle_en.properties
MyBundle_en_NZ.properties
MyBundle_en_US.properties
In this case, MyBundle_en_US.properties
would contain messages for English as used in the United States.
To create a resource bundle, you first create the property files for each locale you want to support. Then, you use the java.util.ResourceBundle
class to load the appropriate property file for the current locale:
ResourceBundle bundle = ResourceBundle.getBundle("MyBundle", locale);
This loads the resource bundle named MyBundle
for the given locale. Java will search for the property file that best matches the requested locale based on the following priority:
Locale.getDefault()
)If no matching property file is found, a MissingResourceException
is thrown.
Once you have a ResourceBundle
, you can retrieve the locale-specific value for a given key using the getString
method:
String greeting = bundle.getString("greeting");
To get an object value, use:
Integer num = (Integer) bundle.getObject("number");
It’s important to note that getString(key)
is effectively a shortcut to:
String val = (String) bundle.getObject(key);
You can use the keys of the matching resource bundle and any of its parents.
The parents of a resource bundle are the ones with the same name but fewer components. For example, the parents of MyBundle_es_ES
are:
MyBundle_es
MyBundle
Let’s assume the default locale is en_US
, and that your program is using these and other property files, all in the default package, with the values:
MyBundle_en.properties
s = buddy
MyBundle_es_ES.properties
s = tío
MyBundle_es.properties
s = amigo
MyBundle.properties
hi = Hola
We can create a resource bundle like this:
Locale spain = new Locale("es", "ES");
Locale spanish = new Locale("es");
ResourceBundle rb = ResourceBundle.getBundle("MyBundle", spain);
System.out.format("%s %s\n",
rb.getString("hi"), rb.getString("s"));
rb = ResourceBundle.getBundle("MyBundle", spanish);
System.out.format("%s %s\n",
rb.getString("hi"), rb.getString("s"));
This would be the output:
Hola tío
Hola amigo
As you can see, each locale picks different values for key s
, but they both use the same for hi
since this key is defined in their parent.
If you don’t specify a locale, the ResourceBundle
class will use the default locale of your system:
ResourceBundle rb = ResourceBundle.getBundle("MyBundle");
System.out.format("%s %s\n",
rb.getString("hi"), rb.getString("s"));
Since we’re assuming that the default locale is en_US
, the output is:
Hola buddy
We can also get all the keys in a resource bundle with the method keySet()
:
ResourceBundle rb =
ResourceBundle.getBundle("MyBundle", spain);
Set<String> keys = rb.keySet();
keys.stream()
.forEach(key ->
System.out.format("%s %s\n", key, rb.getString(key)));
This is the output (notice it also prints the parent key):
hi Hola
s tío
MessageFormat
ClassSometimes, you need to format messages that include variable data. For example, you might want to display a personalized greeting that includes the user’s name, or an error message that includes specific details about the error.
The java.text.MessageFormat
class provides a powerful way to create localized messages by combining a pattern string with arguments. It allows you to define a message template with placeholders for variable data, and then replace those placeholders with actual values at runtime.
The key method in the MessageFormat
class is format
:
static String format(String pattern, Object... arguments)
This static method takes a message pattern and an array of arguments, and returns the formatted message as a string. However, there are two other format
methods defined as instance methods:
final StringBuffer format(Object[] arguments, StringBuffer result, FieldPosition pos)
final StringBuffer format(Object arguments, StringBuffer result, FieldPosition pos)
These methods format an array of objects and append the pattern of the MessageFormat
instance, with format elements replaced by the formatted objects, to the provided StringBuffer
.
Additionally, its parent class, java.text.Format
, defines another format
method:
public final String format(Object obj)
This method formats an object to produce a string. It is equivalent to:
format(obj, new StringBuffer(), new FieldPosition(0)).toString();
The message pattern is a string that includes the static text of the message, as well as placeholders for the variable parts. The placeholders are marked with curly braces {}
and a number that indicates the position of the argument in the argument array.
The syntax of a message format pattern follows this structure:
Literal text {argumentIndex,formatType,formatStyle} Literal text
Where:
Literal Text: This is any text you want to include directly in the message. For example, in Hello, {0}
, Hello,
is literal text.
Argument Index: {argumentIndex}
specifies where to insert an argument. The number inside the curly braces corresponds to the position of the argument in the list you provide when formatting the message. For instance, {0}
will be replaced by the first argument, {1}
by the second argument, and so on.
-. Format Type: {argumentIndex,formatType}
adds a format type to the argument. Common format types include:
number
: Formats the argument as a number.date
: Formats the argument as a date.time
: Formats the argument as a time.choice
: Uses a choice format for the argument.
{argumentIndex,formatType,formatStyle}
further refines the format type with a specific style. For example:
number,currency
: Formats the number as a currency.date,short
: Formats the date in a short style.number,percent
: Formats the number as a percentage.Text can be quoted using single quotes '
, which are escaped as ''
. Any unmatched curly braces must be escaped with a single quote. For example:
"'{0}'"
represents the literal string "{0}"
"'{'"
represents the literal string "{"
"'}'"
represents the literal string "}"
Here are some examples of valid message format patterns:
"The value is {0}"
If the argument is 42
, the result will be “The value is 42”.
"The total amount is {0,number,currency}"
If the argument is 1234.56
, the result will be “The total amount is $1,234.56”.
"The date today is {0,date,full}"
If the argument is a date, the result might be “The date today is Thursday, July 25, 2024”.
"There are {0,choice,0#no files|1#one file|1<many files}"
If the argument is 0
, 1
, or 2
, the results will be There are no files
, There is one file
, or There are many files
respectively.
However, instead of using the static format
method, you can create a MessageFormat
instance by passing to the constructor of the class a pattern string and optionally a Locale
to its constructor:
String pattern = "The disk \"{1}\" contains {0} file(s).";
MessageFormat messageFormat = new MessageFormat(pattern, Locale.US);
You can also set the Locale
for the MessageFormat
instance using setLocale()
. This determines how the arguments are formatted.
In any case, to create a formatted message string, call format()
with an array of argument objects:
Object[] arguments = {42, "MyDisk"};
String result = messageFormat.format(arguments);
// result: "The disk "MyDisk" contains 42 file(s)."
There are some rules about argument indexes:
format()
replaces each format element {argumentIndex}
with the corresponding object from the arguments
array.Suppose we have the following message format pattern and arguments:
String pattern = "Hello, {0}. Today is {1,date,long}. Your balance is {2,number,currency}. Hello again, {0}!";
Object[] arguments = {"John", new Date(), 1234.56};
When we format this pattern with the arguments
array, here’s how it works:
{0}
is replaced with John
.{1,date,long}
is replaced with the current date in long format (for example, July 25, 2024
).{2,number,currency}
is replaced with the currency formatted number (for example, $1,234.56
).{0}
is also replaced with John
.Here is the resulting formatted message:
"Hello, John. Today is July 25, 2024. Your balance is $1,234.56. Hello again, John!"
About the format types and styles :
NumberFormat
).{0,number,#,##0.0}
.formatStyle
is optional and can specify a predefined style like short
, long
, integer
, currency
, etc.Consider the following message format pattern and arguments:
String pattern = "The item costs {0}. The item costs {0,number,currency}. Custom format: {0,number,#,##0.0}. Today is {1,date,long}. Short date: {1,date,short}";
Object[] arguments = {1234.567, new Date()};
When formatted with the arguments
array, it works as follows:
{0}
without a format type defaults to a number format (for example, 1234.567
).{0,number,currency}
specifies the number
format type with the currency
style (for example, $1,234.57
).{0,number,#,##0.0}
uses a custom number format pattern (for example, 1,234.6
).{1,date,long}
specifies the date
format type with the long
style (for example, July 25, 2024
).{1,date,short}
specifies the date
format type with the short
style (for example, 7/25/24
).Here is the resulting formatted message:
"The item costs 1234.567. The item costs $1,234.57. Custom format: 1,234.6. Today is July 25, 2024. Short date: 7/25/24"
In addition to MessageFormat
, there are two other subclasses of Format
: java.text.NumberFormat
and java.text.DateFormat
, for formatting numbers and dates, respectively.
The format type and style values are used to create a Format
instance for the format element. The following table shows how the values map to Format
instances. Combinations not shown in the table are illegal. A SubformatPattern must be a valid pattern string for the Format
subclass used.
FormatType | FormatStyle | Subformat Created |
---|---|---|
(none) | (none) | null |
number |
(none) | NumberFormat.getInstance(getLocale()) |
integer |
NumberFormat.getIntegerInstance(getLocale()) |
|
currency |
NumberFormat.getCurrencyInstance(getLocale()) |
|
percent |
NumberFormat.getPercentInstance(getLocale()) |
|
SubformatPattern | new DecimalFormat(subformatPattern, DecimalFormatSymbols.getInstance(getLocale())) |
|
date |
(none) | DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale()) |
short |
DateFormat.getDateInstance(DateFormat.SHORT, getLocale()) |
|
medium |
DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale()) |
|
long |
DateFormat.getDateInstance(DateFormat.LONG, getLocale()) |
|
full |
DateFormat.getDateInstance(DateFormat.FULL, getLocale()) |
|
SubformatPattern | new SimpleDateFormat(subformatPattern, getLocale()) |
|
time |
(none) | DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale()) |
short |
DateFormat.getTimeInstance(DateFormat.SHORT, getLocale()) |
|
medium |
DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale()) |
|
long |
DateFormat.getTimeInstance(DateFormat.LONG, getLocale()) |
|
full |
DateFormat.getTimeInstance(DateFormat.FULL, getLocale()) |
|
SubformatPattern | new SimpleDateFormat(subformatPattern, getLocale()) |
|
choice |
SubformatPattern | new ChoiceFormat(subformatPattern) |
In the next sections, we’ll review in more detail the NumberFormat
and DateFormat
classes.
NumberFormat
ClassNumberFormat
is the abstract base class for formatting and parsing numbers in Java. It provides a way to handle numeric values in a locale-sensitive manner, allowing you to format and parse numbers, currencies, and percentages according to the conventions of different locales.
To get a NumberFormat
instance for a specific locale, you typically use one of its factory methods:
NumberFormat defaultFormat = NumberFormat.getInstance();
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
NumberFormat percentFormat = NumberFormat.getPercentInstance();
These methods return a locale-specific formatter based on the default FORMAT
locale. You can also specify a locale explicitly:
Locale franceLocale = new Locale("fr", "FR");
NumberFormat franceFormat = NumberFormat.getInstance(franceLocale);
To format a number, use one of the format()
methods:
double value = 1234.56;
String formattedValue = defaultFormat.format(value); // "1,234.56"
NumberFormat
also provides methods for formatting long
and double
values directly into a StringBuffer
, which can be more efficient for heavy-duty formatting:
abstract StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos)
abstract StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos)
On the other hand, to parse a string into a number, use the parse()
method:
String text = "1,234.56";
Number number = defaultFormat.parse(text); // Returns a Long or Double
The parse()
method is locale-sensitive and will recognize the locale-specific decimal and grouping separators.
NumberFormat
also provides several methods to control the formatting output:
setMinimumIntegerDigits()
and setMaximumIntegerDigits()
: Controls the number of digits in the integer part.setMinimumFractionDigits()
and setMaximumFractionDigits()
: Controls the number of digits in the fraction part.setGroupingUsed()
: Enables or disables grouping separators.setRoundingMode()
: Sets the rounding mode for the formatter.For more control, NumberFormat
has several concrete subclasses for specific formatting needs:
DecimalFormat
: A general-purpose formatter for decimal numbers.CompactNumberFormat
: Formats numbers in a compact, locale-sensitive way, like “1.2K” for 1200.ChoiceFormat
: Allows you to map numbers to strings based on a set of ranges.DecimalFormat
ClassThe most commonly used subclass is DecimalFormat
, which provides a high degree of control over the formatting pattern. You can create a DecimalFormat
with a specific pattern and symbols:
DecimalFormat format = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(Locale.US));
The second parameter (DecimalFormatSymbols
) is optional. The pattern specifies the format of the output, with special characters representing the digit positions, decimals, grouping, etc. The DecimalFormatSymbols
object defines the specific characters to use for these special characters based on a locale.
A DecimalFormat
pattern contains a positive and negative subpattern, for example, #,##0.00;(#,##0.00)
. Each subpattern has a prefix, numeric part, and suffix. The negative subpattern is optional, if absent, then the positive subpattern prefixed with the minus sign ('-' U+002D HYPHEN-MINUS
) is used as the negative subpattern.
Many characters in a pattern are taken literally, they are matched during parsing and are unchanged in the output during formatting. On the other hand, special characters stand for other characters, strings, or classes of characters and they must be quoted.
The characters listed in the following table are used in non-localized patterns. Localized patterns use the corresponding characters taken from this formatter’s DecimalFormatSymbols
object instead, and these characters lose their special status. Two exceptions are the currency sign and quote, which are not localized:
Symbol | Location | Localized? | Meaning |
---|---|---|---|
0 |
Number | Yes | Digit |
# |
Number | Yes | Digit, zero shows as absent |
. |
Number | Yes | Decimal separator or monetary decimal separator |
- |
Number | Yes | Minus sign |
, |
Number | Yes | Grouping separator or monetary grouping separator |
E |
Number | Yes | Separates mantissa and exponent in scientific notation. Need not be quoted in prefix or suffix. |
; |
Subpattern boundary | Yes | Separates positive and negative subpatterns |
% |
Prefix or suffix | Yes | Multiply by 100 and show as percentage |
\u2030 |
Prefix or suffix | Yes | Multiply by 1000 and show as per mille value |
¤ (\u00A4 ) |
Prefix or suffix | No | Currency sign, replaced by currency symbol. If doubled, replaced by international currency symbol. If present in a pattern, the monetary decimal and grouping separators are used instead of the decimal and grouping separators. |
' |
Prefix or suffix | No | Used to quote special characters in a prefix or suffix, for example, " '#' " formats 123 to "#123" . To create a single quote itself, use two in a row: "# o''clock" . |
Here are some examples:
// Number Formatting
DecimalFormat numberFormat = new DecimalFormat("###,###.##");
System.out.println("Number Formatting: " + numberFormat.format(1234567.89));
// Percentage Formatting
DecimalFormat percentFormat = new DecimalFormat("##.##%");
System.out.println("Percentage Formatting: " + percentFormat.format(0.1234));
// Per Mille Formatting
DecimalFormat perMilleFormat = new DecimalFormat("##.##‰");
System.out.println("Per Mille Formatting: " + perMilleFormat.format(0.1234));
// Currency Formatting
DecimalFormat currencyFormat = new DecimalFormat("$###,###.##");
System.out.println("Currency Formatting: " + currencyFormat.format(1234.56));
// Scientific Notation
DecimalFormat scientificFormat = new DecimalFormat("0.###E0");
System.out.println("Scientific Notation: " + scientificFormat.format(12345));
// Positive and Negative Subpatterns
DecimalFormat positiveNegativeFormat = new DecimalFormat("###.##;(#.##)");
System.out.println("Positive Number: " + positiveNegativeFormat.format(1234.56));
System.out.println("Negative Number: " + positiveNegativeFormat.format(-1234.56));
// Custom Text with Numbers
DecimalFormat customTextFormat = new DecimalFormat("'Number: '###");
System.out.println("Custom Text with Numbers: " + customTextFormat.format(123));
// Custom Grouping and Decimal Separators
DecimalFormat customGroupingFormat = new DecimalFormat("'Amount: '###,###.##");
System.out.println("Custom Grouping and Decimal Separators: " + customGroupingFormat.format(1234567.89));
// Quoting Special Characters
DecimalFormat quotingSpecialFormat = new DecimalFormat("''#''###");
System.out.println("Quoting Special Characters: " + quotingSpecialFormat.format(123));
// Integer Formatting
DecimalFormat integerFormat = new DecimalFormat("###");
System.out.println("Integer Formatting: " + integerFormat.format(1234.56));
This is the output:
Number Formatting: 1,234,567.89
Percentage Formatting: 12.34%
Per Mille Formatting: 123.4‰
Currency Formatting: $1,234.56
Scientific Notation: 1.234E4
Positive Number: 1234.56
Negative Number: (1234.56)
Custom Text with Numbers: Number: 123
Custom Grouping and Decimal Separators: Amount: 1,234,567.89
Quoting Special Characters: '123'
Integer Formatting: 1235
CompactNumberFormat
ClassCompactNumberFormat
provides support for compact number formatting. This format is particularly useful for displaying large numbers in a more readable, locale-sensitive way.
This class supports two styles:
NumberFormat.Style.SHORT
: Uses abbreviations (for example, K
for thousand, M
for million, B
for billion, and so on)NumberFormat.Style.LONG
: Uses full words (for example, “thousand”, “million”)To get a CompactNumberFormat
instance, you use the getCompactNumberInstance()
factory method:
NumberFormat shortFormat = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
NumberFormat longFormat = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);
Here are some examples of how CompactNumberFormat
works:
double number = 1_234_567.89;
System.out.println(shortFormat.format(number)); // Output: 1M
System.out.println(longFormat.format(number)); // Output: 1 million
number = 1_234;
System.out.println(shortFormat.format(number)); // Output: 1K
System.out.println(longFormat.format(number)); // Output: 1 thousand
CompactNumberFormat
automatically adjusts the number of displayed digits based on the magnitude of the number. It also handles different locales appropriately:
NumberFormat frFormat = NumberFormat.getCompactNumberInstance(Locale.FRANCE, NumberFormat.Style.SHORT);
System.out.println(frFormat.format(1_234_567.89)); // Output: 1 M
By default, CompactNumberFormat
doesn’t display fractional digits. However, you can modify this behavior using setMinimumFractionDigits()
and setMaximumFractionDigits()
methods.
Also, the class also supports various rounding modes by using setRoundingMode
and the java.math.RoundingMode
enum, with RoundingMode.HALF_EVEN
as the default:
Enum Constant | Description |
---|---|
CEILING |
Rounding mode to round towards positive infinity. |
DOWN |
Rounding mode to round towards zero. |
FLOOR |
Rounding mode to round towards negative infinity. |
HALF_DOWN |
Rounding mode to round towards “nearest neighbor” unless both neighbors are equidistant, in which case round down. |
HALF_EVEN |
Rounding mode to round towards the “nearest neighbor” unless both neighbors are equidistant, in which case, round towards the even neighbor. |
HALF_UP |
Rounding mode to round towards “nearest neighbor” unless both neighbors are equidistant, in which case round up. |
UNNECESSARY |
Rounding mode to assert that the requested operation has an exact result, hence no rounding is necessary. |
UP |
Rounding mode to round away from zero. |
Here are some examples:
double number = 1_234_567.89;
NumberFormat shortFormat = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
// Default behavior (no fractional digits)
System.out.println(shortFormat.format(number)); // Output: 1M
// Adding fractional digits
shortFormat.setMinimumFractionDigits(1);
shortFormat.setMaximumFractionDigits(2);
System.out.println(shortFormat.format(number)); // Output: 1.23M
// Changing rounding mode
shortFormat.setRoundingMode(RoundingMode.DOWN);
System.out.println(shortFormat.format(number)); // Output: 1.23M
shortFormat.setRoundingMode(RoundingMode.UP);
System.out.println(shortFormat.format(number)); // Output: 1.24M
// Behavior with smaller numbers
number = 1234.56;
System.out.println(shortFormat.format(number)); // Output: 1.24K
// Resetting to default behavior
shortFormat = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
System.out.println(shortFormat.format(number)); // Output: 1K
DateTimeFormatter
ClassIn the The Date API chapter, we covered the java.time.format.DateTimeFormatter
class, which provides a flexible and modern way to format dates and times represented by java.time
classes like LocalDate
, LocalTime
, and LocalDateTime
.
However, one of the key features of DateTimeFormatter
is its ability to create localized formatters using the ofLocalized...()
methods:
static DateTimeFormatter ofLocalizedDate(FormatStyle dateStyle)
: Creates a formatter that formats a date in the specified style for the current locale.static DateTimeFormatter ofLocalizedTime(FormatStyle timeStyle)
: Creates a formatter that formats a time in the specified style for the current locale.static DateTimeFormatter ofLocalizedDateTime(FormatStyle dateStyle, FormatStyle timeStyle)
: Creates a formatter that formats a date and time in the specified styles for the current locale.static DateTimeFormatter ofLocalizedDateTime(FormatStyle dateTimeStyle)
: Creates a formatter that formats a date and time in the specified combined style for the current locale.The FormatStyle
enum defines the available styles: SHORT
, MEDIUM
, LONG
, and FULL
.
Here’s an example of using these methods:
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM);
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
String formattedDate = date.format(dateFormatter);
String formattedTime = time.format(timeFormatter);
String formattedDateTime = dateTime.format(dateTimeFormatter);
System.out.println("Formatted date: " + formattedDate);
System.out.println("Formatted time: " + formattedTime);
System.out.println("Formatted date-time: " + formattedDateTime);
The ofLocalized...()
methods automatically use the current locale to determine the appropriate formatting. If you want to specify a different locale, you can use the withLocale()
method:
DateTimeFormatter frenchDateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(Locale.FRANCE);
String formattedDateInFrench = date.format(frenchDateFormatter);
System.out.println("Formatted date in French: " + formattedDateInFrench);
These localized formatters provide an easy way to format dates and times according to the conventions of a specific locale without having to specify the format pattern manually.
You can also use a pattern to create a DateTimeFormatter
instance using the ofPattern(String)
and ofPattern(String, Locale)
methods. For example, "d MMM uuuu"
will format 2011-12-03
as '3 Dec 2011'
. A formatter created from a pattern can be used as many times as necessary. It is immutable and thread-safe.
Here’s an example:
LocalDate date = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yy MM dd");
// Format the date
String text = date.format(formatter);
System.out.println("Formatted date: " + text);
// Parse the date
LocalDate parsedDate = LocalDate.parse(text, formatter);
System.out.println("Parsed date: " + parsedDate);
Here’s a sample output:
Formatted date: 24 07 25
Parsed date: 2024-07-25
The following table shows all the pattern letters defined (all letters ‘A’ to ‘Z’ and ‘a’ to ‘z’ are reserved):
Symbol | Meaning | Presentation | Examples |
---|---|---|---|
G |
era | text | AD ; Anno Domini ; A |
u |
year | year | 2004 ; 04 |
y |
year-of-era | year | 2004 ; 04 |
D |
day-of-year | number | 189 |
M/L |
month-of-year | number/text | 7 ; 07 ; Jul ; July ; J |
d |
day-of-month | number | 10 |
g |
modified-julian-day | number | 2451334 |
Q/q |
quarter-of-year | number/text | 3 ; 03 ; Q3 ; 3rd quarter |
Y |
week-based-year | year | 1996 ; 96 |
w |
week-of-week-based-year | number | 27 |
W |
week-of-month | number | 4 |
E |
day-of-week | text | Tue ; Tuesday ; T |
e/c |
localized day-of-week | number/text | 2 ; 02 ; Tue ; Tuesday ; T |
F |
day-of-week-in-month | number | 3 |
a |
am-pm-of-day | text | PM |
B |
period-of-day | text | in the morning |
h |
clock-hour-of-am-pm (1-12) | number | 12 |
K |
hour-of-am-pm (0-11) | number | 0 |
k |
clock-hour-of-day (1-24) | number | 24 |
H |
hour-of-day (0-23) | number | 0 |
m |
minute-of-hour | number | 30 |
s |
second-of-minute | number | 55 |
S |
fraction-of-second | fraction | 978 |
A |
milli-of-day | number | 1234 |
n |
nano-of-second | number | 987654321 |
N |
nano-of-day | number | 1234000000 |
V |
time-zone ID | zone-id | America/Los_Angeles ; Z ; -08:30 |
v |
generic time-zone name | zone-name | Pacific Time ; PT |
z |
time-zone name | zone-name | Pacific Standard Time ; PST |
O |
localized zone-offset | offset-O | GMT+8 ; GMT+08:00 ; UTC-08:00 |
X |
zone-offset ‘Z’ for zero | offset-X | Z ; -08 ; -0830 ; -08:30 ; -083015 ; -08:30:15 |
x |
zone-offset | offset-x | +0000 ; -08 ; -0830 ; -08:30 ; -083015 ; -08:30:15 |
Z |
zone-offset | offset-Z | +0000 ; -0800 ; -08:00 |
p |
pad next | pad modifier | 1 |
' |
escape for text | delimiter | |
'' |
single quote | literal | ' |
[ |
optional section start | ||
] |
optional section end | ||
# |
reserved for future use | ||
{ |
reserved for future use | ||
} |
reserved for future use |
Here are examples using many of those patterns:
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
ZonedDateTime zonedDateTime = ZonedDateTime.now();
// Era
DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("G");
System.out.println("Era: " + date.format(formatter1));
// Year
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy");
System.out.println("Year: " + date.format(formatter2));
// Day of Year
DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("D");
System.out.println("Day of Year: " + date.format(formatter3));
// Month of Year
DateTimeFormatter formatter4 = DateTimeFormatter.ofPattern("MMMM");
System.out.println("Month of Year: " + date.format(formatter4));
// Day of Month
DateTimeFormatter formatter5 = DateTimeFormatter.ofPattern("d");
System.out.println("Day of Month: " + date.format(formatter5));
// Day of Week
DateTimeFormatter formatter6 = DateTimeFormatter.ofPattern("EEEE");
System.out.println("Day of Week: " + date.format(formatter6));
// AM/PM of Day
DateTimeFormatter formatter7 = DateTimeFormatter.ofPattern("a");
System.out.println("AM/PM of Day: " + time.format(formatter7));
// Hour of Day (0-23)
DateTimeFormatter formatter8 = DateTimeFormatter.ofPattern("H");
System.out.println("Hour of Day (0-23): " + time.format(formatter8));
// Minute of Hour
DateTimeFormatter formatter9 = DateTimeFormatter.ofPattern("m");
System.out.println("Minute of Hour: " + time.format(formatter9));
// Second of Minute
DateTimeFormatter formatter10 = DateTimeFormatter.ofPattern("s");
System.out.println("Second of Minute: " + time.format(formatter10));
// Time Zone Name
DateTimeFormatter formatter11 = DateTimeFormatter.ofPattern("z");
System.out.println("Time Zone Name: " + zonedDateTime.format(formatter11));
// ISO 8601 Time Zone
DateTimeFormatter formatter12 = DateTimeFormatter.ofPattern("X");
System.out.println("ISO 8601 Time Zone: " + zonedDateTime.format(formatter12));
The output should be similar to this:
Era: AD
Year: 2024
Day of Year: 207
Month of Year: July
Day of Month: 25
Day of Week: Thursday
AM/PM of Day: PM
Hour of Day (0-23): 20
Minute of Hour: 31
Second of Minute: 16
Time Zone Name: CDT
ISO 8601 Time Zone: -05
For the Java certification exam, you don’t need to memorize every single pattern, but you should be familiar with the key ones that are likely to appear on the exam. Here’s a list of patterns and you should concentrate on:
G
y
M
d
H
h
m
s
a
Y
D
E
, e
B
S
n
N
V
z
X
O
Localization is the process of designing and developing an application so that it can be adapted to various locales without requiring engineering changes. A locale represents a specific geographical, political, or cultural region.
Locales are represented by the java.util.Locale
class. You can get the default locale of the Java Virtual Machine using Locale.getDefault()
, use built-in constants like Locale.US
, or create a new Locale
object using a constructor or the forLanguageTag()
method.
The Locale.Category
enum defines two categories: DISPLAY
(for user interface elements) and FORMAT
(for parsing and formatting data). You can set and get the default locale for each category using Locale.setDefault()
and Locale.getDefault()
.
Resource bundles are used to organize and access locale-specific data. They are typically implemented as property files named in the format <basename>_<language>_<country>_<variant>.properties
.
The java.util.ResourceBundle
class is used to load the appropriate property file for the current locale. You can retrieve locale-specific values using getString()
or getObject()
.
The java.text.MessageFormat
class is used to create localized messages by combining a pattern string with arguments. The pattern includes placeholders marked with {}
and argument indexes. Format types (like number
or date
) and styles can be specified.
The java.text.Format
class has subclasses to format arguments based on their type and the specified format style. The main subclasses are java.text.NumberFormat
for numbers and java.text.DateFormat
for dates.
java.text.NumberFormat
is the abstract base class for formatting and parsing numbers in Java. It provides factory methods for getting locale-specific formatters for numbers, currencies, and percentages.
java.text.DecimalFormat
is a concrete subclass of NumberFormat
that allows for more detailed control over the formatting of decimal numbers using patterns. The pattern can include special characters representing digit positions, decimals, grouping, etc.
java.time.format.DateTimeFormatter
is a class for formatting dates and times from the java.time
package. It works with the modern date/time classes like LocalDate
, LocalTime
, LocalDateTime
.
DateTimeFormatter
provides ofLocalized...()
methods for creating locale-specific formatters in different styles, similar to DateFormat
. It also allows creating formatters from custom patterns using ofPattern()
.
For the certification exam, focus on common patterns like era (G
), year of era (y
), month (M
), day of month (d
), hours (H
, h
), minutes (m
), seconds (s
), AM/PM (a
), among others.
1. Consider the following code snippet:
import java.util.Locale;
public class LocaleTest {
public static void main(String[] args) {
Locale locale1 = new Locale("fr", "CA");
Locale locale2 = new Locale("fr", "CA", "UNIX2024");
Locale locale3 = Locale.CANADA_FRENCH;
System.out.println(locale1.equals(locale2));
System.out.println(locale1.equals(locale3));
System.out.println(locale2.equals(locale3));
System.out.println(locale1.getDisplayName(Locale.ENGLISH));
System.out.println(locale2.getDisplayName(Locale.ENGLISH));
System.out.println(locale3.getDisplayName(Locale.ENGLISH));
}
}
What will be the output when this code is executed?
A)
true
true
true
French (Canada)
French (Canada)
French (Canada)
B)
false
true
false
French (Canada)
French (Canada, UNIX2024)
French (Canada)
C)
false
true
false
French (Canada)
French (Canada, UNIX2024)
Canadian French
D)
false
false
false
French (Canada)
French (Canada, UNIX2024)
Canadian French
E) The code will throw a IllegalArgumentException
because UNIX2024
is not a valid variant.
2. Which of the following statements about Locale
categories is correct?
A) The Locale.Category
enum has three values: DISPLAY
, FORMAT
, and LANGUAGE
.
B) The Locale.setDefault(Locale.Category, Locale)
method can only set the default locale for the FORMAT
category.
C) Using Locale.getDefault(Locale.Category)
always returns the same locale regardless of the category specified.
D) The DISPLAY
category affects the language used for displaying user interface elements, while the FORMAT
category affects the formatting of numbers, dates, and currencies.
E) Locale categories were introduced in Java 8 to replace the older Locale
methods.
3. Which of the following statements about Resource Bundles is correct?
A) Resource bundles can only be stored in .properties
files.
B) The ResourceBundle.getBundle()
method always throws a MissingResourceException
if the requested bundle is not found.
C) When searching for a resource bundle, Java only considers the specified locale and its language.
D) If a key is not found in a specific locale’s resource bundle, Java will look for it in the parent locale’s bundle.
E) Resource bundles are loaded dynamically at runtime, so changes to .properties
files are immediately reflected in the running application.
4. Consider the following code snippet:
import java.util.*;
import java.io.*;
public class ConfigTest {
public static void main(String[] args) throws IOException {
Properties props = new Properties();
props.setProperty("color", "blue");
props.setProperty("size", "medium");
try (OutputStream out = new FileOutputStream("config.properties")) {
props.store(out, "Config File");
}
props.clear();
System.out.println(props.getProperty("color", "red"));
try (InputStream in = new FileInputStream("config.properties")) {
props.load(in);
}
System.out.println(props.getProperty("color", "red"));
}
}
What will be the output when this code is executed?
A)
red
blue
B)
blue
blue
C)
red
red
D) The code will throw a FileNotFoundException
.
E)
null
blue
5. Consider the following code snippet:
import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale;
public class MessageFormatTest {
public static void main(String[] args) {
String pattern = "On {0, date, long}, {1} bought {2,number,integer} {3} for {4,number,currency}.";
Object[] params = {
new Date(),
"Alice",
3,
"apples",
19.99
};
MessageFormat mf = new MessageFormat(pattern, Locale.US);
String result = mf.format(params);
System.out.println(result);
}
}
Which of the following statements about this code is correct?
A) The code will throw a IllegalArgumentException
because the date format is invalid.
B) The output will include the date in long format, the name "Alice"
, the number 3, the word "apples"
, and the price in US currency format.
C) The {2,number,integer}
format will display 3 as "3.0"
.
D) The code will not compile because MessageFormat
doesn’t accept a Locale
in its constructor.
E) The {4,number,currency}
format will always display the price in USD, regardless of the Locale
.
6. Which of the following statements about the NumberFormat
class in Java is correct?
A) The NumberFormat.getCurrencyInstance()
method returns a formatter that can format monetary amounts according to the specified locale’s conventions.
B) NumberFormat
is a concrete class that can be instantiated directly using its constructor.
C) The setMaximumFractionDigits()
method in NumberFormat
can only accept values between 0 and 3.
D) When parsing strings, NumberFormat
always throws a ParseException
if the input doesn’t exactly match the expected format.
E) The NumberFormat
class can only format and parse integer values, not floating-point numbers.
7. Consider the following code snippet:
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class DateTimeFormatterTest {
public static void main(String[] args) {
LocalDateTime ldt = LocalDateTime.of(2023, 6, 15, 10, 30);
ZoneId zoneNY = ZoneId.of("America/New_York");
ZonedDateTime zdtNY = ldt.atZone(zoneNY);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z VV");
System.out.println(formatter.format(zdtNY));
ZoneId zoneTokyo = ZoneId.of("Asia/Tokyo");
ZonedDateTime zdtTokyo = zdtNY.withZoneSameInstant(zoneTokyo);
System.out.println(formatter.format(zdtTokyo));
}
}
What will be the output when this code is executed?
A)
2023-06-15 10:30 EDT America/New_York
2023-06-15 23:30 JST Asia/Tokyo
B)
2023-06-15 10:30 EDT New_York
2023-06-15 23:30 JST Tokyo
C)
2023-06-15 10:30 -04:00 America/New_York
2023-06-15 23:30 +09:00 Asia/Tokyo
D)
2023-06-15 10:30 America/New_York
2023-06-15 23:30 Asia/Tokyo
E) The code will throw a DateTimeException
because the formatter pattern is invalid.
Do you like what you read? Would you consider?
Do you have a problem or something to say?