[Java] 17. ジェネリックタイプ(Generic type)を使う方法


Study / Java    作成日付 : 2019/08/27 19:05:44   修正日付 : 2021/01/21 11:15:49

こんにちは。明月です。


この投稿はJavaのジェネリックタイプ(Generic type)を使う方法に関する説明です。


クラスの内部メンバー変数を設定する時に、データタイプと変数名を作成します。

しかし、メンバー変数のデータタイプをクラス内部で設定することではなく、クラスをインスタンス生成(割り当て)する時に決めたい時があります。

// リンクスタッククラス。
public class LinkedStack {
  // 内部Nodeクラス
  class Node {
    // データを格納変数
    int data;
    // 次のポインタ
    Node next;
    // コンストラクタ
    Node(int data, Node next) {
      // メンバー変数設定
      this.data = data;
      this.next = next;
    }
  }
  // 現在のNodeポインタ
  private Node pointer = null;
  // add関数はデータを格納する。
  public void add(int data) {
    // 現在のポインタにNodeのインスタンスを生成して格納する。
    // 次のポインタは以前のポインタに連結する。
    pointer = new Node(data, pointer);
  }
  // pop関数は現在ポインタのNodeの値をリターンする。
  public int pop() {
    // ポインタがnullの場合はnull Exceptionエラーを発生する。
    if (pointer == null) {
      // null例外処理
      throw new NullPointerException();
    }
    try {
      // 現在ポインタのNode値をリターンする。
      return pointer.data;
    } finally {
      // ポインタを以前のNodeインスタンスに移動
      pointer = pointer.next;
    }
  }
  // 現在ポインタがnullかを確認する関数
  public boolean isNull() {
    return pointer == null;
  }
  // 実行関数
  public static void main(String... args) {
    // LinkedStackインスタンスを生成
    LinkedStack ex = new LinkedStack();
    // 値を入れる。
    ex.add(10); // debug : 10*
    ex.add(20); // debug : 20* 10
    ex.add(30); // debug : 30* 20 10
    ex.add(40); // debug : 40* 30 20 10
    // 値を出力する。
    while(!ex.isNull()) {
      // コンソールに出力
      System.out.println("ex " + ex.pop());
    }
  }
}


上の例を単純なリンクスタックアルゴリズムです。リンクスタックアルゴリズムはリストやマップみたいにデータを挿入して取り出して出力します。

リストと差異があればaddで格納したら逆順でpop関数でデータを取り出すことです。


上の例はint型のデータタイプしか使えません。でも、仕様によってStringタイプも使いたいです。その場合は内部クラスのNodeのメンバー変数のデータタイプとadd関数のパラメータ、pop関数のリターンタイプを変更してソースをコピペするしかないです。

あるいはデータタイプをObjectタイプにしてもよいです。でもObjectタイプに使うとデータタイプの整合性の確認ができなくなります。継承する方法もあります。


でも、簡単に考えるとそのデータタイプをインスタンスを生成する時に決めることにできれば簡単に解決します。その機能がジェネリックタイプです。

ジェネリックは薬のジェネリックと同じ意味で枠だけ作っておいて内部のタイプは外部から設定するという意味です。

