前書き
新卒のメンター(身近な先輩)役をしていて、新卒からの疑問点として挙がった話。こいつらって何が違うのだろうか?
自分が研修で学んだ際もよくわからなくて、一度調べたものの完全に忘れてしまっていました。そこで、復習がてら記事にしようと思います。
共通点
違いが分からない、ということは言い換えると、似ている部分、つまり共通点が存在するということです。
そもそも抽象クラスもインターフェースも意味が分かっていない人は、まず以下の共通点を覚えましょう。
抽象クラスとインターフェースは
「複数のクラスを作成するうえで、共通項を括りだしている」
ポイントは2つ。
①複数のクラスを作成することを前提としていること
②その作成する複数のクラスにおける共通項(変数、メソッド)を括りだしているということ
それではそれぞれの違いを見ていきましょう。
抽象クラス
抽象クラスを理解するポイントは、その名の通り、「クラスを抽象化したものである」ということです。
抽象クラスが親クラスとなり、それを具体化される子クラスが継承する、というような使われ方をします。
抽象化された親クラスから具体化された子クラスを生み出すという流れで使われるため理解しづらいですが、具体クラスから抽象クラスを考えてみると分かりやすいのではないかと思います。
例として2つのクラス、小学生クラスと社会人クラスを見てみましょう。
class ElementaryStudent {
//コンストラクタ
public ElementaryStudent(String name, String school) {
this.name = name;
this.school = school;
}
public String getName() {
return this.name;
}
public void goToSchool() {
System.out.println(school + "に行ってきます!");
}
}
class WorkingAdult {
//コンストラクタ
public WorkingAdult(String name, String company) {
this.name = name;
this.company = company;
}
public String getName() {
return this.name;
}
public void drink() {
System.out.println("今日も" + company + "の同僚と酒を飲むぞ!");
}
}
上記の2つのクラスはnameというフィールド変数と、getNameというメソッドを共通して持っています。
今回は2つのクラスしかなく、クラスの中身も少ないのであまり困りませんが、今後さらに赤ちゃんクラス、幼稚園児クラス、中学生クラス...などと増やしていく際に、毎回これらのフィールド変数とメソッドを記述するのは面倒だし、保守性も下がります。そこで活躍するのが抽象クラスです。共通点を括りだす、まさにクラスを抽象化したものですね。
それでは、抽象クラスであるPersonクラスを見てみましょう。
abstract class Person {
//コンストラクタ
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
これを使えば先ほどのクラスはこのようにして書けます。
class ElementaryStudent extends Person {
//コンストラクタ
public ElementaryStudent(String name, String school) {
super(name);
this.school = school;
}
public void goToSchool() {
System.out.println(school + "に行ってきます!");
}
}
class WorkingAdult extends Person {
//コンストラクタ
public WorkingAdult(String name, String company) {
super(name);
this.company = company;
}
public void drink() {
System.out.println("今日も" + company + "の同僚と酒を飲むぞ!");
}
}
ちょっぴり楽になりましたね。
今回は抽象化できた部分が少ないのでありがたみが少ないですが、具体化されるクラスや共通化できる中身が増えれば増えるほど威力を発揮します。
インターフェース
インターフェースは、「クラスを作成するうえで、特定のメソッドが使えることを保証するもの」です。
理解するためのポイントは、インターフェスは「継承」するわけではなく、「実装」する、ということです。インタフェースを実装する際は、「extends」ではなく、「inplements」を使用します。
インターフェースではメソッドのシグネチャ(メソッドの名前、戻り値の型、引数の型と順序を指定したもの)のみを定義し、メソッドの実装を提供しません。何を使うかは決まっているが、どのような処理をするかは何も決まっていない、いわばメソッドの箱だけがいくつか存在するということです。
「実装」というように、インターフェースに書かれているメソッドは中身がないので、新たに生み出したクラスで処理を書かないと何も実行できません。先ほど見た抽象クラスのgetNameでは、抽象クラスを継承した子クラスであればどんなクラスでも同じ処理を行います。
一方、インターフェースを実装したクラスでは、各クラスによってどのような処理を行うのかを各クラスでコーディングする必要があり、それゆえ処理の内容も変わってきます。(ただし、抽象クラスにも「抽象メソッド」という、中身のないメソッドを書くことができます。)
実際に見てみましょう。
// Personインターフェースの定義
interface Person {
void introduce();
void greet():
}
上記が示しているのは、Personクラスを実装したクラスであるならば、introduce, greetというメソッドを必ず持つ、ということです。この時点では各メソッドがどのような処理を行うのかは決まっていません。
ではこのPersonインターフェースを実装したクラスを見てみましょう。
// ElementaryStudentクラス(Personインターフェースを実装)
class ElementaryStudent implements Person {
private String name;
public ElementaryStudent(String name) {
this.name = name;
}
public void introduce() {
System.out.println("はじめまして。"+ name + "だよ。これからよろしくね。")
}
public void greet() {
System.out.println("あーそーぼー。")
}
}
// WorkingAdultクラス(Personインターフェースを実装)
class WorkingAdult implements Person {
private String name;
public WorkingAdult(String name) {
this.name = name;
}
public void introduce() {
System.out.println("はじめまして。"+ name + "と申します。今後とも、よろしくお願いいたします。")
}
public void greet() {
System.out.println("お疲れ様です。")
}
}
このように、Personインターフェースを実装したクラスはintroduce,greetメソッドを持つことが保証されます。もし存在するはずのメソッドが存在していなければエラーになります。
抽象クラスのメリットはわかりやすかったですが、インターフェースは何が嬉しいのか少しわかりづらいですよね。
いくつかメリットはあると思いますが、中でも一番なるほど!と思ったものを紹介します。
既存のコードに影響を与えずに新しいクラスを追加・変更できる
インターフェースではクラスではない(=インスタンス化できない)けれども、型として扱うことができます。
つまり、インターフェースで定義した型を引数に取る関数のコードを変更せずに、新しいクラスを引数に渡すことができます。
// Personインターフェースを実装したオブジェクトを引数として受け取る関数
public static void sayHello(Person person) {
person.introduce();
person.greet();
}
上記の関数はPersonインターフェースを実装したクラスであれば、引数として使ってよいことが保証されます。
もし、インターフェースがなければ、引数に取る型を指定できなかったり、sayHello関数のコードを変更する必要が出てきたりします。
まとめ
- 共通点は「複数のクラスを作成するうえで、共通項をくくりだしている」こと
- 抽象クラスは「クラスを抽象化したものである」
- インターフェースは「クラスを作成するうえで、特定の動作をすることを保証するもの」