Generics

ジェネリクスとは

Javaなどのオブジェクト指向言語では、様々なデータ型に対して共通の処理を行いたい場合があります。 ジェネリクスは、特定の型に依存しない汎用的なクラスやメソッドを定義するための仕組みです。 これによって、プログラムの安全性を高める「型安全性」を確保することができます。

1. ジェネリクスがない場合の課題

ジェネリクスを使用せずに、様々なデータ型を格納できる汎用的なクラスを作成することを考えます。 Javaにおける全てのクラスの親クラスである Object 型を使用すると、あらゆるオブジェクトを格納できるクラスを作成できます。 プログラムは以下のようになります。

ObjectBox.java

class ObjectBox {
  private Object data;

  public void set(Object data) {
    this.data = data;
  }

  public Object get() {
    return this.data;
  }
}

この ObjectBox クラスを利用して、String 型のデータを格納し、取り出すプログラムは以下のようになります。

void setup() {
  ObjectBox box = new ObjectBox();
  box.set("Hello");

  // Object型からString型へのキャストが必要
  String text = (String) box.get();
  println(text);
}

実行結果

Hello

ObjectBoxget メソッドの戻り値は Object 型であるため、元の String 型として扱うには明示的なキャストを行う必要があります。 これには、次の2つの課題があります。

  • 値を取り出すたびにキャストを記述しなければならず、プログラムが煩雑になります。
  • 誤った型にキャストしてしまう危険性があります。

例えば、ObjectBox に文字列を格納したことを忘れ、整数型として取り出そうとするプログラムは以下のようになります。

void setup() {
  ObjectBox box = new ObjectBox();
  box.set("Hello");

  // コンパイルは通るが、実行時にエラーが発生する
  Integer num = (Integer) box.get();
  println(num);
}

実行結果

ClassCastException: java.lang.String cannot be cast to java.lang.Integer

このプログラムは、コンパイルは正常に成功します。 しかし、実行すると ClassCastException というエラーが発生してプログラムが強制終了してしまいます。 このように、実行するまでバグに気付けない状態は、開発において非常に危険です。

2. ジェネリクスによる解決

これらの課題を解決するために導入されたのがジェネリクスです。 クラスの定義時に、具体的な型の代わりに型パラメータを使用します。 型パラメータの名前は一般的に TE などの一文字で表されます。 プログラムは以下のようになります。

Box.java

class Box {
  private T data;

  public void set(T data) {
    this.data = data;
  }

  public T get() {
    return this.data;
  }
}

この Box クラスを利用するプログラムは以下のようになります。

void setup() {
  // String型を扱うBoxインスタンスを生成する
  Box box = new Box();
  box.set("Hello");

  // キャストなしでString型として取り出せる
  String text = box.get();
  println(text);
}

実行結果

Hello

ジェネリクスを使用すると、インスタンス化するときに <String> のように具体的な型を指定します。 これにより、以下のメリットが得られます。

  • コンパイラが get メソッドの戻り値を String 型であると認識するため、キャストが不要になります。
  • 指定した型とは異なる型のデータを追加しようとすると、コンパイルエラーになります。

例えば、Box<String> に整数を追加しようとするプログラムは以下のようになります。

void setup() {
  Box box = new Box();
  // box.set(100); // エラー:String型ではないためコンパイルエラーになる
}

型が一致しない場合はコンパイル時点でエラーを検知できるため、実行時に予期せぬエラーで終了するリスクを未然に防ぐことができます。 これが、ジェネリクスによる型安全性の確保です。

コレクションフレームワーク

Javaには、複数のオブジェクトを効率的に扱うための仕組みとして コレクションフレームワーク が用意されています。 その代表例が、要素数が自動的に伸縮する動的配列を表現する ArrayList クラスです。 ArrayList もジェネリクスを使用して設計されており、格納する要素の型を指定して利用します。 プログラムは以下のようになります。

import java.util.ArrayList;

void setup() {
  // String型を格納するArrayListを生成する
  ArrayList fruits = new ArrayList();

  // 要素の追加
  fruits.add("Apple");
  fruits.add("Banana");
  fruits.add("Orange");

  // インデックスを指定した要素の取得
  String first = fruits.get(0);
  println("最初のフルーツ: " + first);

  // 拡張for文を用いた全要素の出力
  for (String fruit : fruits) {
    println(fruit);
  }
}

実行結果

最初のフルーツ: Apple
Apple
Banana
Orange

もし、ジェネリクスを使用せずに ArrayList を生成すると、内部のデータは Object 型として扱われます。 その場合、値の取り出し時にキャストが必要になるため、特別な理由がない限りは必ず型パラメータを指定して使用しましょう。

Optionalとジェネリクス

プログラムにおいて、値が存在しない状態を表現するために null がよく使われます。 しかし、null が返される可能性のあるオブジェクトに対して適切なチェックを行わずにメソッドなどを呼び出すと、 NullPointerException が発生してプログラムが停止します。 プログラムは以下のようになります。

String findUser(int id) {
  if (id == 1) {
    return "Alice";
  }
  return null; // 該当するユーザーがいない場合にnullを返す
}

void setup() {
  String user = this.findUser(2);

  // nullチェックを忘れて操作を行うとエラーになる
  println(user.toUpperCase()); 
}

実行結果

NullPointerException

この課題を解決するために、値が存在するかどうかを明示的に表現する Optional<T> クラスが用意されています。 Optional<T> もジェネリクスを使用しており、包み込む値の型を型パラメータ T で指定します。 プログラムは以下のようになります。

import java.util.Optional;

Optional findUser(int id) {
  if (id == 1) {
    return Optional.of("Alice");
  }
  return Optional.empty(); // 値が存在しない状態を表す空のOptionalを返す
}

void setup() {
  Optional userOpt = this.findUser(2);

  // 値が存在するかどうかを確認する
  if (userOpt.isPresent()) {
    // 値が存在する場合のみ、get()メソッドで取り出して処理する
    println(userOpt.get().toUpperCase());
  } else {
    println("ユーザーが見つかりませんでした。");
  }

  // もしくは、orElse()メソッドを使用して値が存在しない場合のデフォルト値を指定する
  String name = userOpt.orElse("ゲスト");
  println("ログインユーザー: " + name);
}

実行結果

ユーザーが見つかりませんでした。
ログインユーザー: ゲスト

Optional を用いることで、「値が返されない可能性がある」という事実がメソッドの戻り値の型として明示されます。 これによって、開発者は null のチェック漏れを回避しやすくなり、より安全なプログラムを記述できます。

ソートとジェネリクス

ジェネリクスは、コレクション内の要素をソートする際にも重要な役割を果たします。 Javaでオブジェクトをソートするためには、要素同士を比較する方法が定義されていなければなりません。 その比較ルールを定義するために用いられるのが Comparable<T> インターフェースです。 Comparable<T> の型パラメータ T には、比較対象の型を指定します。

idnameを持つ Student クラスを定義し、idの昇順でソートできるように Comparable<Student> インターフェースを実装するプログラムは以下のようになります。

Student.java

class Student implements Comparable {
  private int id;
  private String name;

  public Student(int id, String name) {
    this.id = id;
    this.name = name;
  }

  public int getId() {
    return this.id;
  }

  public String getName() {
    return this.name;
  }

  // ComparableインターフェースのcompareToメソッドを実装する
  @Override
  public int compareTo(Student other) {
    // 自身の学籍番号と相手の学籍番号を比較する
    // 自身が小さければ負の整数、等しければ0、大きければ正の整数を返す
    return this.id - other.getId();
  }
}

この Student クラスのリストを作成し、ソートを行って結果を出力するプログラムは以下のようになります。

import java.util.ArrayList;
import java.util.Collections;

void setup() {
  ArrayList students = new ArrayList();
  students.add(new Student(3, "Charlie"));
  students.add(new Student(1, "Alice"));
  students.add(new Student(2, "Bob"));

  println("--- ソート前 ---");
  for (Student s : students) {
    println(s.getId() + ": " + s.getName());
  }

  // Collections.sortを用いてソートを実行する
  Collections.sort(students);

  println("--- ソート後 ---");
  for (Student s : students) {
    println(s.getId() + ": " + s.getName());
  }
}

実行結果

--- ソート前 ---
3: Charlie
1: Alice
2: Bob
--- ソート後 ---
1: Alice
2: Bob
3: Charlie

ソートを実行している Collections.sort メソッドの定義は、ジェネリクスを用いて以下のように宣言されています。

public static > void sort(List list)

この複雑に見える定義は、以下のことをコンパイル時に保証しています。

  • リストに格納されている要素の型 T が、Comparable インターフェースを実装していること。
  • Comparable の型パラメータが、T またはその親クラスであること。

もし、比較ルールが定義されていないクラスのリストを Collections.sort に渡そうとすると、コンパイルエラーが発生します。 このように、ジェネリクスを使用することで、ソート可能な型だけを安全にソートの対象にすることができます。

演習

演習1

ジェネリクスを使用しない場合に発生する問題点と、ジェネリクスを使用することでどのように解決されるかを説明しなさい。 その際、コンパイルエラーと実行時エラーの違いに言及すること。

演習2

キーと値を保持するためのジェネリッククラス Pair<K, V> を作成しなさい。 このクラスは、型パラメータ K のキーと、型パラメータ V の値を保持するものである。 以下の setup 関数が正しく動作するようにクラスを設計しなさい。

void setup() {
  Pair pair = new Pair("Age", 20);

  println("Key: " + pair.getKey());
  println("Value: " + pair.getValue());
}

実行結果

Key: Age
Value: 20

演習3

引数として整数の配列 array と検索対象の値 target を受け取り、配列の中から target と一致する要素のインデックスを返すメソッド indexOf を作成しなさい。 ただし、要素が見つからない可能性があるため、戻り値の型は Optional<Integer> としなさい。 また、以下の setup 関数を用いて、値が見つかった場合と見つからなかった場合の双方の処理が正しく動作することを確認しなさい。

import java.util.Optional;

// ここにindexOfメソッドを定義する

void setup() {
  int[] numbers = {10, 20, 30, 40, 50};

  Optional index1 = indexOf(numbers, 30);
  if (index1.isPresent()) {
    println("30が見つかったインデックス: " + index1.get());
  } else {
    println("30は見つかりませんでした。");
  }

  Optional index2 = indexOf(numbers, 100);
  if (index2.isPresent()) {
    println("100が見つかったインデックス: " + index2.get());
  } else {
    println("100は見つかりませんでした。");
  }
}

実行結果

30が見つかったインデックス: 2
100は見つかりませんでした。

演習4

x座標とy座標を持つ Point クラスを作成しなさい。 このクラスは、原点 (0, 0) からの距離が近い順にソートできるように、Comparable<Point> インターフェースを実装するものとする。 以下の setup 関数が正しく動作するように Point クラスを設計しなさい。

import java.util.ArrayList;
import java.util.Collections;

void setup() {
  ArrayList points = new ArrayList();
  points.add(new Point(3, 4));  // 距離の2乗 = 9 + 16 = 25
  points.add(new Point(1, 1));  // 距離の2乗 = 1 + 1 = 2
  points.add(new Point(0, 2));  // 距離の2乗 = 0 + 4 = 4

  println("--- ソート前 ---");
  for (Point p : points) {
    println("(" + p.getX() + ", " + p.getY() + ")");
  }

  Collections.sort(points);

  println("--- ソート後 ---");
  for (Point p : points) {
    println("(" + p.getX() + ", " + p.getY() + ")");
  }
}

実行結果

--- ソート前 ---
(3, 4)
(1, 1)
(0, 2)
--- ソート後 ---
(1, 1)
(0, 2)
(3, 4)

演習の解答例

演習1の解答例

問題点: Object型を用いたクラスでは、値を取り出す際に明示的なキャストが必要となり、プログラムが複雑になる。 また、誤った型へのキャストを行った場合、コンパイルは成功するものの、実行時に ClassCastException が発生してプログラムが異常終了してしまう。

解決策: ジェネリクスを使用すると、インスタンス化の際に具体的な型を指定するため、値を取り出す際のキャストが不要になる。 さらに、指定された型と異なる値を渡そうとした場合には、コンパイル時にエラーを検知してビルドを阻止するため、実行時の安全性が高まる。

演習2の解答例

型パラメータ KV を用いて、異なる複数の型を安全に保持できる Pair クラスの実装例は以下の通りである。 クラス定義の際にカンマ区切りで複数の型パラメータを指定し、それらをフィールドの型やメソッドの戻り値・引数の型として利用している。

Pair.java

class Pair {
  private K key;
  private V value;

  public Pair(K key, V value) {
    this.key = key;
    this.value = value;
  }

  public K getKey() {
    return this.key;
  }

  public V getValue() {
    return this.value;
  }
}

演習3の解答例

配列の中から指定された値を探索し、結果を Optional<Integer> で返す indexOf メソッドの実装例は以下の通りである。 繰り返し処理を用いて配列の要素を先頭から確認し、一致する値が見つかった場合は Optional.of(i) を返し、最後まで見つからなかった場合は Optional.empty() を返している。

search.pde

import java.util.Optional;

Optional indexOf(int[] array, int target) {
  for (int i = 0; i < array.length; i++) {
    if (array[i] == target) {
      return Optional.of(i);
    }
  }
  return Optional.empty();
}

演習4の解答例

原点からの距離に基づいて比較を行うため、Comparable<Point> インターフェースを実装した Point クラスの実装例は以下の通りである。 compareTo メソッド内で原点からの距離の2乗を計算し、自身の距離と相手の距離の差を戻り値として返すことで、昇順でのソートを実現している。

Point.java

class Point implements Comparable {
  private int x;
  private int y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public int getX() {
    return this.x;
  }

  public int getY() {
    return this.y;
  }

  private int distanceSq() {
    return this.x * this.x + this.y * this.y;
  }

  @Override
  public int compareTo(Point other) {
    return this.distanceSq() - other.distanceSq();
  }
}

results matching ""

    No results matching ""