// クラスの隣の<T>はジェネリックタイプという意味でまだ決まってないデータタイプだ。
public class LinkedStack<T> {
  // 内部Nodeクラス
  class Node {
    // データを格納変数(データタイプをジェネリックによって決める)
    T data;
    // 次のポインタ
    Node next;
    // コンストラクタ(パラメータのデータタイプはジェネリックによって決める)
    Node(T data, Node next) {
      // メンバー変数設定
      this.data = data;
      this.next = next;
    }
  }
  // 現在のNodeポインタ
  private Node pointer = null;
  // add関数はデータを格納する。(パラメータのデータタイプはジェネリックによって決める)
  public void add(T data) {
    // 現在のポインタにNodeのインスタンスを生成して格納する。
    // 次のポインタは以前のポインタに連結する。
    pointer = new Node(data, pointer);
  }
  // pop関数は現在ポインタのNodeの値をリターンする。(リターンのデータタイプはジェネリックによって決める)
  public T pop() {
    // ポインタがnullの場合はnull Exceptionエラーを発生する。
    if (pointer == null) {
      // null例外処理
      throw new NullPointerException();
    }
    try {
      // 現在ポインタのNode値をリターンする。
      return pointer.data;
    } finally {
      // ポインタを以前のNodeインスタンスに移動
      pointer = pointer.next;
    }
  }
  // 現在ポインタがnullかを確認する関数
  public boolean isNull() {
    return pointer == null;
  }
  // 実行関数
  public static void main(String... args) {
    // LinkedStackインスタンスを生成
    // 内部ジェネリックデータタイプはIntegerタイプに設定
    LinkedStack<Integer> ex = new LinkedStack<>();
    // 値を入れる。
    ex.add(10); // debug : 10*
    ex.add(20); // debug : 20* 10
    ex.add(30); // debug : 30* 20 10
    ex.add(40); // debug : 40* 30 20 10
    // 値を出力する。
    while (!ex.isNull()) {
      // コンソールに出力
      System.out.println("ex " + ex.pop());
    }
    // 内部ジェネリックデータタイプはStringタイプに設定
    LinkedStack<String> ex2 = new LinkedStack<>();
    // 値を入れる。
    ex2.add("a"); // debug : a*
    ex2.add("b"); // debug : b* a
    ex2.add("c"); // debug : c* b a
    // 値を出力する。
    while (!ex2.isNull()) {
      // コンソールに出力
      System.out.println("ex2 - " + ex2.pop());
    }
  }
}


上のジェネリックタイプをみれば以前のListやMapでもよく見たことです。

link - [Java] 5. 配列とリスト(List)、マップ(Map)の使い方


ジェネリックはクラスだけではなく、インタフェースや関数などでも使えます。

// ジェネリックを利用したインタフェース
interface Callable<V> {
  V call();
}
// インタフェースを継承、ジェネリックタイプにStringを設定
class Test implements Callable<String> {
  // 再定義する時にリターン値をStringに設定する。
  @Override
  public String call() {
    return "Hello world";
  }
}
public class Example {
  // ジェネリック関数、ジェネリック関数はリターンタイプの前にジェネリックを宣言する。
  public static <T> T test(Callable<T> func) {
    // インタフェースによるcall関数を呼び出す。
    return func.call();
  }

  // 実行関数
  public static void main(String... args) {
    // Testインスタンスを生成
    Test test = new Test();
    // TestクラスはジェネリックがStringタイプなのでStringタイプの結果がリターンする。
    String data = test(test);
    // コンソールに出力
    System.out.println("Test - " + data);
  }
}


インタフェースにジェネリックを宣言して継承したTestクラスでジェネリックタイプをStringに設定しました。そして関数にはインタフェースのジェネリックタイプでリターン値が設定されます。

なので、test関数を使う時には別にジェネリックタイプを設定しなくてよいです。


ジェネリックは基本的にObjectタイプですべてのクラスを設定することができますが、その設定する範囲も設定することができます。

// 一般Subクラス
class Sub {
  // メンバー変数
  private int data;
  // コンストラクタからデータを受け取る。
  public Sub(int data) {
    this.data = data;
  }
  // メンバー変数の値をリターンする。
  public int data() {
    return this.data;
  }
}
// ジェネリックを利用するインタフェース
// TジェネリックはSubクラスやSubクラスを継承するデータタイプに設定する。
// ジェネリックを二つ設定することも可能。
interface Callable<T extends Sub, V> {
  V call(T data);
}
// Callableインタフェースを継承
class Test implements Callable<Sub, String> {
  // TはSubクラスをVはStringタイプを設定する。
  @Override
  public String call(Sub sub) {
    // データリターン
    return "Parameter - " + sub.data();
  }
}
// 実行クラス
public class Example {
  // 実行関数
  public static void main(String... args) {
    // Testインスタンスを生成
    Test test = new Test();
    // Subインスタンスを生成
    Sub sub = new Sub(10);
    // Testクラスのcall関数にsubクラスを渡すと10のデータがリターンする。
    System.out.println(test.call(sub));
  }
}


上の例はデザインパターンのBuildパターンを参考しました。


ジェネリックを知らなかったらプログラムを作成できないことではありません。ジェネリック代わりにObjectタイプに設定すればプログラム作成することはできます。

でもジェネリックを利用すればコンパイル段階で整合性チェックするので別にデータタイプのキャスターエラーや変なデータになる可能性が低くなります。つまり、プログラムの品質を改善することができます。


ここまでJavaのジェネリックタイプ(Generic type)を使う方法に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

最新投稿