Javaについて徹底解説!

Javaのオーバーライドの基本と親子クラスの関係について知ろう

のかな

のかな

開発エンジニア/Java4年/ボールド歴4年

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型の変数tanakaTanakaクラスのインスタンスを生成しています。各メソッドを実行すると、Tanakaクラスで定義したメソッドの内容が処理されます。

 は子クラスTanakaIchiro型の変数ichiroTanakaIchiroクラスのインスタンスを生成しています。introduce()メソッドはオーバーライドしているため、実行するとTanakaIchiroクラスで再定義した内容が処理されます。フィールドもTanakaIchiroクラスのものが使用されています。

対して、オーバーライドしていない howOldAmI()メソッドはフィールドも含めて親クラスの内容のまま処理されます。メソッドは親クラスのものを引き継いでいるからとわかるとして、変数ageは子クラスでも定義しているのにどうして親クラスのものが使われるのでしょうか。

2-1.親クラスと子クラスの関係

冒頭で「子クラスは親クラスのメンバを引き継ぐ」と表現しましたが、これは正確な表現ではありません。

実は、子クラスは自身で定義したメンバに加えて親クラスそのものも内包しているのです。

図にするとこんな感じです。

■子クラスTanakaIchiroクラスの中身
TanakaIchiroクラスの中身

このように、TanakaIchiroクラスには自身のクラスで定義したフィールドやオーバーライドしたメソッドに加え、親クラスTanakaも含まれています。

つまりTanakaIchiroクラスで定義したフィールドやオーバーライドしたメソッドは親クラスのものを上書きして消してしまっているのではなく、あくまで別物として定義し、上から覆いかぶせているだけなのです。

そのため、TanakaIchiroクラスでは親クラスであるTanakaクラスのフィールドやメソッドも同時に持っていることになります。そして、TanakaIchiroクラスをインスタンス化してメソッドを実行した時オーバーライドしたものと、していないものを実行することで何が起こっているのかを見てみましょう。

 ■ichiroインスタンスがメソッドを実行する時
ichiroインスタンスがメソッドを実行する時

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.namenameの二つが出てきます。そしてsuper.nameの方は親クラスのフィールドの値である田中が出力されています。super.をつけると、親クラスの要素を見にいくのです。そのため、super.nameは親クラスの値「田中」を、単にnameとしているものは子クラスの値「二郎」を出力しています。

子クラスは親クラスを継承していることをクラス宣言時のextendsで明示しているため親クラスの要素にもアクセス可能となります(※1)。

反対に、親クラスではどのクラスによって継承されているかまでは判別できないため、子クラスの要素にアクセスすることはできません。クラス図で子から親に向かって矢印が伸びているのは子親としか継承関係を明示できないからなのです。

※1)親クラスの要素のアクセス修飾子がprivateである場合、子クラスからもアクセスできません。そういう場合はgettersetterと呼ばれるアクセサメソッドを使用します。

 そして、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(); //注目
}

67行目の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.メソッドとすることで親クラスのメソッドを呼び出して実行することが可能なのです。そしてこのことで、子クラスは継承した親クラスをそのまま内包しているということがお分かりいただけるかと思います。親クラスをコピーして子クラス自身のメンバとして定義しているのであればthissuperによる区別はできないからです。

 つまり、親クラスの処理内容に加えて子クラスで処理したい内容がある場合、オーバーライドをする際に親クラスの処理部分は改めて記述する必要はなく、親クラスのメソッドを「super.メソッド」で呼び出しつつ子クラスの追加処理だけを追記すればよいということです。

そして、親クラスの処理内容に変更を加える必要が出た場合も、親クラスを修正すればそれを呼び出しているだけの子クラスでは何も修正を加える必要はありません。

これが最初に紹介した「再利用のしやすさ」と「保守のしやすさ」の両面が保つためにオーバーライドが便利な具体例です。

ちなみに、このsuperthisはコンストラクタで使用されるsuper()this()とは別物なので注意しましょう。

2-3.親クラスを型として使う

そしては他にも注目する点があります。それは、親クラスTanakaを型として定義し、そこに子クラスTanakaJiroのインスタンスを代入している点です。

