Javaジェネリクス入門、その4

2019年5月27日

Javaジェネリクス入門、その3の続きである。JavaジェネリクスについてはJavaジェネリクスにまとめがあるので参照されたい。

結局のところジェネリクスとは何をするものか?

何となくジェネリクスの目的がわかってきたかもしれない。これは結局のところ、

  • どのような型を受け入れるのか、その制約を作るものである。その制約範囲外のものをエラーにしてしまう。
  • 制約を作れば、受け入れたものは、その制約内であることがわかっているので、そこで許されることが可能になる。

例えて言えば、こういうことだ。

  • 哺乳類しか受け入れないという制約を作った。だから哺乳類以外のものはエラーにする。
  • 哺乳類という制約があるのだから、その制約内に存在する授乳という機能を使うことができる。

もともと強い型付けの言語はこのような思想で作られているのだが、ジェネリクスはこの制約を「一般化」したものだ。哺乳類という制約の中で、人間用にでも、ライオン用にでも変化させうるのだ。それも、型パラメータに与えるクラス名を変えるだけでよい。

ジェネリックメソッド

クラスまるごとをジェネリック化する(これをジェネリッククラスという)ということの他に、メソッド単体をジェネリック化することもできる。これをジェネリックメソッドという。

例えば、このように書く。これまた簡単さのためにstaticメソッドにしている。

public class Sample {  
  static <T> void put(T value) {    
    System.out.println(value);
  }
  public static void main(String[]args) {
    put("test");
  }
}

ジェネリックメソッドでは、返り値の指定の前に、型パラメータの指定を行う。この型パラメータは、返り値、引数、メソッド内部のどこでも使用できるが、当然ながら、その外部で使用することはできない。

そして、コード例を見ればわかるように、使用する際に、実際の型を指定する必要は無い。単に

    put("test");

と記述すれば、その型がStringであることが「推論」されるのである(明示的に指定することもある。これは後述する)。

なお、上のジェネリックメソッドは、まるで意味の無い例である。TはObject以下、何にでも適用できるので、結局意味としては以下と同じだ。

public class Sample {  
  static void put(Object value) {    
    System.out.println(value);
  }
  public static void main(String[]args) {
    put("test");
  }
}

再度言うが、注意が必要なこととしては、Tが実際には何の型かを指定していないことだ。ジェネリッククラスでは、明示的にTはStringであるとか、Animalであるとかの指定が必ず必要だったが、ジェネリックメソッドでは必要無い。これはほぼ文脈で自動的に決まる。

ジェネリックメソッドの少しだけ実用的な例

では、もう少し実用的な例を見せる。以下のように書くとどうなるだろうか?単に入力されたものを出力するだけである。

  static <T extends Animal> T put(T value) {    
    return value;
  }

TはAnimalかそれ以下のクラスであり(Animalかその派生型)、それを引数として入力し、返り値としても返している。すると、どういう呼び出し方ができるのだろう?以下のように記述してみる。

public class Sample {  

  static class Animal {
    public void breastFeeding() {}
  }
  static class Lion extends Animal {}
  static class Human extends Animal {}

  static <T extends Animal> T put(T value) {    
    return value;
  }

  public static void main(String[]args) {
    Lion lion = put(new Lion());
    Human human = put(new Human());
    Human lionHuman = put(new Lion()); // <-- エラー
  }
}

Lionオブジェクトを入れてみると、返り値をLionとして受け取れ、Humanの場合はHumanで受け取れるが、Lionを入れてHumanで受け取ることはできない。

つまり、文脈でTの型が決まり、この場合は引数と返り値が同じ型でなければならないため、引数がHumanで、なおかつ返り値がLionということはできない。エラーになってしまう。

このように、文脈でTの実際の型が「推論」され、それがメソッド内(引数、返り値、メソッドの本体)で同一でなければならない。

明示的に型を指定する例

上述したように、基本的には文脈によって実際の型が「推論」されるのだが、ときには型を明示したいときもある。

以下の例を考えてみる。

public class Sample {  
  static class Animal {
    public void breastFeeding() {}
  }
  static class Lion extends Animal {}
  static class Human extends Animal {
    public void speak() {}
  }

  static class General {
    <T extends Animal> T get() {    
      return (T)new Human();
    }
  }

  public static void main(String[]args) {
    General foo = new General();

    // 1.これはエラー
    foo.get().speak(); // <-- エラー

    // 2.こうするか
    foo.<Human>get().speak();

    // 3.こうする
    Human h = foo.get();
    h.speak();

  }
}

今回は、Animalか、あるいはその派生型を返す通常のメソッドとしてみた。プログラムを簡単にするために、単純にHumanのインスタンスを返しているが、Animal以下のインスタンスであれば、何でも返すものとする。

そして、このget()メソッドで得られたインスタンスのspeak()というメソッドを呼び出すものであるが、しかし、speakメソッドはHumanにしか存在しない。他の動物は話すことができないのだ。

1.の呼び出し方ではエラーになってしまう。何の文脈も無い場合には、返り値はAnimalと認識されてしまうのだ。だから、Humanのメソッドspeak()の呼び出しはエラーになる。

2.のように、メソッド名の前にHumanであると明示することにより、返り値がHumanであることを示す。するとspeak()メソッドが呼び出せる。

3.のようにしてもよい。返り値を受け取る変数がHumanであるという文脈があるため、返り値がHumanになってくれる。

ジェネリックメソッドの複雑な例(上級者向け)

ややこしい例について、ジェネリックメソッドの文脈で正しい型を指定するに書いた。これは上級者向けである。

まとめ

このように、クラス全体ではなく、一つのメソッド内だけでジェネリクスを使うことができる。これを、ジェネリックメソッドという。一般にジェネリックメソッドの実際の型が何であるかは文脈によって「推論」されるのだが、その情報の無い場合には明示することもできる。

Javaジェネリクス入門、その5に続く