抽象クラス(abstract)とインターフェース(interface)についての復習

今までJavaの抽象クラス(abstract)とインターフェース(interface)の使い分け方がよく分からなかったのだが、このテーマについて、とても分かりやすく解説している記事があったのでその記事の引用を含めながら、理解を固めようと思う。

抽象クラス(abstract)

抽象クラスとは何か

まさにその名の通り、抽象化されたクラスを指す言葉。抽象化とは、例えば、人間/犬/猫を抽象化する場合には「動物」となるような、共通した項目(簡潔な項目)に置き換えることである。

人間クラス/犬クラス/猫クラスが存在する場合、これらは動物クラスを継承して作ることができる、と考えると分かりやすい。

つまり、人間クラス/犬クラス/猫クラスを構成するのに必要な最低限の実装が動物クラスには含まれており、これを継承(extends)して内部のメソッドをオーバーライドすることで動物としての共通した機能を持たせつつ、種族によって独自の機能を持った人間クラス/犬クラス/猫クラスを作れるのである。

抽象クラスのルール

  • 抽象クラスのオブジェクト(抽象クラスのインスタンス)を生成することはできない。
  • 抽象メソッドが存在するクラスは必ず抽象クラスとして宣言しなければならない。
  • 抽象クラスには通常のメソッドを記述することもできる。
  • 抽象メソッドがない抽象クラスを作ることもできる。
  • 「抽象メソッドが存在する抽象クラス」を継承したサブクラスでは抽象メソッドをオーバーライドしなければならない。
  • 抽象クラスやクラスを多重継承(一度に複数個を継承)することは出来ない。

抽象クラスの記述

抽象クラスと抽象メソッドの書式

//抽象クラスの宣言
abstract class クラス名 {
    //抽象メソッドの宣言
    abstract 戻り値の型 メソッド名(引数リスト);
}

抽象クラスの記述例

public abstract class Animal {
    protected boolean isAlive = false;
    
    protected void Born() {
        isAlive = true;
    }
    
    protected void Dead() {
        isAlive = false;
    }
    
    abstract int Metamorphose(int DNAsequencing);
}

継承する場合の書式

class クラス名 extends 抽象クラス・クラス名 {
    //オーバーライドするメソッド等の実装
}

ちなみに継承によって作られたクラスをサブクラス(子クラス)、継承元になったクラスをスーパークラス(親クラス、基底クラス)と呼ぶ。抽象クラスやクラスの多重継承は許可されていないが、インターフェース同士のみ多重継承が可能である。

オブジェクト参照に使用するキーワード

javaではスーパークラスを指定するキーワードとしてsuperが存在する。サブクラスからスーパークラスを参照したい時は、super.variableName;super.MethodName();という書式で扱うことができる。サブクラスからスーパークラスのコンストラクタを実行したい時は、super();のように記述すればよい。

更にsuperに似たキーワードとして、thisが用意されている。thisも同じようにthis.variableName;this.MethodName();の書式で扱うことができる。thisは自分自身を意味するキーワードである。詳説は省くが、thisキーワードはスコープ上に同名のメンバ変数とローカル変数が存在した場合に、メンバ変数の方を指定したい場合などに使用する。

インターフェース(interface)

インターフェースとは何か

インターフェースについて説明する為にはカプセル化という考え方を知っておく必要がある。
カプセル化とは「クラス内部の複雑な実装を見なくても、手続きさえ知っていれば外部からクラスを扱えるようにしよう」という考え方のこと。インターフェースはカプセル化の為に用意されたものである。何故かというと、インターフェースさえ見ればクラス内部の実装を見なくてもクラスを扱うことができるからだ。インターフェース内部に記述されたメソッドは、インターフェースを使用しているクラス内部でオーバーライドして実装されることが約束されている。もし、実装されていなかった場合はコンパイルエラーとなる。

インターフェースのルール

  • インターフェース内では、変数及び、メソッドの宣言のみ行うことが出来る。
  • インターフェースのオブジェクト(インターフェースのインスタンス)を生成することは出来ない。
  • インターフェースを実装(implements)したクラスはインターフェース側で宣言されているメソッドを実装しなければならない。
  • インターフェース内で定義した変数は、定数になり、変更することができない。
  • Javaはクラスの多重継承を許可しておらず(菱型継承問題のため)、単一継承のみ許可しているが、インターフェース同士での多重継承は許可している。

インターフェースの記述

インターフェースの書式

interface インターフェース名 {
    public void メソッド名1();
    public void メソッド名2();
}

インターフェースを利用する場合の書式

class クラス名 implements インターフェース名 {
    public void メソッド名1(){
        System.out.println("メソッド1です。");
    }
    public String メソッド名2(){
        System.out.println("メソッド2です。");
    }
}

インターフェースの変更は困難

一度リリースされたインターフェースに対し、後から変更を加えることは非常に難しい。インターフェースは最初から正しい設計をする必要があるので、正式リリース前にできるだけ多くのプログラマにこのインターフェースを使って実装してもらい、欠陥を早期に発見できるようにすると良い。

何故、Javaでは多重継承が許されていないのか

実は、多重継承はC++やPerlなどの他の言語では許されているが、Javaは許されていない。何故、Javaは許されていないのだろうか。詳しく見ていくために、まずは多重継承が持つメリットとデメリットを説明する。

