JavaRush /Java Blog /Random-JA /コーヒーブレイク#130。Java 配列を正しく操作する方法 - Oracle からのヒント

コーヒーブレイク#130。Java 配列を正しく操作する方法 - Oracle からのヒント

Random-JA グループに公開済み
出典: Oracle 配列の操作には、リフレクション、ジェネリック、ラムダ式を含めることができます。 最近、C で開発する同僚と話をしていました。会話は、配列と、C と比較した Java での配列の仕組みについてになりました。Java は C に似た言語とみなされているため、これは少し奇妙に感じました。実際には多くの類似点がありますが、相違点もあります。簡単に始めましょう。 コーヒーブレイク#130。 Java 配列を正しく操作する方法 - Oracle からのヒント - 1

配列宣言

Java チュートリアルに従うと、配列を宣言する 2 つの方法があることがわかります。最初のものは簡単です:
int[] array; // a Java array declaration
C とどのように異なるかがわかります。構文は次のとおりです。
int array[]; // a C array declaration
もう一度 Java に戻りましょう。配列を宣言した後、それを割り当てる必要があります。
array = new int[10]; // Java array allocation
配列の宣言と初期化を一度に行うことはできますか? 実は違う:
int[10] array; // NOPE, ERROR!
ただし、値がすでにわかっている場合は、すぐに配列を宣言して初期化できます。
int[] array = { 0, 1, 1, 2, 3, 5, 8 };
意味が分からなかったらどうする?int配列の宣言、割り当て、および使用で最も頻繁に使用されるコードは次のとおりです。
int[] array;
array = new int[10];
array[0] = 0;
array[1] = 1;
array[2] = 1;
array[3] = 2;
array[4] = 3;
array[5] = 5;
array[6] = 8;
...
Java プリミティブ データ型の配列であるint配列 を指定したことに注意してくださいプリミティブの代わりに Java オブジェクトの配列を使用して同じプロセスを試してみるとどうなるかを見てみましょう。
class SomeClass {
    int val;
    // …
}
SomeClass[] array = new SomeClass[10];
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
上記のコードを実行すると、配列の最初の要素を使用しようとした直後に例外が発生します。なぜ?配列が割り当てられていても、配列の各セグメントには空のオブジェクト参照が含まれています。このコードを IDE に入力すると、.val も自動的に入力されるため、エラーがわかりにくい可能性があります。バグを修正するには、次の手順に従います。
SomeClass[] array = new SomeClass[10];
for ( int i = 0; i < array.length; i++ ) {  //new code
    array[i] = new SomeClass();             //new code
}                                           //new code
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
しかし、それはエレガントではありません。配列と配列内のオブジェクトを、より少ないコード (場合によってはすべて 1 行) で簡単に割り当てることができないのはなぜだろうかと疑問に思いました。その答えを見つけるために、私はいくつかの実験を行いました。

Java 配列の中から最適な場所を見つける

私たちの目標は、エレガントにコーディングすることです。「クリーン コード」のルールに従って、配列割り当てパターンをクリーンアップするために再利用可能なコードを作成することにしました。最初の試みは次のとおりです。
public class MyArray {

    public static Object[] toArray(Class cls, int size)
      throws Exception {
        Constructor ctor = cls.getConstructors()[0];
        Object[] objects = new Object[size];
        for ( int i = 0; i < size; i++ ) {
            objects[i] = ctor.newInstance();
        }

        return objects;
    }

    public static void main(String[] args) throws Exception {
        SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32); // see this
        System.out.println(array1);
    }
}
「これを参照」とマークされたコード行は、toArray実装のおかげで、まさに私が望んでいたとおりに見えます。このアプローチでは、リフレクションを使用して、提供されたクラスのデフォルトのコンストラクターを見つけ、そのコンストラクターを呼び出してそのクラスのオブジェクトをインスタンス化します。このプロセスは、配列要素ごとにコンストラクターを 1 回呼び出します。素晴らしい!それが機能しないのはただ残念です。コードは正常にコンパイルされますが、実行時にClassCastExceptionエラーがスローされます。このコードを使用するには、 Object 要素の配列を作成し、次のよう に配列の各要素をSomeClassクラスにキャストする必要があります。
Object[] objects = MyArray.toArray(SomeClass.class, 32);
SomeClass scObj = (SomeClass)objects[0];
...
これはエレガントではありません!さらに実験を行った後、リフレクション、ジェネリック、ラムダ式を使用したいくつかのソリューションを開発しました。

解決策 1: リフレクションを使用する

