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 の間でもずいぶん変わってきているのです。
早くフィックスしてくれないかなぁ。
さっそく使ってみましょう。別に難しいことはいりません。単に printf メソッドを実行するだけです。
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 相当のフォーマットを行う立役者なのです。
上記のコードでは Formatter クラスのインスタンス生成には PrintStream オブジェクトを引数にしていました。JavaDoc を見てみると、フォーマットした出力先を引数にできるようです。その出力先として、Appendable インタフェース、File クラス、OutputStream クラス、ファイル名があります。
そのほかにロケールや文字コードも指定できます。
何も引数がない場合や、Appendable インスタンスが null の時には StringBuilder オブジェクトを生成して使用するようです。
生成してしまったら、後は printf のように使うだけです。ただし、メソッド名は printf ではなくて format です。
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); としても同じ結果が得られます。
Formatter クラスが使えるフォーマット記述子は基本的には C の printf と同じですが、拡張も行われています。拡張された部分としては時間に関するフォーマットと引数の順番を指定するものなどがあります。
フォーマットは次のように記述します。
%[argument_index$][flags][width][.precision]conversion
argument_index というのが拡張された部分です。たとえば %1$d と書けば、1 番目の引数ということになります。サンプルを見てもらうのが一番手っ取り早いですね。
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" |
|
文字 | 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 と同じ |
習うより慣れろというわけで、あまりなじみのない時間に関するものを集めたサンプルを作ってみました。
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) で実行してみました。左が日本、右がアメリカです。
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 以外 |
複数のフラグを一緒に使うこともできます。符号付で左詰だったら -+ と表記できます。
フラグの使い方も習うより慣れろで、やってみましょう。
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 例外が発生してしまいます。
どうなってるんでしょ?
前述した 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 クラスを直接使うのもよし、ぐらいの感覚ですね。
せっかくなので、自作したクラスも 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 つの値を使用します。
これらの値は OR をとることができます。
width と precision は指定されていないときには -1 になります。
それでは Formattable インタフェースをインプリメントしたサンプルを作ってみましょう。
単に名前を保持する 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 [%15ドルs] %%10s [%110ドルs]%n", name); formatter.format("%%20s [%120ドルs] %%-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>
printf が使えるということはこんなにも楽だったのか、とあらためて気がつきました。それも C よりも機能が多いのですぐにでも使えそうです。
また、Formatter クラスを意識しなくても String クラスや PrintWriter/PrintStream クラスですぐ使えるのもポイントが高いですね。
もう 1 つの scanf については、項を改めて解説したいと思います。
今回使用したサンプルはここからダウンロードできます。
(Jun. 2004)