Go to Contents Go to Java Page
J2SE 1.5 虎の穴
 
 

C 言語スタイルのフォーマット Formatter

 
 
Tiger Merline の宿題
 
 

C 言語を使われている方ならば、printf の恩恵は少なからず受けているはずです。printf を使ったフォーマットはとても楽チンにもかかわらず、かなり強力です。

Java で printf のようなフォーマットを行おうとしたら、java.text.MessageFormat クラスを使用するしかありません。しかし、このクラスが使いやすいとはお世辞にもいえません。Tiger になって可変長引数が使えるようになったので、少しは改善しましたが、やっぱり使いにくいことは変わりません。

やっぱり printf だよねということで、JSR-51 の New I/O では C 言語の printf/scanf 相当のフォーマッティングを扱えることが目的の 1 つにあげられていました。

ところが、New I/O が導入された J2SE 1.4 Merline ではこの項目だけペンディングになってしまったのです。そして、やっと Tiger で取り入れられることになりました。

しかし、まだ油断はできません。これについては 2003 年の JavaOne の BOF で発表されているのですが、その内容と J2SE 1.5 での内容はかなり食い違いがあります。それだけでなく、alpha, beta1, beta2 の間でもずいぶん変わってきているのです。

早くフィックスしてくれないかなぁ。

 

 
 
Tiger まずは printf でも
 
 

さっそく使ってみましょう。別に難しいことはいりません。単に printf メソッドを実行するだけです。

サンプルのソース FormatSample.java

printf というメソッドが追加されたのですが、同じように format メソッドも追加されているので、これも使っています。

public class FormatSample {
    public static void main(String[] args) {
        int i = 100;
        double x = 52.3;
        String a = "ABC";
  
        System.out.printf("%d %5.2f %s\n", i, x, a);
 
        // format メソッドでも同じ結果になる
        System.out.format("%d %5.2f %s\n", i, x, a);
    }
}

実行してみましょう。

C:\examples>java FormatSample
100 52.30 ABC
100 52.30 ABC
 
C:\examples>

System.out は java.io.PrintStream クラスなので、このクラスの printf メソッドの定義を見てみましょう。printf メソッドは内部で format メソッドを呼び出しています。

    public PrintStream printf(String format, Object ... args) {
        return format(format, args);
    }
        
    public PrintStream format(String format, Object ... args) {
        try {
            synchronized (this) {
                ensureOpen();
                if ((formatter == null) 
                    || (formatter.locale() != Locale.getDefault()))
                    formatter = new Formatter((Appendable) this);
                formatter.format(Locale.getDefault(), format, args);
                if (autoFlush)
                    out.flush();
            }
        } catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        } catch (IOException x) {
            trouble = true;
        }
        return this;
    }

重要なのは赤い字のところです。format メソッドは Formatter オブジェクトを生成して、その format メソッドをコールしていることが分かります。

この java.util.Formatter クラスが C 言語の printf 相当のフォーマットを行う立役者なのです。

 

 
 
Tiger Format クラスを使ってみよう
 
 

上記のコードでは Formatter クラスのインスタンス生成には PrintStream オブジェクトを引数にしていました。JavaDoc を見てみると、フォーマットした出力先を引数にできるようです。その出力先として、Appendable インタフェース、File クラス、OutputStream クラス、ファイル名があります。

そのほかにロケールや文字コードも指定できます。

何も引数がない場合や、Appendable インスタンスが null の時には StringBuilder オブジェクトを生成して使用するようです。

生成してしまったら、後は printf のように使うだけです。ただし、メソッド名は printf ではなくて format です。

サンプルのソース FormatterTest1.java

Appendable インタフェースをインプリメントしているクラスとしてここでは StringBuilder クラスを使用しました。

import java.util.Formatter;
 
public class FormatterTest1 {
    public static void main(String[] args) {
        StringBuilder builder = new StringBuilder();
        Formatter formatter = new Formatter(builder);
 
        int i = 100;
        double x = 26.0;
        String text = "Text";
        formatter.format("Interger %d  Double %5.1f String %s", i, x, text);
 
        System.out.println(formatter.out());
    }
}

PrintStream クラスなどのように出力先があればいいのですが、StringBuilder クラスにはないので out メソッドを用いて、StringBuilder オブジェクトを取り出し、それを出力しています。

Formatter クラスには toString メソッドも定義されているので、System.out.println(formatter); としても同じ結果が得られます。

 

 
 
