torutkのブログ

ソフトウェア・エンジニアのブログ

printf様式の書式化で's'変換指示子を自前のクラスに指定し独自の書式化を実現する〜java.util.Formattableの実装

Java Advent Calendar 2011 : ATNDの一環で、第32日目のエントリです。

Javaには、C言語のprintf関数風に文字列書式化機能が備わっています。intやdoubleなどのプリミティブ型に関する書式化のほかに、オブジェクト型については、%s変換が用意されています。文字列と兼用の変換で、デフォルトではtoString()メソッドの戻り値文字列が表示されるだけという簡素なものです。

Javadoc APIから%sに関する説明を以下に抜粋します。

s、S 一般 引数 arg が null の場合、結果は null になります。arg が Formattable を実装する場合に、arg.formatTo が呼び出されます。そうでない場合、結果は arg.toString() の呼び出しで取得されます。

温度(Temperature)クラスを題材に、この%s変換の実験をしてみます。
フラグを用いて、以下の出力ができるような実装を行います。

機能 変換子
摂氏 %s
華氏 %#s
ケルビン %S

toStringをオーバーライドしないクラスの%s変換

ごく単純な温度クラスを作成します。

public class Temperature {
    private double kelvin;

    public Temperature(double value) {
        kelvin = value;
    }

    public double getValue() {
        return kelvin;
    }
}

この温度クラスをformat(System.out.printf)に渡す単純なMainクラスを作成します。

public class Main {
    public static void main(String[] args) {
        Temperature temperature = new Temperature(273.15);
        System.out.printf("水の凝固点は%n");
        System.out.printf("摂氏 %s°%n", temperature);
        System.out.printf("華氏 %#s°%n", temperature);
        System.out.printf("ケルビン %SK%n", temperature);
    }
}

これを実行すると

$ java Main
水の凝固点は
摂氏 Temperature@156ee8e°
華氏 Temperature@156ee8e°
ケルビン Temperature@156ee8eK 
$

となります。TemperatureクラスでtoStringをオーバーライドしないので、ObjectクラスのtoStringが返却するクラス名@ハッシュ値文字列が、そのまま%s変換として適用されています。

toStringをオーバーライドしたクラスの%s変換

温度クラス(Temperature)にtoStringメソッドをオーバーライドします。

    @Override
    public String toString() {
        return String.valueOf(kelvin);
    }

実行結果は

水の凝固点は
摂氏 273.15°
華氏 273.15°
ケルビン 273.15K  

となります。toStringメソッドは常に同じ文字列を返すので、これは意図には沿わないものの当然の結果です。

Formattableの実装

ここから本題のFormattable実装に入ります。

最低限のFormattable実装

TemperatureクラスにFormattableインタフェースを実装します。まずは、最低限の実装をしてみます。書式のフラグによる制御は無視したコードです。

import java.util.Formattable;
import java.util.Formatter;

public class Temperature implements Formattable {
    private double kelvin;

    public Temperature(double value) {
        kelvin = value;
    }

    public double getValue() {
        return kelvin;
    }

    @Override
    public void formatTo(Formatter formatter, int flags, int width, int precision) {
        formatter.format("%f", kelvin);
    }
}

Formattableを実装すると、メソッドformatToをオーバーライドする必要があります。出力(生成)する文字列は、引数formatterにセットします。

  1. formatterのformatメソッドで文字列をセットする
  2. formatterからAppendableを取り出し、文字列をセット(append)していく

この記事では、1.のformatメソッドを使う方法で実装しています。

これを先ほどのMainから実行すると

水の凝固点は
摂氏 273.150000°
華氏 273.150000°
ケルビン 273.150000K 

となります。

フラグに基づく制御を実装

摂氏/華氏/ケルビンの制御をします。

まず、Temperatureクラスに摂氏・華氏の値を取り出すメソッドを追加します。

    public double getCelsiusValue() {
        return kelvin - 273.15;
    }

    public double getFahrenheitValue() {
        return 9 * getCelsiusValue() / 5 + 32;
    }

続いて、TemperatureクラスのformatToメソッドをフラグに基づき生成する文字列を変える実装に修正します。

import java.util.FormattableFlags;

    @Override
    public void formatTo(Formatter formatter, int flags, int width, int precision) {
        System.out.printf("flags:%x%n", flags);
        if ((flags & FormattableFlags.ALTERNATE) != 0) {
            formatter.format("%f", getFahrenheitValue());
        } else if ((flags & FormattableFlags.UPPERCASE) != 0) {
            formatter.format("%f", kelvin);
        } else {
            formatter.format("%f", getCelsiusValue());
        }
    }

修正にあたって、import文を一つ追加します。
第2引数flagsは、%s変換に付与したフラグに応じた値がビットマップで設定されているので、ビットマスクで判別しています。

実行結果は次のとおりです。

水の凝固点は
摂氏 0.000000°
華氏 32.000000°
ケルビン 273.150000K 
さらなる応用

formatToメソッドの第3、第4引数であるwidth, precisionを、doubleに対する指定と同じように使用するようformatToをさらに修正します。

    @Override
    public void formatTo(Formatter formatter, int flags, int width, int precision) {
        String format = "%f";
        if (width > 0 && precision > 0) {
            format = String.format("%%%d.%df", width, precision);
        } else if (width > 0) {
            format = String.format("%%%df", width);
        } else if (precision > 0) {
            format = String.format("%%.%df", precision);
        }

        if ((flags & FormattableFlags.ALTERNATE) != 0) {
            formatter.format(format, getFahrenheitValue());
        } else if ((flags & FormattableFlags.UPPERCASE) != 0) {
            formatter.format(format, kelvin);
        } else {
            formatter.format(format, getCelsiusValue());
        }
    }
最後に

晦日から家族で実家に泊まっており、そこは非インターネット接続環境なので、ブログどうするかと思っていましたが、年末に携帯電話機をKDDI回線のEVO 3Dに変更していたので、テザリングで無事ブログの作成ができました。