Java の Varargs メカニズムを理解する

黄舟
リリース: 2017-02-23 10:23:36
オリジナル
1222 人が閲覧しました

J2SE 1.5 は「Varargs」メカニズムを提供します。このメカニズムを使用すると、複数の実パラメータと一致する仮パラメータを定義できます。したがって、可変数の実パラメータをより簡単な方法で渡すことができます。この記事では、このメカニズムの使用方法と、このメカニズムが配列、ジェネリック、オーバーロードと相互作用する場合のいくつかの問題について紹介します。

J2SE 1.4 までは、Java プログラム内で可変数の実パラメータを持つメソッドを定義することは不可能でした。Java では、実パラメータ (引数) と仮パラメータ (パラメータ) の数と型が一致する必要があるためです。 1 つと仮パラメータの数はメソッド定義時に固定されます。オーバーロード メカニズムを通じて、同じメソッドに対して異なる数の仮パラメータを持つバージョンを提供することは可能ですが、それでも実際のパラメータの量を任意に変更できるようにするという目的は達成できません。

ただし、一部のメソッドのセマンティクスでは、可変数の実パラメータを受け入れることができる必要があります。たとえば、有名な main メソッドは、すべてのコマンド ライン パラメータを実際のパラメータとして受け入れ、コマンドの数を受け入れることができる必要があります。ラインパラメータは事前​​にまったく決定されていません。

この問題に対しては、伝統的に「実際に渡すパラメータを配列でラップする」というアプローチが使われてきました。

実パラメータを配列でラップする

「実パラメータを配列でラップする」方法は 3 つのステップに分けることができます。まず、このメソッドの配列パラメータを定義し、呼び出し時にすべてのパラメータを含む配列を生成します。実パラメータの配列を渡します。最後に、この配列を実パラメータとして渡します。

このアプローチは、「メソッドが可変数のパラメーターを受け入れられるようにする」という目的を効果的に達成できますが、呼び出しの形式は十分に単純ではありません。

J2SE 1.5 は Varargs メカニズムを提供し、複数の実パラメータと一致する仮パラメータを直接定義できるようにします。したがって、可変数の実パラメータをより簡単な方法で渡すことができます。

Varargsの意味

一般に、「Varargs」とは「可変数の引数」を意味します。単に「変数引数」と呼ばれることもありますが、この用語では何が変数であるかを説明していないため、意味が少し曖昧です。

実パラメータの可変数を定義する方法

仮パラメータの「型」と「パラメータ名」の間に3つ連続する「.」(英語では「...」)を追加するだけです。不特定の数の実際のパラメータと一致させることができます。このような仮パラメータを持つメソッドは、可変数の実パラメータを持つメソッドです。

リスト 1: 可変数の実パラメータを持つメソッド

private static int sumUp(int... values) {    
}
ログイン後にコピー


「無制限の数の実パラメータと一致する」として定義できるのは最後の仮パラメータのみであることに注意してください。したがって、メソッド内にこのようなパラメーターは 1 つだけ存在できます。さらに、このメソッドに他の仮パラメータがある場合は、それらを先頭の位置に置きます。

コンパイラは、この最後の仮パラメータを秘密裏に配列パラメータに変換し、コンパイルされたクラス ファイルにマークを付けて、これが可変数の実パラメータを持つメソッドであることを示します。

リスト 2: 可変数の実パラメーターを持つメソッドの秘密形式

private static int sumUp(int[] values) {    
}
ログイン後にコピー


このような変換が存在するため、同じものを使用してこのクラスのメソッドを定義することはできなくなりました。変換されたメソッドとしてのシグネチャ。

リスト 3: コンパイル エラーを引き起こす組み合わせ

private static int sumUp(int... values) {   
}   

private static int sumUp(int[] values) {   
}
ログイン後にコピー


空白の存続問題

J2SE 1.5 の構文によれば、「...」の前の空白文字はオプションです。このように、「...」の前に空白文字を追加する(「Object... args」の形式)と、「...」の前に空白文字を追加しない(「」の形式)の2つの書き方があります。オブジェクト...引数")。現在、J2SE 1.5と互換性のあるJavaコード規約は正式にリリースされていないため、どの記述方法がよりオーソドックスであるかを知ることはできません。ただし、配列パラメータには「Object [] args」と「Object[] args」の2通りの書き方があり、「[]」の前に空白文字を入れないのがオーソドックスな書き方であることを考慮すると、 「オブジェクト... args」の空白を省略すると、全体的にまとまりのある書き方になります。

実引数を可変にしてメソッドを呼び出す

