web-dev-qa-db-ja.com

未チェックの型キャストを介してJava)でジェネリック配列を作成する

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

_Bar[] bars = new Bar[];
_

(これにより、「バーの汎用配列を作成できません」というエラーが発生します)。

しかし、 この質問(Javaの方法:ジェネリック配列の作成) への回答でdimo414によって示唆されているように、私はできます以下:

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

(これにより、「タイプセーフティ:オブジェクト[]からバー[]へのチェックされていないキャスト」という警告が「のみ」生成されます)。

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

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

bars-配列にタイプBarのエントリのみが含まれている場合でも、配列またはそのエントリを使用するときに実行時の問題が発生する可能性はありますか?または、実行時に配列を他の何か(_String[]_など)に技術的にキャストして、Bar以外の型の値で配列を埋めることができるという唯一の危険があります。 ?

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

19
Markus A.

質問でおっしゃっていたので、チャイムを鳴らします。

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

配列の実際の実行時型は_Object[]_です。したがって、_Bar[]_は_Object[]_のサブタイプではないため(ObjectBarでない限り)、タイプ_Bar[]_の変数に入れることは事実上「嘘」です。ただし、この嘘は、クラス内でBarObjectに消去されるため、クラス内にとどまっている場合は問題ありません。 (この質問では、Barの下限はObjectです。Barの下限が別のものである場合は、この説明のObjectのすべての出現箇所をその境界に置き換えてください。)ただし、この嘘が何らかの形で外部(最も簡単な例は、bars変数をタイプ_Bar[]_として直接返すことです。そうすると、問題が発生します。

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

配列内の特定の要素を取得および設定するためのメソッドと、配列全体を取得するためのメソッドを使用した、_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つの要素を取得し、配列全体を取得する場合です。配列に入れることができるのはBarだけなので、1つの要素を取得するためのキャストは失敗しないはずです。したがって、取得できるのはBarだけです。ただし、配列には実際の実行時型_Object[]_があるため、配列全体を取得するときのキャストは失敗します。

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

このgetArray()メソッドがなかった場合、このクラスは安全です。この方法では安全ではありません。安全でない理由は何ですか? barsをタイプ_Bar[]_として返します。これは、前に作成した「嘘」によって異なります。嘘は真実ではないので、それは問題を引き起こします。メソッドが代わりに配列をタイプ_Object[]_として返した場合、それは「嘘」に依存しないため、安全です。

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

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

21
newacct

さて、私はこの構成を少し遊んだことがありますが、それは本当に混乱する可能性があります。

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

  • _Foo<Bar>_内で、示されているように配列を作成し、それをうまく処理できます。これは、(私が正しく理解していれば)コンパイラがBar型を「消去」し、どこでも単純に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];
      _

      動作します...

    ちなみに、これらのうちnoneはコンパイル時エラーをスローします。

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

    _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(...)-関数が実際にはBarとは無関係に_Object[]_を返すことに気付いたら意味があります。これが、BarStringとして設定されていても、(実行時に)例外を生成せずに戻り値を_String[]_に割り当てることができない理由です。ただし、最初に配列を_Object[]_にキャストし直さずに配列にインデックスを付けることができないのはなぜですか? _Baz<Bar>_-クラスで問題なく動作する理由は、_Bar[]_もBarとは無関係に_Object[]_に変換されるためです。したがって、これは、配列を_Object[]_にキャストし、インデックスを作成してから、返されたエントリをStringにキャストすることと同じです。

全体として、これを見た後、この方法を使用して配列を作成することは、一般的なクラスの外部に配列を戻さない限り、本当に悪い考えであると確信しています。私の目的では、配列の代わりに_Collection<...>_を使用します。

3
Markus A.

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

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

実行時型Object[]の配列を作成し、それをBar[]に「キャスト」しました。これは実際のチェックキャスト操作ではないため、引用符で囲んで「キャスト」と言います。これは、Object[]Bar[]型の変数に割り当てることができるコンパイル時のディレクティブです。当然、これにより、あらゆる種類のランタイムタイプエラーへの扉が開かれます。それが実際にエラーを引き起こすかどうかは、完全にあなたのプログラミングの腕前と注意力次第です。したがって、あなたがそれに満足しているなら、それをしても大丈夫です。そうでない場合、またはこのコードが多くの開発者がいる大規模なプロジェクトの一部である場合、それは危険なことです。

3
Marko Topolnik

キャスト:

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

実行時に発生する操作です。 Bar[]の実行時型がObject[]以外の場合、これによりClassCastExceptionが生成されます。

したがって、<Bar extends Something>のようにBarに境界を設定すると、失敗します。これは、Barの実行時型がSomethingになるためです。 Barに上限がない場合、その型はObjectに消去され、コンパイラーは、オブジェクトを配列に配置するため、または配列から読み取るために、関連するすべてのキャストを生成します。

実行時型がObject[]ではないもの(例:String[] z = bars)にbarsを割り当てようとすると、操作は失敗します。コンパイラは、このユースケースについて「チェックされていないキャスト」警告で警告します。したがって、警告付きでコンパイルしても、以下は失敗します。

class Foo<Bar> {
    Bar[] get() {
       return (Bar[])new Object[1];
    }
}
void test() {
    Foo<String> foo = new Foo<>();
    String[] z = foo.get();
}
0
cyon

実際の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();
} 

ジェネリックスと配列はうまく混ざり合わないため、使用を避けることをお勧めします。

0
m3th0dman