LoginSignup
3
5

More than 5 years have passed since last update.

クラスを定義するときは toString だけでなく formatTo を書こう(Formattable の使い方)

Posted at

3行で言うと

  • クラスを作成するときは toString()formatTo() を定義しておくと便利
  • toString にはデバッグ用に開発者目線でわかりやすいフォーマットを
  • formatTo にはユーザ目線でのわかりやすいフォーマットを

はじめに

JJUG CCC 2018 fall で Edson Yanaga 氏の「Revisiting Effective Java in 2018」というセッションに出て、地味に印象的だった formatTo についての話を自分なりに整理してみました。

セッションは Effective Java 第3版+α のノウハウについてライブコーディングしつつ語る、というようなもので非常に面白かったです。中でも formatTo については「java.lang パッケージにそんな便利なものがあったのか」と驚いたんですが、Qiita で検索してもほとんど記事が無いので自分で書いてみようと思った次第です。

toString メソッド

まずはおなじみ toString メソッド。

概要

オブジェクトの文字列表現を返すメソッド。
以下のような場面で呼ばれます。

  • 文字列とオブジェクトを結合したとき
  • System.out.println() など、文字列として出力するメソッドに渡したとき
  • デバッガで変数を表示させたとき

Object クラスでは クラス名 + "@" + ハッシュ値の16進数表現 として定義されていますが、あまり扱いやすいものではないので、サブクラスでオーバーライドすることが推奨されています。

参考
Object#toString

toStringをオーバーライド
class Person {
  private String name;
  private int age;

  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  @Override
  public String toString() {
    return name + ", " + age + " years old.";
  }
}

こんなふうに Object#toString をオーバーライドしておけば、標準出力するときなどは変数をそのまま渡すだけで整形された文字列を出力できます。

toStringの利用例
Person marty = new Person("Marty", 17);
Person doc = new Person("Doc", 65);

System.out.println(marty); // Marty, 17 years old.
System.out.println(doc); // Doc, 65 years old.

toString を使うメリット

  • getter を作成する必要がない(フィールドを外部に公開する必要がない)
  • オブジェクトを文字列化する、という処理を隠蔽できる

特に、具体的なデータの持ち方に依存せず、オブジェクト自身がどのように文字列化すべきかを知っている、というのは非常に「オブジェクト指向的」だと思います。

toString で困ること

  • どのような場所でも同じフォーマットで文字列化される
  • ユーザー出力としては使わないけどデバッグ時に見れると便利な private 変数などが出力できない

上記の例で言えば、デバッガやログ出力でもすべて Marty, 17 years old. という形で出力されてしまいます。

ユーザー向けの出力は toString を使いたいけど、デバッグにはもっと細かい情報が欲しい。
そこで formatTo メソッドの出番です。

formatTo

formatTo の前に書式指定文字列について

書式指定文字列

C言語で言うところの printf や sprintf と似たようなことができる仕組みです。

System.out.printf("%s, %d years old.%n", "Marty", 17); // --> Marty, 17 years old.
String str = String.format("%s, %d years old.", "Marty", 17); // --> "Marty, 17 years old." という文字列が生成される

書式の指定方法については結構複雑なので Formatter クラスの Javadoc を見て頂きたいですが、とりあえず知っておいてほしいことは書式は次のようなパラメータで指定する、ということだけです。

Formatterの書式指定方法
 %[argument_index$][flags][width][.precision]conversion

それぞれの意味は次のようになります。
argument_index は今回関係ないので省略します

//
// conversion(引数の変換方法。整数値は %d、実数値は %f というように型に応じた変換を指定)
//
String.format("%d", 12); // ---> "12"
System.out.printf("%f", Math.PI); // ---> 3.141593

//
// width (最小文字数、足りない分はスペースで埋められる)
//
String.format("%5d", 12); // ---> "   12"

//
// flag (-, +, # など、文字通り書式設定のオプション制御のためのフラグ)
//
// マイナスを付けると左揃えに
String.format("%-5d", 12); // ---> "12   "
// 0を付けるとスペースではなくゼロ埋め
String.format("%05d", 12); // ---> "00012"
// 先頭に+を付けると符号を表示
System.out.printf("%+5d", 12); // ---> "+12"
System.out.printf("%+5d", -12); // ---> "-12"

//
// precision(基本的には最大文字数という意味合いで使われる)
//
// 実数値の場合は小数点以下桁数(この場合は「小数点以下の最大文字数」という扱いのようです)
System.out.printf("%4.2f", Math.PI); // ---> 3.14

たとえば %-4.2f であれば、それぞれ次のような値になります。

conversion flag width precision
f - 4 2

引数にオブジェクトを渡した場合の処理

