В этой статье я хочу рассказать о классе ![Логотип Java: чашка кофе с паром и надпись Core JAVA]()
Полная таблица типов и стилей есть в документации Oracle.
Вывод будет такой:
MessageFormat, который используется в задаче 2412 (task2412).
Класс MessageFormat предназначен для сборки строк по шаблону. Вы пишете текст, оставляя в нем места для подстановки вида {0} и {1}, а класс подставляет туда переданные аргументы и попутно форматирует их: число превращает в число с разделителем разрядов, дату в дату, и все это по правилам текущей локали. Живет он в пакете java.text, и придуман был именно ради интернационализации: шаблон целиком выносится в ресурсы и переводится вместе с порядком слов.
Данный класс Java принимает набор объектов, форматирует их, а затем вставляет форматированные строки в шаблон в соответствующих местах. Это своего рода альтернатива (или даже дополнение) к статическому методу String.format.Кратко
MessageFormatсобирает строку по шаблону, подставляя аргументы в места вида{0},{1},{2}.- В фигурных скобках указывают не только номер аргумента, но и тип форматирования:
number,date,time,choice, а к нему еще и стиль, напримерshortилиinteger. - Работать можно двумя способами: разово через статический
MessageFormat.format()или через объектnew MessageFormat(шаблон), если шаблон нужен много раз. - Один и тот же аргумент разрешается вставить в шаблон несколько раз и каждый раз отформатировать по-своему.
ChoiceFormatвыбирает вариант текста по числовому диапазону, это штатный способ получить правильные формы множественного числа.- Результат зависит от локали, поэтому вывод примеров ниже на другой машине может отличаться.

