1. ホーム
  2. java

[解決済み] 汎用配列の作成方法 重複

2023-05-13 07:36:57

質問

ジェネリックと配列の関係がよくわからないのですが。

ジェネリック型で配列の参照を作ることができるのですが。

private E[] elements; //GOOD

しかし、ジェネリックタイプの配列オブジェクトを作成することができません。

elements = new E[10]; //ERROR

しかし、それは動作します。

elements = (E[]) new Object[10]; //GOOD

どのように解決するのですか?

配列とジェネリクスを混同してはいけません。両者は相性が悪いのです。配列とジェネリック型では、型チェックを行う方法に違いがあります。配列は再定義されると言いますが、ジェネリックはそうではありません。その結果、配列とジェネリクスで作業していると、このような違いが見えてくるのです。

配列は共変であり、ジェネリックはそうではない。

どういうことでしょうか?次の代入が有効であることは、もうお分かりでしょう。

Object[] arr = new String[10];

基本的には Object[] のスーパータイプです。 String[] の上位型であり、なぜなら Object のスーパータイプだからです。 String . これはジェネリックでは真ではありません。そのため、以下の宣言は無効であり、コンパイルされません。

List<Object> list = new ArrayList<String>(); // Will not compile.

理由は、ジェネリックが不変だからです。

型チェックを強制する。

ジェネリックは、コンパイル時の型チェックを強化するためにJavaに導入されました。そのため、ジェネリック型は、以下の理由により、実行時にいかなる型情報も持ちません。 型消去 . そのため List<String> は静的な型である List<String> という静的な型を持っていますが、動的な型は List .

しかし、配列は構成要素の型の実行時型情報を一緒に運びます。実行時に、配列はArray Store checkを使用して、実際の配列の型と互換性のある要素を挿入しているかどうかをチェックします。そのため、以下のようなコードになります。

Object[] arr = new String[10];
arr[0] = new Integer(10);

はコンパイルはうまくいきますが、ArrayStoreCheckの結果、実行時に失敗します。ジェネリックでは、これは不可能です。コンパイラは、上記のように、このような参照の作成を避けることによって、コンパイル時のチェックを提供し、実行時例外を防ごうとします。

では、ジェネリック配列の作成の問題点は何でしょうか?

構成要素の型が 型パラメータ , a 具体的なパラメタライズド型 または 境界付きワイルドカードパラメータ化された型 型安全でない .

以下のようなコードを考えてみましょう。

public <T> T[] getArray(int size) {
    T[] arr = new T[size];  // Suppose this was allowed for the time being.
    return arr;
}

の型は T の型は実行時には分からないので、作成される配列は実際には Object[] . ですから、実行時の上記のメソッドは次のようになります。

public Object[] getArray(int size) {
    Object[] arr = new Object[size];
    return arr;
}

では、このメソッドを次のように呼び出したとします。

Integer[] arr = getArray(10);

ここで問題です。今、あなたは Object[] の参照に Integer[] . 上記のコードはコンパイルはうまくいきますが、実行時に失敗します。

これが汎用配列の作成が禁止されている理由です。

なぜタイプキャストなのか new Object[10]E[] は動作しますか?

さて、最後の疑問ですが、なぜ以下のコードが動作するのでしょうか?

E[] elements = (E[]) new Object[10];

上記のコードは、上で説明したのと同じ意味合いを持っています。お気づきのように、コンパイラはあなたに チェックされていないキャストの警告 という警告が表示されます。つまり、実行時にキャストが失敗する可能性があるということです。例えば、上記のメソッドにそのようなコードがあった場合。

public <T> T[] getArray(int size) {
    T[] arr = (T[])new Object[size];        
    return arr;
}

で、このように呼び出す。

String[] arr = getArray(10);

のようにすると、実行時にClassCastExceptionが発生して失敗します。つまり、この方法は常に動作するわけではありません。

型の配列を作成するのはどうでしょうか? List<String>[] ?

問題は同じです。型消去のため List<String>[] は何もありませんが List[] . では、このような配列の作成が許されていた場合、どのようなことが起こりうるか見てみましょう。

List<String>[] strlistarr = new List<String>[10];  // Won't compile. but just consider it
Object[] objarr = strlistarr;    // this will be fine
objarr[0] = new ArrayList<Integer>(); // This should fail but succeeds.

これで、ArrayStoreExceptionが投げられるはずなのに、上記の場合のArrayStoreCheckは実行時に成功します。それは、両方の List<String>[]List<Integer>[] がコンパイルされると List[] にコンパイルされます。

では、境界のないワイルドカードのパラメタライズドタイプの配列を作成することができるのでしょうか?

はい、その理由は List<?> は再利用可能な型だからです。そしてそれは、型が全く関連付けられていないので、理にかなっています。つまり、型消去の結果として失うものは何もないのです。ですから、このような型の配列を作成することは完全に型安全です。

List<?>[] listArr = new List<?>[10];
listArr[0] = new ArrayList<String>();  // Fine.
listArr[1] = new ArrayList<Integer>(); // Fine

上記のケースはどちらも問題ありません。 List<?> は汎用型である List<E> . ですから、実行時にArrayStoreExceptionを発行することはありません。このケースはraw型の配列でも同じです。raw型も再利用可能な型であるため、配列を作成する際に List[] .

つまり、reifiableな型の配列しか作れず、non-reifiableな型の配列は作れないということになります。上記のすべてのケースで、配列の宣言は問題なく、配列の作成は new 演算子による配列の作成が問題となります。しかし、これらの参照型の配列を宣言しても意味がありません。なぜなら、これらの参照型は null ( 非束縛型は無視する ).

を回避する方法はありますか? E[] ?

はい、配列の作成は Array#newInstance() メソッドで作成できます。

public <E> E[] getArray(Class<E> clazz, int size) {
    @SuppressWarnings("unchecked")
    E[] arr = (E[]) Array.newInstance(clazz, size);

    return arr;
}

型キャストが必要なのは、このメソッドが Object . しかし、それが安全なキャストであることを確認することができます。ですから、その変数に@SuppressWarningsを使用することもできます。