LoginSignup
516
472

More than 5 years have passed since last update.

【詳解】抽象クラスとインタフェースを使いこなそう!!

Last updated at Posted at 2018-04-22

はじめに

皆さんは抽象クラス、インタフェースこの2つの違いについて理解していますか?
また、理解して使いこなせていますか?

多くの先輩に聞いたり、ネットで調べても....
クラス仕様としての型定義をしたいならインタフェース
継承関係にあり、処理の再利用をしたいなら抽象クラス
と返ってくる。
それはもう知ってるし、理解しているつもり、でも使いこなせない。

ならどうすれば理解して、使いこなせるようになるのか?
表面上の違いを知るのではなく、本質(なぜそのような仕様になっているのか?)を理解するべし
と思い、調べてまとめたものがこちらの記事です。

抽象クラスとは?

結論から言うと抽象クラスは継承関係にあり、処理の再利用をしたい時に使うものです。
※この結論に至るまでのプロセスを説明します。
そのプロセスを知った上で結論を持つことが本質的な理解に繋がると思うので、気長に読み進めて言ってください。

概念

抽象クラスと具象クラスの「継承」関係はIS Aの関係と言われています。
犬 IS A 動物犬は動物だ のような親子関係です。
だから動物クラス(抽象クラス)に犬や人間が持っている共通動作(処理)を書きましょう!
と言われている。
一旦まとめると、子供のみんなが出来ることを抽象的に抽象クラスに書いて、その子にしか出来ない具体的なことは具象クラスに書く

サンプルコード

上記のことを例にとってサンプルコードを書いてみます。
では抽象的に抽象クラスを考えると、動物は名前持っている。
そして夜は寝るし、ご飯を食べて、話すことが出来る。
※寝るのは共通しているとする。

寝る行動はどの動物も同じなので共通した抽象クラスで実装。(寝方が違うとは言わないでください)
ご飯を食べるのも話すのも出来るが、具体的なことは動物によって異なるので、抽象メソッドとして定義。

Animal.java
public abstract class Animal {
    String name;

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

    public void sleep(){
        System.out.println("寝る");
    }
    public abstract void speak();

    public abstract void eat();
}

犬の話し方と食べ方は違うので具象クラスで具体的に実装。

Dog.java
public class Dog extends Animal{

    public Dog(String name){
        super(name);
    }

    @Override
    public void speak() {
        System.out.println("私の名前は"+name+"だワン!!");
    }

    @Override
    public void eat() {
        System.out.println("ガブガブガブ");
    }
}

人間も同様に話すことと食べることは具象クラスで具体的に実装。

Human.java
public class Human extends Animal{

    public Human(String name) {
        super(name);
    }

    @Override
    public void speak() {
        System.out.println("初めまして、私の名前は"+name+"です。");       
    }

    @Override
    public void eat() {
        System.out.println("もぐもぐ");     
    }
}

作ったクラスを呼び出すためのクラス

Main.java
public class Main {
    public static void main(String[] arg){
        Human tarou = new Human("太郎");
        Dog pochi = new Dog("ポチ");

        System.out.println("****太郎の処理****");
        tarou.sleep();
        tarou.eat();
        tarou.speak();

        System.out.println("****ポチの処理****");
        pochi.sleep();
        pochi.eat();
        pochi.speak();
    }
}

/* 出力結果
****太郎の処理****
寝る
もぐもぐ
初めまして、私の名前は太郎です。
****ポチの処理****
寝る
ガブガブガブ
私の名前はポチだワン!!
*/

インタフェースとは?

結論から言うとインタフェースとはクラス仕様としての型定義をするものです。

概念

まずインタフェースの言葉の意味は「コンピューターと周辺機器を接続するための規格や仕様、またはユーザーがコンピューターなどを利用するための操作方法や概念のこと。ハードウェアでは、機器同士を接続するコネクターの規格を指す。」などが挙げられます。
もっと簡単に言うと、外から見える何かと何かをくっつける窓口のことです。

そしてインターフェイスと実装クラスの「実装」の関係はCAN DOの関係と言われています。
レジ CAN bill クレジットカードレジはクレジットカード会計が出来る。のような関係です。
他にもレジは現金会計もできます。
なのでインタフェースで定義するものはレジが出来ること、会計のみを定義します。
他の視点から言うと、中は意識せず、外から見てレジが出来ることを定義する感じです。
そうするとレジを使う店員はいちいちどんな会計か考えずにただ会計をする事が出来る*はずです。

サンプルコード

上記のことを例にとってサンプルコードを書いてみます。
レジインタフェースには出来る事(=会計)のみを定義して、レジを使う店員クラスは会計のみを実行します。
クレジット会計や現金会計は、インタフェースで定義した会計を実装します。

レジインタフェースに会計メソッドを定義します。

Cashier.java
public interface Cashier {
    public void bill();
}

インタフェースを実装しクレジット会計の詳細を実装します。

CreditCard.java
public class CreditCard implements Cashier{
    @Override
    public void bill() {
        System.out.println("クレジットカードでお会計をいたします。");
    }
}

インタフェースを実装し現金会計の詳細を実装します。

Cash.java
public class Cash implements Cashier{
    @Override
    public void bill() {
        System.out.println("現金でお会計いたします。"); 
    }
}

そして店員はレジの会計を実施します。
この時Cashier#billを呼び出しているので、これが現金会計なのかクレジット会計なのかは
意識せずに使う事ができます。

Staff.java
public class Staff {
    public static void main(String[] arg) {
        Cashier cash = new Cash();
        Cashier credit = new CreditCard();

        System.out.println("***現金対応***");
        cash.bill();

        System.out.println("***クレジット対応***");
        credit.bill();
    }
}

抽象クラスとインタフェースについて一度整理

一旦ここまでが、ざっくりとした抽象クラスとインタフェースについての説明です。
これからもっと深く考察していきますが、まずはここで整理しましょう。
抽象クラスとは、
抽象クラスと具象クラスの「継承」関係はIS Aの関係であり、親クラスには抽象的に子供達が出来ることをまとめます。

インタフェースとは、
インターフェイスと実装クラスの「実装」の関係はCAN DOの関係であり、インタフェースには出来る事のみを定義し、具体的なことは実装クラスにお任せします。なので使う側は中で実装された詳細は気にする必要が無くなります。

個人的な考え

自分の考えでは、抽象クラスとインタフェースは基本的に合わせて使うもの(使うことになる)だと思っています。
インタフェースを使い、外から使える機能を定義して、中では共通処理をまとめるために抽象クラスを使うものだと思っています。
これは後で説明する「アクセス修飾子」のところで詳しく説明します。

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

ここから本質を理解するためになぜそのような仕様になっているのか?といった視点で深掘っていきます。
まずは抽象クラスとインタフェースの違いをまとめて見ましょう。

f 抽象クラス インタフェース
アクセス修飾子 public,protected publicのみ
変数の定義 インスタンス変数、ローカル変数、クラス変数なんでも定義できる。 public static final な定数(クラス変数)しか持てない。
継承した先で上書きすることはできない。
継承 多重継承不可 多重継承可
メソッド定義 具体的な処理もかける メソッドの型しか定義できない。
java8からはdefaultメソッドを使って処理もかける。

なぜ指定できるアクセス修飾子が違うのか?

インタフェースはpublicのみ。
抽象クラスはpublicとprotectedのみを定義できる。
publicで定義したメソッドはどこからでも呼び出せるよ!といった意味で、
protectedで定義したメソッドはそのパッケージ内だけから呼び出せるよ!といった意味だ。

と言うことはpublicメソッドは外から呼び出すためのもの→外で使う側の人ため→インタフェースでは使う人のためにメソッドを定義していると言うことだ。

ではprotectedメソッドはパッケージ内でしか使えない→中で使う人のため→抽象クラスは中で使う人のためのものと言うことだ。

まとめ

