20

ジェネリック クラスがある場合Foo<Bar>、次のように配列を作成することはできません。

Bar[] bars = new Bar[];

(これにより、「Bar のジェネリック配列を作成できません」というエラーが発生します)。

しかし、この質問への回答でdimo414が示唆しているように (Java how to: Generic Array creation)、次のことができます。

Bar[] bars = (Bar[]) new Object[];

(これは、「型の安全性: Object[] から Bar[] へのチェックされていないキャスト」という警告のみを生成します)。

dimo414の回答へのコメントでは、この構造を使用すると特定の状況で問題が発生する可能性があると主張する人もいれば、配列への唯一の参照がbars既に目的の型である であるため、問題ないと言う人もいます。

どの場合にこれで問題がなく、どの場合に問題が発生する可能性があるのか​​、少し混乱しています。たとえば、 newacctAaron McDaidによるコメントは、互いに直接矛盾しているようです。残念ながら、元の質問のコメント ストリームは、「なぜこれが「もはや正しくない」のか?」という未回答で終わるだけなので、新しい質問を作成することにしました。

bars-array に type のエントリしか含まれていない場合Bar、配列またはそのエントリを使用するときに実行時の問題が発生する可能性はありますか? または、実行時に配列を技術的に別のもの ( などString[]) にキャストできるという唯一の危険がありBarます。

代わりに使用できることはわかっていますArray.newInstance(...)が、たとえば GWT ではnewInstance(...)-option を使用できないため、上記の型キャスト構造に特に関心があります。

4

5 に答える 5

24

質問で私が言及されたので、私はチャイムを鳴らします。

この配列変数をクラス外に公開しなければ基本的には問題ありません。(ちょっと、ベガスで何が起こるかはベガスにとどまります。)