Tiger フォーマットのやり方
 
 

Formatter クラスが使えるフォーマット記述子は基本的には C の printf と同じですが、拡張も行われています。拡張された部分としては時間に関するフォーマットと引数の順番を指定するものなどがあります。

フォーマットは次のように記述します。

%[argument_index$][flags][width][.precision]conversion
  argument_index 引数のインデックス
  flags 符号などを表示するかを決めるフラグ
  width 表示幅
  precision 精度 浮動小数点数の場合、小数点以下の表示幅
  conversion 変換記述子

argument_index というのが拡張された部分です。たとえば %1$d と書けば、1 番目の引数ということになります。サンプルを見てもらうのが一番手っ取り早いですね。

サンプルのソース FormatterTest2.java

argument_index を使用することで引数の順番と、表示の順番を変えることができます。また、繰り返し使うこともできます。

import java.util.Formatter;

public class FormatterTest2 {
    public static void main(String[] args) {
        StringBuilder builder = new StringBuilder();
        Formatter formatter = new Formatter(builder);
 
        int i1 = 100;
        int i2 = 200;
        int i3 = 300;
        formatter.format("i3 = %3$d  i2 = %2$d i1 = %1d i2 = %2$d\n", i1, i2, i3);
 
        System.out.println(formatter.out());
    }
}

実行すると次のようになります。

C:\examples>java FormatterTest2
i3 = 300  i2 = 200 i1 = 100 i2 = 200
 
C:\examples>

flag 以下の書き方は C の printf と同じです。

変換記述子は次に示すカテゴリに分けられています。

カテゴリ 説明 適用される型
一般 すべての型  
文字 Unicode 文字として扱えるもの char, Character, byte, Byte, short, Short
int, Integer は Character.isValidCodePoint(int) の結果が true の場合のみ
数字 整数、もしくは浮動小数点数 byte, Byte, short, Short, int, Integer, long, Long, BigInteger,
float, Float, double, Double, BIgDecimal
時間 時間として扱えるもの long, Long, Calendar, Date
long は 1970/1/1 00:00:00 GMT からのミリ秒として扱われる

それぞれ、次のような文字を使用することができます。大文字と小文字は区別されます。

カテゴリ 記述子 説明
一般 b, B boolean/Boolean の場合は値 (true or false)
null の場合は "false"
その他は "true"
h, H integer/Integer の場合は値
null は "null"
その他はI nteger.toHexString(arg.hashCode()) の結果
s, S

null は "null"
引数が Formatterble ならば arg.formatTo()
それ以外は arg.toString()

文字 c, C Unicode の文字
数字 d 整数
o 8 進数
x, X 16 進数
e, E 10 のべき乗表現 (1.5e1 など)
f 浮動小数点表示
g, G 数字が大きい場合は e、小さい場合は f と同じ
a, A 16 進数による、2 のべき乗表現 (2.5 = 0x1.4p1 など)
パーセント % パーセント
行末記号 n 行末記号
時間 tH, TH 時間 (00 - 23) 必要に応じて 0 が付加される
tI, TI 時間 (01 - 12) 必要に応じて 0 が付加される
tk, Tk 時間 (0 - 23) 0 は付加されない
tl, Tl 時間 (1 - 12) 0 は付加されない
tM, TM 分 (00 - 59) 必要に応じて 0 が付加される
tS, TS 秒 (00 - 60) 必要に応じて 0 が付加される
tL, TL ミリ秒 (000 - 999) 必要に応じて 0 が付加される
tN, TN ナノ秒 (000000000 - 999999999) 必要に応じて 0 が付加される
tp, Tp 午前・午後の表記(小文字) am, pm など ロケールに依存する 日本では 午前/午後
tP, TP 午前・午後の表記(大文字) AM, PM など ロケールに依存する 日本では 午前/午後
tz, Tz GMT からのタイムゾーンのオフセット 日本なら +0900
tZ, TZ タイムゾーンを省略形で示したもの 日本なら JST
ts, Ts 1970/1/1 00:00:00 UTC からの秒
tQ, TQ 1970/1/1 00:00:00 UTC からのミリ秒
tB, TB 月の表示 US では "January" などだが、日本では "1月"
tb, Tb 月表示の省略形 US では "Jan"などだが、日本では "1" のように数字だけ
th, TH tb と同じ
tA, TA 曜日の表示 US では "Monday" 日本では "月曜日"
ta, Ta 曜日の省略形 US では "Mon" 日本では "月"
tC, TC 年を上 2 桁で表したもの (00 - 99) 必要に応じて 0 が付加される
tY, TY 年を 4 桁で表したもの 必要に応じて 0 が付加される
ty, Ty 年を下 2 桁で表したもの (00 - 99) 必要に応じて 0 が付加される
tj, Tj 1 月 1 日からの経過日 (グレゴリオ暦であれば 001 - 366) 必要に応じて 0 が付加される
tm, Tm 月を 2 桁の数字で表したもの (01 - 12 だと思うのだが、JavaDoc には 01 - 13 と書いてある) 必要に応じて 0 が付加される
td, Td 日にちを 2 桁の数字で表したもの (01 - 31) 必要に応じて 0 が付加される
te, Te 日にち (1 - 31)
tR, TR %tH:%tM と同じ
tT, TT %tH:%tM:%tS と同じ
tr, Tr %tH:%tM:%tS %tP と同じ
tD, TD %tm/%td/%ty と同じ
tF, TF %tY-%tm-%td と同じ
tc, Tc %ta %tb %td %tT %tZ %tY と同じ

