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

復習

オーバーライド

親クラスで宣言したメソッドを、子クラスで上書きすることができた。これをメソッドのオーバーライドと呼ぶ。

public class Person {
    
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
    
    public void introduceMyself() {
        System.out.println(getName() + "です。");
    }
}
public class Student extends Person {

    private final String id;
    
    public Student(String name, String id) {
        super(name);
        this.id = id;
    }

    public String getId() {
        return this.id;
    }
    
    public void introduceMyself() {
        System.out.println(getName() + "です。");
        System.out.println("学籍番号は" + getId() + "です。");
    }
}

オーバーライドされた親クラスのメソッドは super というキーワードを用いて子クラスから参照することができる。

public class Student extends Person {
    ...
    public void introduceMyself() {
        super.introduceMyself();
        System.out.println("学籍番号は" + getId() + "です。");
    }
}

ポリモーフィズム

is-a 関係にあるインスタンスは親クラスの型を持つ変数に代入できた。

Person p = new Student("ほげほげ", "00k0000");

そして、このときに p.introduceMyself() を呼び出すと、Person クラスの introduceMyself() ではなく オーバーライドした Student の introduceMyself() が実行される。

このように、同じメッセージを送って、メッセージを受け取ったインスタンスがそのクラスによって違う振る舞いをすることを、ポリモーフィズム (多態性, 多様性, 多相性) と呼ぶ。上記の例では、introduceMyself() を受け取ったのが「学生」であるので、「学生としての自己紹介」が行われる。

抽象クラス

前回の演習で、多角形、長方形、三角形を扱った。これらのクラスとしての階層は次のようになっている。


このうち、長方形と三角形はどのようなものか想像が付くが、多角形はどのようなものであるか想像がつきにくい。多角形は、長方形 (四角形) や三角形が属する抽象的な部類であり、具体例を挙げることができない (具体例を挙げたとしても、三角形や四角形などのほかの部類に属してしまう)。

部類とはクラスであり、具体例とはインスタンスであった。具体例を挙げることができない多角形に対して、次のプログラムでは Polygon のインスタンスを作成してしまっている。

// 多角形の一例
Polygon p = new Polygon();

上記のように、実際には存在しない物体を、new 演算子を使って作成できてしまった。Java ではこのような抽象的な部類を表すためのクラスを表すために、抽象クラスというものが存在する。

// 多角形を表すクラス
public abstract class Polygon {

    // 空のコンストラクタ
    public Polygon() {
        super();
    }

    // 面積を取得する
    public double area() {
        return 0.0;
    }
    
    // 全ての辺の長さの合計
    public double perimeter() {
        return 0.0;
    }
}

上記のように、class Polygon の前に abstract の指定をすることによって、このクラスを抽象クラスとして宣言できる。

抽象クラスは、他のクラスの継承関係を階層化するために存在し、インスタンス化することができない。abstract class Polygon を new 演算子によってインスタンス化しようとすると、エラーになる。

// 抽象クラスはインスタンス化できない
Polygon p = new Polygon();

抽象メソッド

先の項では、Polygon クラスを抽象クラスとして指定した。

// 多角形を表すクラス
public abstract class Polygon {

    // 空のコンストラクタ
    public Polygon() {
        super();
    }

    // 面積を取得する
    public double area() {
        return 0.0;
    }
    
    // 全ての辺の長さの合計
    public double perimeter() {
        return 0.0;
    }
}

Polygon が抽象クラスとなると、このクラスが持つメソッド area() と perimeter() が意味を成さなくなる。しかし、これらのメソッドを消してしまうと問題が起こる。

// 多角形を表すクラス
public abstract class Polygon {
    public Polygon() {
        super();
    }
    
    // メソッドを消す
}
Polygon p = new Triangle(3.0, 4.0, 5.0);
// Polygon は area() メソッドを持たないのでエラー
System.out.println(p.area());

今度は、Polygon が area() メソッドを持たないため、「多角形 (実際には3.0,4.0,5.0の3辺を持つ三角形) の面積を求める」という操作が行えなくなってしまう。(実行時の) ポリモーフィズムを実現するには、親クラスで同じ名前のメソッドを定義していなくてはならない。

やはり area() と perimeter() は削除することはできないが、「多角形」という抽象的な図形で面積を求めたりするような具体的な方法は存在しない。このように振る舞いを具体的に定義できないメソッドを abstract として宣言することにより、このクラスでメソッドの中身を記述しないで済ますことができる。このようなメソッドを抽象メソッドと呼ぶ。

// 多角形を表すクラス
public abstract class Polygon {

    // 空のコンストラクタ
    public Polygon() {
        super();
    }

    // 面積を取得する
    public abstract double area();
    
    // 全ての辺の長さの合計
    public abstract double perimeter();
}

抽象メソッドを書く際には、振る舞い (メソッドの本体) を書くブロック {} を用意せずに、そのままセミコロン「;」でメソッドの宣言を終了する。

抽象メソッドは次のような制約を持つ。

抽象メソッドを使うと、抽象クラス内でメソッドの振る舞いを定義せずに、そのクラスを継承した子クラスに具体的なメソッドの振る舞いを定義させることができる。つまり、子クラスが持つべき機能を親クラスが指定することができる。今回の例では、「多角形に属するクラス (三角形や長方形など) は、その面積と外周の長さを計算する機能を持たなくてはならない」という指定を行った。


その他の抽象クラスの例

その他、抽象クラスの例では「乗り物」が挙げられる。


public class Vehicle {

    public void go(String destination) {
        // 目的地へ向かう処理
    }
}
public class Taxi extends Vehicle {

    public void go(String destination) {
        this.accelerate();
        // destination へ向かう処理
        this.brake();
    }
    ...
}
public class Helicopter extends Vehicle {

    public void go(String destination) {
        this.takeOff();
        // destination へ向かう処理
        this.land();
    }
    ...
}

「ここからタクシーを使って東京駅へ」「ここからヘリコプターを使って東京駅へ」と言われれば、どのように実現するかは予想が付く。しかし、「ここから乗り物を使って東京駅へ」と言われても乗り物では抽象的なので実現方法が予想しにくい

乗り物は「タクシー」や「ヘリコプター」、「新幹線」などといった物の上の階層に存在する抽象的な概念で、「乗り物」という物体は存在しない (繰り返すが、乗り物の具体例を挙げても自動車やヘリコプターといったその下の階層にある物体になってしまう)。

このような乗り物を表す Vehicle といったクラスは抽象クラスで宣言されるべきであるし、また「目的地へ行く」というメソッド go(String destination) といったメソッドは具体的な振る舞いを規定できないため抽象メソッドとして宣言されるべきである。

public abstract class Vehicle {

    // 目的地へ向かう
    public abstract void go(String destination);
}

インターフェース

現実世界に「複合コピー機」という商品が存在する。これは、コピー機としての機能を持ちながら、その他にもスキャナ、ファクシミリ、プリンタなどの機能を持つ機械である。


これを素直に Java で表現しようとすると、次のようになる。

public class MultiPurposeCopier extends Copier, Scanner, Fax, Printer {

    // コピー機の機能
    public Paper copy(Paper origin) .. 

    // スキャナの機能
    public Document scan(Paper document) ..

    // ファクシミリの機能
    public Paper receive() ..
    public void send(Paper document, String to) ..

    // プリンタの機能
    public Paper print(Document document) ..
}

しかし、Java では「extends Copier, Scanner, Fax, Printer」のような書き方をすることはできない。

多重継承

「単一継承」と「多重継承」という用語がある。これはクラスの継承をする際にとれる親クラスの数を表しており、単一継承ならば親クラスの数は1つ、多重継承ならば親クラスの数はいくつでも取れることになっている。

Java や最近の言語では、多重継承を行えないようなものが多い。ここでは詳しく触れないが、多重継承を許すといくつかの問題が起こる可能性があるためである。このような場合、Java ではインターフェースと呼ばれるものを使って複数の親を持っているように見せかける。

インターフェースは抽象メソッドしか持てないクラスのようなもので、仕様 (機能を呼び出す際の決まり事, メソッド名や引数) を表現することができる。そして、一つのクラスがいくつもこのインターフェースというものを実装 (クラスの継承に近い) することができる。

インターフェースの宣言は次のように class の代わりに interface を指定する。

public interface Scanner {
    // スキャナの機能
    public abstract Document scan(Paper document);
}
public interface Fax {
    // ファクシミリの機能
    public abstract Paper receive();
    public abstract void send(Paper document, String to);
}
public interface Printer {
    // プリンタの機能
    public abstract Paper print(Document document);
}

そして、インターフェースを実装 (クラスでいうところの継承) する場合、implements というキーワードを指定して、カンマ「,」区切りで実装するインターフェースを列挙する。

public class MultiPurposeCopier extends Copier implements Scanner, Fax, Printer {

    // コピー機の機能
    public Paper copy(Paper origin) .. 

    // スキャナの機能
    public Document scan(Paper document) ..

    // ファクシミリの機能
    public Paper receive() ..
    public void send(Paper document, String to) ..

    // プリンタの機能
    public Paper print(Document document) ..
}

このように、「複合コピー機はコピー機を拡張して、スキャナ, ファクシミリ, プリンタを実装する」と読むことができる。

インターフェースは、上記の複合コピー機ならば「スキャンのボタン」「ファックス送受信のボタン」「プリンタのボタン」というイメージが近い。あくまで提供するのは仕様だけであって、中身までは提供してくれない。そこで、インターフェースを実装した場合、それぞれの仕様 (抽象メソッド) に対して実際の振る舞い (メソッド本体) を定義してやる必要がある。つまり、インターフェースは特定の機能を実装する (与えられた仕様を満たす) という契約であり、implements を指定した場合にはこの契約に従い、必要なメソッドを実装する必要がある。

Eclipse を用いてクラスを作成する際に、パッケージを右クリックして「新規作成>クラス」を選択していた。インターフェースを作成する際には「新規作成>インターフェース」を選択する。

インターフェースは次のような制約を持つ。

また、上記の制約から、メソッドに public abstract をわざわざ指定しなくても、自動的に public abstract なメソッドとして定義される。

public interface Fax {
    // 暗黙に public abstract
    Paper receive();
    void send(Paper document, String to);
}

インターフェースと型

クラスは変数の型を表すと考えることが出来る。たとえばクラス Cls があったときに

Cls c;

と宣言すると、変数 c にはクラス Cls のインスタンスを代入することができる。

これは、整数型の変数を

int n;

と宣言するのと同じ形であり、c は Cls 型の変数であると考えることも出来る。

同様に、interfaceも型として扱うことが出来る。interface Intf があったとき

Intf i;

と宣言すると、i には Intf 型のインスタンス (Intf を実装しているクラスのインスタンス) を代入することができる。

上記を使うと、先ほど作成した MultiPurposeCopier のインスタンスを様々な方の変数に代入することができる。

MultiPurposeCopier mcp = new MultiPurposeCopier();

// 親クラス型に代入
Copier cp = mcp;

// 実装インターフェース型に代入
Scanner scanner = mcp;
Fax fax = mcp;
Printer printer = mcp;

このように実装したインターフェースの型を使えることによって、複合コピー機をスキャナ、ファクシミリ、プリンタのように扱うことができる。

// スキャナとして扱う
Scanner scanner = new MultiPurposeCopier();
scanner.scan(...);
// ファクシミリとして扱う
Fax fax = new MultiPurposeCopier();
fax.send(...);
// プリンタとして扱う
Printer printer = new MultiPurposeCopier();
printer.print(...);

MultiPurposeCopier is-a Fax とは言いがたいが、Fax を実装しているので Fax の機能を提供するという契約は満たしている。このため、上記のように MultiPurposeCopier のインスタンスを Fax として扱うことができるのである。

同様に、引数の型としてインターフェースを書くこともできる。その場合、それを実装しているクラスのインスタンスを渡すことになる。

骨格と機能の集まり

抽象クラスとインターフェースは似たような概念であるが、利用される目的が大きく違う。

両者の相違点を、以下にまとめる。

骨格実装

抽象クラスのアドバンテージは、本体を持つメソッド (abstract でないメソッド) を宣言できるという点である。これを利用すると、プログラムの骨格だけを抽象クラスで定義して、その骨格を再利用しながら子クラス内で具体的なプログラムを書くことができるようになる。子クラスで記述すべきプログラムは、骨格の部分を除いた子クラス特有の処理だけであるので、似たようなプログラムをいくつか用意する場合には、この骨格実装が有効である。

例として、2つの double[] 型の値の各要素に対して、一斉に「何らかの操作」を行うようなプログラムを考える。

public double[] operation(double[] a1, double[] a2) {
    double[] result = new double[a1.length];
    for (int i = 0; i < a1.length; i++) {
        result[i] = a1[i] (何らかの操作) a2[i];
    }
    return result;
}

この、「何らかの操作」という部分だけ変更したい場合、通常は operation(double[], double[]) というメソッド全体を変更する必要がある。しかし、変更したい部分は「何らかの操作」というだけなのに、全体を全て書き直すのは手間がかかるし、DRY (Don't Repeat Yourself) の原則にも反している。

そこで、次のような骨格を形成するクラスを用意する。

package j2.lesson06;

// 2つの配列の各要素に対して一斉に「何らかの処理」を行うための骨格
public abstract class DoubleArrayOperator {

    // 2つの配列の各要素に対して一斉に「何らかの処理」を行い、その結果を返す
    public double[] operate(double[] a1, double[] a2) {
        double[] result = new double[a1.length];
        for (int i = 0; i < a1.length; i++) {
            // 何らかの処理をして、結果を result[i] に代入
            result[i] = operate(a1[i], a2[i]);
        }
        return result;
    }

    // 現時点では、「何らかの処理」という抽象的なものにする
    protected abstract double operate(double d1, double d2);
}

この骨格によって、「2つの配列の各要素に対して一斉に何らかの処理を行い、その結果を返す」というプログラムのうち、「2つの配列の各要素に対して一斉に…、その結果を返す」という部分だけを記述することができた。「何らかの処理」というのは具体的に決まっていないため、抽象メソッドとして宣言する。DoubleArrayOperator は抽象メソッドを持つので抽象クラスとして宣言している。

この骨格を元に、「2つの配列の各要素に対して一斉に足し算を行い、その結果を返す」というプログラムと、「2つの配列の各要素に対して一斉に掛け算を行い、その結果を返す」という2つのプログラムを作成してみる。

package j2.lesson06;

// 2つの配列の各要素に対して一斉に足し算を行う
public class DoubleArrayAdder extends DoubleArrayOperator {

    // 足し算を行う
    protected double operate(double d1, double d2) {
        return d1 + d2;
    }
}
package j2.lesson06;

//2つの配列の各要素に対して一斉に掛け算を行う
public class DoubleArrayMultiplier extends DoubleArrayOperator {

    // 掛け算を行う
    protected double operate(double d1, double d2) {
        return d1 * d2;
    }
}

「何らかの処理を行う」という部分が、それぞれ「足し算」と「掛け算」に置き換わっただけなので、親クラスのメソッドのうち「何らかの処理を行う」という抽象メソッドを上書きしているだけである。これだけで、次のように2つの配列に対して一斉に足し算や掛け算を行うことができる。

DoubleArrayOperator op = new DoubleArrayMultiplier();
double[] multiplicand = {1.0, 2.0, 3.0, 4.0, 5.0};
double[] multiplier = {2.0, 1.0, 2.0, 1.0, 2.0};

// 一斉に掛け算を行う
double[] result = op.operate(multiplicand, multiplier);

for (int i = 0; i < result.length; i++) {
    System.out.println(result[i]);
}

上記のプログラムを実行すると、次のようになる。それぞれが掛けられた結果になっていることが確認できる。

2.0
2.0
6.0
4.0
10.0

Java に標準で組み込まれているクラスには、このように骨格だけ用意されているものが多数ある。次回以降に詳しく触れるが、上記の考え方は非常に重要なので覚えておくこと。

継承と集約

インターフェースと抽象クラスにはそれぞれ利点と欠点がある。例えば、最近の携帯電話などの多数の機能を持ったクラスを表現する際に大変である。

// (最近の)携帯電話は、電話を拡張し、カメラ、電子マネー.. を実装している
public class CellPhone extends Phone implements Camera, DigitalCash {
    ...
}

上記のようなクラスを作ると、直接継承している Phone は問題ないとしても、カメラ、電子マネーの機能を全てクラス内に書かなければならない。Java では多重継承を許していないため、これらの機能を継承によって受け継ぐことはできない。

このような問題を解決するには、継承関係を表す is-a の他にクラスを階層化する方法が必要である。そこで、上記クラスの見方を変えてみると、次のように言い表すこともできる。

これによって、インターフェースを実装する部分を has-a の関係に変換することができた。しかし、Camera や DigitalCash はインターフェースであるため、それらを実装するクラスを別に作る。

public class CameraModule implements Camera {
    // カメラの機能
    public void takePicture() {
        ...
    }
}
public class DigitalCashModule implements DisitalCash {
    // 電子マネーの機能
    public void charge(int money) {
        ...
    }
    public void pay(int money) {
        ...
    }
}

そして、これらの機能を部品として CellPhone に集約させることによって、機能を追加する。

public class CellPhone extends Phone implements Camera, DigitalCash {

    // 部品として持たせる
    private Camera camera;
    private DigitalCash digitalCash;

    public CellPhone() {
        ...
        this.camera = new CameraModule();
        this.digitalCash = new DigitalCashModule();
    }

    // 携帯電話の各機能が呼ばれたら、部品の各機能を代わりに呼び出す
    public void takePicture() {
        this.camera.takePicture();
    }
    public void charge(int money) {
        this.digitalCash.charge(money);
    }
    public void pay(int money) {
        this.digitalCash.pay(money);
    }
}

implements Camera, DigitalCash (カメラと電子マネーを実装する) という部分は、CellPhone そのものではなく、CellPhone に集約された部品が実装してくれている。CellPhone はその部品の機能を呼び出しているので、結果として CellPhone もそれらの機能を実装していることになる。

上記のように書くと、プログラムの再利用が簡単になる。

// 電子マネーカードは、カードを拡張し、電子マネーを実装する
public class DigitalCashCard extends Card implements DigitalCash {

    // 部品として電子マネーを持つ
    private DigitalCash digitalCash;

    public DigitalCashCard() {
        ...
        this.digitalCash = new DigitalCashModule();
    }

    public void charge(int money) {
        this.digitalCash.charge(money);
    }
    public void pay(int money) {
        this.digitalCash.pay(money);
    }
}

このようなプログラムの書き方をまとめると、次のようになる。

  1. 部品のインターフェースを宣言する
  2. 部品の機能を実現するクラスを作成する
  3. それぞれの部品のインターフェースを持つクラスを作成する

実際に、携帯電話を製作している会社も、全て同じ部署でカメラや電子マネーを製作したりすることはほとんどない。それらは仕様 (インターフェース) だけ統一しておいて、部品として別の部署や別の会社に製作してもらい、最終的に携帯電話に集約させることになる。

まとめ

抽象クラスやインターフェースは、抽象的な概念を表すために用意されている。

抽象クラスは主に継承の階層を表すために導入された抽象的な部類を宣言する場合や、骨格実装を行う場合に用いられる。インターフェースは、いくつかの機能の規格 (名前や呼び出し方) を宣言する場合や、多重継承の代わりに用いられる。

どちらも利点と欠点を持つ。