渡す実引数を対応する位置に一つずつ書いていけば、実引数を可変にしてメソッドを呼び出すことができます。他の手順は必要ありません。

リスト 4: 複数の実パラメータを渡すことができます

sumUp(1, 3, 5, 7);
ログイン後にコピー


コンパイラは、この呼び出しプロセスを「実パラメータをラップする配列」の形式に秘密裏に変換します。

リスト 5: 秘密裏に配列を作成すると表示されます

sumUp(new int[]{1, 2, 3, 4});
ログイン後にコピー


さらに、ここで言及されている「不確実な数」にはゼロも含まれるため、このような呼び出しも合理的です:

リスト 6: ゼロの実パラメータも渡すことができます

sumUp(); 

这种调用方法被编译器秘密转化之后的效果,则等同于这样:
ログイン後にコピー


リスト 7: ゼロの実パラメーターは空の配列に対応します

sumUp(new int[]{});
ログイン後にコピー

null ではなく、空の配列が渡されることに注意してください。このようにして、どの状況に属するかを検出することなく、統一された形式で処理できます。


4. 处理个数可变的实参

处理个数可变的实参的办法,和处理数组实参的办法基本相同。所有的实参,都被保存到一个和形参同名的数组里。根据实际的需要,把这个数组里的元素读出之后,要蒸要煮,就可以随意了。

清单8:处理收到的实参们

private static int sumUp(int... values) {   
    int sum = 0;   
    for (int i = 0; i < values.length; i++) {   
        sum += values[i];   
    }   
    return sum;   
}
ログイン後にコピー


5. 转发个数可变的实参

有时候,在接受了一组个数可变的实参之后,还要把它们传递给另一个实参个数可变的方法。因为编码时无法知道接受来的这一组实参的数目,所以“把它们 逐一写到该出现的位置上去”的做法并不可行。不过,这并不意味着这是个不可完成的任务,因为还有另外一种办法,可以用来调用实参个数可变的方法。

在J2SE 1.5的编译器的眼中,实参个数可变的方法是最后带了一个数组形参的方法的特例。因此,事先把整组要传递的实参放到一个数组里,然后把这个数组作为最后一个实参,传递给一个实参个数可变的方法,不会造成任何错误。借助这一特性,就可以顺利的完成转发了。

清单9:转发收到的实参们

public class PrintfSample {   

    public static void main(String[] args) {   
        // 打印出“Pi:3.141593 E:2.718282”   
        printOut("Pi:%f E:%f/n", Math.PI, Math.E);   
    }   

    private static void printOut(String format, Object... args) {   
        // J2SE 1.5里PrintStream新增的printf(String format, Object... args)方法   
        System.out.printf(format, args);   
    }   
}
ログイン後にコピー


Java里的“printf”和“sprintf”

C语言里的printf(按一定的格式输出字符串)和sprintf(按一定的格式组合字符串)是十分经典的使用Varargs机制的例子。在 J2SE 1.5中,也分别在java.io.PrintStream类和java.lang.String类中提供了类似的功能。

按一定的格式输出字符串的功能,可以通过调用PrintStream对象的printf(String format, Object… args)方法来实现。

按一定的格式组合字符串的工作,则可以通过调用String类的String format(String format, Object… args)静态方法来进行。

6. 是数组?不是数组?

尽管在背地里,编译器会把能匹配不确定个实参的形参,转化为数组形参;而且也可以用数组包了实参,再传递给实参个数可变的方法;但是,这并不表示“能匹配不确定个实参的形参”和“数组形参”完全没有差异。

一个明显的差异是,如果按照调用实参个数可变的方法的形式,来调用一个最后一个形参是数组形参的方法,只会导致一个“cannot be applied to”的编译错误。

清单10:一个“cannot be applied to”的编译错误

private static void testOverloading(int[] i) {   
    System.out.println("A");   
}   

public static void main(String[] args) {   
    testOverloading(1, 2, 3);// 编译出错   
}
ログイン後にコピー


由于这一原因,不能在调用只支持用数组包裹实参的方法的时候(例如在不是专门为J2SE 1.5设计第三方类库中遗留的那些),直接采用这种简明的调用方式。

如果不能修改原来的类,为要调用的方法增加参数个数可变的版本,而又想采用这种简明的调用方式,那么可以借助“引入外加函数(Introduce Foreign Method)”和“引入本地扩展(Intoduce Local Extension)”的重构手法来近似的达到目的。

7. 当个数可变的实参遇到泛型

