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

# ３行で言うと

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

# はじめに

JJUG CCC 2018 fall で Edson Yanaga 氏の「Revisiting Effective Java in 2018」という[セッション](https://jjug-cfp.cfapps.io/submissions/0fb90e6a-1717-4ce9-a2b9-56cef379a131)に出て、地味に印象的だった formatTo についての話を自分なりに整理してみました。

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


# toString メソッド

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

### 概要

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

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

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

参考
[Object#toString](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Object.html#toString)


### 例

```java: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` をオーバーライドしておけば、標準出力するときなどは変数をそのまま渡すだけで整形された文字列を出力できます。

```java: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 と似たようなことができる仕組みです。

```java
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](https://docs.oracle.com/javase/jp/8/docs/api/java/util/Formatter.html) を見て頂きたいですが、とりあえず知っておいてほしいことは書式は次のようなパラメータで指定する、ということだけです。

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

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

```java
//
// 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|


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

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

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

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


### Formattable インターフェース

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

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

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

### 基本的な使い方

```java
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);
  }
}
```


```java: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` メソッドではロケール情報を扱うことが可能です。
たとえば次のようにして多言語対応が可能です。

```java
@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 の引数にロケールを渡して表示を切り替えることも可能です。

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

### 応用例...1

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

```java:元号表記を自動的に切り替える例
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);
    }
  }
}
```
```java
System.out.printf("今年は %3s 30 年です。%n", Gengou.HEISEI); // 今年は 平成 30 年です。
System.out.printf("今年は %1s 30 年です。%n", Gengou.HEISEI); // 今年は H 30 年です。
```

### 応用例...2

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

```java
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());
  }
}
```

```java
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 しておいて良いような気がします。