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

2019年5月27日

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

前回のおさらい

型パラメータを付けたクラス、つまりジェネリッククラスは以下のように書く。

※内部クラスとして記述したのでstaticがついていることに注意

  static class General<T> {
    T object;
    void put(T object) { this.object = object; }
    T get()            { return object; }
  }

これを使うときには、この型の部分に対象とする具体的な型を記述することで、指定された型を扱うためのものになる。

General<String> foo = new General<>();

指定された型以外は扱えないことに注意

何度でも言うが、Javaにジェネリクスのある理由は、Javaが強い型付けの言語だからである。スクリプト言語のようないい加減な言語には存在しないものだ。もちろん、いい加減さにも多大なメリットがある。これについては、スクリプト言語とはを参照のこと。

強い型付けであることの巨大なメリットは先に書いたが、両方の種類の言語を経験し、両者でそれぞれ巨大なプログラムを記述し、メンテナンスするようになれば、このメリットがいかにありがたいものか理解できるだろう。初心者の時点でこれを理解することは100%不可能である。

したがって、初心者であればあるほど、巨大なシステムの経験の無い者ほどスクリプト言語を選びたがるのだが、そのようなワナに落ちないようにしてもらいたい。

さて、指定された型以外は扱えなくなることを実感する必要がある。例えば、以下のように書いてみる。

public class Sample {

  static class General<T> {
    T object;
    void put(T object) { this.object = object; }
    T get()            { return object; }
  }

  public static void main(String[]args) {
    General<String>foo = new General<>();
    foo.put(123);
    int intResult = foo.get();
  }
}

型パラメータにStringを指定し、これを文字列専用の入れ物にしたにも関わらず、整数を入れようとし、また整数を取り出そうとしている。もし、EclipseのようなIDEを使用しているのであれば(必ず使用すべきだが)、書いた時点でエラーが示される。

定められた型以外は受け入れないのである。この機能によって、プログラム実行のはるか以前、プログラム記述の時点で「入れてはならないものを入れてしまう」などといったミスを排除することができるのである。スクリプト言語には決してできない芸当であることを強調しすぎることはない。

哺乳類だけを対象にする

さて、先の型パラメータTは結局のところ、どんな型でも指定できてしまう。つまり、Object以下の何でも指定できるのである。以前の例えに戻れば、「生物」が指定できるのであり、そこには「人間」も「魚」も「昆虫」も指定できてしまう。

しかし、ある種の状況では、「哺乳類」しか受け入れたく無いという場合が出てくる。例えば、授乳させるといった機能を付けたい場合に、そこに受け入れられるものは「哺乳類」のみである。

種の階層構造と、クラス階層構造は似たようなものだ。例えば、種の階層は以下のようなものであり、そのピラミッド階層のトップは生物である(学術的に何というのか知らないが)。

生物
 +-- 無脊椎動物
 +-- 脊椎動物
    +-- 魚類
    +-- 爬虫類
    +-- 哺乳類
      +-- ライオン
      +-- 人間

これと全く同じように、オブジェクト指向言語のクラス階層の構造はObjectをトップとし、それ以下のクラスが連なっている。

Object
  +-- String
  +-- Number
    +-- Integer

さて、いまここで、必ず哺乳類以下を対象とする機能を作りたいと思う。授乳機能があるのは哺乳類だけだからだ。仮にこんなクラスを作るとする(哺乳類の英語はMammalsだが簡単のためにAnimalとする)。

※再度、内部クラスとして使用するのでstaticがついていることに注意。

static class Animal {
  // 授乳させる
  public void breastFeeding() {}
}
static class Lion extends Animal {}
static class Human extends Animal {}

AnimalにbrestFeeding()というメソッドがあり、このクラスを継承したLion、Humanはこのメソッドを継承している。

型パラメータに制約を課す

では、このAnimal型を受け入れ、breastFeedingメソッドを利用するようなクラスはどう書けばよいのだろうか?。もちろんこの場合のお題としては、対象をライオンとした場合は、ライオンのみを受け入れ、人間の場合は人間のみにしたいのである。

仮にこう書いてみる。インスタンスをputで格納した時点で、そのbreastFeedingメソッドを呼び出すのである。

public class Sample {

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

 public static class General<T> {
   void put(T object) {
     object.breastFeeding(); // <-- エラー!
   }
 }
}

当然エラーになる。なぜか?この状態でGeneralクラスの受け入れるものは「生物」だからだ。つまり、魚もトカゲも受け入れてしまうので、breastFeedingというメソッドが無いことがわかっているのである。

つまり、この場合の型パラメータT(あるいは前回述べたようにどんな名称でも良いのだが)、これはObjectを表しているのである。使うときにObject以下の特定のクラスにすることができるのだが、このGeneralクラスの中ではObjectとして認識されているのだ。

したがって、Generalクラスの中ではObjectに備わる機能しか使うことはできない。では、どうすれば良いのか?

答えは以下である。

  public static class General<T extends Animal> {

「T extends Animal」は「Tは、Animalか、あるいはその下位のクラス」という意味だ。Animalかそれ以下であれば、必ずbreastFeedingというメソッドが存在する。したがって、以下の記述はエラーにならない。

 public static class General<T extends Animal> {
   void put(T object) {
     object.breastFeeding();
   }
 }

型パラメータに制約を課し、多態性を用いた例

TがAnimal以下であり、さらに多態性によってbreastFeedingの動作を変更する例が以下である。

※多態性がわからない人は、検索して調べて欲しい。

public class Sample {

 static class Animal {
   public void breastFeeding() { System.out.println("Animal feeding"); }
 }
 static class Lion extends Animal {
   @Override
   public void breastFeeding() { System.out.println("Lion feeding"); }
 }
 static class Human extends Animal {
   @Override
   public void breastFeeding() { System.out.println("Human feeding"); }
 }

 public static class General<T extends Animal> {
   void put(T object) {
     object.breastFeeding();
   }
 }

 public static void main(String[]args) {
   General<Human>foo = new General<>();
   foo.put(new Human());
 }
}

結果として以下が表示される。

Human feeding

規定の動作以外の拒否

もちろん、想定された使い方以外だと(IDEの場合は)その場でエラーになる。

 public static void main(String[]args) {
   General<String>foo = new General<>(); // <-- エラー。StringはAnimalではない
 }

何度でも繰り返すが、これが強い型付け言語のパワーというものである。プログラムの実行以前にエラーを容易に発見することができる。

まとめ

今回のまとめとしてはこうだ。

  • 制約の無い型パラメータは、ジェネリッククラスの中ではObjectとして解釈され、Objectに備わる機能以外を使用することはできない。
  • 制約を課した型パラメータ付のジェネリッククラスを使用する場合、その制約範囲内のクラスしか指定することはできない。

逆に言えば、こういうことだ、強い型付けの言語によるプログラミングの目標は、自由奔放にプログラムを書くことではない。そうではなく、いかにして制約を作り、それを守るかということである。

その制約、つまりはルールを課すことによって、混乱が起こるのを防いでいるのである。

実社会の考え方と同じことであることは容易に理解できるだろう。

今回の例でいえば、すべての物に何でも入れられてしまっては困るのだ。巨大な開発の場合には、プログラマもピンからキリまでいる。一人の人間の単独開発であっても、すべてを覚えているとは限らない。

自由奔放にプログラム記述されては困るのである。書いたときは良くても、メンテやら他の者が見る場合、本人であっても忘れてしまっている場合を考えてみてほしい。

あらかじめ適切な制約を作っておけば、IDEやコンパイラが自動的に間違いを指摘してくれるのである。それも制約が強ければ強い方が良い。これが、強い型付けを持つ言語のパワーである。

もちろん、強い制約がありながらも柔軟性をもたせることはできる。それがジェネリクスの機能であるとも言える。

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