【Javaお勉強日記】ポリモーフィズムと抽象クラスと具象クラス(実装クラス)の話
一月前のわたしへ。
「実装クラスって何なの!!!」の、謎が、今回なんと、解かれます。
今日のわたしより。
今回の記事は、以下の記事で勉強したことの上に成り立っています
【Javaお勉強日記】クラスの継承についてまとめる|yucco|note
【Javaお勉強日記】メソッドとコンストラクタのオーバーロード|yucco|note
【Javaお勉強日記】型の自動変換についてまとめる|yucco|note
【Javaお勉強日記】メソッドのオーバーライド|yucco|note
長い道のりでしたね。
オーバーライドしたメソッドを含むサブクラスのインスタンスを、スーパークラス型の変数に代入して使う
横文字多すぎてなんのこっちゃ!って感じですが、上の四つの記事の内容について勉強してきたわたしならもう理解できます。
この図のことを思い出しましょう。
Book型を継承して作ったサブクラスComic型のインスタンスを、無理矢理Book型の変数に代入する(アップキャストする)と、「実体はComic型だけど見た目はBook型」というインスタンスを作ることができたんでした。
で、このとき、例えばComicクラスのgetTitleはオーバーライドして、「seriesTitle + title」を返すようにしてあったとします。
すると、スーパークラスであるBook型の変数として振る舞っているにもかかわらず、getTitleを呼び出された時だけは、Comicクラスで定義した仕事をしてくれるのです。
(見た目はBook型なので、Comic型にしかない変数seriesTitleにはアクセスできないような気もしますが、「getTitleをオーバーライドして、getTitle内でseriesTitleにアクセスする」なら、アクセスできちゃいます)
(※でも、Comicクラス内でgetTitleをさらにオーバーロードしていた場合、Book型にアップキャストして使っている間は、オーバーロードした引数の構成では使うことができません。オーバーロード後の引数構成になっているメソッドがBook型にないから)
さらに、BookのサブクラスとしてMagazineクラスなんかも作っちゃって、こっちではgetTitleをすると、発行年月を入れて「雑誌名-YY年MM月号」みたいに表示されるようにする、じゃあ図鑑クラスも作ろう、辞書クラスも欲しい、その時はタイトルはこうやって表示したい……なんて、バリエーションがどんどん増えていったとしても、新しいサブクラスを作ってgetTitleをオーバーライドして使う様にすれば、元のクラスは一切弄る必要が無い。
呼び出す側も、「とにかくタイトルを使いたい時は絶対getTitle()」とするようにしておけば、どのサブクラスを使うかを変えるだけで挙動を変えることが出来る。便利だね!
……っていう考え方を、ポリモーフィズム(Polymorphism)と言う。
さらに便利に使う
public class Exec {
public static void main(String[] args){
Book book1 = new Book("わんわん大百科");
Comic comic1 = new Comic("にゃんにゃん冒険記","冒険記シリーズ");
Magazine magazine1 = new Magazine("ぴょんぴょん大集合",2021,1);
print(book1);
print(comic1);
print(magazine1);
}
public static void print(Book book){
System.out.println(book.getTitle());
}
}
こんな具合に、スーパークラス型の変数を引数に取るメソッドを用意しておいて、スーパークラスに存在するメソッドだけを使った処理を書いておけば、どのサブクラスの変数であってもまとめて受け入れることができる。
※「メソッドに引数として渡す」ときに、変数への代入が行われる(=自動的にアップキャストされる)ので、上記の例で言えばBook型のサブクラスならどんな型でも渡せる。
(「何渡してもObject型で返ってきちゃうメソッド」も、全てのクラスはObjectクラスのサブクラスだから、Objectクラスへアップキャストして受け取り、Objectクラスのまま返してる、ということなのか…)
じゃあ、最初から全部サブクラスに処理書けば良いんじゃないか??
どうせサブクラスごとにやることが違って、どうせ全部サブクラスがオーバーライドしちゃうんだから、スーパークラスに処理書く必要ないのでは?!
ということに人類は気がつきました。
で、ならスーパークラスには空のメソッド置いておけばいいじゃん、ってことになるんですが、流石に「何かの仕事をする」ために存在するはずのメソッドが空のままでは、オーバーライドを忘れたときに「虚無」が実行されてしまってエラーの原因になります。
なので、「このメソッドはちゃんとオーバーライドしないと動かないぜ!!」っていうことを明示的に書いておきたい。そうすればコンパイルエラーでオーバーライド忘れに気づける。
そこで登場したのが「抽象クラス」です。
抽象クラスの宣言の仕方
// クラス宣言にabstructを付ける
public abstract class Book {
// フィールド変数や普通のメソッドは普通に作れる
private String title;
// コンストラクタのアクセス修飾子はprotectedにした方がいい
protected Book(String title){
this.title = title;
}
}
クラス宣言に abstruct を付けるだけで、抽象クラスになる。abstruct を付けるのは、アクセス修飾子の前か後で、classより前。
その際、コンストラクタを protected にするくらいで、後は普通にフィールド変数やメソッドを書けばOK。
抽象クラスの特徴
抽象クラスにすることで、例の「後から書き換えて使うから空っぽで作っておくメソッド」=抽象メソッドを作ることが出来る。
さらに、抽象クラスはnewを使ってインスタンスを作ることが出来なくなる。(必ずサブクラスを作って、抽象メソッドをオーバーライドして使う)
インスタンスを作れない=抽象クラスのコンストラクタを呼ぶのはサブクラスのコンストラクタだけなので、抽象クラスのコンストラクタのアクセス範囲はprotectedにしておく。
抽象メソッドの作り方
// 抽象クラスを作る
public abstract class Book {
private String title;
protected Book(String title){
this.title = title;
}
// abstractを付与して、ボディを削除すると抽象メソッドになる
public abstract String getTitle();
}
抽象クラスを宣言した後、抽象メソッドにしたいメソッドにも abstruct を付与する。
それから、{}(ボディ)部分を削除し、文末には ; を追加する。
これで抽象メソッドのできあがり。
使う時はクラスを継承→オーバーライドして使います。(抽象メソッドをオーバーライドして使えるようにすることを「実装する」という)
これで、「抽象クラスを作って、サブクラスでオーバーライドして実装する」という一連の流れが完成です。
「なんでわざわざ空っぽなクラスを一個挟まなきゃならんのだ??」の謎がこれで解決しましたね!!!!後から拡張しやすくするためだよ!!!
細かいお約束事項
・別に、抽象クラスだからといって必ず抽象メソッドを持たなければいけないわけではない。具象メソッド(ふつうのメソッド)だけでもいい。
・抽象クラスを継承したクラスが具象クラスである必要もない。抽象クラスを継承して抽象クラスを作っても良い。抽象クラスを作った場合、メソッドをオーバーライド(実装)しなくてもコンパイルエラーにならない
・具象クラスを継承して抽象クラスを作っても良い(そもそも、全てのクラスの基底クラスであるObjectも具象クラスだけど、それを継承して抽象クラスを作っている)
この記事が気に入ったらサポートをしてみませんか?