J2SE 1.5中新增了“泛型”的机制,可以在一定条件下把一个类型参数化。例如,可以在编写一个类的时候,把一个方法的形参的类型用一个标识符(如T)来代表, 至于这个标识符到底表示什么类型,则在生成这个类的实例的时候再行指定。这一机制可以用来提供更充分的代码重用和更严格的编译时类型检查。

不过泛型机制却不能和个数可变的形参配合使用。如果把一个能和不确定个实参相匹配的形参的类型,用一个标识符来代表,那么编译器会给出一个“generic array creation”的错误。

清单11:当Varargs遇上泛型

private static <T> void testVarargs(T... args) {   
    // 编译出错   
}
ログイン後にコピー


造成这个现象的原因在于J2SE 1.5中的泛型机制的一个内在约束——不能拿用标识符来代表的类型来创建这一类型的实例。在出现支持没有了这个约束的Java版本之前,对于这个问题,基本没有太好的解决办法。

不过,传统的“用数组包裹”的做法,并不受这个约束的限制。

清单12:可以编译的变通做法

private static <T> void testVarargs(T[] args) {   
    for (int i = 0; i < args.length; i++) {   
        System.out.println(args[i]);   
    }   
}
ログイン後にコピー


8. 重载中的选择问题

Java支持“重载”的机制,允许在同一个类拥有许多只有形参列表不同的方法。然后,由编译器根据调用时的实参来选择到底要执行哪一个方法。

传统上的选择,基本是依照“特殊者优先”的原则来进行。一个方法的特殊程度,取决于为了让它顺利运行而需要满足的条件的数目,需要条件越多的越特殊。

在引入Varargs机制之后,这一原则仍然适用,只是要考虑的问题丰富了一些——传统上,一个重载方法的各个版本之中,只有形参数量与实参数量正 好一致的那些有被进一步考虑的资格。但是Varargs机制引入之后,完全可以出现两个版本都能匹配,在其它方面也别无二致,只是一个实参个数固定,而一 个实参个数可变的情况。

遇到这种情况时,所用的判定规则是“实参个数固定的版本优先于实参个数可变的版本”。

清单13:实参个数固定的版本优先

public class OverloadingSampleA {   

    public static void main(String[] args) {   
        testOverloading(1);// 打印出A   
        testOverloading(1, 2);// 打印出B   
        testOverloading(1, 2, 3);// 打印出C   
    }   

    private static void testOverloading(int i) {   
        System.out.println("A");   
    }   

    private static void testOverloading(int i, int j) {   
        System.out.println("B");   
    }   

    private static void testOverloading(int i, int... more) {   
        System.out.println("C");   
    }   
}
ログイン後にコピー


如果在编译器看来,同时有多个方法具有相同的优先权,它就会陷入无法就到底调用哪个方法作出一个选择的状态。在这样的时候,它就会产生一个 “reference to 被调用的方法名 is ambiguous”的编译错误,并耐心的等候作了一些修改,足以免除它的迷惑的新源代码的到来。

在引入了Varargs机制之后,这种可能导致迷惑的情况,又增加了一些。例如现在可能会有两个版本都能匹配,在其它方面也如出一辙,而且都是实参个数可变的冲突发生。

清单14:左右都不是,为难了编译器

public class OverloadingSampleB {   

    public static void main(String[] args) {   
        testOverloading(1, 2, 3);// 编译出错   
    }   

    private static void testOverloading(Object... args) {   
    }   

    private static void testOverloading(Object o, Object... args) {   
    }   
}
ログイン後にコピー


另外,因为J2SE 1.5中有“Autoboxing/Auto-Unboxing”机制的存在,所以还可能发生两个版本都能匹配,而且都是实参个数可变,其它方面也一模一样,只是一个能接受的实参是基本类型,而另一个能接受的实参是包裹类的冲突发生。

清单15:Autoboxing/Auto-Unboxing带来的新问题

public class OverloadingSampleC {   

    public static void main(String[] args) { /* 编译出错 */  
        testOverloading(1, 2); /* 还是编译出错 */  
        testOverloading(new Integer(1), new Integer(2));   
    }   

    private static void testOverloading(int... args) {   
    }   

    private static void testOverloading(Integer... args) {   
    }   
}
ログイン後にコピー


9. 归纳总结

和“用数组包裹”的做法相比,真正的实参个数可变的方法,在调用时传递参数的操作更为简单,含义也更为清楚。不过,这一机制也有它自身的局限,并不是一个完美无缺的解决方案。

 以上就是Java 中 Varargs 机制的理解 的内容,更多相关内容请关注PHP中文网(www.php.cn)!

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート