Java Localization: Locales, Resource Bundles and Formatting Numbers, Dates and Messages (1Z0-831)
Localization in Java is adapting a program's text and formatting to a user's language and region, using java.util.Locale to name the target, ResourceBundle to load the translated text, and locale-aware formatters for messages, numbers, currency, percentages, dates and times. Oracle 1Z0-831 objective 10.1 covers all of it. This lesson explains how to create locales and how the default locale and its FORMAT and DISPLAY categories are used, exactly which bundle getBundle returns and how keys resolve through parent bundles, property files versus ListResourceBundle classes, MessageFormat placeholders and quoting, NumberFormat, compact and DecimalFormat output with HALF_EVEN rounding, number parsing, and DateTimeFormatter patterns, localized styles and resolver styles.
On this page10 sections
- Creating locales: Locale.of, constants, forLanguageTag and Locale.Builder
- The default locale and its FORMAT and DISPLAY categories
- How ResourceBundle.getBundle chooses a bundle
- Resolving keys through the parent chain, and MissingResourceException
- Property files versus ListResourceBundle classes
- Formatting messages with MessageFormat
- Formatting numbers, currency and percentages with NumberFormat
- Parsing numbers with NumberFormat.parse
- DecimalFormat patterns and String.format with a Locale
- Formatting and parsing dates and times with DateTimeFormatter
- Create locales with Locale.of, constants, forLanguageTag and Locale.Builder, and tell toString from toLanguageTag
- Predict which default (general, FORMAT or DISPLAY) each localization API reads after Locale.setDefault calls
- Trace ResourceBundle.getBundle's candidate search and a bundle's parent chain to the value, getLocale() result or MissingResourceException
- Choose between property-file and ListResourceBundle bundles and use getString, getObject and getStringArray safely
- Write and parse MessageFormat patterns with reordered placeholders, escaped apostrophes and number, date and choice format types
- Format and parse numbers, currency, percentages and compact numbers with NumberFormat and DecimalFormat, including HALF_EVEN rounding
- Format and parse dates and times with DateTimeFormatter patterns, localized FormatStyles and resolver styles
Creating locales: Locale.of, constants, forLanguageTag and Locale.Builder
A java.util.Locale names a language, optionally with a region (country), script and variant, and you get one in four ways: the factory Locale.of(language, country) (Java 19+; the old constructors are deprecated), a constant such as Locale.US, Locale.UK, Locale.GERMANY, Locale.FRANCE, Locale.JAPAN, Locale.CANADA_FRENCH or the language-only Locale.FRENCH and Locale.ENGLISH, parsing a BCP 47 tag with Locale.forLanguageTag("de-CH"), or the validating Locale.Builder. Locale.ROOT is the empty locale: no language, no region.
The four routes differ in how forgiving they are, and that difference is where code goes wrong:
| API | Input | Bad input |
|---|---|---|
Locale.of("de", "CH") | Language and country as separate strings | No validation. Case is normalized (Locale.of("DE", "ch") is de_CH); a three-letter country such as Locale.of("de", "CHE") is accepted as de_CHE; Locale.of("de-CH") treats the whole string as a language and prints de-ch. |
Locale.forLanguageTag("de-CH") | One BCP 47 tag, hyphen-separated | Never throws for an ill-formed tag (only a null argument throws NullPointerException). It stops at the first ill-formed subtag; "de_CH" has no valid language, so the result is Locale.ROOT. |
new Locale.Builder().setLanguage("de").setRegion("CH").build() | Field by field | Validates every field and throws the unchecked IllformedLocaleException. A region must be two letters or three digits (such as 419, Latin America), so setRegion("CHE") throws. |
Constants (Locale.JAPAN) | Predefined | Not applicable. |
A locale has two text forms. toString() joins the parts with underscores (ja_JP) and is the suffix used in resource-bundle file names; toLanguageTag() produces the BCP 47 tag with hyphens (ja-JP), the form forLanguageTag reads back. Locale.ROOT.toString() is the empty string, so printing it prints nothing, and its language tag is und. Two locales are equals only when every field matches, so a root locale never equals the locale the tag was meant to name.
import java.util.Locale;
public class Main {
public static void main(String[] args) {
Locale a = Locale.of("ja", "JP");
Locale b = Locale.forLanguageTag("de_CH");
System.out.println(a + " " + a.toLanguageTag());
System.out.println("[" + b + "] " + Locale.of("DE", "ch"));
}
}This prints ja_JP ja-JP and then [] de_CH: the underscore tag silently became the root locale, while Locale.of lower-cased the language and upper-cased the country.
The default locale and its FORMAT and DISPLAY categories
The JVM keeps one general default locale plus two category defaults, and every locale-sensitive API that is not given a Locale reads one of them. Locale.Category.FORMAT is used to format and parse numbers, dates, currencies and messages; Locale.Category.DISPLAY is used for the human-readable names of locales themselves; and the general default (Locale.getDefault() with no argument) is what ResourceBundle.getBundle falls back to.
| Call | Effect |
|---|---|
Locale.setDefault(loc) | Sets the general default and overwrites both FORMAT and DISPLAY with loc |
Locale.setDefault(Locale.Category.FORMAT, loc) | Changes only FORMAT |
Locale.setDefault(Locale.Category.DISPLAY, loc) | Changes only DISPLAY |
Order therefore matters: a category call followed by the one-argument setDefault is undone, while the one-argument call followed by a category call leaves a mixed setup.
| Reads FORMAT | Reads DISPLAY | Reads the general default |
|---|---|---|
NumberFormat.getInstance() and the other no-argument factories, MessageFormat.format, DateTimeFormatter.ofPattern(p) and ofLocalized… without withLocale, String.format, "…".formatted, printf without a locale | getDisplayLanguage(), getDisplayCountry(), getDisplayName() with no argument | Locale.getDefault(); the fallback step of ResourceBundle.getBundle |
import java.text.NumberFormat;
import java.util.Locale;
public class Main {
public static void main(String[] args) {
Locale nl = Locale.of("nl", "NL");
Locale.setDefault(Locale.UK);
Locale.setDefault(Locale.Category.FORMAT, nl);
System.out.println(NumberFormat.getInstance().format(9876.25));
System.out.println(nl.getDisplayCountry());
System.out.println(Locale.getDefault());
Locale.setDefault(Locale.UK);
System.out.println(Locale.getDefault(Locale.Category.FORMAT));
}
}The output is 9.876,25 (FORMAT is Dutch, which groups with a point and uses a decimal comma), Netherlands (DISPLAY is still English, so the country name is in English), en_GB (the general default ignores category calls), and finally en_GB again, because the last one-argument setDefault reset FORMAT too.
The safest habit in real code is to pass a Locale explicitly; an explicit argument always beats any default.
How ResourceBundle.getBundle chooses a bundle
ResourceBundle.getBundle(baseName, locale) returns the first bundle that exists in a fixed candidate search: the requested locale from most to least specific, then the default locale the same way, and finally the base bundle. A resource bundle is a set of key–value pairs for one locale, found by name: base name Menu plus a locale suffix gives Menu_es_MX, Menu_es and the base bundle Menu.
For getBundle("Menu", Locale.of("es", "MX")) with default de_DE the search is:
Menu_es_MX, thenMenu_es(the requested locale)- Only if neither exists:
Menu_de_DE, thenMenu_de(the general default locale) - Only if those fail as well: the base bundle
Menu - Nothing at all:
MissingResourceException
Three details decide most outcomes:
- A partial match wins over the default. Requesting
es_ARores_CLwhen onlyMenu_esexists returnsMenu_es; the default locale is never consulted. - The fallback reads the general default, not FORMAT or DISPLAY. If the default has no files either (say a Korean default with no Korean bundle), the search ends at the base bundle.
- A
Locale.ROOTrequest accepts the base bundle without trying the default locale. Its own candidate list is just the base bundle, so when the base bundle exists it is returned straight away. The default locale is tried only when there is no base bundle: with onlyOnly_de.propertieson the class path and a German default,getBundle("Only", Locale.ROOT)returns thedebundle rather than throwing.
getLocale() on the result tells you which bundle was actually loaded: es for Menu_es, de for Menu_de, and Locale.ROOT (printed as an empty string) for the base bundle. It is not the locale you asked for.
Given Menu.properties (title=Welcome, exit=Quit), Menu_de.properties (title=Willkommen, exit=Beenden), Menu_es.properties (title=Bienvenido) and Menu_es_MX.properties (title=Hola):
import java.util.*;
public class Main {
public static void main(String[] args) {
Locale.setDefault(Locale.GERMANY);
show(Locale.of("es", "MX"));
show(Locale.of("es", "AR"));
show(Locale.JAPAN);
show(Locale.ROOT);
}
static void show(Locale loc) {
var rb = ResourceBundle.getBundle("Menu", loc);
System.out.println("[" + rb.getLocale() + "] "
+ rb.getString("title") + " " + rb.getString("exit"));
}
}The program prints [es_MX] Hola Quit, [es] Bienvenido Quit, [de] Willkommen Beenden and [] Welcome Quit. Argentina has no file of its own, so the partial match Menu_es wins; Japanese has no files at all, so the search falls to the default de_DE and lands on Menu_de; ROOT takes the base bundle that exists. The Quit in the first two lines comes from the parent chain, explained next.
Resolving keys through the parent chain, and MissingResourceException
Once a bundle is chosen, a key that it does not define is looked up in its parents: the same base name with the locale shortened one level at a time, ending at the base bundle. Menu_es_MX has parent Menu_es, whose parent is Menu. That is why exit above resolved to Quit from the base file even though Menu_de, the default locale's file, defines it.
- The default locale is never a parent. Its files only matter when they are chosen as the result bundle in the fallback step.
- Sibling locales are never parents.
Menu_es_EScontributes nothing to anes_MXlookup. - A fallback result keeps its own chain. If the search lands on
Menu_de_DE, keys missing there come fromMenu_de, thenMenu. keySet()andcontainsKey()cover the whole chain; only the protectedhandleKeySet()is limited to the bundle's own keys.
java.util.MissingResourceException is thrown in two situations: by getBundle when no candidate exists at all (it never returns null or an empty bundle), and by getString/getObject when the key is absent from the bundle and every parent. It extends RuntimeException, so the compiler never forces you to catch it; getKey() on the exception returns the missing key.
| Situation | Result |
|---|---|
No Alerts bundle for any candidate | getBundle throws: Can't find bundle for base name Alerts, locale es_MX |
| Bundle found, key in neither it nor its parents | getString throws; getKey() names the key |
| Key only in the default locale's file, which is not in the chain | getString throws |
Property files versus ListResourceBundle classes
A bundle can be a .properties file, loaded as a PropertyResourceBundle whose values are always Strings, or a class that extends ListResourceBundle and returns an Object[][] of key–value pairs from getContents(), whose values can be any object. When both exist for the same candidate name (a class Tips_fr and a file Tips_fr.properties), the default ResourceBundle.Control tries the class format first, so the class wins and the file is ignored.
| Getter | Behaviour |
|---|---|
getObject(key) | Returns the stored object, no cast |
getString(key) | Equivalent to (String) getObject(key): an Integer value throws ClassCastException |
getStringArray(key) | Equivalent to (String[]) getObject(key) |
A properties file containing retries=3 therefore yields the String "3" from getObject (and "3".equals(3) is false, because an Integer is never equal to a String), while a class can store a real Integer.
import java.util.ListResourceBundle;
public class Quota extends ListResourceBundle {
@Override
protected Object[][] getContents() {
return new Object[][] {
{"retries", 3},
{"colors", new String[] {"red", "green", "blue"}}
};
}
}import java.util.*;
public class Main {
public static void main(String[] args) {
var rb = ResourceBundle.getBundle("Quota", Locale.UK);
Integer retries = (Integer) rb.getObject("retries");
String[] colors = rb.getStringArray("colors");
System.out.println(retries * 5 + " " + colors[2]);
System.out.println(rb.getString("retries"));
}
}Run with java -cp out Main after compiling both classes, this prints 15 blue and then throws ClassCastException at getString("retries"): the value is an Integer, and getString casts it to String.
Formatting messages with MessageFormat
java.text.MessageFormat fills numbered placeholders {0}, {1}… in a pattern with arguments, formatting each one for a locale, so that translators can reorder and repeat arguments in a pattern stored in a resource bundle without changing code. The static MessageFormat.format(pattern, args…) uses the FORMAT default locale; new MessageFormat(pattern, locale) fixes one.
Placeholder rules
- The index refers to the argument position, not the order in the pattern; an index can appear several times.
- A placeholder with no matching argument is printed literally (such as
{2}), not asnulland without an exception. - A single quote starts a quoted (literal) section and is not printed, so
Don't stop {0}printsDont stop {0}: the placeholder is swallowed. Write two quotes (Don''t) for one apostrophe. A backslash is not an escape character here. - A plain
{0}formats aNumberwith the locale's number format (grouping included), aDatewith a date-time format, and anything else (including aStringor aLocalDate) withtoString().
Format types and styles
| Element | Result |
|---|---|
{0,number} | General number format |
{0,number,integer} | Integer format, rounding HALF_EVEN (4.5 → 4) |
{0,number,percent} | Multiplies by 100, no fraction digits (0.738 → 74%) |
{0,number,currency} | Currency format of the locale |
{0,date}, {0,time} (styles short|medium|long|full) | A legacy DateFormat: it accepts java.util.Date or a Number, and throws IllegalArgumentException ("Cannot format given Object as a Date") for a LocalDate |
{0,choice,0#an empty cart|1#a single item|1<{0,number,integer} items} | Picks the text for the last limit not above the value; # means "from this value", < means "greater than"; placeholders in the chosen text are formatted again |
import java.text.MessageFormat;
import java.util.Locale;
public class Main {
public static void main(String[] args) {
Locale.setDefault(Locale.UK);
System.out.println(MessageFormat.format(
"{0} meets {1}; {1} thanks {0}", "Raj", "Mia"));
System.out.println(MessageFormat.format(
"{0} booked {1} seats on {2}", "Omar", 4321));
System.out.println(MessageFormat.format(
"Don't stop {0}", "Omar"));
System.out.println(MessageFormat.format(
"Don''t stop {0}", "Omar"));
System.out.println(MessageFormat.format(
"{0,number,percent} {1,number,integer}", 0.738, 4.5));
String p = "{0,choice,0#an empty cart|1#a single item"
+ "|1<{0,number,integer} items}";
for (int n : new int[] {0, 1, 5}) {
System.out.print(MessageFormat.format(p, n) + "; ");
}
System.out.println();
}
}Output, line by line: Raj meets Mia; Mia thanks Raj; Omar booked 4,321 seats on {2}; Dont stop {0}; Don't stop Omar; 74% 4; an empty cart; a single item; 5 items; . To show a java.time value in a message, format it with a DateTimeFormatter first and pass the String.
Parsing messages
A MessageFormat also works in reverse: parse(String) matches the text against the pattern and returns an Object[] of the parsed arguments, indexed like the placeholders. Plain {n} elements come back as Strings and {n,number} elements as a Long or Double. It declares the checked ParseException, thrown when the text does not fit the pattern (the message is MessageFormat parse error!).
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args)
throws ParseException {
var mf = new MessageFormat(
"{0} owes {1,number}", Locale.UK);
Object[] parts = mf.parse("Omar owes 18.75");
System.out.println(parts[0] + " | " + parts[1] + " "
+ parts[1].getClass().getSimpleName());
}
}This prints Omar | 18.75 Double.
Formatting numbers, currency and percentages with NumberFormat
java.text.NumberFormat is the locale-aware formatter for numbers, and its static factories return a ready-made format for a locale (or for the FORMAT default when no locale is passed). The locale decides the symbols: the US and UK group with a comma and use a decimal point (54,321.6), Germany and the Netherlands do the reverse (5.432,1).
| Factory | Fraction digits | Locale.UK example |
|---|---|---|
getInstance / getNumberInstance (same thing) | 0 to 3; no trailing zeros | 4321.98765 → 4,321.988 |
getIntegerInstance | 0 | 4.5 → 4 |
getCurrencyInstance | The currency's digits (2 for GBP, USD and EUR), always shown | 88.4 → £88.40; -7.5 → -£7.50 |
getPercentInstance | 0 | 0.738 → 74% (the value is multiplied by 100) |
getCompactNumberInstance(locale, Style.SHORT|LONG) | 0 by default | 3,500 → 4K; LONG 7,000,000 → 7 million |
Rounding is HALF_EVEN in every NumberFormat and DecimalFormat: an exact tie goes to the even neighbour, so 4.5 → 4, 5.5 → 6, 0.625 → 0.62 at two digits, and both 3,500 and 4,500 become 4K. Only exact binary ties count (0.625 and 4.25 are exact; many decimal fractions are not). setRoundingMode changes it.
setMinimumFractionDigits(2)pads 7 to7.00but leaves the maximum at 3, so 2.71828 still prints2.718. A minimum above the maximum raises the maximum to match (and vice versa).- Percent formats expect a ratio: passing 40 prints
4,000%. - The UK and US currency formats write negatives with a leading minus, not parentheses (the accounting style is a separate variant).
setCurrency(Currency.getInstance("EUR"))swaps the symbol but keeps the locale's layout, so a UK instance formats 88.4 as€88.40. It does not adjust the fraction digits for you.- Compact SHORT English styles use K, M and B and leave numbers below 1,000 unchanged (
640); aftersetMaximumFractionDigits(1), 2,250 and 2,350 print2.2Kand2.4K.
import java.text.NumberFormat;
import java.util.*;
public class Main {
public static void main(String[] args) {
var uk = Locale.UK;
var nl = Locale.of("nl", "NL");
System.out.println(NumberFormat.getInstance(uk)
.format(4321.98765));
System.out.println(NumberFormat.getInstance(nl)
.format(5432.1));
System.out.println(NumberFormat.getIntegerInstance(uk)
.format(4.5));
System.out.println(NumberFormat.getCurrencyInstance(uk)
.format(-7.5));
System.out.println(NumberFormat.getCurrencyInstance(uk)
.format(0.625));
System.out.println(NumberFormat.getPercentInstance(uk)
.format(0.738));
var shortF = NumberFormat.getCompactNumberInstance(
uk, NumberFormat.Style.SHORT);
var longF = NumberFormat.getCompactNumberInstance(
uk, NumberFormat.Style.LONG);
System.out.println(shortF.format(3_500) + " "
+ shortF.format(4_500) + " " + longF.format(7_000_000));
}
}This prints 4,321.988, 5.432,1, 4, -£7.50, £0.62, 74% and 4K 4K 7 million.
Parsing numbers with NumberFormat.parse
NumberFormat.parse(String) reads the longest number it can from the start of the text using the locale's symbols, ignores whatever follows, and returns a Number: a Long when the value is integral and fits, otherwise a Double. It throws the checked java.text.ParseException only when the text does not begin with a parsable number, so code that calls it must catch or declare ParseException (parseObject declares the same exception).
import java.text.*;
import java.util.Locale;
public class Main {
public static void main(String[] args)
throws ParseException {
var uk = NumberFormat.getInstance(Locale.UK);
var nl = NumberFormat.getInstance(Locale.of("nl", "NL"));
var whole = NumberFormat.getIntegerInstance(Locale.UK);
show(uk.parse("7.75 litres"));
show(uk.parse("8.0"));
show(nl.parse("8,25.1"));
show(whole.parse("99.9"));
show(NumberFormat.getCurrencyInstance(Locale.UK)
.parse("£3.40"));
}
static void show(Number n) {
System.out.println(n + " "
+ n.getClass().getSimpleName());
}
}The program prints 7.75 Double (parsing stops at the space before litres), 8 Long (8.0 is integral), 8.25 Double (in Dutch the comma is the decimal separator and the point after it ends the number), 99 Long (an integer instance stops at the decimal separator; parsing never rounds) and 3.4 Double (a currency instance expects and consumes the symbol). Parsing "3.40" with that currency instance would throw ParseException, because the required £ prefix is missing.
| Exception | Thrown by | Checked? |
|---|---|---|
ParseException | NumberFormat.parse, MessageFormat.parse | Yes |
NumberFormatException | Integer.parseInt, Double.parseDouble | No |
DateTimeParseException | java.time parsing | No |
DecimalFormat patterns and String.format with a Locale
DecimalFormat formats with an explicit pattern, while the locale supplies only the symbols through DecimalFormatSymbols. In the pattern, 0 is a required digit (padded with zeros), # is an optional digit (dropped when zero), , marks the grouping size, . the decimal separator, % multiplies by 100, and a ; introduces a negative subpattern whose prefix and suffix replace the minus sign (its digits are ignored).
| Pattern (UK symbols) | Value | Output |
|---|---|---|
0.00 or #0.00 | 0.7 | 0.70 |
#.## or 0.0# | 0.7 | 0.7 |
00.# | 4.25 | 04.2 (HALF_EVEN) |
#,##0.0;[#,##0.0] | -98765.25 | [98,765.2] |
#,##0.00 with Dutch symbols | 87654.321 | 87.654,32 |
String.format, "…".formatted(…), printf and java.util.Formatter are a separate engine with its own rules. Without a Locale argument they use the FORMAT default (formatted has no locale parameter at all); String.format(Locale.UK, …), printf(Locale.JAPAN, …) and new Formatter(locale) override it. In %,.2f the , flag turns on grouping and .2 is the precision; %,2f is a width of 2 with the default precision of six. %d with a double throws IllegalFormatConversionException. Formatter rounds HALF_UP (6.25 → 6.3 with %.1f), unlike DecimalFormat, which would give 6.2.
String concatenation of a double calls Double.toString, which is never localized.
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
var uk = DecimalFormatSymbols.getInstance(Locale.UK);
var nl = DecimalFormatSymbols.getInstance(
Locale.of("nl", "NL"));
System.out.println(new DecimalFormat("0.00", uk).format(0.7)
+ " " + new DecimalFormat("#.##", uk).format(0.7));
System.out.println(new DecimalFormat("00.#", uk).format(4.25));
System.out.println(new DecimalFormat("#,##0.00", nl)
.format(87654.321));
System.out.println(new DecimalFormat("#,##0.0;[#,##0.0]", uk)
.format(-98765.25));
System.out.println(new DecimalFormat("#%", uk).format(0.738));
Locale.setDefault(Locale.of("nl", "NL"));
double total = 5432.1;
System.out.println(total + " " + "%.1f".formatted(6.25) + " "
+ String.format("%,d", 98765) + " "
+ String.format(Locale.UK, "%,.2f", total));
}
}Output: 0.70 0.7, 04.2, 87.654,32, [98,765.2], 74%, then, with the Netherlands as the default, 5432.1 6,3 98.765 5,432.10.
Formatting and parsing dates and times with DateTimeFormatter
java.time.format.DateTimeFormatter formats and parses java.time values either from a pattern (ofPattern) or from a locale's built-in style (ofLocalizedDate, ofLocalizedTime, ofLocalizedDateTime with a FormatStyle). Formatters are immutable and thread-safe; withLocale returns a copy. ofPattern(p, locale) or .withLocale(locale) decides the language of month and day names; without one, the FORMAT default is used.
Pattern letters
| Letter | Meaning | 21:08 on 4 June 2027, Locale.UK |
|---|---|---|
M / MM / MMM / MMMM | Month: number, padded, short name, full name | 6 / 06 / Jun / June |
m / mm | Minute | 8 / 08 |
d / dd | Day of month (dd requires two digits when parsing) | 4 / 04 |
EEE / EEEE | Day of week | Fri / Friday |
H / h / K | Hour 0–23 / 1–12 / 0–11 | 21 / 9 / 9 |
a | AM/PM marker (its text depends on the locale: UK English prints lower case) | pm |
uuuu / yyyy | Proleptic year / year of era | 2027 |
'from' | Quoted literal text (quotes not printed) | from |
The pattern is just a String, so mistakes compile. Using mm where MM was meant silently prints minutes; asking a LocalDate for an hour or minute (HH, mm) throws UnsupportedTemporalTypeException when formatting.
Localized styles
Each locale has its own text for the four FormatStyles. For Locale.CANADA the date styles of 4 June 2027 are FULL Friday, June 4, 2027, LONG June 4, 2027, MEDIUM Jun 4, 2027 and SHORT 2027-06-04; SHORT is 6/4/27 for Locale.US and 04/06/2027 for Locale.UK, which put day and month in opposite orders. Date-only styles never need a zone, and ofLocalizedDate formats just the date part of a LocalDateTime. The LONG and FULL time styles print a time-zone name, so applying ofLocalizedTime or ofLocalizedDateTime with those styles to a LocalDateTime throws DateTimeException: Unable to extract ZoneId. Use a ZonedDateTime or the SHORT/MEDIUM styles.
Parsing and resolver styles
LocalDate.parse(text) uses ISO_LOCAL_DATE, which needs exactly uuuu-MM-dd ("2027-6-4" fails) and is STRICT. A pattern formatter defaults to SMART, which clamps an out-of-range day to the month's last day; withResolverStyle can change it. All parsing failures are the unchecked DateTimeParseException.
| Resolver style | "2027-04-31" with uuuu-MM-dd |
|---|---|
| SMART (pattern default) | 2027-04-30 |
| STRICT (ISO formatters) | DateTimeParseException: Invalid date 'APRIL 31' |
| LENIENT | 2027-05-01 (extra days roll over) |
With STRICT, use uuuu: yyyy is year-of-era, and without an era field (G) it cannot be resolved, so even a valid date fails. Parsing month names needs the right locale and width: "4 Dezember 2027" parses with d MMMM uuuu and a German locale (Locale.GERMANY or Locale.GERMAN), but not with MMM (which expects the short form Dez.), not with dd (the day has one digit) and not with an English locale.
import java.time.*;
import java.time.format.*;
import java.util.Locale;
public class Main {
public static void main(String[] args) {
var when = LocalDateTime.of(2027, 6, 4, 21, 8);
var f = DateTimeFormatter.ofPattern(
"EEEE dd/MM 'from' hh:mm a", Locale.UK);
System.out.println(when.format(f));
System.out.println(when.format(
DateTimeFormatter.ofPattern("dd/mm/uuuu")));
for (var s : FormatStyle.values()) {
System.out.println(s + ": " + when.format(DateTimeFormatter
.ofLocalizedDate(s).withLocale(Locale.CANADA)));
}
System.out.println(when.format(DateTimeFormatter
.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.US)));
var smart = DateTimeFormatter.ofPattern("uuuu-MM-dd");
System.out.println(LocalDate.parse("2027-04-31", smart));
System.out.println(LocalDate.parse("2027-04-31", smart
.withResolverStyle(ResolverStyle.LENIENT)));
try {
LocalDate.parse("2027-04-31");
} catch (DateTimeParseException e) {
System.out.println(e.getMessage());
}
try {
LocalDate.parse("2027-04-30", DateTimeFormatter
.ofPattern("yyyy-MM-dd")
.withResolverStyle(ResolverStyle.STRICT));
} catch (DateTimeParseException e) {
System.out.println("yyyy STRICT fails");
}
try {
DateTimeFormatter.ofLocalizedTime(FormatStyle.LONG)
.withLocale(Locale.UK).format(when);
} catch (DateTimeException e) {
System.out.println(e.getMessage());
}
}
}Output: Friday 04/06 from 09:08 pm, 04/08/2027 (the minute bug: 8 minutes past, not August), the four Canadian styles, Jun 4, 2027, 2027-04-30, 2027-05-01, the STRICT message, yyyy STRICT fails, and Unable to extract ZoneId from temporal 2027-06-04T21:08.
Tip. Objective 10.1 is examined through short, complete programs, often with the contents of several .properties files or a ListResourceBundle class listed beside them, followed by "What is the result?" or a "Which two…" placeholder to fill. Expect to trace getBundle with a given default locale to the bundle chosen, its getLocale() and each key's value through the parent chain, or to a MissingResourceException or ClassCastException; to follow Locale.setDefault calls in order and decide which category each API reads; to compare how Locale.of, forLanguageTag and Locale.Builder treat unusual input; and to compute exact output of MessageFormat patterns (quotes, missing or repeated arguments, percent, integer and choice types), NumberFormat, currency, percent, compact and DecimalFormat instances where HALF_EVEN ties and locale symbols decide the digits, and String.format with and without a Locale. Parsing items ask for the Number type and value returned, whether ParseException must be handled, and how SMART, STRICT and LENIENT resolve an impossible date. Date-time items hinge on pattern letters (M versus m, h versus H), localized FormatStyles per locale, and the zone that LONG and FULL time styles need.
- Locale.of never validates, forLanguageTag never throws for an ill-formed tag ("de_CH" gives Locale.ROOT), and Locale.Builder throws IllformedLocaleException; toString uses underscores, toLanguageTag hyphens.
- Locale.setDefault(loc) resets the general default and both categories; a category call changes only FORMAT or DISPLAY. Formatters read FORMAT, getDisplay… names read DISPLAY, getBundle's fallback reads the general default.
- getBundle searches the requested locale (most specific first), then the default locale, then the base bundle; any partial match on the requested locale beats the default. A Locale.ROOT request takes the base bundle when it exists and tries the default only when it does not.
- Missing keys resolve up the parent chain (es_MX → es → base); the default locale's files and sibling locales are never parents. getLocale() reports the bundle actually loaded.
- MissingResourceException is unchecked and comes from getBundle (no bundle) or getString/getObject (no key anywhere in the chain).
- A ListResourceBundle class beats a same-named properties file; property values are Strings, and getString on a non-String value throws ClassCastException.
- In MessageFormat a lone apostrophe starts literal text (use two), unmatched placeholders print as {n}, {0,date} cannot format a LocalDate, and parse returns an Object[] or throws the checked ParseException.
- NumberFormat and DecimalFormat round HALF_EVEN (4.5 → 4, 5.5 → 6); java.util.Formatter (String.format, formatted, printf) rounds HALF_UP.
- NumberFormat.parse reads a leading number, returns Long or Double and throws the checked ParseException; java.time parsing throws the unchecked DateTimeParseException.
- Pattern formatters resolve SMART (April 31 → April 30), LocalDate.parse is STRICT, and STRICT needs uuuu; LONG/FULL time styles need a zone.
Frequently asked questions
What is the difference between Locale.toString() and toLanguageTag() in Java?
toString() joins the locale's parts with underscores, such as ja_JP, which is the suffix used in resource bundle names like Menu_ja_JP.properties. toLanguageTag() returns the IETF BCP 47 tag with hyphens, such as ja-JP, which is the form Locale.forLanguageTag accepts. Locale.ROOT prints as an empty string with toString() and as "und" with toLanguageTag().
In what order does ResourceBundle.getBundle look for bundles?
ResourceBundle.getBundle tries the requested locale from most to least specific (for example Menu_es_MX, then Menu_es), then the JVM's general default locale the same way, and finally the base bundle Menu. The first bundle that exists is returned, and missing keys are then looked up in that bundle's parents. If nothing exists at all it throws the unchecked MissingResourceException.
Is the default locale's bundle a parent of the bundle getBundle returns?
No. A bundle's parents are only the same base name with its locale shortened, ending at the base bundle, so Menu_es_MX has parents Menu_es and Menu. The default locale's bundles are used only when no bundle exists for the requested locale; if a key is missing from the chosen bundle's chain, getString throws MissingResourceException even when the default locale's file defines it.
What do the FORMAT and DISPLAY locale categories control?
Locale.Category.FORMAT is the default used to format and parse numbers, currencies, dates, times and messages when no Locale is passed, including NumberFormat factories, MessageFormat.format, DateTimeFormatter.ofPattern and String.format. Locale.Category.DISPLAY is the default for human-readable locale names such as getDisplayCountry(). Locale.setDefault(locale) with one argument overwrites both categories, while setDefault(category, locale) changes only one.
How do you put an apostrophe in a MessageFormat pattern?
Write two single quotes, as in "Don''t stop {0}", which prints one apostrophe and still substitutes the argument. A single quote on its own starts a quoted literal section and is not printed, so everything up to the next quote, including placeholders, appears as plain text; "Don't stop {0}" prints Dont stop {0}. A backslash is not an escape character in MessageFormat.
Why does Java NumberFormat round 4.5 to 4?
NumberFormat and DecimalFormat use RoundingMode.HALF_EVEN by default, which sends an exact tie to the even neighbour: 4.5 becomes 4, 5.5 becomes 6 and 0.625 at two decimals becomes 0.62. Only exactly representable ties are affected. setRoundingMode changes the mode, and java.util.Formatter (String.format, printf) uses HALF_UP instead.
Does NumberFormat.parse throw an exception on extra text?
No. NumberFormat.parse(String) parses the longest number at the start of the text and ignores the rest, so "7.75 litres" returns 7.75 with a UK locale. It returns a Long when the value is integral and fits, otherwise a Double. It throws the checked java.text.ParseException only when the text does not start with a number the format can read.
Why does formatting a LocalDateTime with FormatStyle.FULL throw an exception?
The LONG and FULL time styles of ofLocalizedTime and ofLocalizedDateTime include a time-zone name, and a LocalDateTime has no zone, so formatting throws DateTimeException: Unable to extract ZoneId. Use a ZonedDateTime, the SHORT or MEDIUM styles, or ofLocalizedDate, whose date-only styles never need a zone.
Source
This lesson covers the "Developing Applications with Localization Support" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.
- Java SE 25 Developer Professional (1Z0-831) exam page and topics — Oracle University
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.