Person doc = new Person("Doc", 65);
String.format("%30s", doc);
String.format("%30s", doc.toString()); // これと同じ出力になる

このように、引数にオブジェクトを渡した場合は、そのクラスの toString メソッドが呼ばれて、その結果が文字列として扱われます。

しかし、引数に渡したクラスが Formattable インターフェースを実装していると、toString の結果そのままではなく、出力方法を制御することが可能になります。

Formattable インターフェース

次のメソッドが定義されたインターフェース。

Formattableインターフェース
void formatTo(Formatter formatter,
            int flags,
            int width,
            int precision)

このインターフェースを実装したクラスのインスタンスを printf 系メソッドに渡すと、対応する箇所の書式設定のために formatTo メソッドが呼ばれます。

基本的な使い方

class Person implements Formattable {
  private String name;
  private int age;

  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  @Override
  public String toString() {
    return "{name=" + name + ",age=" + age + "}";
  }

  @Override
  public void formatTo(Formatter formatter, int flags, int width, int precision) {
    // formatTo メソッド内で formatter を操作することで書式操作を行います
    // 使い方は String#format と同じです
    formatter.format("%s, %d years old.", name, age);
  }
}
formatToを実装したクラスの利用例
Person marty = new Person("Marty", 17);
Person doc = new Person("Doc", 65);

System.out.printf("%s%n", marty); // Marty, 17 years old.
System.out.printf("%s%n", doc); // Doc, 65 years old.

System.out.println(marty); // {name=Marty,age=17}
System.out.println(doc); // {name=Doc,age=65}

このように、ユーザー向けの出力は formatTo メソッドに任せることで、デバッグ用の出力は開発者目線で使いやすいように自由に扱うことが可能になります。

多言語対応

さらに便利なことに、formatTo メソッドではロケール情報を扱うことが可能です。
たとえば次のようにして多言語対応が可能です。

@Override
public void formatTo(Formatter formatter, int flags, int width, int precision) {
  if (formatter.locale() == Locale.JAPAN) {
    formatter.format("%s は %d 歳です。", name, age);
  } else {
    formatter.format("%s, %d years old.", name, age);
  }
}

デフォルトではシステムロケールになりますが、printf の引数にロケールを渡して表示を切り替えることも可能です。

System.out.printf("%s%n", doc); // Doc は 65 歳です。
System.out.printf(Locale.ENGLISH, "%s%n", doc); // Doc, 65 years old.

応用例...1

formatTo の引数に表示幅が渡されるので、出力する幅によって自動的に切り替えることなどが可能です。

元号表記を自動的に切り替える例
private enum Gengou implements Formattable {
  MEIJI("明治", 'M'), TAISHO("大正", 'T'), SHOWA("昭和", 'S'), HEISEI("平成", 'H');

  private String name;
  private char initialLetter;

  private Gengou(String japaneseName, char initial) {
    this.name = japaneseName;
    this.initialLetter = initial;
  }

  /**
   * 表示幅が2文字以上ある場合は正式名称、1文字の場合はアルファベット1文字で出力する
   */
  @Override
  public void formatTo(Formatter formatter, int flags, int width, int precision) {
    if (width >= 2) {
      formatter.format("%s", name);
    } else {
      formatter.format("%s", initialLetter);
    }
  }
}
System.out.printf("今年は %3s 30 年です。%n", Gengou.HEISEI); // 今年は 平成 30 年です。
System.out.printf("今年は %1s 30 年です。%n", Gengou.HEISEI); // 今年は H 30 年です。

応用例...2

formatTo の引数の扱い方は自由なので、precision を省略記号の文字数、width を全体の文字数として、段落(Paragraph)を表すクラスの出力を制御することなども考えられます。

private static class Paragraph implements Formattable {
  private String content;

  public Paragraph(String content) {
    this.content = content;
  }

  @Override
  public void formatTo(Formatter formatter, int flags, int width, int precision) {
    StringBuilder sb = new StringBuilder();

    if (content.length() < width) {
      sb.append(content);
    } else {
      sb.append(content.substring(0, width - precision));

      if (precision > 0) {
        for (int i = 0; i < precision; i++) {
          sb.append(".");
        }
      }
    }

    formatter.format(sb.toString());
  }
}
Paragraph p = new Paragraph("This is very very long text!");
System.out.printf("%15.3s%n", p); // This is very...
System.out.printf("%10.4s%n", p); // This i....

使いどころ

記事を書いていて気づきましたが、例にあげた Person クラスのように出力が文章になるようなものの場合は、メッセージ成形用のクラスを別途作成したほうがシンプルだと思います。

逆に「金額」や「ユーザーID」などのように、それ単体が値としての意味を持つ、バリューオブジェクト的なクラスの場合は常に Formattable を implement しておいて良いような気がします。

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5