Chapter FOURTEEN
Localization


Exam Objectives

Implement localization using locales and resource bundles. Parse and format messages, dates, times, and numbers, including currency and percentage values.

Chapter Content


Introduction to Localization

It’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).

The Locale Class

Locales 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

Locale Categories

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.

  1. Locale.Category.DISPLAY:
    • This category is used for user interface strings, such as messages, labels, and menus.
    • It is applied when you need to localize the content that is displayed to the user.
    • For instance, when an application needs to show date, time, or currency in a localized manner, it uses the DISPLAY locale to ensure the formats and messages are appropriate for the user’s language and region.
  2. Locale.Category.FORMAT:
    • This category is used for formatting dates, numbers, and other values that are part of the data processing or backend systems.
    • It is applied when you need to parse, format, or validate locale-sensitive data.
    • For instance, when you need to format a date for storage or further processing, the 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.

Resource Bundles

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:

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:

For 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:

  1. Language + Country + Variant
  2. Language + Country
  3. Language
  4. Default locale (as returned by Locale.getDefault())
  5. Root resource bundle (basename only, no locale info)

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

The MessageFormat Class

Sometimes, 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:

-. Format Type: {argumentIndex,formatType} adds a format type to the argument. Common format types include:

Text can be quoted using single quotes ', which are escaped as ''. Any unmatched curly braces must be escaped with a single quote. For example:

Here are some examples of valid message format patterns:

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:

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:

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 :

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:

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.

The NumberFormat Class

NumberFormat 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:

For more control, NumberFormat has several concrete subclasses for specific formatting needs:

The DecimalFormat Class

The 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
&#92;u2030 Prefix or suffix Yes Multiply by 1000 and show as per mille value
&#164; (&#92;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

The CompactNumberFormat Class

CompactNumberFormat 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:

  1. NumberFormat.Style.SHORT: Uses abbreviations (for example, K for thousand, M for million, B for billion, and so on)
  2. 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

The DateTimeFormatter Class

In 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:

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:

Key Points

Practice Questions

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?

Report an issue with the book

Contact me