14.クラスの親子
~ポリモーフィズムとオーバーライド~

 ポリモーフィズムの便利さは、前章でわかったかと思いますが、ここではオーバーライドとポリモーフィズムの関係を、 もう少し詳しく見ていきましょう。

継承あれこれ

 販売管理のアプリケーションを考えます。商品、顧客、仕入先、いろいろなクラスが必要になりそうですね。

 スーパーのレジでは、商品に付いたバーコードでピッピッピと素早くお買い上げ金額を計算してくれます。 バーコードは何桁かの数字(普通13桁)に対応していて、これでどの商品かがわかり、レジの中に記憶された、 あるいはレジとつながったコンピュータに記憶された対応表から、単価を引っ張り出してきているわけです。 商品名が同じ「野沢菜袋詰め」が、350円と500円との2種類あっても、バーコードが違えば別の商品だとわかります。

 近頃はレジでカードを渡すと、お買い上げ金額でポイントを加算してくれて、年末などに還元してくれたりします。 この場合も顧客のカードにバーコードが付いていて、どのお客かわかるようになっています。

 ビジネスの世界では、「やあ人違いでした」ではすまされないので、なんにでも番号を振って識別しなければならないのです。 商品、顧客、仕入先、さまざまな伝票に至るまで、番号が付いています。 では「商品」クラスや「顧客」クラスのスーパークラスとして例えば「ビジネスオブジェクト」クラスを作り、 そのインスタンス変数として、idとlabelを持てばどうでしょう。idは識別のための番号、labelはオブジェクトの名前が入ります。 共通のものをくくりだすことができますね。

 食堂兼みやげ物店を考えてみましょう。ここで扱う商品は、かたや在庫管理が必要なみやげ物、 かたや売上数量だけわかればよい食堂メニューになります。 そんな場合「商品」クラスのサブクラスとして、「みやげ物」と「食堂メニュー」の二つを導入することも手です。

 支払いについて考えましょう。 「支払い」クラスには金額と支払日があって、そのサブクラスとして、「現金払い」クラスと「クレジットカード払い」クラスが考えられます。 「クレジットカード払い」クラスには、カード番号などのインスタンス変数が必要で、また認証も必要になってきます。

 販売管理といえば、インターネットでのショッピングサイトが思い浮かびます。 JavaでWebアプリケーションを作る場合、一からコツコツやっていたのでは大変なので、よくフレームワークと呼ばれる、 土台となるソフトウェアを利用したりします。 土台がしっかりできているので、例えば「ユーザが画面でここをクリックしたときは、このクラスを継承したクラスを作って、 ここに言われたとおりの変数やメソッドを使って処理を記述してください。」となるわけです。 つまりフレームワークの既成のクラスを「継承する」という形で利用するのです。

クラスのご先祖様は?

 「青さん」は「保育園児」の一種です。 「商品」も「顧客」も「ビジネスオブジェクト」の一種です。 英語で言うなら「is a type of」になります。「青さん」 is a type of 「保育園児」.です。

 クラス同士の関係を考えるとき、継承が妥当かどうかをチェックするために、 この「~は~の一種です。」にそれぞれのクラスを当てはめるという方法があります。 最初の~がサブクラス、2番目の~がスーパークラスになります。

 サブクラス側から見ると、スーパークラスは各サブクラスの共通部分になります。 そこには共通に持っている、データやできることが、インスタンス変数やメソッドという形で定義されます。 この共通部分をくくりだすことを汎化(はんか)といいます。

 よくスーパークラスとサブクラスの関係を、親子関係に例えることがあります。

 親であるスーパークラスの財産はすべて子であるサブクラスに引き継がれます。 子はさらに財産を増やすために引き継ぐのです。親子ならば、孫やひ孫、祖父母やご先祖様はどうでしょう。

 実は継承は階層化できるのです。サブクラスのそのまたサブクラスを作ることができます。 そしてどんどん親の財産を受け継いでいくことができます。

 そしてすべてのクラスの親をたどっていくと、必ずjava.lang.Objectに行き着きます。 これがクラスのご先祖様になるのです。なおObjectクラスにはスーパークラスはありません。



呼べる?どうやる?

 前章ではオーバーライドとポリモーフィズムをざっとした話で済ませてしまいました。 今度は少しプログラムで見ていきましょう。

 次はクラスAをクラスBが継承し、クラスBをクラスCが継承しています。 同じ名前のメソッドはシグネチャも同じです。 (実はすべて同じ)つまりクラスAのメソッドm2はクラスBでオーバーライドされていることになります。 何が出力されるか考えてみましょう。

[クラスA]
public class A {
 void m1() {
  System.out.println("Aのm1");
 }

 void m2() {
  System.out.println("Aのm2");
 }
}

[クラスB]
public class B extends A {
 void m2() {
  System.out.println("Bのm2");
 }

 void m3() {
  System.out.println("Bのm3");
 }
}

[クラスC]
public class C extends B {
 void m3() {
  System.out.println("Cのm3");
 }

 void m4() {
  System.out.println("Cのm4");
 }
}

