インターフェイスと抽象クラスの違いについて自分を納得させるための説明。
はじめに
なぜこの2つがあるのか。勉強会に参加していると同じ疑問をもつ人が多いことに気づいた。一度は自分で納得したのだけど忘れてしまったので思い出すことにした。また忘れたとしてもいつでも参照できるようにここにメモしておく。疑問を自分に投げかけ、それに納得できるような説明を行うことを繰り返したので、ここでは会話形式で記録しておきたい。Java初心者のAさんが次々質問し、Java中級者Bさんがそれに答えるといった感じ。
多分、Javaの文法を一通り勉強してみた初心者の人のモヤモヤを解消できる程度の説明にはなっていると思う。なお、会話の内容は以下の書籍に書かれている文章を自分なりに咀嚼したものです。Javaをまともに勉強せずにAndroidのプログラミングを始めた僕にとってはとてもわかりやすく正確な説明で、ためになった。
Bさんが発する言葉の一部について、「こういうことだろう」という独自の解釈も多少含まれているので、間違っているところがあれば指摘してもらえるとありがたいです。
インターフェイスと抽象クラス
A:「インターフェイスと抽象クラスってどう使い分ければいいんですか?」
B:「基本的には多重継承が可能なインターフェイスを使えばいいんじゃない?」
A:「じゃあ抽象クラスは何のためにあるんですか?インターフェイスみたいに多重継承できないし、メリットがいまいちよくわかりません。」
B:「抽象クラスがインターフェイスよりも優れているのは、具体的な実装が持てるというところだよ。インターフェイスは定数と抽象メソッドしか持てないのに対して(AndroidエンジニアのためのモダンJavaにはネストしたstaticクラス/インターフェイスも持てると書いてあるが、ここでは触れない)、抽象クラスは通常のメソッド(以下、「通常メソッド」とする)やコンストラクタも持つことができる。」
A:「ふむふむ」
B:「だから、例えば配下のクラス全部に、処理内容が同じメソッドと、個別に処理内容を変えたいメソッドの両方を持たせたいなら、インターフェイスよりも抽象クラスを使う方がいい。処理が全く同じメソッドなら、抽象クラスの中の通常メソッドに書いてしまえば、その子クラスはそれをそのまま使えるからね。」
A:「具体例で教えて欲しいです。」
B:「例えば「図形(Figure)」が親クラスで、「三角形(Triangle)」「四角形(Rectangle)」が子クラスであるとしよう。これらのクラスは以下のフィールド、メソッドを持つとする。」
1.それぞれのクラスは「幅(width)」と「高さ(height)」をフィールドに持つ。
2.「幅(width)」と「高さ(height)」を出力するメソッド「show_width_and_height()」を持つ。
3.面積を求めるメソッド「getArea()」を持つ。
B:「コードで表すと、以下のような感じになる。」
public abstract class Figure {
//フィールド
protected double width;
protected double height;
//コンストラクタ
public Figure(double width, double height) {
this.width = width;
this.height = height;
}
//幅と高さを出力
public String show_width_and_height() {
return "幅は" + String.valueOf(width) + "、"
+ "高さは" + String.valueOf(height);
}
//面積を出力(抽象メソッド)
pubic abstract double getArea();
}
public class Triangle extends Figure {
//コンストラクタ
public Triangle(int width, int height) {
super(width, height);
}
//三角形の面積を出力
@override
pubic double getArea() {
return this.width * this.height / 2;
}
}
public class Rectangle extends Figure {
//コンストラクタ
public Rectangle(double width, double height) {
super(width, height);
}
//四角形の面積を出力
@override
public double getArea(){
return this.width * this.height;
}
}
B:「Figureは抽象クラスとして書いている。フィールドとしてwidthとheightを定義し、show_width_and_height()を通常メソッド、getArea()を抽象メソッドとしている。インターフェイスのメソッドは抽象メソッドのみであるのに対し、抽象クラスの中には通常メソッドを持てることを思い出して欲しい。width、height、show_width_and_height()は、FigureからTriangle、Rectangleに引き継がれるので、子クラス内で再び書く必要はない。インターフェイスだとこのような具体的な実装はクラスに引き継がせることはできない。」
A:「なるほど!」
B:「それからgetArea()だが、これを抽象メソッドにしている。なぜかというと、三角形と四角形で面積の求め方は異なるので、中身はそれぞれの子クラスに任せたいという意図があるからだ。」
A:「でも面積を求めるメソッドを中身がダミーの通常メソッドにして、それをオーバーライドさせるっていうのもありなのでは? もっと言うと、getArea()はFigureの中で書かないで、それぞれの個別の子クラスの中で書くこともできますよね?」
B:「可能ではあるが、それだとコードを書く人が、子クラスにgetArea()を実装してくれる保証はない。それを保証したいとき、抽象クラスはいい働きをするんだ。抽象クラスは自身が持つ抽象メソッドを、配下のクラスに強制的にオーバーライドさせることができるので、実装漏れを防止することにつながる。まあこの点に関してはインターフェイスと同じかな。」
A:「この機能は必須だぞ! と主張するわけですね。」
B:「その他の違いとしては、抽象クラスはクラス階層の一部に属するのに対して、インターフェイスは独立しているんだ。正確ではないが、Javaの世界のクラス階層のざっくりとした模式図を書いてみたので確認してほしい。」
B:「クラスを定義するときは継承元を明示しなくても、クラス階層の頂点にあるObjectクラスを暗黙的に継承することになる。このことは、既に存在するクラス階層に追加されるということを意味している。インターフェイスはクラス階層から独立しているからどんなクラスにも自由に紐づけられる。また、AbstractMapという抽象クラスはObjectクラスの子クラスであり、HashMap、TreeMapといった具象クラスの親クラスだ。抽象クラスがクラス階層の内部に埋め込まれているというわけだ。インターフェイスはクラスではないので、こんなことはできない。」
A:「説明してもらうと全然違いますね。」
最後に
2つはどう違うのかという点では、かなり理解を深められたつもりだが、具体的な使い分けをどうするのかという点に関しては、きっとまだまだ奥が深いのだろう。どうやら増補改訂版 Java言語で学ぶデザインパターン入門とかに詳しく書かれているらしい。実は以前購入したがあまり読めてない。新しくわかったことがあれば追記するかも。