Javaのインターフェースと抽象クラスの使い分けについて

Javaで開発しているとき、インターフェースや抽象クラスを使用することがあると思います。
Java8でインターフェースにデフォルト実装ができるようになり、インターフェースと抽象クラスで理論的には同じことができるようになりました。
でも同じことができるからって、どちらかだけ使えればいいというわけではありません。

今回はそんなJavaのインターフェースと抽象クラスの使い分けの話です。

スポンサーリンク

はじめに

そもそもインターフェースと抽象クラスって何?って人もいると思うので、それぞれ簡単に説明します。

インターフェースとは

インターフェースとは抽象メソッド定数のみを定義するものです。
※Java8からデフォルト実装ができるようになったため、抽象クラスとの使い分けが理解しにくくなりました。
インターフェースの定義方法は以下となります。

public interface インターフェース名 {
    データ型 変数名 = 値;
    戻り値のデータ型 メソッド名(引数型宣言);
}

クラスはインターフェースを実装(implements)することで、インターフェースに定義されたメソッドがクラスによって使用されます。
インターフェースは、クラスで定義できる型を定義するために使用されます。

メンバ変数定義

インターフェースのメンバ変数にはpublic static finalが自動的に付与されます。
そのため、インターフェースに定義できるメンバ変数は定数のみとなります。

メソッド定義

インターフェースで定義する抽象メソッドはpublic abstractが自動的に付与されます。
Java8から抽象メソッドの他にデフォルト実装が可能となりました。
デフォルト実装の記述方法は以下となります。

public interface インターフェース名 {
    データ型 変数名 = 値;
    default 戻り値のデータ型 メソッド名(引数型宣言) {
        ・・・
    }
}

デフォルト実装のメソッドには自動的にpublicが付与されます。

抽象クラスとは

抽象クラスとは、名前の通り抽象的なクラスのことです。
例えば、「人間」「犬」「猫」のクラスがあったとして、それらが継承している抽象クラスが「哺乳類」みたいな感じです。
抽象クラスは抽象メソッドの定義の他に、メソッドの実装も行えます。
メンバ変数については通常のクラスと同じように定義することができます。
抽象クラスを継承したクラスは抽象メソッドを実装する必要があります。

メソッド定義

抽象クラスにおけるメソッドの記述方法は以下となります。

public abstract class クラス名 {
    // 抽象メソッド
    アクセス修飾子 abstract 戻り値のデータ型 メソッド名(引数型宣言);
    // メソッドの実装
    アクセス修飾子 戻り値のデータ型 メソッド名(引数型宣言) {
        ・・・
    }
}

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

Java7まではインターフェースでメソッドを実装することができなかったため、そこが大きな違いの1つでした。
しかし、Java8からデフォルト実装が可能となったため、インターフェースと抽象クラスで事実上同じことができるようになりました。
例えば以下のようなインターフェースと抽象クラスがあったとします。

public interface ISample {
    default void output(int i1, int i2) {
        System.out.println("ISample:計算結果は" + add(i1, i2) + "です。");
    }
    int add(int i1, int i2);
}
public abstract class ASample {
    public void output(int i1, int i2) {
        System.out.println("ASample:計算結果は" + add(i1, i2) + "です。");
    }
    public abstract int add(int i1, int i2);
}

上記で作成されたインターフェースを実装するクラスと抽象クラスを継承するクラスを以下のように作成します。

public class Main {
    public static void main(String[] args) {
        new ISampleImpl().output(1, 2);
        new ASampleExt().output(1, 2);
    }
}
class ISampleImpl implements ISample {
    @Override
    public int add(int i1, int i2) {
        return i1 + i2;
    }
}
class ASampleExt extends ASample {
    @Override
    public int add(int i1, int i2) {
        return i1 + i2;
    }
}

このMainクラスの実行結果は以下となります。

ISample:計算結果は3です。
ASample:計算結果は3です。

このようにインターフェースと抽象クラスで同じ処理ができてしまいます。
Java7まではインターフェースでデフォルト実装ができなかったので、明確な使い分けが可能でしたが、Java8からそれができなくなりました。
インターフェースと抽象クラスにはもう違いはないんじゃないかと思われるかもしれません。
しかし、インターフェースと抽象クラスにはまだ大きな違いが残されています。
それは多重継承の可否です。

多重継承はクラス設計時に問題を起こしやすいため、Javaではクラスの多重継承を禁止しています。
ただ、禁止しているのはクラスの多重継承であり、インターフェースの多重継承は許容されています。
インターフェースの多重継承というのは以下のようなもののことです。

public class Sample implements InterfaceAB {
    @Override
    public void methodA() {
    }
    @Override
    public void methodB() {
    }
    @Override
    public void methodAB() {
    }
}
interface InterfaceAB extends InterfaceA, InterfaceB {
    void methodAB();
}
interface InterfaceA {
    void methodA();
}
interface InterfaceB {
    void methodB();
}

InterfaceABはInterfaceAとInterfaceBを継承し、SampleはInterfaceABを実装することでInterfaceAとInterfaceBの抽象メソッドも実装する必要があります。
抽象クラスではこのように複数のクラスを継承することができないため、同じことをやるためには以下のようにする必要があります。

public class Sample extends AbstractAB {
    @Override
    public void methodA() {
    }
    @Override
    public void methodB() {
    }
    @Override
    public void methodAB() {
    }
}
abstract class AbstractAB extends AbstractA {
    public abstract void methodAB();
}
abstract class AbstractA extends AbstractB {
    public abstract void methodA();
}
abstract class AbstractB {
    public abstract void methodB();
}

これで一応同じことは実現可能ですが、インターフェースの時と意味合いが全く異なります。
上記のインターフェースと抽象クラスの違いを図にすると以下のようになります。
Java_Interface_Abstract
クラス設計的にこれらは意味が大きく異なります。
多重継承の可否がインターフェースと抽象クラスの違いとなるわけです。

インターフェースと抽象クラスの使い分け

違いはなんとなくわかったけど、結局のところ使い分けはどうするの?って感じですよね。
これに関しては様々な説があるのですが、私は以下のように考えています。

インターフェースはクラスの型を定義し、抽象クラスは処理を定義する。

クラスとして実装していてほしいメソッドが存在する場合はインターフェースを使用し、処理の骨組みを作りたい場合は抽象クラスを使用するということです。
処理の骨組みというのは以下のようなものです。
「哺乳類」という抽象クラスがあり、それを継承する「人間」「犬」「猫」クラスがあったとします。
「哺乳類」に〈食べる〉メソッドを実装し、〈食べ物判定〉メソッドを抽象メソッドとして定義します。
〈食べ物判定〉処理を「哺乳類」の〈食べる〉メソッドで呼び出すことで処理の骨組みを作ることができます。
あとは「哺乳類」を継承した「人間」「犬」「猫」の各クラスで〈食べ物判定〉メソッドを実装することで何が食べられるかを個別に実装できます。
抽象クラス「哺乳類」を作るとしたら以下のような感じになります。

public abstract class Honyurui {
    public boolean taberu(int tabemono) {
        boolean flg = isTabemono(tabemono);
        if (flg) {
            // 食べられる場合の処理
            System.out.println("食べられる");
        } else {
            // 食べられない場合の処理
            System.out.println("食べられない");
        }
        return flg;
    }
    public abstract boolean isTabemono(int tabemono);
}

まとめ

インターフェースはクラスの型を定義するもの、抽象クラスは処理の骨組みを作るもの。
同じことができるようになったとしても、根本が異なるものなのでしっかりと使い分けましょう。

まぁ以下のような記事があるので、Javaがいつまで使われるかわかりませんが…。

Javaのエンタープライズ向け機能セットであるJava Enterprise Edition(Java EE)の開発が停滞しており、この背景にはJava EEに対するOracleの取り組みが著しく減速している現状があると報じられている。

引用元:ZDNet Japan