
Javaのオーバーライドとは?オーバーライドとみなされる条件や使い方
Javaにおける「継承」とは、親クラスのメンバ(フィールドやメソッド)をすべて引き継いだ子クラスを定義することです。このとき、単に引き継ぐだけでなく、親クラスと同じメソッド名を子クラスで再定義することができます。これをオーバーライドといいます。
目次
1.オーバーライドってなんだ?
オーバーライドとは、前述した通り「継承した親クラスのメソッドの内容を子クラスで再定義すること」です。
例えば親クラスで定義したメソッドを子クラスで使用する際、いくつかの条件をクリアした上で処理内容だけを子クラス独自のものに変えることができます。
そしてオーバーライドしても親クラスで定義した処理内容が使えなくなるわけではなく、子クラスで親クラスのメソッドを呼び出して使用することもできます。(再利用がしやすい)
親クラスの処理内容を変えたいときも、メソッドを呼び出しているだけの子クラスで変更を加える必要はありません。また、子クラスで新たに定義した処理内容だけを変更するとき、親クラスや他の子クラスに影響を与えません。(保守がしやすい)
このように、オーバーライドを使用すると「再利用のしやすさ」と「保守のしやすさ」の2点を保つのに非常に便利なのです。
では、これらをひとつずつ解説していきたいと思います。具体例を見てもらう前に、まずはオーバーライドの条件についてです。
1-1.オーバーライドとみなされる条件
オーバーライドとみなされるためにはいくつかの条件があります。
- 戻り値(※)、メソッド名、引数の型・数・順番が同じであること。
- アクセスレベルが親クラスと同じか緩い制限であること。(例:親クラスがprotectedなら子クラスはpublicかprotectedを指定可能)
※例外として「共変戻り値」という仕様がありますが、オーバーライドの基本的な理解を優先したいためここでは詳しい説明は割愛いたします。
1-2.オーバーライドができない条件
- オーバーライド元のメソッドにfinalが指定されている場合、オーバーライドはできません。処理内容を変更させたくないメソッドにはfinalをつけましょう。
- クラスメソッド(staticメソッド)はオーバーライドできません。オーバーライドできるのはインスタンスメソッド(非staticメソッド)だけです
1-3.オーバーライドしなければいけない条件
抽象クラスである親クラスを継承して具象クラスを定義する場合、抽象メソッドは必ずオーバーライドしなければなりません。
抽象メソッドが残ったままでは具象クラスとみなされないからです。抽象クラス・抽象メソッドについてはこちらの記事を参考にしてみてください。
関連記事2.オーバーライドを使おう
続いて、オーバーライドの具体的な使い方についてご紹介します。
親クラスTanakaと子クラスTanakaIchiroとTanakaJiroを定義し、実行クラスMainでインスタンス化してそれぞれのメソッドを実行します。
// 親クラス class Tanaka { public String name = "田中"; public int age = 45; public void introduce() { System.out.println("Tanakaクラスのメソッド"); System.out.println("この人は" + name + "です。"); } public void howOldAmI() { System.out.println("Tanakaクラスのメソッド"); System.out.println("家長は現在" + age + "才です。"); System.out.println(""); } } // 子クラスTanakaIchiro class TanakaIchiro extends Tanaka { public String name = "一郎"; public int age = 20; @Override public void introduce() { System.out.println("TanakaIchiroクラスのメソッド"); System.out.println("あの人は" + name + "です。"); System.out.println(name + "は大きな声で挨拶をします"); } } // 子クラスTanakaJiro class TanakaJiro extends Tanaka { public String name = "二郎"; public int age = 10; @Override public void introduce() { System.out.println("TanakaJiroクラスのメソッド"); System.out.println("その人は" + super.name + " " + name + "です。"); System.out.println(name + "は小さな声ですが必ず頭を下げて挨拶をします"); this.howOldAmI(); super.howOldAmI(); } @Override public void howOldAmI() { System.out.println("TanakaJiroクラスのメソッド"); System.out.println(name + "は" + age + "才です。"); System.out.println(""); } } //実行クラス public class Main{ public static void main(String[] args){ Tanaka tanaka = new Tanaka(); System.out.println("①"); tanaka.introduce(); tanaka.howOldAmI(); TanakaIchiro ichiro = new TanakaIchiro(); System.out.println("②"); ichiro.introduce(); ichiro.howOldAmI(); Tanaka tanaka2 = new TanakaJiro(); System.out.println("③"); tanaka2.introduce(); } }
ソースコードの内容を見ると、親クラスTanakaでは変数nameとageを定義し二つのメソッドintroduce()とhowOldAmI()を定義しています。
そして、Tanakaを継承したTanakaIchiroクラスではintroduce()をオーバーライドしています。howOldAmI()はオーバーライドしていません。もう一つの子クラスTanakaJiroクラスでは両方オーバーライドしています。
そして最後に、実行クラスMainでは3つのパターンに分けてインスタンスを作り、それぞれのメソッドを実行しています。
以下が実行結果です。
① Tanakaクラスのメソッド この人は田中です。 Tanakaクラスのメソッド 世帯主は現在45才です。 ② TanakaIchiroクラスのメソッド あの人は一郎です。 一郎は大きな声で挨拶をします Tanakaクラスのメソッド 世帯主は現在45才です。 ③ TanakaJiroクラスのメソッド その人は田中 二郎です。 二郎は小さな声ですが必ず頭を下げて挨拶をします TanakaJiroクラスのメソッド 二郎は10才です。 Tanakaクラスのメソッド 世帯主は現在45才です。
一つずつ見ていきましょう。
①は親クラスTanaka型の変数tanakaにTanakaクラスのインスタンスを生成しています。各メソッドを実行すると、Tanakaクラスで定義したメソッドの内容が処理されます。
②は子クラスTanakaIchiro型の変数ichiroにTanakaIchiroクラスのインスタンスを生成しています。introduce()メソッドはオーバーライドしているため、実行するとTanakaIchiroクラスで再定義した内容が処理されます。フィールドもTanakaIchiroクラスのものが使用されています。
対して、オーバーライドしていない howOldAmI()メソッドはフィールドも含めて親クラスの内容のまま処理されます。メソッドは親クラスのものを引き継いでいるからとわかるとして、変数ageは子クラスでも定義しているのにどうして親クラスのものが使われるのでしょうか。
2-1.親クラスと子クラスの関係
冒頭で「子クラスは親クラスのメンバを引き継ぐ」と表現しましたが、これは正確な表現ではありません。
実は、子クラスは自身で定義したメンバに加えて親クラスそのものも内包しているのです。
図にするとこんな感じです。
このように、TanakaIchiroクラスには自身のクラスで定義したフィールドやオーバーライドしたメソッドに加え、親クラスTanakaも含まれています。
つまりTanakaIchiroクラスで定義したフィールドやオーバーライドしたメソッドは親クラスのものを上書きして消してしまっているのではなく、あくまで別物として定義し、上から覆いかぶせているだけなのです。
そのため、TanakaIchiroクラスでは親クラスであるTanakaクラスのフィールドやメソッドも同時に持っていることになります。そして、TanakaIchiroクラスをインスタンス化してメソッドを実行した時オーバーライドしたものと、していないものを実行することで何が起こっているのかを見てみましょう。
TanakaIchiroクラスをインスタンス化したオブジェクトを持つ変数ichiroで実行したメソッドはまず子クラスTanakaIchiroで定義されているメソッドを実行しようとします。introduce()メソッドはオーバーライドされているためTanakaIchiroクラスのものが実行されます。このとき、処理に使われる変数は、単にnameとしているので子クラスで定義されたものが使用されます。
続いて、howOldAmI()メソッドを実行しようとしますが、TanakaIchiroでは同メソッドはオーバーライドされていません。そこで、継承で引き継いだ親クラスTanakaの同メソッドを実行することになります。
このとき、親クラスで定義されたhowOldAmI()メソッドは、自身のクラスで定義されたフィールドを見にいきます。このhowOldAmI()メソッドはあくまで親クラスTanakaで定義されたメソッドであるため、その処理内容で参照される値は同じ親クラス内にあるものが使用されます。そのため、子クラスで定義された変数age=20ではなく、親クラスの変数age=45が使われるのです。
TanakaIchiroの中に存在するTanakaクラスのメソッドを実行する時、内なるTanakaが目覚めるのです。
2-2.superで親クラスにアクセスしよう
③のTanakaJiroクラスではオーバーライドしたintroduce()メソッドでsuper.nameとnameの二つが出てきます。そしてsuper.nameの方は親クラスのフィールドの値である“田中“が出力されています。super.をつけると、親クラスの要素を見にいくのです。そのため、super.nameは親クラスの値「田中」を、単にnameとしているものは子クラスの値「二郎」を出力しています。
子クラスは親クラスを継承していることをクラス宣言時のextendsで明示しているため親クラスの要素にもアクセス可能となります(※1)。
反対に、親クラスではどのクラスによって継承されているかまでは判別できないため、子クラスの要素にアクセスすることはできません。クラス図で子から親に向かって矢印が伸びているのは子→親としか継承関係を明示できないからなのです。
(※1)親クラスの要素のアクセス修飾子がprivateである場合、子クラスからもアクセスできません。そういう場合はgetterやsetterと呼ばれるアクセサメソッドを使用します。
そして、superはもう一つ出てきます。TanakaJiroクラスで定義したintroduce()メソッドをもう一度見てください。
@Override public void introduce() { System.out.println("TanakaJiroクラスのメソッド"); System.out.println("その人は" + super.name + " " + name + "です。"); System.out.println(name + "は小さな声ですが必ず頭を下げて挨拶をします"); this.howOldAmI(); //注目 super.howOldAmI(); //注目 }
6〜7行目のthis.howOldAmI()とsuper.howOldAmI()に注目してください。
そして、TanakaJiroクラスのインスタンスが持つintroduce()メソッド内でthis.howOldAmI()とsuper.howOldAmI() を実行した結果は以下のコメントアウト部分です。
③ TanakaJiroクラスのメソッド その人は田中 二郎です。 二郎は小さな声ですが必ず頭を下げて挨拶をします // TanakaJiroクラスのthis.howOldAmI()を実行した結果 TanakaJiroクラスのメソッド 二郎は10才です。 // TanakaJiroクラスのsuper.howOldAmI()を実行した結果 Tanakaクラスのメソッド 家長は現在45才です。
this.をつけてメソッドやフィールドを指定した場合、そのクラス内で定義したものを指します。(this.は省略可能。)そして、super.をつけるとフィールドの場合と同様、親クラスで定義したメソッドを指していることになります。
このように、super.メソッドとすることで親クラスのメソッドを呼び出して実行することが可能なのです。そしてこのことで、子クラスは継承した親クラスをそのまま内包しているということがお分かりいただけるかと思います。親クラスをコピーして子クラス自身のメンバとして定義しているのであればthisやsuperによる区別はできないからです。
つまり、親クラスの処理内容に加えて子クラスで処理したい内容がある場合、オーバーライドをする際に親クラスの処理部分は改めて記述する必要はなく、親クラスのメソッドを「super.メソッド」で呼び出しつつ子クラスの追加処理だけを追記すればよいということです。
そして、親クラスの処理内容に変更を加える必要が出た場合も、親クラスを修正すればそれを呼び出しているだけの子クラスでは何も修正を加える必要はありません。
これが最初に紹介した「再利用のしやすさ」と「保守のしやすさ」の両面が保つためにオーバーライドが便利な具体例です。
ちなみに、このsuperやthisはコンストラクタで使用されるsuper()やthis()とは別物なので注意しましょう。
2-3.親クラスを型として使う
そして③は他にも注目する点があります。それは、親クラスTanakaを型として定義し、そこに子クラスTanakaJiroのインスタンスを代入している点です。
③の処理を言葉で説明すると、Tanakaクラスであるtanaka2さんに「田中さん、挨拶して(tanaka2.introduce();)」と言ったところ、tanaka2さんはTanakaJiroとして挨拶(TanakaJiroクラスのintroduce()を実行)したということです。このtanaka2への代入をTanakaIchiroインスタンスにすればTanakaIchiroのintroduce()を実行しますし、他に子クラスがあってそれを代入すれば、その子クラスのメソッドを実行します。
みんな一様に「田中さん、挨拶して」としか言われないのに、それぞれが持つ挨拶の仕方を実践するのです。これは多態性(polymorphism)といい、オブジェクト指向を学ぶ上で避けては通れない重要な機能です。多態性とはこのように同じクラスの型であっても中身のインスタンスの種類によって振る舞いが変えられるのです。別の言い方をすると、宣言されている型ではなく、変数に代入されているインスタンスの型によってその振る舞いが決まるのです。
ここではこれ以上は触れませんが、多態性はオーバーライドと密接に関連しているものなのでぜひ調べてみてください。
3.@override(アノテーション)をつけよう
オーバーライドしたメソッドの上に@overrideと書いてありますね。
一つはソースコードを読んだ人がこれはオーバーライドしたメソッドだよと一目でわかる目印です。
ここでアノテーションをつけていなかった場合、コンパイラはintooduce()という新しいメソッドを定義したんだなと解釈し、エラーにはなりません。
しかし、アノテーションをつけていればコンパイラ以前にeclipseなどの開発ツール側でオーバーライドされているかの判断がなされます。仮にスペルを間違って親クラスにはないメソッドを定義することになっていれば、その時点でエラーとなり誤りを教えてくれます。
なので、オーバーライドの際はアノテーションをつけるようにしましょう。
※アノテーションは他にも種類がたくさんあるので気になった方は調べてみてください。
4.オーバーロードと何が違うの?
よくオーバーライドと並んで紹介される機能に、オーバーロードがあります。
この二つは名前が似ていることに加え、それぞれの特徴を生かして同じメソッド名を複数定義することができるため、混同されやすいです。
オーバーライドは引数は全く同じまま、親クラスから継承したメソッドを子クラスで再定義する機能です。当然ですが同一クラスに複数定義はできません。
5.さいごに
オーバーライドについて解説をしましたが、ここで紹介した内容は基本的な部分のみです。
途中、少しだけ紹介した多態性などと関連してくることでさらにオーバーライドの有用性、必要性が理解でき、知るほどによくできているなぁと感心することでしょう。
ぜひ、この先も楽しんで学習していきましょう。
コメント