きり丸の技術日記

技術・エンジニアのイベント・資格等はこちらにまとめる予定です

Javaで○○様等の○○を埋める(MessageFormat)

あまり明示的に書くことが無かったので、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。

JavaDoc

f:id:nainaistar:20210429003458p:plain