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

可変長引数

 
 
Tiger printf がほしい !!
 
 

C にあって、Java にないものはいろいろあります。struct, union, enum, プリプロセッサなどなど。

struct や union はクラスで代用できるので、別にほしいとは思いません。また、プリプロセッサは人によっては熱望している人もいると思います。携帯電話の Java アプリケーションなんかは機種によってかなり動作が違いますが、機種ごとにソースを作らず、1 つのソースでやりくりしたいときなどは有効だと思います。

enum は C のものよりずっと便利なものが Tiger から導入されました。

それ以外のもので C にあって Java にないものといえば... 可変長引数があります。たとえば、printf の引数はいくら続けても大丈夫ですよね。Java ではこれができないのです。ということは、printf は作れない !!

でも、Tiger で printf 相当のものも提供されることになりました。

ということは、可変長引数もできるようになったということです。

 

 
 
Tiger 今までの可変長と、Tiger の可変長
 
 

printf のような文字列をフォーマットして出力するには java.text.MessageFormatt クラスを使います。

MessageFormat クラスも可変長引数であればよかったのですが、今まではそういうわけにはいきませんでした。擬似的に可変長引数を実現するために、引数に Object クラスの配列を使用しています。

たとえば、このように記述します。

import java.text.MessageFormat;
import java.util.Date;
 
public class VarargsSample1 {
    public static void main(String[] args) {
 
        Object[] arg = new Object[]{new Integer(10), new Date()};
        String str = MessageFormat.format("{0,number} {1,date}", arg);
 
        System.out.println(str);
    }
}

arg が引数に使うための配列です。

しかし、この方法だと Object クラスの配列なので、プリミティブ型の変数は使用できません。また、Object クラスなので、どんなオブジェクトを代入することも可能なため、コンパイル時の型チェックができません。

そこで、可変長引数です。

上の例は次のように書きかえられます。

import java.text.MessageFormat;
import java.util.Date;
 
public class VarargsSample2 {
    public static void main(String[] args) {
        String str = MessageFormat.format("{0,number} {1,date}",
                                          10, new Date());
        System.out.println(str);
    }
}

便利だと思いません ?

それでは format メソッドはどのように定義されているのでしょう。リファレンスインプリメンテーションのソースを見てみると次のようになっています。

    public static String format(String pattern, Object... arguments);

可変長引数にするには、引数の型指定のあとに "..." と 3 つピリオドを続けて記述します。format メソッドの場合は Object の可変長引数になっています。

Object なので、型チェックはできませんが、メソッドの性質上しょうがないでしょう。

 

 
 
Tiger 可変長引数なメソッドを記述する
 
 

可変長引数にするには、型指定の後に ... を記述することが分かったので、実際に書いてみましょう。

ところで、可変長引数には制約があるのでしょうか。次のようなコードで確かめてみました。

public class VarargsTest1 {
    // あやしいけど、現バージョンでは OK
    public void foo(int... x) {}     
 
    // あやしいけど、現バージョンでは OK
    public void foo(Integer... x) {}     
 
     // OK
    public void foo(int x, int... y) {}
 
    // OK 
    public void foo(int x, Integer... y) {}
 
    // NG
    public void foo(int x, Integer... y, int z) {}
 
    public VarargsTest1() {}
 
    public static void main(String[] args) {
        new VarargsTest1();
    }
}

コンパイルしてみましょう。

C:\JSR201\examples>javac VarargsTest1.java
VarargsTest1.java:15: ')' がありません。
    public void foo(int x, Integer... y, int z) {}
                                       ^
VarargsTest1.java:22: ';' がありません。
}
^
エラー 2 個
 
C:\JSR201\examples>

はじめのメソッドは第 1 引数で可変長ができるかどうかです。C/C++ の可変長は第 1 引数では使えないのですが、Java ではなんかできるみたいです。でも、今のところ Early Access なので本当はだめなのかもしれません。ということで、怪しいです ^^;;

プリミティブでもクラスでも同じように可変長にすることができるようです。

ただし、可変長にするのは最後の引数でないとだめなようです。これは C/C++ でも同じですね。

すっきりしたところで、メソッドを書いてみましょう。

やはり、printf でしょう。でも、ちゃんと書くのは大変なので「えせprintf」です ^^;;

public class VarargsSample3 {
    public String format(String pattern, int... x) {
        StringBuffer buffer = new StringBuffer();
 
        for (int i = 0, j = 0; i < pattern.length() && j < x.length; i++) {
            char c = pattern.charAt(i);
            if (c == '%') {
                buffer.append(x[j]);
                j++;
            } else {
                buffer.append(c);
            }
        }
 
        return buffer.toString();
    }
 
    public static void main(String[] args) {
        VarargsSample3 sample = new VarargsSample3();
        System.out.println(sample.format("% is less than %.", 5, 6));
        System.out.println(sample.format("% >= %",
                                         new Integer(10), new Integer(2)));
    }
}

format メソッドが「えせ printf」みたいなメソッドです。先ほど示したように引数には ... をつければいいので int... としています。

宣言は分かったのですが、実際に可変の引数はどのように扱えばいいのでしょう。いいかえれば、引数の 1 つ 1 つにはどうすればアクセスできるのでしょうか。

