あまり明示的に書くことが無かったので、MessageFormatを知りませんでした。
この記事では、メール等のタイトルや文言に対して、MessageFormatを使うことで「○○様」というフォーマットから「きり丸様」という文言を導き出すことをゴールとします。
環境
- Java
- 15
要約
- Object型ではなく、String型で設定したほうが良い
- MessageFormatは実行のたびに生成すべき
- 処理速度は犠牲になるが、スレッドセーフではない
使い方
MessageFormatの使い方は大きく2つあります。
静的staticメソッドを使用するMessage.format
、またはnewするnew MessageFormat
です。個人的には、パラメータの扱いやすさの点から前者を使ったほうが好きです。
静的staticメソッドの場合は、第1パラメータに文言のフォーマット、第2パラメータ以降に{0}
等に設定する文言を設定します。
@Test void test_01() { assertThat( MessageFormat.format( "{0}が{1}の時、{2}は必須です。", "契約者", "未成年", "保護者" ) ).isEqualTo("契約者が未成年の時、保護者は必須です。"); } @Test void test_02() { assertThat( new MessageFormat("{0}が{1}の時、{2}は必須です。") .format(new String[]{"契約者", "未成年", "保護者"}) ).isEqualTo("契約者が未成年の時、保護者は必須です。"); }
特殊な使い方
個人的には推奨していません。
Stringではなく、number型やdate型、time型をパラメータに指定すると、フォーマットしてくれます。詳細はMessageFormatのJavaDocを参照してください。
number型の場合、フォーマットを指定しないとデフォルトでは3桁ずつカンマが入ります。
@Test void test_01() { assertThat( MessageFormat.format("{0}", 123456) ).isEqualTo("123,456"); // デフォルトフォーマット } @Test void test_03() { assertThat( MessageFormat.format("{0,number,#.##}", 123456.123456) ).isEqualTo("123456.12"); // 小数点第二位まで表示 }
date型、time型の場合は、アプリケーションを実行しているロケールの影響を受けます。明示的に、実行対象のロケールを指定することでフォーマットを固定することができます。
@Test void test_03() { Date now = Date.from(Instant.ofEpochSecond(1619358874)); MessageFormat mf = new MessageFormat("{0,date} {0,time}"); assertThat( mf.format(new Date[]{now}) ).isEqualTo("2021/04/25 22:54:34"); } @Test void test_04() { Date now = Date.from(Instant.ofEpochSecond(1619358874)); MessageFormat mf = new MessageFormat("{0,date} {0,time}", Locale.ENGLISH); assertThat( mf.format(new Date[]{now}) ).isEqualTo("Apr 25, 2021 10:54:34 PM"); }
String型以外を推奨しない理由
どのような挙動になるかハンドリングしきれないと考えているからです。
小数点以下の丸めの問題がありますので、MessageFormatではなく、きちんと事前に文字列にしておいた方が安全です。テストもしやすいですし。
@Test void test_04() { assertThat( MessageFormat.format("{0,number,#.##}", 123456.999) ).isEqualTo("123457"); // 四捨五入した?切り上げ? // 123457.00 ではない? }
あと、ロケールの影響を受けるようですが、それがどの程度影響受けるのかが調査しきれませんでした。少なくとも、和暦西暦だけでなく仏歴という概念もありますので、ロケールの指定が誤っていると意図しない年次を表示してしまいそうです。
和暦 | 西暦 | 仏歴 |
---|---|---|
平成30年 | 2018年 | 2561年 |
令和元年 | 2019年 | 2562年 |
令和2年 | 2020年 | 2563年 |
令和3年 | 2021年 | 2564年 |
令和4年 | 2022年 | 2565年 |
絶対にやってはいけないこと
MessageFormatは使いまわさないでください。都度都度、staticメソッドか、newしてインスタンスを生成してください。
JavaDocにスレッドセーフではないことが書かれていますので、相当速度に気を使わない場合でもない限り、使いまわしは止めましょう。
JavaDoc引用。
同期 メッセージフォーマットは同期化されません。スレッドごとに別のフォーマットインスタンスを作成することをお勧めします。複数のスレッドがフォーマットに並行してアクセスする場合は、外部的に同期化する必要があります。
もちろん、適切に処理をすることができる腕があれば問題ありません。
ただ、性能が遅いと言っても、1回あたりの実行は1マイクロ秒以内で処理できます。私のローカル環境で1000万回実行すると、事前に生成して使いまわすと2207ミリ秒、都度生成すると9226ミリ秒掛かりましたが、その程度です。
ローカルでスレッドセーフではないことは確認できておりませんので、実際は問題ないのかもしれませんが、無理する必要はないでしょう。
ソースコード
MessageFormatの挙動確認。 https://github.com/hirotoKirimaru/cucumber-sample/blob/61af9de01ebe1cc1459f4206422fd3bea65915ef/src/test/java/kirimaru/biz/domain/MessageFormatTest.java
インスタンス使いまわしと都度生成の性能確認。 https://github.com/hirotoKirimaru/cucumber-sample/blob/61af9de01ebe1cc1459f4206422fd3bea65915ef/src/test/java/kirimaru/biz/domain/MessageFormatTest.java#L131
終わりに
普段使わないので、このクラスがスレッドセーフではないことに後々気付きました。この記事を書かなければ、JavaDocを見返すこともなかったので、スレッドセーフではない処理をリリースする前に気づけて良かったです。
他のMessageFormatについて書かれている記事には、スレッドセーフについて何も書かれていなかったので、性能優先にしちゃっていました。
Java16が出ている今では`DateTimeFormatter
等のスレッドセーフな型を使うことが多いので、完全に考慮から漏れていました。
今後も、ちゃんとJavaDoc見たり、スレッドセーフなアプリケーションを作れるように勉強していきたいです。
この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。
参考
Java 6 のJavaDoc。