ここでは、基本java.lang.Objectクラスを使用する代わりに、java.lang.reflect.Arrayクラスを使用して、指定したクラスの配列をインスタンス化しています。これは基本的に 1 行のコード変更です。
public static Object[] toArray(Class cls, int size) throws Exception {
    Constructor ctor = cls.getConstructors()[0];
    Object array = Array.newInstance(cls, size);  // new code
    for ( int i = 0; i < size; i++ ) {
        Array.set(array, i, ctor.newInstance());  // new code
    }
    return (Object[])array;
}
このアプローチを使用して、目的のクラスの配列を取得し、次のように操作できます。
SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32);
これは必須の変更ではありませんが、2 行目は Array リフレクション クラスを使用して各配列要素の内容を設定するように変更されています。これは素晴らしいです!しかし、完全に正しくないと思われる詳細がもう 1 つあります。SomeClass []へのキャストがあまり良くないようです。幸いなことに、ジェネリック医薬品を使用した解決策があります。

解決策 2: ジェネリックを使用する

Collectionsフレームワークは型バインディングにジェネリックスを使用し、その操作の多くでジェネリックスへのキャストを排除します。ここではジェネリックも使用できます。java.util.List を例に考えてみましょう。
List list = new ArrayList();
list.add( new SomeClass() );
SomeClass sc = list.get(0); // Error, needs a cast unless...
上のスニペットの 3 行目は、最初の行を次のように更新しない限り、エラーをスローします。
List<SomeClass> = new ArrayList();
MyArrayクラス でジェネリックを使用しても、同じ結果を得ることができます。新しいバージョンは次のとおりです。
public class MyArray<E> {
    public <E> E[] toArray(Class cls, int size) throws Exception {
        E[] array = (E[])Array.newInstance(cls, size);
        Constructor ctor = cls.getConstructors()[0];
        for ( int element = 0; element < array.length; element++ ) {
            Array.set(array, element, ctor.newInstance());
        }
        return arrayOfGenericType;
    }
}
// ...
MyArray<SomeClass> a1 = new MyArray(SomeClass.class, 32);
SomeClass[] array1 = a1.toArray();
見た目も良いですね。ジェネリックスを使用し、宣言にターゲットの型を含めることにより、その型を他の操作で推論できます。さらに、次のようにすることで、このコードを 1 行に減らすことができます。
SomeClass[] array = new MyArray<SomeClass>(SomeClass.class, 32).toArray();
ミッションは達成されましたね?まあ、完全ではありません。どのクラス コンストラクターを呼び出すかを気にしない場合はこれで問題ありませんが、特定のコンストラクターを呼び出したい場合は、この解決策は機能しません。この問題を解決するためにリフレクションを使用し続けることもできますが、コードが複雑になってしまいます。幸いなことに、別の解決策を提供するラムダ式があります。

解決策 3: ラムダ式を使用する

正直に言うと、以前はラムダ式にあまり興味がありませんでしたが、ラムダ式の良さを理解するようになりました。特に、オブジェクトのコレクションを処理するjava.util.stream.Streamインターフェイスが気に入りました。Streamのおかげで、Java 配列の境地に到達することができました。これがラムダを使用する最初の試みです。
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .toArray(SomeClass[]::new);
読みやすくするために、このコードを 3 行に分割しました。これはすべての条件を満たしていることがわかります。シンプルかつエレガントで、オブジェクト インスタンスの値が設定された配列を作成し、特定のコンストラクターを呼び出すことができます。toArrayメソッドのパラメータSomeClass []::newに注意してください。これは、指定された型の配列を割り当てるために使用されるジェネレーター関数です。ただし、現状では、このコードには小さな問題があります。それは、無限サイズの配列を作成するということです。これはあまり最適ではありません。しかし、この問題は、 limitメソッドを呼び出すことで解決できます。
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .limit(32)   // calling the limit method
    .toArray(SomeClass[]::new);
配列の要素は 32 個に制限されました。以下に示すように、配列の各要素に特定のオブジェクト値を設定することもできます。
SomeClass[] array = Stream.generate(() -> {
    SomeClass result = new SomeClass();
    result.val = 16;
    return result;
    })
    .limit(32)
    .toArray(SomeClass[]::new);
このコードはラムダ式の威力を示していますが、コードはすっきりしていなく、コンパクトでもありません。私の意見では、別のコンストラクターを呼び出して値を設定する方がはるかに優れています。
SomeClass[] array6 = Stream.generate( () -> new SomeClass(16) )
    .limit(32)
    .toArray(SomeClass[]::new);
私はラムダ式ベースのソリューションが好きです。特定のコンストラクターを呼び出したり、配列の各要素を操作したりする必要がある場合に最適です。もっとシンプルなものが必要な場合、私は通常、ジェネリクスベースのソリューションを使用します。その方がシンプルだからです。ただし、ラムダ式がエレガントで柔軟なソリューションを提供することが自分の目でわかります。

結論

今日は、Java で リフレクション、ジェネリックス、ラムダ式を使用して、プリミティブの配列の宣言と割り当て、オブジェクト要素の配列の割り当てを行う方法を学びました。
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION