Revised: Sep./09th/2002: Since: Dec./30th/2001
前節で紹介した抽象クラスを利用するサンプルを紹介します。
抽象メソッドは、実際の処理を記述せず、その入出力だけ宣言したメソッドとも言えます。インスタンス化するには継承して、抽象クラスをオーバーライドして実装する必要がありますが、そのときシグネチャ(引数とそのデータ型の組)と戻り値型は抽象クラスで宣言されたものに合致している必要があります。
抽象メソッドは実装の無い引数と戻り値の宣言です。従って、これを実装する場合、色々な実装方法が考えられます。同じ引数を与えても、目的に応じて異なる戻り値が返ります。逆に言うと、目的ごとに別のクラスをインスタンス化してメソッド呼び出ししても、入出力の型が同一なので、呼び出す側で変更する必要がなくなるわけです。
抽象メソッドを持ったクラスは抽象クラスになり、利用するときには抽象メソッドを実装したサブクラスをインスタンス化する必要があります。抽象メソッドを一つでも持つクラスはインスタンス化できません。
現在のシステム開発では、設計から実装まで一人でこなすということは事実上ありえません。このとき、「このクラスを継承して実装せよ」と文書化するよりも、共通機能を実装し、個別機能は抽象クラスで用意しておく抽象クラスを用意しておく方が、遥かに強制力があります。
こんなときに抽象クラスは有意義です。例えば、銀行口座クラスを考えたとき、入金、出金のメソッドは抽象クラスで実装しておきますが、利率計算は異なるクラスが複数あると考えましょう。このとき、入出金は実装して、利率計算は抽象クラスで用意しておきます。こうしておけば、この抽象クラスを実装したサブクラスでは、利率計算メソッドだけ実装すれば、入出金のロジックは実装の必要がありません。勿論、入出金のメソッドも自分で実装すればよいのですが、そうすると同じ機能を複数箇所で実装する事になり、二度手間というだけでなく、保守性の点で問題がありますし、型が代入可能でなくなるというデメリットもあります。
もう少し現実的なケースを考えましょう。クラスAとクラスBがあると考えてください。クラスBはクラスAのメンバー変数(フィールド)に宣言されています。但し、クラスAには、クラスA1、クラスA2、クラスA3などがあり、それぞれでメンバー変数に宣言するクラスもクラスB1、クラスB2、クラスB3などがあります。このとき、クラスAのゲッターでクラスBnを取得する際、戻り値型がばらばらだったらどうでしょう。クラスAnを利用したいだけなのに、クラスBnの実装までさかのぼって型を確認する必要があります。これは面倒です。こんなとき、クラスBnが全て抽象クラスBのサブクラスであれば、同じ型クラスBで代入可能です。
ClassB objB = objAn.getBn();
アプリケーションの設計をするとき、複数のクラスで構成されるようにします。オブジェクト指向では複数のオブジェクトがメッセージを交換して処理が進むので、一つ一つのオブジェクトの機能は絞っておいたほうが拡張性に富み、再利用しやすくなります。また、全てのクラスをばらばらに実装するのではなく、適宜継承して、重複する機能を二度実装しないようにしておきます。このとき、コアとなる共通機能だけを実装したクラスを用意して、実際には継承クラスをインスタンス化してオブジェクトを利用するようにします。
このような設計では、継承されることを予め想定して、自分自身はインスタンス化されないようにしておくことが必要です。また、メソッド宣言だけしておいて、実装はサブクラスに任せる仕様も必要になります。これに応えるのが抽象メソッドと抽象クラスです。複数のサブクラスに継承されることを想定している場合は、その実装を指示できるので、有効な機能といえます。
抽象メソッドは、メソッド宣言だけで実装が無いものです。実装とは、メソッド直後の中カッコ内にロジックを記述することです。抽象メソッドを一つでも持つクラスは抽象クラスであり、インスタンス化できません。抽象クラス/抽象メソッド共に、修飾子にabstractを指定する必要があります。抽象メソッドは継承クラスで実装する必要がありますが、実装時にはabstract修飾子以外は全て同じものを指定する必要があります。
抽象クラスはインスタンス化できませんが、抽象クラス型の変数は宣言できます。抽象クラスを使う場合は、複数のサブクラスに継承されることを想定しており、それらの型のオブジェクト(の参照ID)を全て代入できるので便利です。
次の場合は、クラス Oya
を継承して Ko
クラスを定義しています。
class Oya { private int price; void setPrice(int i) { price = i; } int getPrice() { return price; } } class Ko extends Oya { int sales() { double d = getPrice()*0.9; return (int)d; } }
このとき、 Ko
クラス型をインスタンス化したオブジェクトを参照する変数 koObj
を Oya
型に変換すると、 Ko
クラスで定義したメソッド sales()
は使えなくなります。
CastTest.java
:
class CastTest { public static void main(String[] args) { Ko koObj = new Ko(); Oya oyaObj = koObj; oyaObj.setPrice(1980); int i = oyaObj.sales(); // コンパイルエラー System.out.println(i); } }
C:\Java>javac CastTest.java CastTest.java:6: シンボルを解釈処理できません。 シンボル: メソッド sales () 位置 : Oya の クラス int i = oyaObj.sales(); ^ エラー 1 個 C:\Java>
sales()
メソッドは Ko
クラスで定義されているので、 Oya
クラス型に型変換を受けた後では利用できなくなります。
継承の目的は、あるクラスで実装したメソッドなどの機能を利用して、さらに実装を追加する差分コーディングです。一つのクラスから複数のサブクラスを作ることも良くあります。このような時、それらのサブクラス型オブジェクトの参照を代入できる変数として、スーパークラス型変数を使うことがしばしばあります。
しかし、上に見たように、スーパークラス型に型変換を受けると、スーパークラスで宣言したメソッドしか利用することが出来ません。次の回避策が考えられます:
回避策として、サブクラス型にキャストするのは、あまり汎用性/拡張性がありません。キャストするにしても、オブジェクトの実体がどのようなサブクラス型であるのかを完全に把握しておかないとキャストしようにも出来ないからです。
これに比べると、スーパークラスでも実装しておくのは良いことです。但し、サブクラスでオーバーライドされることが前提になっているような場合に、スーパークラスでわざわざ実装しておくのも面倒な話です。
そこで、抽象クラスのメリットが挙げられます。
勿論、スーパークラスで宣言したメソッド以外のものをサブクラスで実装していれば、そのメソッドは使えません。
Oya.java
:
// 抽象クラス abstract class Oya { private int price = 1980; int getPrice() { return price; } // 抽象メソッド abstract int sales(); } // 実装クラスその1 class Ko1 extends Oya { int sales() { double d = getPrice()*0.9; return (int)d; } } // 実装クラスその2 class Ko2 extends Oya { int sales() { double d = getPrice()*0.8; return (int)d; } }
CastTest.java
:
class CastTest { public static void main(String[] args) { Ko1 koObj1 = new Ko1(); Oya oyaObj = koObj1; System.out.println("Price: " + oyaObj.getPrice()); System.out.println("90%: " + oyaObj.sales()); Ko2 koObj2 = new Ko2(); oyaObj = koObj2; System.out.println("80%: " + oyaObj.sales()); } }
C:\Java>javac Oya.java C:\Java>javac CastTest.jav C:\Java>java CastTest Price: 1980 90%: 1782 80%: 1584 C:\Java>