習うより慣れろというわけで、あまりなじみのない時間に関するものを集めたサンプルを作ってみました。

サンプルのソース FormatterTest3.java

import java.util.Date;
import java.util.Formatter;
 
public class FormatterTest3 {
    public static void main(String[] args) {
        Formatter formatter = new Formatter((Appendable)System.out);
        Date date = new Date();
 
        formatter.format("tH: %tH%n", date);
        formatter.format("tI: %tI%n", date);
        formatter.format("tk: %tk%n", date);
        formatter.format("tl: %tl%n", date);
        formatter.format("tM: %tM%n", date);
        formatter.format("tS: %tS%n", date);
        formatter.format("tL: %tL%n", date);
 
         ... 以下、略 ...

このプログラムはロケール依存なので、デフォルトロケール(ja_JP) とアメリカのロケール (en_US) で実行してみました。左が日本、右がアメリカです。

ロケール 日本 ja_JP ロケール アメリカ en_US
C:\examples>java FormatterTest3
tH: 19
tI: 07
tk: 19
tl: 7
tM: 42
tS: 49
tL: 296
tN: 296000000
tp: 午後
tP: 午後
tz: +0900
tZ: JST
ts: 1086777769
tQ: 1086777769296
tB: 6月
tb: 6
th: 6
tA: 水曜日
ta: 水
tC: 20
tY: 2004
ty: 04
tj: 161
tm: 06
td: 09
te: 9
tR: 19:42
tT: 19:42:49
tr: 07:42:49 午後
tD: 06/09/04
tF: 2004-06-09
tc: 水 6 09 19:42:49 JST 2004
tI: 07
tk: 19
tl: 7
tM: 42
tS: 49
tL: 296
tN: 296000000
tp: 午後
tP: 午後
tz: +0900
tZ: JST
ts: 1086777769
tQ: 1086777769296
tB: 6月
tb: 6
th: 6
tA: 水曜日
ta: 水
tC: 20
tY: 2004
ty: 04
tj: 161
tm: 06
td: 09
te: 9
tR: 19:42
tT: 19:42:49
tr: 07:42:49 午後
tD: 06/09/04
tF: 2004-06-09
tc: 水 6 09 19:42:49 JST 2004
 
C:\examples>
C:\examples>java FormatterTest3
tH: 19
tI: 07
tk: 19
tl: 7
tM: 43
tS: 31
tL: 968
tN: 968000000
tp: pm
tP: PM
tz: +0900
tZ: JST
ts: 1086777811
tQ: 1086777811968
tB: June
tb: Jun
th: Jun
tA: Wednesday
ta: Wed
tC: 20
tY: 2004
ty: 04
tj: 161
tm: 06
td: 09
te: 9
tR: 19:43
tT: 19:43:31
tr: 07:43:31 PM
tD: 06/09/04
tF: 2004-06-09
tc: Wed Jun 09 19:43:31 JST 2004
tI: 07
tk: 19
tl: 7
tM: 43
tS: 31
tL: 968
tN: 968000000
tp: pm
tP: PM
tz: +0900
tZ: JST
ts: 1086777811
tQ: 1086777811968
tB: June
tb: Jun
th: Jun
tA: Wednesday
ta: Wed
tC: 20
tY: 2004
ty: 04
tj: 161
tm: 06
td: 09
te: 9
tR: 19:43
tT: 19:43:31
tr: 07:43:31 PM
tD: 06/09/04
tF: 2004-06-09
tc: Wed Jun 09 19:43:31 JST 2004
 
C:\examples>

次にフラグです。フラグに使用されるのは次の 6 種類です。

フラグ 説明
- 左詰めするかどうか
# 他の表現を使用するかどうか
+ 常に符号を付加するかどうか
' ' (スペース) 正の数値の時に符号の分の空白をおくかどうか
0

0 で埋めるかどうか

, 数値をセパレターで区切るかどうか (ロケール依存)
( 負の数値の時にカッコで囲むかどうか

これらのフラグはカテゴリによって適用できるものとできないものがあります。

フラグ 一般 文字 数字
整数
数字
浮動小数点数
時間 補足
-  
#     一般の場合は Formattable の定義による
整数では o, x, X のみ
+        
' ' (スペース)        
0      

 

,       整数では d のみ
浮動小数点数では a 以外
(       浮動小数点数では a 以外

複数のフラグを一緒に使うこともできます。符号付で左詰だったら -+ と表記できます。

フラグの使い方も習うより慣れろで、やってみましょう。

サンプルのソース FormatterTest4.java

import java.util.Formatter;

public class FormatterTest4 {
    public static void main(String[] args) {
        Formatter formatter = new Formatter((Appendable)System.out);
 
        int i = 1000;
        formatter.format("%%10d:  [%10d]%n", i);
        formatter.format("%%-10d: [%-10d]%n%n", i);
 
        formatter.format("%%o:  [%5o] %%x:  [%5x] %%X:  [%5X]%n", i, i, i);
        formatter.format("%%#o: [%#5o] %%#x: [%#5x] %%#X: [%#5X]%n%n", i, i, i);
 
        formatter.format("%%+d: [%+d] %% d: [% d]%n", i, i);
        formatter.format("%%010d: [%010d] %%,d: [%,d]%n", i, i);
        formatter.format("%%d: [%d] %%(d: [%(d]%n", -i, -i);
    }
}

実行してみれば、すぐにどういう出力になるか理解できるので、一度はやってみましょう ^^;;

C:\examples>java FormatterTest4
%10d:  [      1000]
%-10d: [1000      ]

%o:  [ 1750] %x:  [  3e8] %X:  [  3E8]
%#o: [01750] %#x: [0x3e8] %#X: [0X3E8]

%+d: [+1000] % d: [ 1000]
%010d: [0000001000] %,d: [1,000]
%d: [-1000] %(d: [(1000)]
 
C:\examples>

Formatter クラスの JavaDoc にはここに記したフラグだけしか記述されていないのですが、フラグをあらわす FormattableFlags クラスにはもう 1 つ '^' というフラグが書いてあります。

このフラグはすべて大文字で表記するかを示しているようなのですが、実際にフラグとして使用すると UnknownFormatConversionException 例外が発生してしまいます。

どうなってるんでしょ?

 

 
 
Tiger Formatter クラス以外のフォーマットが使えるクラス
 
 

前述した FormatSample クラスでは PrintStream クラスの format メソッドを使用しました。

このクラス以外に PrintWriter クラス、String クラスが内部的に Format クラスを使用しています。PrintWriter クラスは PrintStream クラスと同じ使い方でいいのですが、String クラスはちょっとだけ違っています。

String クラスの format メソッドは戻り値が String になります。ちょうど、C 言語の sprintf のような感じです。

String#format メソッドの定義は次のようになっています。

    public static String format(String format, Object ... args) {
        return new Formatter().format(format, args).toString(); 
    }

デフォルトコンストラクタで Formatter オブジェクトを生成しているので、StringBuilder クラスを使用したものと同じです。

String クラスを使うもよし、Formatter クラスを直接使うのもよし、ぐらいの感覚ですね。

 

 
 
Tiger 自作のクラスをフォーマットする
 
 

せっかくなので、自作したクラスも Formatter クラスでフォーマットできるようにしてみましょう。

自作したクラスはカテゴリとしては一般になります。とすると b と h は動作が決まってしまっているので、s の時にどのようにフォーマットすればいいかを考えることになります。

自作したクラスでフォーマットできるようにするには java.util.Formattable インタフェースをインプリメントします。Formattable インタフェースでは formatTo メソッドが定義されており、ここにフォーマットのための処理を記述します。

    public void formatTo(Formatter formatter, int flags, int width, int precision) 

第 2 引数の flags は java.util.FormattableFlags クラスで定義されている以下の 3 つの値を使用します。

  • LEFT_JUSTIFY フラグの '-' に相当する
  • UPPERCASE フラグの '^' に相当するというんだけど...
  • ALTERNATE フラグの '#' に相当する

これらの値は OR をとることができます。

width と precision は指定されていないときには -1 になります。

それでは Formattable インタフェースをインプリメントしたサンプルを作ってみましょう。

サンプルのソース FormatterTest5.java

単に名前を保持する Name というクラスを作ってそれをフォーマットできるようにしました。日本語の処理もいれると複雑になってしまうので英語だけに対応しています。

class Name implements Formattable {
    private String firstName;
    private String lastName;
 
    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

formatTo メソッドはフラグと幅を考えなくてはいけないのでロジックはちと面倒になります。precision はここでは省略しています。

次のようなルールでフォーマットするようにしました。

表示幅が苗字より短い場合 表示幅 - 1 の分だけラストネームを出力し、最後に '*' をつける
表示幅がフルネームより短い ラストネームだけ出力
表示幅がフルネームより長く、ALTERNATE が指定されている ファーストネームはイニシャルにする
その他 フルネーム
LEFT_JSTIFY 左詰にする
UPPERCASE 大文字にする
    public void formatTo(Formatter formatter, int flags, int width, int precision) {
        StringBuilder builder = new StringBuilder();
 
        boolean hasLimit = width != -1;
 
        if (hasLimit && width < lastName.length()) {
            createLimittedLastName(builder, width);
        } else if (hasLimit && width < firstName.length() + lastName.length()) {
            createLastName(builder);
        } else {
            createFullName(builder, flags, formatter.locale());
        }
 
        // 幅に足りない分のスペースを入れる
        if (hasLimit && builder.length() < width) {
            if ((flags & LEFT_JUSTIFY) != LEFT_JUSTIFY) {
                int num = width - builder.length();
                for (int i = 0; i < num; i++) {
                    builder.insert(0, ' ');
                }
            }
        }
 
        if ((flags & UPPERCASE) == UPPERCASE) {
            formatter.format(builder.toString().toUpperCase());
        } else {
            formatter.format(builder.toString());
        }
    }
 
    private void createLimittedLastName(StringBuilder builder, int width) {
        builder.append(lastName.substring(0, width - 1));
        builder.append('*');
    }
 
    private void createLastName(StringBuilder builder) {
        builder.append(lastName);
    }
 
    private void createFullName(StringBuilder builder, int flags, Locale locale) {
        if ((flags & ALTERNATE) == ALTERNATE 
          && Character.getType(firstName.charAt(0)) == Character.UPPERCASE_LETTER) {
            builder.append(firstName.substring(0, 1));
        } else {
            builder.append(firstName);
        }
        
        builder.append(' ');
        builder.append(lastName);
    }

width は表示幅が指定されていないときは -1 なので、先にそれを調べてしまいます。表示幅が指定されている場合、その幅に応じてラストネームだけにするかフルネームにするかなどを決めています。

左詰と大文字化は、表示幅のロジックとは独立してできるので if 分は別にできます。

フォーマットを試す部分は次のようにしてみました。

    public static void main(String[] args) {
        Formatter formatter = new Formatter((Appendable)System.out);
        
        Name name = new Name("Yuichi", "Sakuraba");
        
        formatter.format("%%s [%1$s] %%#s [%1$#s]%n", name);
        formatter.format("%%5s [%1$5s] %%10s [%1$10s]%n", name);
        formatter.format("%%20s [%1$20s] %%-20s [%1$-20s]%n", name);
    }

これを実行してみると、次のようになります。

C:\examples>java FormatterTest5
%s [Yuichi Sakuraba] %#s [Y Sakuraba]
%5s [Saku*] %10s [  Sakuraba]
%20s [     Yuichi Sakuraba] %-20s [Yuichi Sakuraba]
 
C:\examples>

 

 
 
Tiger おわりに
 
 

printf が使えるということはこんなにも楽だったのか、とあらためて気がつきました。それも C よりも機能が多いのですぐにでも使えそうです。

また、Formatter クラスを意識しなくても String クラスや PrintWriter/PrintStream クラスですぐ使えるのもポイントが高いですね。

もう 1 つの scanf については、項を改めて解説したいと思います。

 

今回使用したサンプルはここからダウンロードできます。

 

(Jun. 2004)

 
 
Go to Contents Go to Java Page