継承は「is-a」の再利用
継承を使うと、新しいクラスを既存のクラスの上に構築できます。新しいクラス、つまりサブクラスは、スーパークラスのフィールドとメソッドを自動的に受け継ぎ、その後に必要なものを追加したり変更したりします。ある型が別の型のより具体的な一種であるときに使います。Dog は Animal であるし、SavingsAccount は BankAccount である、というように。
これは extends キーワードで記述します。親の public または protected なものはすべて、書き直すことなく子で利用できます。
Dog は name も eat() も一度も宣言していませんが、その両方を持っています。これこそが目的です。共有される振る舞いが一箇所に存在するのです。
super:親に到達する
サブクラスのコンストラクタは、まず親が初期化されることを保証しなければなりません。それを行うのが super(...) で、これは親のコンストラクタを呼び出し、サブクラスのコンストラクタの最初の文でなければなりません。これを省くと、Javaは親の引数なしコンストラクタへの呼び出しを暗黙的に挿入します。そして、親にそのようなコンストラクタがなければ、コードはコンパイルできません。
出力を見ると、親のコンストラクタが子の本体より先に実行されることがわかります。構築は階層の上から下へと流れていくのです。
メソッドのオーバーライド
サブクラスは、継承したメソッドを同じシグネチャで再定義することで置き換えられます。これがオーバーライドで、必ず @Override を付けるべきです。このアノテーションは必須ではありませんが、実際に親のメソッドと一致しているかをコンパイラに検証させます。toString() のつもりが tostring() のようなタイプミスを検出してくれます。さもなければ、それは黙ってまったく新しいメソッドを作ってしまうのです。
配列の型が Animal であっても、各要素はそれぞれ自身の speak() を実行します。Javaは変数の宣言された型ではなく、実行時の実際のオブジェクトに基づいてメソッドを選びます。これがポリモーフィズムの基礎です。
super.method() で親のバージョンを呼び出す
オーバーライドは親の処理を捨て去る必要はありません。super.method() を使って継承したバージョンを実行し、その上に追加しましょう。
super. がなければ、TimestampLogger.log の中で log を呼び出すと自分自身を呼び、無限に再帰してしまいます。super. は明示的に「親のバージョン」を意味します。
継承されるフィールドとアクセス
サブクラスは親の public および protected なメンバーを見られますが、private なメンバーは見られません。private フィールドはオブジェクトの中に依然として存在し、親自身のメソッドはそれに触れられますが、サブクラスは直接参照できません。無関係なコードからはメンバーを隠したまま、サブクラスにはアクセスを許したいときは protected を使いましょう。
class Base {
private int secret; // サブクラスからは見えない
protected int shared; // サブクラスから見える
}
class Derived extends Base {
void demo() {
shared = 5; // OK
// secret = 5; // コンパイルエラー - Base に private
}
}
これもまた、サブクラスのコンストラクタがしばしば super(...) を呼び出さなければならない理由です。それが親の private な状態を初期化する唯一の方法なのです。
final で継承を止める
ときには、クラスをまったく継承させたくないことがあります。String がまさにこの理由で final です。クラスを final にすると、サブクラス化が禁止されます。メソッドを final にすると、クラスの継承は許しつつ、そのメソッドのオーバーライドを禁止します。
final class Constants { } // サブクラス化できない
class Config {
final void load() { } // サブクラスは Config を継承できるが
// load() はオーバーライドできない
}
クラスの振る舞いがプログラム全体で保証され、不変でなければならないときに final を使いましょう。これはデフォルトではなく、意図的な「継承するな」というシグナルです。
よくある落とし穴:「is-a」でないときはコンポジションを優先する
継承はコードを再利用できるため魅力的ですが、子を親に強く結びつけてしまいます。関係が本当の「is-a」でない場合、たとえば Engine をたまたま必要とする Car のような場合は、Car extends Engine としてはいけません。車はエンジンを持っているのであって、エンジンであるわけではありません。代わりにフィールド(コンポジション)でモデル化しましょう。
class Car {
private Engine engine = new Engine(); // Car は Engine を HAS-A(持っている)
void start() { engine.ignite(); }
}
継承は、サブクラスが本当にスーパークラスの特殊化された形態であり、その振る舞いを継承しかつ置き換えたいときにのみ使いましょう。
次は:インターフェース
extends による継承は、単一の親と共有された実装を与えてくれます。しかし、クラスは1つのクラスしか継承できません。では、無関係なクラスに共通の能力を持たせるにはどうすればよいのでしょうか。そのためにあるのがインターフェースです。多くのクラスが実装できる契約であり、次のページのテーマです。
よくある質問
Javaの継承とは何ですか?
継承とは、extends キーワードを使って、あるクラス(サブクラス)が別のクラス(スーパークラス)のフィールドやメソッドを再利用できる仕組みです。サブクラスは親の public・protected なメンバーを自動的に受け継ぎ、新しいものを追加したり、オーバーライドによって継承した振る舞いを置き換えたりできます。これは「is-a(〜である)」の関係を表現します。Dog は Animal である、というように。
Javaの super キーワードは何をしますか?
super は親クラスを指します。コンストラクタ内の super(...) は親のコンストラクタを呼び出し(そして最初の文でなければなりません)、super.method() はオーバーライドしたメソッドの親バージョンを呼び出します。これにより、サブクラスは親のロジックを完全に置き換えるのではなく、その上に積み重ねることができます。
Javaのオーバーライドとオーバーロードの違いは何ですか?
オーバーライドは、継承したメソッドを同じシグネチャでサブクラス内に再定義し、振る舞いを変えるものです。@Override を付けましょう。オーバーロードは、同じクラス内に同じ名前で異なる引数リストを持つ複数のメソッドを定義するものです。オーバーライドは継承と実行時のディスパッチに関わるもので、オーバーロードは単に名前が同じだけの2つのメソッドです。