配列の実際の実行時の型はObject[]. したがって、それを型の変数に入れることはBar[]事実上「嘘」Object[]です。しかし、この嘘はクラス内に消されてしまうので、クラス内に残っていれば大丈夫です。( の下限はこの質問にあります。 の下限が何か他のものである場合、この議論での のすべての出現をその境界が何であれ置き換えます。) しかし、この嘘が何らかの形で外部にさらされた場合 (最も単純な例は、変数を type として直接返している場合、問題が発生します。Bar[]ObjectBarBarObjectBarObjectBarObjectbarsBar[]

実際に何が起こっているのかを理解するには、ジェネリックを使用した場合と使用しない場合のコードを確認することをお勧めします。ジェネリック プログラムは、ジェネリックを削除して適切な場所にキャストを挿入するだけで、同等の非ジェネリック プログラムに書き直すことができます。この変換は型消去と呼ばれます。

Foo<Bar>配列内の特定の要素を取得および設定するためのメソッドと、配列全体を取得するためのメソッドを使用して、の単純な実装を検討します。

class Foo<Bar> {
    Bar[] bars = (Bar[])new Object[5];
    public Bar get(int i) {
        return bars[i];
    }
    public void set(int i, Bar x) {
        bars[i] = x;
    }
    public Bar[] getArray() {
        return bars;
    }
}

// in some method somewhere:
Foo<String> foo = new Foo<String>();
foo.set(2, "hello");
String other = foo.get(3);
String[] allStrings = foo.getArray();

型消去後、これは次のようになります。

class Foo {
    Object[] bars = new Object[5];
    public Object get(int i) {
        return bars[i];
    }
    public void set(int i, Object x) {
        bars[i] = x;
    }
    public Object[] getArray() {
        return bars;
    }
}

// in some method somewhere:
Foo foo = new Foo();
foo.set(2, "hello");
String other = (String)foo.get(3);
String[] allStrings = (String[])foo.getArray();

そのため、クラス内にキャストはもうありません。ただし、呼び出しコードにはキャストがあり、1 つの要素を取得するときと、配列全体を取得するときです。1 つの要素を取得するためのキャストは失敗しないはずです。なぜなら、配列に入れることができるBarのは だけであり、取り出すことができるのも だけだからBarです。ただし、配列全体を取得するときのキャストは失敗します。これは、配列が実際の実行時の型を持っているためObject[]です。

非一般的に書かれているので、何が起こっているのか、そして問題はより明白になります。特に厄介なのは、ジェネリックでキャストを記述したクラスではキャストの失敗が発生しないことです。このクラスを使用する他の誰かのコードで発生します。そして、その他人のコードは完全に安全で無害です。また、ジェネリック コードでキャストを行った時点では発生しません。後で誰かが を呼び出したときにgetArray()、警告なしに発生します。

このメソッドがなければgetArray()、このクラスは安全です。この方法では、安全ではありません。安全ではない特徴は何ですか?前に作成した「嘘」に依存するbarstype として返されます。Bar[]嘘は真実ではないので、問題を引き起こします。メソッドが代わりに配列を type として返した場合Object[]、「嘘」に依存しないため安全です。

人々は、このようなキャストをしないように言うでしょう。なぜなら、チェックされていないキャストがあった元の場所ではなく、上記のように予期しない場所でキャスト例外が発生するからです。コンパイラは、それが安全でないことを警告しません (コンパイラgetArray()の観点からすると、指定した型が安全であるためです)。したがって、プログラマーがこの落とし穴に注意を払い、危険な方法で使用しないようにする必要があります。

ただし、これは実際には大きな問題ではないと私は主張します。適切に設計された API は、内部インスタンス変数を外部に公開することはありません。(内容を配列として返すメソッドがあっても、内部変数を直接返すことはありません。外部コードが配列を直接変更するのを防ぐために、それをコピーします。)getArray()とにかく、のように実装されるメソッドはありません。

于 2013-07-25T04:50:50.583 に答える
3

わかりました、私はこの構造を少し試してみましたが、本当に混乱する可能性があります。

私の質問に対する答えは次のとおりだと思います。配列を常にジェネリックとして処理する限り、すべてが正常に機能します。しかし、それを非一般的な方法で処理しようとすると、すぐに問題が発生します。いくつか例を挙げましょう。

  • 内部Foo<Bar>では、示されているように配列を作成し、問題なく操作できます。これは、(私が正しく理解していれば)コンパイラがBar-type を「消去」し、単純にObjectどこにでも変換するためです。したがって、基本的に内部Foo<Bar>では を処理しているだけですObject[]。これで問題ありません。
  • Foo<Bar>しかし、配列へのアクセスを提供する内部に次のような関数がある場合:

    public Bar[] getBars(Bar bar) {
        Bar[] result = (Bar[]) new Object[1];
        result[0] = bar;
        return result;
    }
    

    他の場所で使用すると、深刻な問題が発生する可能性があります。以下は、あなたが得た狂気の例です (ほとんどは実際には理にかなっていますが、一見すると狂っているように見えます)。

    • String[] bars = new Foo<String>().getBars("Hello World");

      java.lang.ClassCastException: [Ljava.lang.Object;が発生します。[Ljava.lang.String; にキャストできません。

    • for (String bar: new Foo<String>().getBars("Hello World"))

      同じjava.lang.ClassCastExceptionも発生します

    • しかし

      for (Object bar: new Foo<String>().getBars("Hello World"))
          System.out.println((String) bar);
      

      作品...

    • これは私には意味をなさないものです:

      String bar = new Foo<String>().getBars("Hello World")[0];
      

      どこにも String[] を割り当てていなくても、java.lang.ClassCastExceptionも発生します。

    • Object bar = new Foo<String>().getBars("Hello World")[0];
      

      同じjava.lang.ClassCastExceptionが発生します。

    • それだけ

      Object[] temp = new Foo<String>().getBars("Hello World");
      String bar = (String) temp[0];
      

      作品...

    ちなみに、これらのどれもコンパイル時エラーをスローしません。

  • ただし、別のジェネリック クラスがある場合は、次のようになります。

    class Baz<Bar> {
        Bar getFirstBar(Bar bar) {
            Bar[] bars = new Foo<Bar>().getBars(bar);
            return bars[0];
        }
    }
    

    以下は問題なく動作します。

    String bar = new Baz<String>().getFirstBar("Hello World");
    

これのほとんどは理にかなっています.型消去の後、getBars(...)関数は実際には ,Object[]とは無関係に を返しますBar. これが、(実行時に)として設定されていたとしてString[]も、例外を生成せずに戻り値を a に割り当てることができない理由です。ただし、これにより、最初に配列をキャストして戻さずに配列にインデックスを付けることができなくなるのはなぜですか。クラスで問題なく動作する理由は、 とは無関係に、 も に変わるからです。したがって、これは、配列を にキャストしてからインデックスを付け、返されたエントリを にキャストすることと同じです。BarStringObject[]Baz<Bar>Bar[]Object[]BarObject[]String

全体として、これを見た後、この方法を使用して配列を作成することは、ジェネリッククラスの外部に配列を返さない限り、本当に悪い考えであると確信しています。Collection<...>ここでは、配列の代わりにa を使用します。

于 2013-07-24T11:51:18.980 に答える
3

リストとは対照的に、Java の配列型は具体化されています。つまり、実行時の型Object[]とは異なりString[]ます。したがって、あなたが書くとき

Bar[] bars = (Bar[]) new Object[];

ランタイム型の配列を作成し、Object[]それを に「キャスト」しましたBar[]。これは実際のチェック キャスト操作ではないため、引用符で囲んで「キャスト」と言いObject[]ますBar[]。当然、これはあらゆる種類のランタイム タイプ エラーへの扉を開きます。実際にエラーが発生するかどうかは、プログラミングの腕前と注意力次第です。したがって、やる気があれば、それを行っても問題ありません。そうしない場合、またはこのコードが多くの開発者が参加する大規模なプロジェクトの一部である場合、これを行うのは危険です。

于 2013-07-24T11:12:01.870 に答える
0

Barその配列内の何かを実際の としてではなく、型(またはジェネリック クラスを初期化する型)として使用するまで、すべてが正常に機能しますObject。たとえば、次のメソッドがあります。

<T> void init(T t) {
    T[] ts = (T[]) new Object[2];
    ts[0] = t;
    System.out.println(ts[0]);
}

すべてのタイプで問題なく動作するようです。次のように変更した場合:

<T> T[] init(T t) {
    T[] ts = (T[]) new Object[2];
    ts[0] = t;
    System.out.println(ts[0]);
    return ts;
}

そしてそれを呼び出す

init("asdf");

それでも問題なく動作します。ただし、実際の T[] 配列 (上記の例では String[] である必要があります) を実際に使用する場合:

String[] strings = init("asfd");

Object[]String[]は2つの異なるクラスであり、あなたが持っているものはObject[]とてもClassCastException投げられるので、問題があります。

バインドされたジェネリック型を試すと、問題がより迅速に発生します。

<T extends Runnable> void init(T t) {
    T[] ts = (T[]) new Object[2];
    ts[0] = t;
    System.out.println(ts[0]);
    ts[0].run();
} 

良い習慣として、ジェネリックと配列の使用を避けるようにしてください。それらはうまく混ざり合わないからです。

于 2013-07-24T11:34:45.197 に答える