以前、「クラスとインスタンス、Javaで整理してみた」という記事で、Javaにおける「クラス」と「インスタンス」の基本を紹介しました。今回はその続きとして、クラス同士の関係を扱う概念、継承、オーバーライド、ポリモーフィズム、抽象クラス、インターフェースを紹介します。
対象読者
- 前回の「クラスとインスタンス」の内容を理解していて、次のステップに進みたい方
- Javaにおける継承・インターフェース・ポリモーフィズムの違いを整理しておきたい方
- 抽象クラスとインターフェースの使い分けで迷っている方
デザインパターンなど、より発展的な設計論までは扱いません。ジェネリクスやコレクションと組み合わせた応用的な使い方も対象外です。あくまで、それぞれの概念が何をするためのものかという基本の整理に絞っています。
検証環境
Java 25(LTS、2025年9月リリース)を前提にしています。2026年3月にリリースされたJava 26は非LTSでサポート期間が6か月に限定されているため、学習用途で環境を作るなら、現行LTSであるJava 25を使うのが無難です。
継承:クラス同士に親子関係を持たせる
継承(extends)を使うと、あるクラス(スーパークラス)が持つフィールドやメソッドを、別のクラス(サブクラス)が引き継ぐことができます。Oracleの公式チュートリアルによると、Objectクラスを除くすべてのJavaのクラスは、直接のスーパークラスをちょうど1つだけ持ちます(単一継承)。
Animalクラスを継承したDogクラスを例にします。
public class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + "はご飯を食べた");
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name); // スーパークラスのコンストラクタを呼び出す
}
public void bark() {
System.out.println(name + ": ワン!");
}
}
Dogはextends Animalと宣言することで、Animalが持つnameフィールドとeat()メソッドをそのまま使えるようになり、さらにbark()という独自のメソッドを追加しています。
継承にあたっては、引き継げるものと引き継げないものがある点に注意が必要です。
-
public・protectedメンバーはそのまま引き継がれる -
privateメンバーは直接は引き継がれない(スーパークラス側の公開メソッド経由でのみアクセス可能) -
コンストラクタは継承されない。そのためサブクラスのコンストラクタでは、
super(...)によってスーパークラスのコンストラクタを明示的に呼び出す必要がある
オーバーライド:親クラスのメソッドを上書きする
サブクラスは、スーパークラスと同じシグネチャ(メソッド名・引数の型と数・戻り値の型)を持つメソッドを再定義できます。これをオーバーライドと呼びます。
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(name + "はカリカリを食べた");
}
}
@Overrideアノテーションは必須ではありませんが、付けておくとコンパイラが「スーパークラスに本当に同じシグネチャのメソッドがあるか」を検証してくれます。もしタイプミスでシグネチャがずれていた場合、オーバーライドではなく意図しない別メソッドの定義になってしまいますが、@Overrideを付けていればその時点でコンパイルエラーとして検出できます。
なお、名前は同じでも引数の型や数が異なるメソッドを定義した場合はオーバーライドではなく「オーバーロード」であり、スーパークラスのメソッドとは無関係な、サブクラス独自の新しいメソッドとして扱われます。
ポリモーフィズム:同じ呼び出しでも実際の型によって振る舞いが変わる
**ポリモーフィズム(多態性)**とは、スーパークラス型の変数を通じて呼び出したメソッドが、実際に代入されているインスタンスの型に応じて異なる振る舞いをする性質のことです。
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(name + "はお魚を食べた");
}
}
Animal[] animals = {
new Dog("ポチ"),
new Cat("タマ")
};
for (Animal animal : animals) {
animal.eat();
}
// ポチはカリカリを食べた
// タマはお魚を食べた
animals配列の型はAnimalですが、実際に呼び出されるのは各インスタンスが実際に持つ型(DogまたはCat)でオーバーライドされたeat()です。Oracleの公式チュートリアルでは、これを「JVMは変数の型ではなく、実際のオブジェクトの型に基づいて適切なメソッドを呼び出す」(仮想メソッド呼び出し)と説明しています。呼び出し側はAnimal型としてしか扱っていないのに、実際の種類ごとに異なる動作が起きる、というのがポリモーフィズムの中心的な性質です。
抽象クラス:実装を持たないメソッドを含むクラス
抽象クラスはabstractキーワードで宣言するクラスで、インスタンス化することはできません[^oracle-abstract]。抽象クラスは、実装を持たない抽象メソッドを含むことができ、抽象メソッドを持つクラスは必ずabstractとして宣言する必要があります。
public abstract class Animal {
String name;
public Animal(String name) {
this.name = name;
}
abstract void makeSound(); // 実装を持たない抽象メソッド
public void eat() {
System.out.println(name + "はご飯を食べた");
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + ": ワン!");
}
}
Animalを抽象クラスにすると、new Animal("名無し")のように直接インスタンス化することはできなくなります。一方でeat()のように共通の実装を持たせつつ、makeSound()のように「動物の種類ごとに必ず実装しなければならないが、中身はサブクラスに任せる」という設計を表現できます。
インターフェース:実装を持たない「契約」
インターフェースもクラスと同様に参照型の一種ですが、基本的にはメソッドのシグネチャ(実装を持たない宣言)の集まりであり、「このインターフェースを実装するクラスは、これらのメソッドを提供しなければならない」という契約を表します。
public interface Swimmable {
void swim();
}
public class Dog extends Animal implements Swimmable {
public Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + ": ワン!");
}
@Override
public void swim() {
System.out.println(name + "は泳いだ");
}
}
DogはAnimalを継承しつつ、Swimmableインターフェースも実装しています。Javaのクラスが継承できるスーパークラスは1つだけですが、インターフェースは複数実装できるため、「動物である」という縦の関係(継承)とは別に、「泳げる」という横断的な性質(インターフェース)を組み合わせて表現できます。
抽象クラスとインターフェース、結局どう使い分けるのか
どちらも「インスタンス化できず、サブクラス/実装クラスに具体的な実装を求める」という点は共通していますが、性質は異なります。Oracleの公式チュートリアルでは、次のような基準が示されています。
| 観点 | 抽象クラス | インターフェース |
|---|---|---|
| 継承・実装できる数 | 1つのみ | 複数実装可能 |
| フィールド | 通常のフィールドを持てる(staticでなくてもよい) |
基本的にpublic static finalの定数のみ |
| メソッドの実装 | 通常のメソッドと抽象メソッドを混在させられる | デフォルトメソッド・静的メソッド以外は実装を持てない |
| 向いているケース | 密接に関連するクラス群で、共通のフィールドや実装を共有したいとき | 無関係なクラス同士に、共通の振る舞いの契約だけを持たせたいとき |
今回の例で言えば、「動物である」という密接な関係を表す部分(nameフィールドやeat()の共通実装)は抽象クラスAnimalが担い、「泳げる」という動物の種類を問わない横断的な性質はインターフェースSwimmableが担う、という役割分担になります。
まとめ
- 継承(
extends)を使うと、スーパークラスのフィールドやメソッドをサブクラスが引き継げる。ただしコンストラクタは継承されず、super(...)で明示的に呼び出す - オーバーライドは、サブクラスがスーパークラスと同じシグネチャのメソッドを再定義すること。
@Overrideを付けるとタイプミスをコンパイル時に検出できる - ポリモーフィズムにより、スーパークラス型の変数を通して呼び出しても、実際のインスタンスの型に応じたメソッドが実行される
- 抽象クラスは、実装を持たない抽象メソッドを含められるクラスで、インスタンス化はできない
- インターフェースは実装を持たない契約であり、1つのクラスが複数のインターフェースを実装できる
- 抽象クラスは「密接に関連するクラス間での実装の共有」、インターフェースは「無関係なクラス間での振る舞いの契約」に向いている
クラスとインスタンスの関係が「モノをどう作るか」の基本だとすると、継承・ポリモーフィズム・抽象クラス・インターフェースは「複数のクラスをどう関係づけ、どう共通化するか」を扱う概念です。ここまでが揃うと、Javaで書かれた一般的なコードの構造がだいぶ読めるようになってくるはずです。