[クラスMain]
public class Main {
 public static void main(String[] args) {
  B x;//①

  x = new B();//②
  x.m1();
  x.m2();
  x.m3();
  //x.m4(); m4()は型Bで未定義です
  System.out.println(x.toString());

  x = new C();//③
  x.m1();
  x.m2();
  x.m3();
  //x.m4(); m4()は型Bで未定義です
  System.out.println(x.toString());

  A y;//④
  y = new C();
  y.m1();
  y.m2();
  //y.m3(); m3()は型Aで未定義です
  //y.m4(); m4()は型Aで未定義です
  System.out.println(y.toString());
 }
}

 クラスの関係を図にしてみます。



 図のクラスBを見てください。
ここで定義されたm2はクラスAのm2をオーバーライドしています。 m3は新たに定義されたメソッドです。 さらにスーパークラスのAにあるm1も受け継いでいます。 これは箱の横に書いておきました。

 次はクラスCを見ましょう。m3はクラスBのm3をオーバーライドしています。 m4は新たに定義されています。直接のスーパークラスBのm2や、もうひとつ上の階層のスーパークラスAのm1も、クラスB経由で引き継いでいます。

 また図には入っていませんが、Objectクラスで定義されているメソッドtoStringやequalsは、どのクラスでも利用できます。



 今度はプログラムを見ましょう。 図で、①②③でやっていることを、図と文で示します。 クラスBからnewで生成されたオブジェクトを「クラスBのオブジェクト」と呼びます。 青い箱です。赤い箱は「クラスCのオブジェクト」です。

 ①で変数xの型は、クラスBとして宣言されています。 つまりxができることは、図の中のm2とm3に加え、スーパークラスから受け継いだm1になります。 (ここではObjectクラスのメソッドは省略します)

x.メソッド名( );
   ↑
m1,m2,m3のどれかしか指定できない。

 変数xのできること、即ち呼び出せるメソッドは、その宣言の型を見ればわかるのです。 コメントアウトしてある「x.m4( );」は、できないm4を呼び出しているので、コンパイルエラーになります。

 もう一度13章の「保育園児」クラスの図を思い出しましょう。 「保育園児」クラスには、「豚汁の準備をする」と「ラジオ体操をする」の二つのメソッドがありました。 つまり「保育園児」クラスの変数は、この二つのことができるのです。そしてこの二つのことしかできません。

 ②で変数xには、「クラスBのオブジェクト」が代入されます。 よってm1の呼び出しでは、Aで定義された内容が、m2ではBでオーバーライドした内容が、m3ではBで新たに定義された内容が行われます。

x.m3();

は、クラスBで定義されたm3が呼び出されるので、出力は

Bのm3

となります。

 ここから話は複雑になります。 ③で変数xにはクラスBではなく「クラスCのオブジェクト」が代入されています。 つまり各メソッドの中身は、クラスCに依存するわけです。 メソッドm3についてはCの中でオーバーライドしているので、こちらが採用されます。

x.m3();

は、クラスCで定義されたm3が呼び出されるので、出力は

Cのm3

となります。

 保育園児の例では、オブジェクトが、青さんか黄さんか桃さんかによって、「豚汁の準備をする」の具体的内容が異なりました。

 このようにソース上は同じ呼び出しでも、実行時変数に代入されたオブジェクトによって、 異なる処理を行えることをポリモーフィズム(polymorphism)といい、多態性などと呼ばれることもあります。 Javaではメソッドのオーバーライドでこれを実現しています。

 ④の変数yの型はクラスAなので、先ほどよりできることが少なくなっています。 「y.m4( );」に加え「y.m3( )」の呼び出しもコンパイルエラーになります。

 ObjectクラスのメソッドのtoStringは、オブジェクトの出身となるクラスを無理やり文字に変換してくれます。 クラス名の後に@、続けて何文字かが出力されますが、これは今のところ気にしないでください。 普通新たに作るクラスでは、toStringをオーバーライドして、オブジェクトの中身がわかるような情報に変換します。 変数xは最初クラスBのオブジェクトが代入されるので、出力は「B@」で始まっています。 次にはクラスCのオブジェクトが入るので、yとともに、出力は「C@」で始まります。

Aのm1
Bのm2
Bのm3
B@197d257
Aのm1
Bのm2
Cのm3
C@7259da
Aのm1
Bのm2
C@16930e2

 プログラムと図と出力結果の三つを、目を皿のようにして見比べてください。 ここが理解できるとオブジェクト指向の考え方の土台がひとつ出来上がるので、じっくりがんばりましょう。

どうしてもどうしても

 上のプログラムの③では、変数xの型はBなので、xが呼び出せるメソッドはm1、m2、m3の三つでした。 m4はクラスCで新たに定義されたメソッドなので呼び出せませんでしたね。 でもこの場合、xにはクラスCのオブジェクトが代入されているので、無理矢理m4を呼ぶことができるのです。 それは変数xの型を、強制的にCにしてm4を呼ぶのです。方法は簡単。第4章でやった型の変換を使います。(intとdoubleの間で変換しましたね)

((C)x).m4();

これで

Cのm4

と出力されます。(C)xのまわりの括弧がないと、演算順序の関係でエラーになってしまいます。