インタフェースは外で使う人のためなのでpublicメソッドしか定義できない。
抽象クラスは中で使う人のためなのでprotectedまで定義できる。

抽象クラスの多重継承は出来ないがインタフェースはできる。なんで?

多重継承とは親クラスを複数継承することです。
抽象クラスは多重継承は仕様上許されていませんが、インタフェースは許されています。
菱形継承問題と言うのがよく多重継承の例として挙げられるそうです。
多重継承.png

多重継承(Mix-Inと呼ばれるらしい)によって起こる問題を考えてみると、以下のような事が挙げられます。
・メソッド名の重複による名前解決の問題
 ClassBとClassCのメソッド名が同じなためどちらをClassDで持つのか?または実装するべきなのか?
・クラスの複数回継承による問題
 ClassB,ClassCは共にClassAを継承しているので、ClassDはClassAを2度継承することになる。

抽象クラスはインスタンス化(実態、抽象クラスが持っているメソッドを実態にする感覚!?)できるため、
この問題が起こる。なので仕様上許されていない。
ならインタフェースはどうだろうか
この2つの問題を解決するためにある決まりがある。
・メソッドの呼び出しクラスに最も近い位置にいるメソッドが呼ばれる
・メソッドの呼出優先順位が判断できない、または同一となった場合、コンパイルエラーとしてオーバーライドを促す
・インタフェースはインスタンス化できない
 (インタフェースで書かれたメソッドは実態と紐づけているだけなのでインスタンス化はしていない。)

まとめ

抽象クラスはインスタンス化した時に、名前解決などの問題が生じるから多重継承できないような仕様担っている。
インタフェースはインスタンス化できず、あくまで「インタフェース」としてインスタンスの中に入っているだけなので多重継承が可能。
こちらの記事がすごく参考になった。

インタフェースはなんでインスタンス変数をもてないの?

これは多重継承の時に述べたようにインタフェースはインスタンス化できないから
インスタンス変数を持てないようにしているだけだ。
逆の言い方をするとインスタンス変数を持つと、問題が生じてしまうと言うことだ。

まとめ

多重継承による問題が生じるからpublic static final な定数しか持てないような仕様にしている。

なんでjava8からdefaultメソッドが導入されたのか?

まずdefaultメソッドについて説明すると、
defaultメソッドはインタフェースに定義できる具体的な処理内容を実装したメソッドのこと。
これによって、実装するクラス内でオーバーライドしてメソッドの処理内容を記述する必要がなくなりました。

これにより疑問が生じると思います。
抽象クラスとインタフェースの違いがなくなってしまった。。。
調べていくとそもそも作られた背景が全く違うようです。

java8からはStream APIが使えるようになりました。
※Stream APIの使い方についてはこちらを参照してください。
Stream APIはコレクションの操作(Listの操作とか)ができるようになってます。
この操作をするためにListインタフェースに型を定義(Stream APIなど外から使えるように)して、具体的な処理は実装クラスに書くことになると、継承しているクラスを全て書き換えたりとても大変なことになります。
この互換性を失わないためのものとして出来たのがdefaultメソッドです。

Listインタフェースを見てみると実際にdefaultメソッドが存在しました。

    default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }

まとめ

インタフェースのdefaultメソッドはjava7とjava8の互換性のために出来たメソッド。
なので共通の処理をまとめるためのものではないので、抽象クラスとは全く別物だと言う事がわかりました。

全体を通してのまとめ

抽象クラスとインタフェースはできる事が似ているかもしれませんが、作られた背景や仕様が全く異なると言う事がわかりました。
抽象クラスは、共通の処理をまとめたりする中で使う人のためにあり、
インタフェースは、詳細は見せず出来ることを定義し、外から使う人のためにある全く別物でした。

余談ですが、実際に記事を書いていく中で、理解が深まりましたし、実際に間違った使われた方をしている事が多くあることにも気づけました。
何かわからないこと、おかしいと思う事があればコメントしていただけると幸いです。

516
472
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
516
472