オブジェクト指向プログラミング


 

抽象クラスとインタフェース

1 抽象クラス

どのような処理をするかが決まっていない、抽象メソッドを持つクラスを抽象クラスと呼びます。このようなクラスは、継承され、派生クラスでオーバーライドされることによって当該メソッドの実装を果たし、はじめてインスタンス化されます。

(1)抽象メソッド

 

抽象メソッドは修飾子 abstract で宣言します。

<アクセス修飾子> abstract <型> <メソッド名>([<引数>[, ...]]);

通常のメソッドならば、実際の処理が { } の中に記述されているはずですが、抽象メソッドの場合はセミコロン ; になっています。 抽象メソッドでは、メソッド名と引数、戻り値の型の宣言だけ行います。

抽象メソッドは、実際の処理を記述せず、その入出力だけ宣言したメソッドとも言えます。インスタンス化するには継承して、抽象メソッドをオーバーライドして実装する必要がありますが、そのときシグネチャ(引数とそのデータ型の組)と戻り値型は抽象メソッドと同じである必要があります。

(2)抽象クラス

 

抽象メソッドを持つクラスは抽象クラスであり、修飾子で abstract 宣言しておかなければなりません。

<アクセス修飾子> abstract class <クラス名> { 抽象メソッド }

抽象クラスを利用するときには、抽象メソッドをオーバーライドし、処理内容を記述した派生クラスをインスタンス化する必要があります。しかし、抽象メソッドを一つでも持つクラスはインスタンス化できません。



抽象クラスは共通の機能を表現し、個々が持つ独自の機能はそれぞれの派生クラスで実装したい場合に使用します。例えば、今まで見てきました Car クラスの work メソッドは「何もしない」処理が書かれていました。

Car.java
public class Car { // 途中略 public void work() { System.out.println(""); //何もしない } }

しかし、自動車の種類によって必ず処理が異なるならば、規定値のような work メソッドの処理は要りませんし、さらにいえば、Car クラスの work メソッドに処理が書かれていることによって、Car クラスを継承した派生クラスに work メソッドを記述し忘れても問題ないことになってしまいます。

したがって、各派生クラスに work メソッドを記述させるためにも、Car クラスの work メソッドは抽象メソッドにすべきものなのでした。


それでは、abstract を記述して、Car クラスを抽象クラスにしてみます。

抽象クラスの例
Car.java
public abstract class Car { // 途中略 public abstract void work(); //抽象メソッド }

しかし、Car クラスの work メソッドを抽象メソッドにすると、Car クラスを継承したすべての派生クラスには work メソッドを記述しなければならなくなります。よって、今度は work メソッドを記述していない Ambulance クラスがエラーになってしまいます。

Ambulance.java
public class Ambulance extends Car { // work()を実装していないためエラーになる。 public Ambulance() { super("救急車"); } public void siren() { System.out.println("ピーポーピーポー"); } }

Ambulance クラスがエラーにならないようにするために、work メソッドを記述します。

Ambulance.java
public class Ambulance extends Car { // work()を実装したためエラーにならない。 public Ambulance() { super("救急車"); } public void siren() { System.out.println("ピーポーピーポー"); } public void work() { System.out.println("救急患者を運ぶ。"); } }

このように、Car クラスの work メソッドを抽象メソッドとして定義することによって、すべての派生クラスに work メソッドを記述させることができるようになります。

2 ポリモーフィズムと抽象クラス

基底クラスに抽象メソッドを作成しておくべきであるという理由として、各派生クラスに共通のメソッドを記述させるためとしましたが、この他にもうひとつ理由があります。

「基底クラス」型の変数に、「派生クラス」型のオブジェクトを代入することで、メソッドの働きが変わることをポリモーフィズムと呼びました。

 

ポリモーフィズムの例
Example.java
public class Example { public static void main(String[] args) { Car car = new Truck(); car.work(); // Truck クラスの work メソッドが呼ばれる car = new Ambulance(); car.work(); // Ambulance クラスの work メソッドが呼ばれる } }

ポリモーフィズムとは、「基底クラス」型の変数を利用して同じように work メソッドを呼び出しても、「基底クラス」型の変数に代入されている派生クラスの work メソッドが呼び出されることです。


ポリモーフィズムを行うためには、各派生クラスに共通のメソッドがあることは当然ですが、基底クラスにも記述する必要があります。各派生クラスに共通のメソッドがあったとしても、基底クラスになければポリモーフィズムはできません。よって、そのために基底クラスに抽象メソッドを作成しておくわけです。

たとえば、基底クラスの Car クラスに存在しない siren メソッドを Car 型の変数で car.siren() と呼び出してみても、コンパイルエラーとなってしまいます。

Example.java
public class Example { public static void main(String[] args) { Car car = new Ambulance(); car.siren(); // 基底クラスにないメソッドなのでエラー } }


Java、C++、C#、VB などでは、派生クラス型のオブジェクトが基底クラス型変数に代入されたときには、基底クラスに存在しない、派生クラス独自に宣言して実装したメソッドは使えません。そうすると、ポリモーフィズムを考えた場合、派生クラスでは、基底クラスで宣言されたメソッド以外は記述しない方が望ましいことになります。一方、基底クラスで宣言されていないメソッドやフィールドを追加することも極めてオブジェクト指向的なことです。

つまり、継承/実装には相反する二つの側面があるのです。

何れを選ぶかは、適用する状況に依存するもので、デザインパターンの勉強、コーディング経験の積み上げによって習得されます。

3 インタフェース

インタフェースは、抽象メソッドのみを持つ抽象クラスだと考えることができます。インタフェースの定義は以下のようにして行います。クラスを継承する場合はひとつしか指定できませんが、インタフェースの場合は複数指定できます。継承が複数ある場合には ,(コンマ)で区切って記述します。

 

interface <インタフェース名> [extends <継承元のインタフェース名> [, ...]] { フィールド; 抽象メソッド; }

抽象クラスとよく似ていますが、インタフェースには以下に挙げるような特徴があります。

  • フィールド(メンバ変数)は定数となり、必ず値が代入されなければならない。自動的に final public static になる
  • static メソッドを持つことができない
  • 宣言したメソッド・プロパティはすべて public abstract になる(public abstract を記述してはいけない)
  • 1つのクラスが複数のインターフェースを実装(継承)できる

インタフェースの例です。緊急車両が持つべきメソッドを定義しています。

IEmergency.java
interface IEmergency { void siren(); void redLight(); }

インタフェースを実装するには、extends ではなく implements で、次のように行います。


サイレンを鳴らす緊急車両クラス EmergencyCar を定義してみます。インタフェースを実装するクラスでは、インタフェースで定義されたすべてのメソッドを記述する必要があります。メソッドが不足しているとエラーとなりますので、インタフェースを利用することによって、メソッドの定義のし忘れがなくなります。この例では、siren メソッドを抽象メソッドとして定義していますが、処理の実態のあるメソッドとして定義してももちろんかまいません。

EmergencyCar.java
public abstract class EmergencyCar extends Car implements IEmergency { public EmergencyCar() { super(); } public EmergencyCar(String kind) { super(kind); } public abstract void siren(); public void redLight() { //(2) (5) System.out.println("赤色灯点灯"); } }

EmergencyCar クラスを継承して Ambulance クラスと FireEngine クラスを定義し直してみます。

Ambulance.java
public class Ambulance extends EmergencyCar { public Ambulance() { super("救急車"); } public void work() { System.out.println("救急患者を運ぶ。"); } public void siren() { //(3) System.out.println("ピーポーピーポー"); } }
FireEngine.java
public class FireEngine extends EmergencyCar { public FireEngine() { super("消防車"); } public void work() { System.out.println("火事を消す。"); } public void siren() { //(6) System.out.println("ウーーカンカンカン"); } }

EmergencyCar クラスを継承して Ambulance クラスと FireEngine クラスを定義し直したことによって、siren メソッドについても IEmergency インタフェース型の変数を使用してポリモーフィズムを行うことができるようになります。もちろん EmergencyCar クラス型の変数を使用してもかまいませんが、オブジェクトにアクセスする際に、特定のクラスの型を使うよりも、インタフェースの型を使う方が、特定のクラスへの癒着がなくなり、より「疎」な結合 (疎結合) が実現できるようになります。

Main.java
public class Main { public static void main(String[] args) { IEmergency emc = new Ambulance(); //(1) emc.redLight(); //(2) emc.siren(); //(3) emc = new FireEngine(); //(4) emc.redLight(); //(5) emc.siren(); //(6) } }
  1. IEmergency インタフェース型の変数 emc を宣言しAmbulance のオブジェクトを設定します。
  2. emc 内に保持している Ambulance オブジェクトの redLight メソッドが呼び出されます。しかし、無いので EmergencyCar の redLight メソッドが実行されます。
  3. emc 内に保持している Ambulance オブジェクトの siren メソッドが呼び出されます。siren メソッドが実行されます。
  4. FireEngine のオブジェクトを生成しIEmergency インタフェース型の変数 emc に設定します。
  5. emc 内に保持している FireEngine オブジェクトの redLight メソッドが呼び出されます。しかし、無いので EmergencyCar の redLight メソッドが実行されます。
  6. emc 内に保持している FireEngine オブジェクトの siren メソッドが呼び出されます。siren メソッドが実行されます。

実行結果です。

赤色灯点灯
ピーポーピーポー
赤色灯点灯
ウーーカンカンカン