Статический метод MessageFormat.format
Для начала простой пример использования данного Java класса без создания объекта, а используя статический метод:
int planet = 7;
String event = "a disturbance in the Force";
String result = MessageFormat.format(
"At {0, time, medium} on {0, date}, there was {1} on planet {2, number, integer}.",
new Date(), event, planet);
System.out.println(result);
Здесь вызывается статический метод MessageFormat.format, в который передаются аргументами шаблон строки и, собственно, объекты, которые будут вставлены в места, ограниченные скобками {}. В скобках задается позиция объекта начиная с 0, а также тип форматирования, если таковой имеется.Что можно писать в фигурных скобках
Форма записи такая:{номер}, {номер, тип} или {номер, тип, стиль}. Типов всего четыре:
| Тип | Стили | Чем форматируется |
|---|---|---|
{0}, без типа |
нет | Значение подставляется как есть, через toString() |
number |
integer, currency, percent или свой шаблон |
NumberFormat для текущей локали |
date |
short, medium, long, full или свой шаблон |
DateFormat.getDateInstance() для текущей локали |
time |
short, medium, long, full или свой шаблон |
DateFormat.getTimeInstance() для текущей локали |
choice |
шаблон ChoiceFormat |
ChoiceFormat, выбирает вариант текста по числу |
At 21:25:54 on 28 апр. 2018 г., there was a disturbance in the Force on planet 7.
Локаль решает все
Если вы запустите первый пример у себя, вывод может отличаться от напечатанного выше. Причина в локали. Посмотрите еще раз на таблицу: каждый тип форматирования создает свойFormat «для текущей локали», то есть для той, что стоит в системе по умолчанию. На русских настройках дата напечаталась как 28 апр. 2018 г., а число 1273 во втором примере получит неразрывный пробел между разрядами. На американских настройках те же строки дадут Apr 28, 2018 и 1,273.
Локаль можно не отдавать на волю случая, а задать явно, вторым аргументом конструктора:
MessageFormat form = new MessageFormat(
"At {0, time, short} on {0, date, medium}, there was {1}.",
Locale.US);
System.out.println(form.format(
new Object[]{new Date(), "a disturbance in the Force"}));
Ради этого класс и создавался. В настоящем многоязычном приложении шаблон не пишут в коде, а держат в ResourceBundle: для каждого языка свой файл со своей строкой, а MessageFormat подставит аргументы туда, куда требует порядок слов именно этого языка. Код при этом не меняется вообще.
Отдельно про даты. Типы date и time создают под капотом DateFormat, а он, если верить документации, принимает только Date или число миллисекунд от начала эпохи. Передать туда LocalDate или LocalDateTime из пакета java.time не получится, будет IllegalArgumentException. Поэтому в примерах и стоит старый new Date(). Если у вас в коде java.time, значение придется либо превратить в Date через Date.from(instant), либо заранее отформатировать через DateTimeFormatter и подставить в шаблон уже готовой строкой.
Объект MessageFormat вместо статического вызова
В следующем примере уже создается объект классаMessageFormat:
int fileCount = 1273;
String diskName = "MyDisk";
Object[] testArgs = {fileCount, diskName};
MessageFormat form = new MessageFormat(
"The disk \"{1}\" contains {0} file(s).");
System.out.println(form.format(testArgs));
При создании объекта класса MessageFormat в его конструктор передается шаблон строки. Далее, при вызове метода format у объекта, туда в качестве аргумента передается массив объектов, которые будут вставлены в шаблон строки.
Вывод будет такой:
The disk "MyDisk" contains 1 273 file(s).
Выбор текста по числу: ChoiceFormat
Также возможно сделать так, что в зависимости от значения переменной будет выбираться необходимый текст. Своего рода реализация оператораif...else, только с помощью класса ChoiceFormat.
Вот код:
MessageFormat form = new MessageFormat("Я могу {1} {0}.");
int count = 2;
String exercise = "подтянуться";
Object[] testArgs = {count, exercise};
double[] filelimits = {0,2,5};
String[] filepart = {"{0} раз","{0} раза","{0} раз"};
ChoiceFormat fileform = new ChoiceFormat(filelimits, filepart);
form.setFormatByArgumentIndex(0, fileform);
System.out.println(form.format(testArgs));
Данный код в зависимости от значения переменной count меняет вывод строки следующим образом:
если
count = 1вывод будет такойЯ могу подтянуться 1 раз.если
count = 2вывод будет такойЯ могу подтянуться 2 раза.если
count = 7вывод будет такойЯ могу подтянуться 7 раз.
double[] filelimits, в котором указываются пределы значений, при которых будет меняться вывод строк. А в массиве String[] filepart указываются те самые варианты строк, которые могут быть использованы.
Условия выбора варианта строки от значения переменной определяются следующим образом: выберется вариант filepart[j], если filelimits[j] <= count < filelimits[j+1].
Далее создается объект ChoiceFormat fileform и в его конструктор передаются массивы double[] filelimits и String[] filepart.
С помощью метода form.setFormatByArgumentIndex(0, fileform) мы говорим объекту MessageFormat form, что когда вызовут метод format, то для индекса 0 в шаблоне строки используй форматирование, которое было задано в объекте ChoiceFormat fileform.Свой формат для каждого места подстановки: setFormats
Хорошо. У вас уже в принципе могла появиться мысль никогда не использовать Java классMessageFormat, но знайте, что это еще не все примочки.
Имеется вот такой код:
MessageFormat pattform = new MessageFormat("There {0} on {1}.\n{2} {2}");
int count = 0;
Date date = new Date();
Object[] testArgs = {count, "ADisk", date, date};
double[] filelimits = {0,1,2};
String[] filepart = {"are no files","is one file","are {0} files"};
ChoiceFormat fileform = new ChoiceFormat(filelimits, filepart);
Format[] testFormats
= {fileform, null, DateFormat.getDateInstance(DateFormat.SHORT), DateFormat.getTimeInstance(DateFormat.SHORT)};
pattform.setFormats(testFormats);
System.out.println(pattform.format(testArgs));
Здесь основная фишка в массиве Format[] testFormats.
В данный массив мы заносим объекты (реализующие абстрактный класс Format), которые отвечают за форматирование объектов, определенных в массиве Object[] testArgs. Если объект не нуждается в форматировании, то в массиве Format[] testFormats записывается null.
Далее с помощью метода pattform.setFormats(testFormats) мы говорим объекту MessageFormat pattform, какое форматирование применять к местам подстановки в шаблоне строки. Важная деталь: этот метод раздает форматы по порядку мест подстановки в шаблоне, а не по номерам аргументов. В шаблоне "There {0} on {1}.\n{2} {2}" таких мест четыре, поэтому и в массиве Format[] testFormats четыре элемента, хотя разных аргументов всего три: {2} встречается дважды и форматируется двумя разными способами, сначала как дата, потом как время.
Для данного примера вывод будет такой:
There are no files on ADisk.
28.04.18 22:10
И совет напоследок. Документация Oracle рекомендует вместо setFormats брать метод setFormatsByArgumentIndex: порядок мест подстановки в шаблоне при переводе на другой язык почти всегда меняется, а номера аргументов остаются прежними. Тот же пример через него выглядел бы так:
Format[] testFormats
= {fileform, null, DateFormat.getDateInstance(DateFormat.SHORT)};
pattform.setFormatsByArgumentIndex(testFormats);
Теперь элементы массива соответствуют аргументам: нулевому, первому и второму. За удобство приходится платить: оба вхождения {2} получат один и тот же формат, и развести дату и время, как в примере выше, уже не выйдет. Так что setFormats тоже нужен, просто применять его стоит осознанно.
На этом все, и я надеюсь, что при решении задачи 2412 у вас не возникнет сложностей.Вопросы и ответы
Чем MessageFormat отличается от String.format?
Разметкой шаблона и назначением. ВString.format место подстановки задается типом значения (%s, %d) и заполняется строго по порядку аргументов. В MessageFormat место подстановки задается номером аргумента, поэтому при переводе на другой язык порядок слов в предложении можно менять, не трогая код. Плюс у MessageFormat есть choice для форм множественного числа, чего в String.format нет.
Как вставить в шаблон фигурную скобку или апостроф?
Через одинарные кавычки. Пара одинарных кавычек экранирует любые символы: шаблон'{0}' выдаст текст {0}, а не подстановку. Сам апостроф записывается двумя одинарными кавычками подряд. Это главная ловушка класса: строка вида Don't {0} сломается, потому что апостроф в слове откроет экранирование, и {0} останется в тексте как есть. Писать надо Don''t {0}.
Можно ли использовать один аргумент в шаблоне несколько раз?
Да, и это одно из главных отличий отString.format. Номер в скобках это индекс в массиве аргументов, а не порядковый номер подстановки, поэтому {2} можно написать хоть пять раз. Более того, каждое вхождение можно отформатировать по-своему: в последнем примере статьи один и тот же объект Date выводится сначала как дата, потом как время.
Как получить правильные формы множественного числа?
Для этого и нуженChoiceFormat: вы задаете границы диапазонов и вариант текста для каждого. Учтите ограничение: выбор идет по числовым границам, а не по правилам языка. Схема из примера выше верна для 1, 2, 5 и 7, но на 22 она даст «22 раз» вместо «22 раза», потому что 22 попадает в последний диапазон. Для полностью правильных русских форм границы приходится расписывать подробнее или брать библиотеку с поддержкой правил ICU.
Работает ли MessageFormat с LocalDate и LocalDateTime?
Напрямую нет. Типdate в шаблоне создает DateFormat, а он, по документации, принимает только Date или число миллисекунд от начала эпохи. Передадите LocalDate, получите IllegalArgumentException. Обходных путей два: превратить значение в Date через Date.from(instant) либо заранее отформатировать его через DateTimeFormatter и подставить в шаблон уже готовой строкой.
Потокобезопасен ли MessageFormat?
Нет. Документация говорит прямо: объектыMessageFormat не синхронизированы, и для каждого потока рекомендуется создавать свой экземпляр, а при общем доступе синхронизировать его самостоятельно. Держать один объект в статическом поле и звать из нескольких потоков нельзя. Статический MessageFormat.format() этой проблемы не имеет: он создает новый объект на каждый вызов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