の処理を言葉で説明すると、Tanakaクラスであるtanaka2さんに「田中さん、挨拶して(tanaka2.introduce();)」と言ったところ、tanaka2さんはTanakaJiroとして挨拶(TanakaJiroクラスのintroduce()を実行)したということです。このtanaka2への代入をTanakaIchiroインスタンスにすればTanakaIchirointroduce()を実行しますし、他に子クラスがあってそれを代入すれば、その子クラスのメソッドを実行します。

みんな一様に「田中さん、挨拶して」としか言われないのに、それぞれが持つ挨拶の仕方を実践するのです。これは多態性(polymorphism)といい、オブジェクト指向を学ぶ上で避けては通れない重要な機能です。多態性とはこのように同じクラスの型であっても中身のインスタンスの種類によって振る舞いが変えられるのです。別の言い方をすると、宣言されている型ではなく、変数に代入されているインスタンスの型によってその振る舞いが決まるのです。

 ここではこれ以上は触れませんが、多態性はオーバーライドと密接に関連しているものなのでぜひ調べてみてください。


3.@override(アノテーション)をつけよう

オーバーライドしたメソッドの上に@overrideと書いてありますね。これはアノテーションといい「オーバーライドするよ」という注釈を意味します。なぜアノテーションをつけるのかというと、目印になるからです。

一つはソースコードを読んだ人がこれはオーバーライドしたメソッドだよと一目でわかる目印です。もう一つは、コンパイラや実行環境に対しての目印として機能します。例えば、introduce()メソッドをオーバーライドしようとして、スペルを間違って intooduce()としてしまったとします。

ここでアノテーションをつけていなかった場合、コンパイラはintooduce()という新しいメソッドを定義したんだなと解釈し、エラーにはなりません。

しかし、アノテーションをつけていればコンパイラ以前にeclipseなどの開発ツール側でオーバーライドされているかの判断がなされます。仮にスペルを間違って親クラスにはないメソッドを定義することになっていれば、その時点でエラーとなり誤りを教えてくれます。

なので、オーバーライドの際はアノテーションをつけるようにしましょう。

アノテーションは他にも種類がたくさんあるので気になった方は調べてみてください。


4.オーバーロードと何が違うの?

よくオーバーライドと並んで紹介される機能に、オーバーロードがあります。

この二つは名前が似ていることに加え、それぞれの特徴を生かして同じメソッド名を複数定義することができるため、混同されやすいです。オーバーロードは引数の数、型、順番を変えることで同一クラスに同名メソッドを複数定義する機能です。

オーバーライドは引数は全く同じまま、親クラスから継承したメソッドを子クラスで再定義する機能です。当然ですが同一クラスに複数定義はできません。最初はごっちゃになりやすいですが、違いがわかると使い分けができるようになるので安心してください。


5.さいごに

オーバーライドについて解説をしましたが、ここで紹介した内容は基本的な部分のみです。

途中、少しだけ紹介した多態性などと関連してくることでさらにオーバーライドの有用性、必要性が理解でき、知るほどによくできているなぁと感心することでしょう。

ぜひ、この先も楽しんで学習していきましょう。

私たちは、全てのエンジニアに市場価値を高め自身の望む理想のキャリアを歩んでいただきたいと考えています。もし、今あなたが転職を検討しているのであればこちらの記事をご一読ください。理想のキャリアを実現するためのヒントが見つかるはずです。

『技術力』と『人間力』を高め市場価値の高いエンジニアを目指しませんか?

私たちは「技術力」だけでなく「人間力」の向上をもって遙かに高い水準の成果を出し、関わる全ての人々に感動を与え続ける集団でありたいと考えています。

高い水準で仕事を進めていただくためにも、弊社では次のような環境を用意しています。

  • 定年までIT業界で働くためのスキル(技術力、人間力)が身につく支援
  • 「給与が上がらない」を解消する6ヶ月に1度の明確な人事評価制度
  • 平均残業時間17時間!毎週の稼動確認を徹底しているから実現できる働きやすい環境

現在、株式会社ボールドでは「キャリア採用」のエントリーを受付中です。

まずは以下のボタンより弊社の紹介をご覧いただき、あなたの望むキャリアビジョンをエントリーフォームより詳しくお聞かせください。

コメント

公式アカウントLINE限定!ボールドの内定確率が分かる無料診断実施中
公式アカウントLINE限定!
ボールドの内定確率が分かる無料診断実施中