※本記事は、“Curly Braces #3: Let’s have fun with Java arrays” の翻訳記事です。

2022年4月8日| 8分読む

Eric J. Bruno


洗練された配列開発には、リフレクション、ジェネリックス、ラムダが含まれる場合があります

 

最近、Cで開発している同僚と話をしました。配列の話となり、同僚はJavaの配列がCと比較してあまりに動作が違うことに驚きました。JavaはCと似た言語だと見なされていたからです。

ここでのJava配列の調査は、最初は単調ですが、すぐに興味深いものとなります、特にCを学習や使用しているなら。

 

配列の宣言

Javaのチュートリアルに従うと、配列を宣言する方法は2つあります。1つ目は単純です。

int[] array; // Java 配列宣言

Cとどのように異なるか見ていきましょう。ここでは、正しい構文は次のとおりです。

int array[]; // C 配列宣言

ここではJavaに焦点を当てますが、配列を宣言した後、それを割り当てる必要があります。

array = new int[10]; // Java 配列の割当て

1ステップで配列を宣言および初期化できるでしょうか。残念ながら次のようにショートカットして操作することはできません。

int[10] array; // 残念、エラー!

ただし、すでに値がわかっている場合は、1つのステップで配列を宣言および初期化できます。

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;

前述のコードを実行した場合、最初の配列要素を使用しようとするとすぐに例外が発生します。なぜでしょうか。配列は割り当てられますが、配列バケットにはそれぞれNULLオブジェクト参照が含まれます。このコードをIDEに入力すると、 .val がオートコンプリートされるため、混乱するようなエラーが発生する可能性があります。エラーを解決するには、次の手順を実行します。

SomeClass[] array = new SomeClass[10];
for ( int i = 0; i < array.length; i++ ) {  //新しいコード
    array[i] = new SomeClass();             //新しいコード
}                                           //新しいコード
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配列の涅槃(Nirvana)を見つける

ここでの目標は、純粋主義者となることではなく、エレガントにコーディングをすることです。ニルヴァーナの曲をもじって、スメルズ・ライク「クリーン・コード」スピリットって感じですね!そしてそのスピリットで、配列割当てパターンをきれいにするために再利用可能なコードを作成することに着手しました。これが最初の試みです。

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);
    }
}

「see this」とマークされた1行のコードはエレガントで、 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 クラス を使用し、指定したクラスの配列をインスタンス化することです。これは基本的に、目標に近づくための単一行コードの変更です。

public static Object[] toArray(Class cls, int size) throws Exception {
    Constructor ctor = cls.getConstructors()[0];
    Object array = Array.newInstance(cls, size);  // 新しいコード
    for ( int i = 0; i < size; i++ ) {
        Array.set(array, i, ctor.newInstance());  // 新しいコード
    }
    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); // エラー。キャストが必要...

前述のスニペットの3行目では、1行目を次のようにしないかぎり、エラーとなります。

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)   // limit メソッドの呼び出し
    .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について、プリミティブの配列の宣言と割当て、Object 要素の配列の割当て、リフレクションの使用、ジェネリックスの使用、ラムダの使用など、多くのことを調査しました。
何よりも私がそうしたのは、エレガントでコンパクトなJavaコードを書くという高潔な理由のためです。ミッション完了です!

最後に、お楽しみとして、Java Magazineで最近公開されたクイズに挑戦してみてください。

 

より詳しい情報