Javaにおける日時の文字列化

Javaで日時を表示する簡単なお仕事も、時と場合でパフォーマンスを考慮したほうが良いよ、というお話。

思いのほか前の記事の参照数が多いので、次はJavaで思い当たることを書いてみます。

Javaで日時を表示のために文字列化するのはSimpleDateFormatが強力なので非常に簡単です。関数化する必要もないほどですがちょっと書いてみましょう。

public static String toString(Date d) {
  SimpleDateFormat f = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
  return f.format(d);
}

SimpleDateFormatをstaticにして使いまわしたほうが良いとかはありますが、この記事がフォーカスするのは「そもそもSimpleDateFormatを使わないほうが良い場合がある」というほうです。

SimpleDateFormatを使わないほうが良いというのは、format()メソッドの内側で生成されるオブジェクトがあまりに多いことに拠ります。format()の内側では正規表現オブジェクトやそのたのオブジェクトが大量に生成されます。そのためそもそもリソース制約の厳しいAndroidや、多数のリクエストを処理するサーバ用途、何度も呼ばれるログフォーマッタなどで使うと頻繁にGCが起き、目に見えて動作速度が落ちたりします。特にAndroidはGCが発生するとUIの反応が鈍るため、昔に比べてGCが格段に速くなったとはいえ、ヌルサクが好まれる傾向があるので深刻に捉えて良いでしょう。

さて、ではそれを考慮した版をStringBuilderとCalendar使うだけですが書いてみましょう。

public static String toString(Date d) {
  StringBuilder s = new StringBuilder();
  Calendar c = Calnedar.getInstance(c);
  c.setTime(d);
  s.append(c.get(Calendar.YEAR)).append('/')
    .append(c.get(Calendar.MONTH) + 1).append('/')
    .append(c.get(Calendar.DAY_OF_MONTH)).append(' ')
    .append(c.get(Calendar.HOUR_OF_DAY)).append(':')
    .append(c.get(Calendar.MINUTE)).append(':')
    .append(c.get(Calendar.SECOND));
  return s.toString();
}

%02d なフォーマットをかけないと同じ動作とは言えませんが、ソースコードを簡略化するために上では省略してます。ただ書くならこんな感じで書くことになるでしょう。

public StringBuilder format_02d(StringBuilder s, int n) {
  if (n < 10) {
    s.append('0');
  }
  return s.append(n);
}

なおSimpleDateFormatの実装はJDKのsrc.zipに入っていますので、具体的になにがどう無駄になってるかを見てみるのも良いかもしれません。

こういう問題はだいたいパフォーマンスの問題として現れ、JvisualVMとか使って詳しく調査してみてこれが原因だったと判明したりします。仮に納品直前のQAや納品後に見つかって指摘されるのはかなりハズカシイですから、こういう視点を持って試験したりあらかじめ対処しておくと良いのではないでしょうか。Androidの話ですが、実際に納品されたコードの動作があまりに遅いので調査をしたら原因でコレで、こんな感じで書き直したら10倍近くも速くなったとか…そういう話もなくはないのです。