メリット

  • 様々な機能を簡単に他のクラスから取り寄せることができる

デメリット

  • 構成が複雑になってしまう
  • クラス群の優先順位が分からなくなる
  • メソッドの名称が衝突してしまうことがある

上記のようなデメリットがあるので、Javaでは多重継承を禁止するという方法で安全を確保している。

継承には2種類の目的がある

仕様の継承

どのようなメソッドを持っているか、どのように振る舞うかを継承する。

実装の継承

どのようなデータ構造を使い、どのようなアルゴリズムで処理するかを継承する。

仕様の継承は、インターフェースにあたる。
実装の継承は、「extends」での継承にあたる。

Javaでは、二種類の継承を分けて、仕様の継承のみ多重にできるようにしている。このようにして、データ構造の衝突やクラス階層の複雑化などを回避している。

抽象クラスとインターフェースをどう使い分けるか

Javaは、複数の実装を許す型を定義するために抽象クラスとインターフェースの2つの仕組みを提供している。 抽象クラスとインターフェースの違いは以下の2点である。

  1. 抽象クラスはいくつかのメソッドに対する実装が許されていて、インターフェースは許されていない。
  2. 抽象クラスで定義された型を実装するには、クラスはその抽象クラスのサブクラスでなければならないが、インターフェースの場合には、ルールに従って要求されるメソッドを全て定義しているクラスであれば、クラス階層のどこに位置していても、インターフェースを実装することが許されている。

これにより、抽象クラスとインターフェースは次のように使い分けることが出来る。

  • 抽象クラスは、実装の継承にあたり、主に同一の継承階層に属するクラスに共通する処理を持ったスーパークラスとして定義する。
  • インタフェースは、仕様の継承にあたり、主に継承階層の異なる複数の型をオブジェクトに持たせるために定義する。

抽象クラスは、クラスを抽象化したものなので子クラスに継承され、具象が実装される。ただし、普通に共通処理を記述することも出来る。抽象メソッドは子クラスでオーバーライドして必ず書き換えなければならない(具象の実装)。

インターフェースは、クラス内を見ずともクラスを扱えるようにする為の約束事(カプセル化)である。約束事なので、インターフェース内部ではメソッドは宣言のみしか行うことができず、定められた約束を破った場合はエラーとなる。

以下より、効果的に抽象クラスとインターフェースを利用する方法を説明する。

型定義にインターフェースを利用する

Javaは単一継承のみを許可しているので、抽象クラスに対する単一継承という制約は、「型定義」として抽象クラスを使用することを著しく妨げている。しかし、インターフェースに限っては多重継承が許されているので、以下のように複数のインターフェースを継承して型を混ぜ合わせることで非常に柔軟な型定義を行うことが出来る。これをミックスイン(Mixin)と呼ぶ(ミックスインには「実装の混ぜ合わせ」と「仕様の混ぜ合わせ」があるが、ここでは後者をミックスインと呼ぶ)。

インターフェース同士の多重継承の例

public interface Singer {
    AudioClip sing(Song s);
}

public interface Songwriter {
    Song compose(boolean hit);
}

public interface SingerSongwriter extends Singer, Songwriter {
    AudioClip strum();
    void actSensitive();
}

骨格(スケルトン)実装に抽象クラスを利用する

Javaでは、インターフェースに実装を含むことは許されていないため、それ単体で提供しても実装者にとっては使い辛いものである。そこで、インターフェースと一緒に骨格実装クラスを提供することでこの問題を解決する。骨格実装は、抽象クラスが型定義の役割を果たす際の深刻な制約に縛られることなく、抽象クラスで実装補助を提供することが出来る。骨格実装を作る場合、それが継承して使われることを想定して実装すべきである。ドキュメントと設計の指針に正しく従って設計を行う。
骨格実装を行うには、最初にインターフェースを調べて基本操作がどのメソッドであるかを決める必要がある。それらの基本メソッドは、骨格実装では抽象メソッドになる。

次のコードは、Map.Entryインターフェースの骨格実装である。

public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
    //基本操作
    public abstract K getKey();
    public abstract V getValue();
    
    //変更可能なマップでのエントリーはこのメソッドをオーバーライドしなければならない
    public V setValue(V value) {
        throw new UnsupportedOperationException();
    }
    
    //equalsのオーバーライド
    @Override public boolean equals(Object o) {
        if(o == this) {
            return true;
        }
        
        if(!(o instanceof Map.Entry)) {
            return false;
        }
        
        Map.Entry<?,?> arg = (Map.Entry) 0;
        return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue());
}

    private static boolean equals(Object o1, Object o2) {
        return o1 == null ? o2 == null : o1.equals(o2);
    }

    //hashCodeのオーバーライド
    @Override public int hashCode() {
        return hashCode(getKey()) ^ hashCode(getValue());
    }

    private static int hashCode(Object obj) {
    }
}

このようにインターフェースと合わせて骨格実装を提供すれば、実装者にとって使いやすいものになる。

参考
書籍 Effective Java
Effective Java 18章 抽象クラスよりインタフェースを選ぶ – Qiita
Mixinについて – メメメモモ
第10章:抽象クラス Java入門 (Java言語編) Accel Works