Java
 Computer >> コンピューター >  >> プログラミング >> Java

Javaジェネリクス入門:基本概念から実践的な使い方まで徹底解説

ジェネリクス(Generics)は、Java言語における最も重要な機能のひとつです。その背後にある考え方は非常にシンプルですが、通常の構文とは異なる記法が使われるため、一見複雑に感じられることがあります。

本チュートリアルでは、この便利なジェネリクスの概念を、初心者にもわかりやすい形で解説していきます。

ジェネリクスそのものに入る前に、そもそもなぜJavaにジェネリクスが必要だったのかを見ていきましょう。

Javaジェネリクスの目的

Java 5でジェネリクスが導入される以前は、以下のようなコードをエラーや警告なしに書いてコンパイルすることができました。

List list = new ArrayList();
list.add("hey");
list.add(new Object());

リストなどのJavaコレクションには、格納するデータ型を宣言することなく、任意の型の値を追加できました。しかし、リストから値を取り出す際には、明示的に特定の型へキャストする必要がありました。

上記のリストを反復処理する場合を考えてみましょう。

for (int i=0; i< list.size(); i++) {
    String value = (String) list.get(i);  // i=1のときClassCastExceptionが発生
}

このように、格納するデータ型を事前に宣言せずにリストを作成できると、プログラマーが上記のようなミスを犯し、実行時にClassCastExceptionがスローされる結果になりかねません。

ジェネリクスは、まさにこの種のミスを防ぐために導入されました。

ジェネリクスを使えば、次の例のように、Javaコレクションを作成する際に格納されるデータ型を明示的に宣言できます。

注意:
格納するデータ型を指定せずにJavaコレクションオブジェクトを作成することは今でも可能ですが、推奨されません。

List<String> stringList = new ArrayList<>();

こうすることで、String型のリストに誤ってIntegerを格納しようとした場合にコンパイル時エラーが発生するため、プログラムが実行時エラーに陥るのを未然に防げます。

stringList.add(new Integer(4)); // コンパイル時エラー

Javaにジェネリクスが導入された主な目的は、実行時にClassCastExceptionが発生するのを回避することにありました。

Javaジェネリクスの作成方法

ジェネリクスは、Javaのクラスやメソッドの作成に活用できます。それぞれの作成例を見ていきましょう。

ジェネリッククラス

ジェネリッククラスを作成する際は、クラス名の末尾に山括弧 <> で囲んだ型パラメータを追加します。

public class GenericClass<T> {
    private T item;
    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return this.item;
    }
}

ここで、T がデータ型パラメータです。Javaの慣習では、データ型パラメータとして TNE などの文字がよく使われます。

上記の例では、GenericClassオブジェクトを作成するときに、具体的なデータ型を渡すことができます。

public static void main(String[] args) {

    GenericClass<String> gc1 = new GenericClass<>();
    gc1.setItem("hello");
    String item1 = gc1.getItem(); // "hello"
    gc1.setItem(new Object()); // エラー

    GenericClass<Integer> gc2 = new GenericClass<>();
    gc2.setItem(new Integer(1));
    Integer item2 = gc2.getItem(); // 1
    gc2.setItem("hello"); // エラー
}

ジェネリッククラスのオブジェクトを作成する際、型パラメータにプリミティブ型を渡すことはできません。Object型を継承するデータ型のみが、型パラメータとして指定可能です。

例:

GenericClass<float> gc3 = new GenericClass<>(); // エラー

ジェネリックメソッド

ジェネリックメソッドの作成は、ジェネリッククラスとほぼ同じパターンに従います。ジェネリッククラスの中でも、非ジェネリッククラスの中でも、ジェネリックメソッドを実装できます。

public class GenericMethodClass {

    public static <T> void printItems(T[] arr){
        for (int i=0; i< arr.length; i++) {
            System.out.println(arr[i]);
        }
    }

    public static void main(String[] args) {
        String[] arr1 = {"Cat", "Dog", "Mouse"};
        Integer[] arr2 = {1, 2, 3};

        GenericMethodClass.printItems(arr1); // "Cat", "Dog", "Mouse"
        GenericMethodClass.printItems(arr2); // 1, 2, 3
    }
}

ここでは、特定の型の配列を渡してメソッドをパラメータ化できます。ジェネリックメソッド printItems() は、渡された配列を反復処理し、通常のJavaメソッドと同じように格納された要素を出力します。

境界型パラメータ(Bounded Type Parameters)

これまでに作成したジェネリッククラスやメソッドは、プリミティブ型以外の任意のデータ型でパラメータ化できます。しかし、ジェネリクスに渡せるデータ型を制限したい場合はどうすればよいのでしょうか? そこで役立つのが「境界型パラメータ」です。

別のデータ型のサブクラスであることを指定することで、ジェネリッククラスやメソッドが受け入れるデータ型に境界(上限)を設けられます。

例:

// Listのサブクラスのみを受け入れる
public class UpperBoundedClass<T extends List>{
    // Listのサブクラスのみを受け入れる
    public <T extends List> void UpperBoundedMethod(T[] arr) {
    }
}

この場合、UpperBoundedClassUpperBoundedMethod は、List データ型のサブタイプでのみパラメータ化できます。