それには配列を使用します。ここでは int... x とされている引数はメソッドの中では int[] x と int 型の配列として扱うことができます。

配列だと分かればあとは普通に書くだけです。

このアクセスに関しては C/C++ の va_arg などのマクロを使用した方法より全然楽です。

さて、プログラムの中身ですが、pattern の中に % という文字があればそこには数字を出力するようにしているだけです。桁数指定も何もなしです。サンプルなのでゆるしてください。

main メソッドの中で format メソッドを使用していますが、引数に int だけでなく Integer オブジェクトも使用しています。これは Autoboxing を使って勝手に int に変換してくれているため OK なのです。

 

 
 
Tiger 可変長引数のメソッドと普通のメソッドはどちらがお好み ?
 
 

というのはメソッドの評価の話しです。

たとえば、同じ名前でオーバロードされたメソッドで、一方が可変長引数、もう一方が通常のメソッドであった場合、どちらがコールされるかということです。

さっそくやってみましょう。

public class VarargsEvalTest {
    public static void foo(int a, int b, int c){
        System.out.println("Call foo(int a, int b, int c)");
    }
 
    public static void foo(int a, Integer... args){
        System.out.println("Call foo(int a, Integer... args)");
    }
 
    public static void foo(int a, Number... args){
        System.out.println("Call foo(int a, Number... args)");
    }
 
    public static void foo(int a, Object... args){
        System.out.println("Call foo(int a, Object... args)");
    }
 
    public static void main(String[] args) {
        foo(0);
        foo(0, 1);
        foo(0, 1, 2);
        foo(0, new Integer(1), 2);
        foo(0, 1, 2, 3);
        foo(0, 1.5);
        foo(0, "abc");
    }
}

このプログラムは JavaPress Vol.33 の Tiger の紹介記事で使用したものです。

さっそく、実行してみましょう。

C:\JSR201\examples>java VarargsEvalTest
Call foo(int a, Integer... args)
Call foo(int a, Integer... args)
Call foo(int a, int b, int c)
Call foo(int a, int b, int c)
Call foo(int a, Integer... args)
Call foo(int a, Number... args)
Call foo(int a, Object... args)
 
C:\JSR201\examples>

1 つめの結果は、可変長引数が選ばれています。可変長引数というのは引数が 0 の場合も含まれるということです。

2 番目は当然の結果です。3 番目、4 番目が通常のメソッドと可変長引数のメソッドの両方に当てはまる場合です。結果を見てみると通常のメソッドがコールされていることが分かります。ということは、通常のメソッドが優先的に評価されるということです。

5 番目からの 3 つはどのような型に評価されるかを見てみたものです。これを見ると、もっとも自分の型に適合するものが選ばれていることが分かります。引数が Integer オブジェクトだったら、Object オブジェクトと評価される前に、Integer オブジェクト、Number オブジェクトと評価されるわけですね。まぁ、当たり前といえば当たり前ですが。

 

 
 
Tiger やっぱり気になる裏側が
 
 

いつものように、逆コンパイラの Jad を使って調べてみましょう。

逆コンパイルしたのは VarargsSample3 クラスです。肝心の format メソッドの部分は次のようになりました。

    public String format(String s, int ai[])
    {
        StringBuffer stringbuffer = new StringBuffer();
        int i = 0;
        for(int j = 0; i < s.length() && j < ai.length; i++)
        {
            char c = s.charAt(i);
            if(c == '%')
            {
                stringbuffer.append(ai[j]);
                j++;
            } else
            {
                stringbuffer.append(c);
            }
        }
 
        return stringbuffer.toString();
    }

変数名が変わっているのは気にしないでください。

さて、引数ですが、なんと配列に置き換わっています。結局、見た目は配列が隠されましたが、裏側ではやっぱり配列を使っているんですね。

メソッドの中で可変長引数の要素をあつかうときに配列としてだったのも、これで納得です。それにしても JSR-201 ではコンパイラが大活躍です。

 

 
 
Tiger

MessageFormat クラス以外に変わったクラスは ?

 
 

MessageFormat クラスが可変長引数に対応したことは前述しましたが、他にも可変長引数に対応したクラスはあるのでしょうか。

そんなときはソースを検索してみましょう。src.zip を解凍して、grep で "..." を探してみました。

そうすると出てきたのがリフレクション関連のクラスです。

たとえば、java.lang.reflect.Method クラスの invoke メソッドの定義は次のようになっています。

    public Object invoke(Object obj, Object... args)
                            throws IllegalAccessException, 
                                   IllegalArgumentException,
                                   InvocationTargetException;

Method#invoke メソッドはリフレクションでオブジェクトのメソッドをコールするためのメソッドです。args はそのメソッドの引数になります。任意のメソッドをコールするわけですから、引数を決めるわけにはいかず、可変長にする必要があるのです。

同じように Class#getMethod メソッドや Constructor#newInstance メソッドなども可変長引数になっています。

 

 
 
Tiger おわりに
 
 

可変長引数のメソッドを自分で書くことはほとんどないと思いますが、あれば便利です。

でも一番便利なのは、やっぱり printf です。Tiger で printf 相当のものが提供されることになったのは、とても喜ばしきことです。この機能についてはまた別の項で。

 

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

参考

(Dec. 2003)
(改訂 Jan. 2004)

 
 
Go to Contents Go to Java Page