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進数表現
として定義されていますが、あまり扱いやすいものではないので、サブクラスでオーバーライドすることが推奨されています。
例
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
をオーバーライドしておけば、標準出力するときなどは変数をそのまま渡すだけで整形された文字列を出力できます。
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 を見て頂きたいですが、とりあえず知っておいてほしいことは書式は次のようなパラメータで指定する、ということだけです。
%[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 インターフェース
次のメソッドが定義されたインターフェース。
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);
}
}
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 しておいて良いような気がします。