List 型は、型パラメータの上限(アッパーバウンド)として機能します。List のサブタイプではないデータ型を使用しようとすると、コンパイル時エラーがスローされます。

境界として指定できるのはクラスだけではなく、インターフェースも渡せます。この場合、「extends」とはインターフェースを実装することを意味します。

さらに、次の例のように、1つのパラメータに複数の境界を設定することもできます。

// MammalとAnimalの両方のサブクラスのみを受け入れる
public class MultipleBoundedClass<T extends Mammal & Animal>{

    // MammalとAnimalの両方のサブクラスのみを受け入れる
    public <T extends Mammal & Animal> void MultipleBoundedMethod(T[] arr){

    }
}

受け入れられるデータ型は、AnimalクラスとMammalクラスの両方のサブクラスである必要があります。境界のうち1つがクラスである場合、そのクラスは境界宣言の先頭に置かなければなりません。

上記の例で、MammalがクラスでAnimalがインターフェースである場合、Mammalを先頭に記述する必要があります。順序を守らないと、コードはコンパイル時エラーをスローします。

Javaジェネリクスのワイルドカード

ワイルドカードは、ジェネリック型のパラメータをメソッドに渡すために使われます。ジェネリックメソッドとは異なり、ここではジェネリックなパラメータが、メソッドが受け入れる引数に対して渡される点が特徴です。これは、これまで説明してきたデータ型パラメータとは異なる仕組みです。ワイルドカードは ? 記号で表されます。

public void printItems(List<?> list) {
    for (int i=0; i< list.size(); i++) {
        System.out.println(list.get(i));
    }
}

上記の printItems() メソッドは、任意のデータ型のリストを引数として受け入れます。これにより、ジェネリクスがない場合に必要となる、データ型ごとのリスト向けのコード重複を避けられます。

上限境界ワイルドカード(Upper Bounded Wildcards)

メソッドが受け入れるリストに格納されるデータ型を制限したい場合は、境界付きワイルドカードを使用できます。

例:

public void printSubTypes(List<? extends Color> list) {
    for (int i=0; i< list.size(); i++) {
        System.out.println(list.get(i));
    }
}

printSubTypes() メソッドは、Colorのサブタイプを格納するリストのみを受け入れます。RedColorやBlueColorオブジェクトのリストは受け入れますが、Animalオブジェクトのリストは受け入れません。AnimalはColorのサブタイプではないためです。これが上限境界ワイルドカードの例です。

下限境界ワイルドカード(Lower Bounded Wildcards)

同様に、次のようなメソッドがあるとします。

public void printSuperTypes(List<? super Dog> list) {
    for (int i=0; i< list.size(); i++) {
        System.out.println(list.get(i));
    }
}

この printSuperTypes() メソッドは、Dogクラスのスーパータイプを格納するリストのみを受け入れます。MammalやAnimalオブジェクトのリストは受け入れますが、LabDogオブジェクトのリストは受け入れません。LabDogはDogのスーパークラスではなくサブクラスだからです。これが下限境界ワイルドカードの例です。

まとめ

Javaジェネリクスは、その導入以来、プログラマーにとって欠かせない機能となりました。

この人気の理由は、プログラマーの開発作業を大きく楽にしてくれる点にあります。コーディングミスを防ぐだけでなく、ジェネリクスを活用することでコードの重複も削減できます。異なるデータ型ごとに同じようなコードを何度も書かなくて済むよう、クラスやメソッドを汎用化できることに気づいたでしょうか?

ジェネリクスをしっかりと理解することは、Javaのエキスパートを目指すうえで非常に重要です。まずは、本チュートリアルで学んだ内容を実際のコードに応用してみることが、次のステップへの近道となるでしょう。

  1. Googleアシスタントのルーチン機能とは?使い方とカスタム設定方法を徹底解説

    Googleアシスタントの「ルーチン」は、特定の言葉を話しかけるだけで、あらかじめ設定した一連の操作を自動的に実行してくれる便利な機能です。すぐに使える6種類の既製ルーチンが用意されているので、ほとんど手間をかけずに活用を始められます。さらに、自分好みにカスタマイズすれば、Googleアシスタントが対応しているあらゆる操作を組み合わせたオリジナルルーチンを作ることも可能です。この記事では、6つの既製ルーチンの概要と設定内容を紹介したうえで、日常を自動化するオリジナルルーチンの作り方まで、わかりやすく解説していきます。Googleアシスタントのルーチン画面へのアクセス方法スマートフォンでGoog

  2. Windows Sysinternalsとは?概要から使い方まで徹底解説

    自分のWindowsパソコンを完全にコントロールできたら素晴らしいと思ったことはありませんか?ほぼすべてのWindowsプロセスやアプリケーションの内部を覗き見できるだけでなく、アプリがアクセスしているファイルやレジストリエントリをリアルタイムで確認できるのは、実に魅力的な機能です。 Sysinternalsは、最も信頼性が高く強力なWindowsシステム管理ツールキットのひとつです。そのほとんどのユーティリティはシンプルなEXEファイルなので、USBフラッシュドライブに入れておけば、持ち運び可能なシステム管理ツールとしていつでも活用できます。 Windows Sysinternalsとは?