読者です 読者をやめる 読者になる 読者になる

【Effective Java】項目42:可変長引数を注意して使用する

可変引数メソッド

Java 5 から可変長引数メソッドが利用できるようになりました。 正式には『可変引数メソッド(variable arity method)』と呼ばれています。

可変引数メソッドは任意の0個以上の引数を受け付けることができます。 実際の動作は次のとおりです。

  1. 呼び出し時に渡された引数の数と同じ大きさの配列を生成する
  2. その配列に引数値を代入する
  3. その配列をメソッドに渡す

たとえば int 列を受け取り、合計を返すメソッドは以下のようになります。

static int sum(int ... args) {
    int sum = 0;
    for (int arg: args) {
        sum += arg;
    }
    return sum;
}

0個以上の引数ではなく、1個以上の引数を受け取る可変引数メソッドを作成したいことがあります。

この場合、1つの引数を通常の引数として受け取り、残りを可変引数として受け取るメソッドを作るとスマートに目的を達成できます。

static int min(int first, int ... remains) {
    int min = first;
    for (int arg: remains) {
        if (arg < min) {
            min = arg;
        }
    }
    return min;
}

これ以外に、すべての引数を可変引数として受け取り、実行時に配列の長さをチェックする実装方法が考えられます。 しかし、この方式には以下のような欠点があるため、上述したメソッドのやり方のほうがスマートです。

  • コンパイル時ではなく実行時にエラーが判明する
  • min を Integer.MAX_VALUE で初期化しなければならない

配列としての引数と可変引数

最後の引数が配列であるようなメソッドを、可変引数に変更することはとても簡単です。 しかし、簡単だからといって安易にそうするべきではありません。

たとえば次のような Object 型の配列を出力するメソッドを考えてみます。

static void printArray(Object[] objects) {
    for (Object o : objects) {
        System.out.println(o);
    }
}

これに対し、String[] を引数として渡せば意図通りに出力されます。

String[] strings = {"1", "2", "3"};
printArray(strings);
// => "1", "2", "3" と出力される

String は問題なく渡せましたが、実は int はこのメソッドに渡すことはできません。

int[] ints = {1, 2, 3}
VariableArity.printArray(ints); // => コンパイルエラー!

Object[]int[] は継承関係がないのでメソッドに引数を渡すことができず、コンパイルエラーとなります。

しかし、このエラー意図通りのエラーです。 なぜなら、int はプリミティブ型であり toString() を持っていませんから、コンパイルできたとしたら実行時エラーが発生してしまいます。

もし、この printArray を可変引数メソッドを使って書き換えたとしたらどうなるでしょうか?

static void printArray(Object ... objects) {
    for (Object o : objects) {
        System.out.println(o);
    }
}

可変引数メソッドを使うと、先ほどエラーとなっていた int[] を渡せるようになります。

int[] ints = {1, 2, 3}
printArray(ints); // => コンパイル OK
// => "[I@1b1c7295" と出力される

Object には int[] を渡すことができるためコンパイルできてしまいます。 しかし、出力は意図通りのものではありません。 int[] 型の要素を1つ持つ配列と解釈されてしまっているためです。

配列を持つメソッドを可変メソッドに修正したことで型検査が一つ失われたことになります。

このように配列引数を安易に可変引数に変更してはいけません。 実際、Java の Arrays.asList() は配列引数から可変引数に変更され、混乱を引き起こしています。

パフォーマンス

冒頭で説明したように可変引数メソッドは呼び出しごとに配列を生成します。

そのため、頻繁に呼び出されるメソッドが可変引数だとパフォーマンス上の問題が発生する場合があります。

もし、パフォーマンスが問題となる上で、多くの場合パラメータが3個以下というような条件付けができるならば、通常のメソッドをいくつかと、可変引数1つというメソッド群で対応できるかもしれません。

public void foo();
public void foo(int a1);
public void foo(int a1, int a2);
public void foo(int a1, int a2, int a3);
public void foo(int a1, int a2, int a3, int ... rest);

このようにしておけばほとんどの場合、通常のメソッド呼び出しとなるためパフォーマンス上の問題が起こりにくくなります。

このような最適化はほとんどの場合はやるべきではありませんが、パフォーマンスが厳しく要求されるような箇所では役に立ちます。

たとえば Java の EnumSet はビットフィールドに対してパフォーマンスを損なわないことを目的としているため、この方法を採用しています。