- JAVA
Java継承とは|extends・superの使い方とオーバーライドを図解
Javaの継承とは、既にあるクラス(親クラス)のフィールドとメソッドを受け継いで、新しいクラス(子クラス)を作る仕組みです。 class 子 extends 親 と書くだけで、親の機能をそのまま使え、必要な部分だけを足したり上書き(オーバーライド)したりできます。
この記事では、extends と super の基本から、3段継承でコンストラクタがどの順に動くか、super() を書かなかったときのコンパイルエラー、ポリモーフィズム、フィールドの隠蔽、sealed、そして「継承にするか、委譲にするか」の判断基準までを扱います。掲載したコードはすべて OpenJDK 21.0.10 でコンパイル・実行し、出力とエラー文をそのまま載せています。Java 25 で変わった点は公式の JEP で確認して注記しました。
Java継承の早見表
まず、継承で使うキーワードを一覧にします。この表の6つを押さえれば、継承のコードはほぼ読めます。
| キーワード | 書く場所 | 役割 | 書き方の例 |
|---|---|---|---|
extends |
クラス宣言 | 親クラスを1つ指定して継承する | class Book extends Item { } |
super(...) |
子のコンストラクタ | 親のコンストラクタを呼ぶ | super(name, price); |
super.xxx |
子のメソッド | 親のメソッド・フィールドを指定して使う | super.label() |
@Override |
子のメソッド | 「上書きしている」ことをコンパイラに確認してもらう | @Override String label() |
final |
クラス・メソッド | 継承/オーバーライドをここで止める | final class Token { } |
abstract |
クラス・メソッド | 子に実装を任せる(そのクラスは new できない) |
abstract double area(); |
sealed / permits |
クラス宣言 | 継承できる子クラスを名前で限定する(Java 17〜) | sealed class Notice permits Mail, Chat |
non-sealed |
sealed の子 | その子から先は自由に継承できるようにする | non-sealed class Push extends Notice |
継承で「受け継がれるもの・受け継がれないもの」も表にしておきます。
| 親クラスの要素 | 子クラスから使えるか | 補足 |
|---|---|---|
public / protected のフィールド・メソッド |
使える | そのまま name や getPrice() と書ける |
| 修飾子なし(パッケージプライベート) | 同じパッケージなら使える | 別パッケージの子からは見えない |
private のフィールド・メソッド |
直接は使えない | オブジェクトの中には存在する。親の public メソッド経由で使う |
| コンストラクタ | 受け継がれない | 子で定義し、super(...) で親のものを呼ぶ |
static メソッド |
呼べるがオーバーライドはできない | 同名で定義すると「隠蔽」になる |
| 初期化ブロック | 受け継がれないが、new のたびに親の分も動く |
動く順番は後述 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java継承とは|extends・superの使い方とオーバーライドを図解」
継承の基本:extendsで親の機能を受け継ぐ
コピーして動かせる最小のコード
商品(Item)を親、本(Book)を子にした例です。1つのファイル Basic.java に保存して実行できます。
class Item {
protected String name;
private int price;
Item(String name, int price) {
this.name = name;
this.price = price;
}
int getPrice() { return price; }
String label() { return name + "(" + price + "円)"; }
}
class Book extends Item {
private String author;
Book(String name, int price, String author) {
super(name, price); // 親のコンストラクタを呼ぶ
this.author = author;
}
@Override
String label() {
return super.label() + " 著者:" + author; // 親のメソッドを呼んで付け足す
}
}
public class Basic {
public static void main(String[] args) {
Book b = new Book("Java入門", 2800, "山田");
System.out.println(b.label()); // 子で上書きしたメソッド
System.out.println(b.getPrice()); // 親から受け継いだメソッド
System.out.println(b.name); // 親から受け継いだフィールド
System.out.println(b instanceof Item);
}
}
実行コマンドと結果(Java 21.0.10)です。この記事のコードはすべて同じ手順で動かしています。
javac -encoding UTF-8 Basic.java
java Basic
Java入門(2800円) 著者:山田
2800
Java入門
true
Book には getPrice() も name も書いていませんが、extends Item と書いたことで使えるようになっています。 最後の true は「Book のオブジェクトは Item でもある」ことを表しています。これを is-a 関係(本は商品である)と呼び、継承を使うかどうかを考えるときの出発点になります。
用語の整理
| 呼び方 | 別の呼び方 | この例では |
|---|---|---|
| 親クラス | スーパークラス、基底クラス | Item |
| 子クラス | サブクラス、派生クラス | Book |
| 継承する | 拡張する(extend) | Book extends Item |
| オーバーライド | 上書き、再定義 | Book の label() |
図にすると次のようになります。矢印は「子から親へ」向けて描くのが UML の決まりです。
classDiagram
Object <|-- Item
Item <|-- Book
class Item {
#String name
-int price
+getPrice() int
+label() String
}
class Book {
-String author
+label() String
}
extends を書かなかったクラスは、自動的に java.lang.Object を継承します。 そのため、どのクラスでも toString()・equals()・hashCode() が使えます。
継承できる親は1つだけ(単一継承)
Javaのクラスは、親を1つだけ指定できます。2つ書くとコンパイルの段階で知らせてくれます。
class X { } class Y { }
class Z extends X, Y { }
Multi.java:2: error: '{' expected
class Z extends X, Y { }
^
1 error
複数の型の性質を持たせたいときは、インターフェースを使います(class Z extends X implements Runnable, Comparable<Z> のように、implements はいくつでも書けます)。インターフェースと抽象クラスの選び方は、別記事「Javaインターフェースと抽象クラスの違い|比較表と使い分け」で詳しく扱っているので、ここでは要点だけにとどめます。
superの2つの使い方
super には、形の違う2つの使い方があります。
| 書き方 | 意味 | 書ける場所 | 省略したとき |
|---|---|---|---|
super(引数) |
親のコンストラクタを呼ぶ | 子のコンストラクタの先頭(Java 21 まで) | 引数なしの super(); が自動で入る |
super.メソッド(引数) |
親のメソッドを呼ぶ | 子のインスタンスメソッド・コンストラクタ | 自分のクラスのメソッドが呼ばれる |
super.フィールド |
親のフィールドを指定する | 同上 | 子に同名フィールドがあれば子の方が使われる |
this(引数) |
同じクラスの別のコンストラクタを呼ぶ | コンストラクタの先頭 | ― |
super(...) と this(...) は、1つのコンストラクタにどちらか片方だけ書けます。 this との対応で覚えると整理しやすくなります。
| 自分のクラス | 親クラス | |
|---|---|---|
| コンストラクタを呼ぶ | this(...) |
super(...) |
| メンバーを指定する | this.name |
super.name |
なお、super.super.label() のように「親の親」を直接指定する書き方はありません。1つ上の親だけを指定できます。
コンストラクタの呼び出し順を3段継承で確認する
継承でいちばんつまずきやすいのが、new したときに何がどの順で動くかです。A ← B ← C の3段継承で、static 初期化・初期化ブロック・フィールド初期化・コンストラクタのすべてに出力を入れて確かめます。
class A {
static { System.out.println("1. A の static 初期化"); }
{ System.out.println("4. A のインスタンス初期化ブロック"); }
A() { System.out.println("5. A のコンストラクタ"); }
}
class B extends A {
static { System.out.println("2. B の static 初期化"); }
private String field = init("6. B のフィールド初期化");
B() { System.out.println("7. B のコンストラクタ"); }
static String init(String s) { System.out.println(s); return s; }
}
class C extends B {
static { System.out.println("3. C の static 初期化"); }
{ System.out.println("8. C のインスタンス初期化ブロック"); }
C() {
// ここに super(); が自動で入る
System.out.println("9. C のコンストラクタ");
}
}
public class Order {
public static void main(String[] args) {
System.out.println("--- 1回目の new C() ---");
new C();
System.out.println("--- 2回目の new C() ---");
new C();
}
}
実行結果(Java 21.0.10):
--- 1回目の new C() ---
1. A の static 初期化
2. B の static 初期化
3. C の static 初期化
4. A のインスタンス初期化ブロック
5. A のコンストラクタ
6. B のフィールド初期化
7. B のコンストラクタ
8. C のインスタンス初期化ブロック
9. C のコンストラクタ
--- 2回目の new C() ---
4. A のインスタンス初期化ブロック
5. A のコンストラクタ
6. B のフィールド初期化
7. B のコンストラクタ
8. C のインスタンス初期化ブロック
9. C のコンストラクタ
この結果から、次の順番が読み取れます。
| 順番 | 動くもの | 回数 |
|---|---|---|
| 1 | 親から子の順に static 初期化(A → B → C) | クラスが初めて使われるときに1回だけ |
| 2 | いちばん上の親の フィールド初期化・初期化ブロック → コンストラクタ本体 | new のたび |
| 3 | 次の親の フィールド初期化・初期化ブロック → コンストラクタ本体 | new のたび |
| 4 | 最後に自分の フィールド初期化・初期化ブロック → コンストラクタ本体 | new のたび |
「親が先、子が後」。そして各クラスの中では「フィールド初期化と初期化ブロック → コンストラクタ本体」の順です。 C() には super() を書いていませんが、コンパイラが先頭に super(); を入れてくれるので、B()、さらに A() へとさかのぼってから戻ってきます。
sequenceDiagram
participant Main as main
participant C as C()
participant B as B()
participant A as A()
Main->>C: new C()
C->>B: super()(自動)
B->>A: super()(自動)
A-->>B: 4, 5 を出力して戻る
B-->>C: 6, 7 を出力して戻る
C-->>Main: 8, 9 を出力して完了
親が先に組み立てられるのは、子のコンストラクタが親のフィールドやメソッドを安心して使えるようにするためです。土台ができてから上の階を作る、と考えると覚えやすくなります。
super()がないときのコンパイルエラーと直し方
再現:親に引数なしコンストラクタがない
親クラスが「引数ありのコンストラクタだけ」を持っているとき、子で super(...) を書かないとコンパイルエラーになります。
class Member {
String name;
Member(String name) { this.name = name; } // 引数ありだけを定義
}
class PremiumMember extends Member {
int point;
PremiumMember(String name, int point) {
this.point = point; // super(name) を書いていない
}
}
public class NoSuper {
public static void main(String[] args) {
System.out.println(new PremiumMember("佐藤", 100).name);
}
}
コンパイル結果(Java 21.0.10):
NoSuper.java:7: error: constructor Member in class Member cannot be applied to given types;
PremiumMember(String name, int point) {
^
required: String
found: no arguments
reason: actual and formal argument lists differ in length
1 error
エラー文の読み方は次のとおりです。
| エラー文の部分 | 意味 |
|---|---|
constructor Member in class Member cannot be applied |
Member のコンストラクタを呼ぼうとしたが、合うものがない |
required: String |
親のコンストラクタは String を1つ必要としている |
found: no arguments |
実際には引数なしで呼ぼうとした(=自動で入った super();) |
自分では super() を書いていないのに「引数なしで呼んだ」と言われるのは、コンパイラが先頭に super(); を補っているからです。 親に引数なしコンストラクタがないので、補った呼び出しが合わなくなっています。
子クラスにコンストラクタを1つも書かない場合も、同じエラーになります(コンパイラが作るデフォルトコンストラクタの中身が super(); だからです)。
class PremiumMember extends Member { } // コンストラクタを書かない
NoSuper2.java:5: error: constructor Member in class Member cannot be applied to given types;
直し方:先頭でsuper(引数)を呼ぶ
class Member {
String name;
Member(String name) { this.name = name; }
}
class PremiumMember extends Member {
int point;
PremiumMember(String name, int point) {
super(name); // 先頭で親のコンストラクタを呼ぶ
this.point = point;
}
}
public class Fixed {
public static void main(String[] args) {
PremiumMember m = new PremiumMember("佐藤", 100);
System.out.println(m.name + " / " + m.point + "pt");
}
}
佐藤 / 100pt
直し方は2通りあります。
| 直し方 | 向いている場面 |
|---|---|
子のコンストラクタの先頭に super(引数) を書く |
親の設計を変えずに済む。まずこちらを選ぶ |
| 親に引数なしコンストラクタを追加する | 親を自分で管理していて、初期値なしの生成に意味があるとき |
再現:super()を2行目に書いた
Java 21 では、super(...) はコンストラクタの最初の文である必要があります。
PremiumMember(String name, int point) {
this.point = point;
super(name); // 2行目に書いた
}
NoSuper3.java:9: error: call to super must be first statement in constructor
super(name); // 2行目に書いた
^
1 error
Java 21 までは「super(...) は先頭に1回」と覚えておけば確実です。
Java 25での変更点(公式で確認・未実行)
Java 25 では JEP 513「Flexible Constructor Bodies」が正式機能になり、super(...) や this(...) の前に文を書けるようになりました。 引数のチェックをしてから親のコンストラクタを呼ぶ、といった書き方ができます。
// Java 25 以降で書ける形(JEP 513 の内容をもとにした例。この記事の環境は Java 21 のため未実行)
PremiumMember(String name, int point) {
if (point < 0) throw new IllegalArgumentException("point は 0 以上");
this.point = point; // 自分のクラスの、初期化子を持たないフィールドへの代入は可
super(name);
}
ただし super(...) より前の部分では、this を通じたフィールドの読み取りやインスタンスメソッドの呼び出し、super.xxx の利用はできない、という制限が JEP に書かれています。Java 21 と 25 の両方でコンパイルするコードでは、従来どおり先頭に書いておくと安心です。
オーバーライドとポリモーフィズム
親の型の変数で、子のメソッドが動く
ポリモーフィズム(多態性)とは、同じ呼び出し方をしても、オブジェクトの実体に応じて動くメソッドが変わる性質です。 継承のいちばんの利点はここにあります。
import java.util.List;
abstract class Shape {
private final String name;
Shape(String name) { this.name = name; }
abstract double area(); // 子に実装を任せる
String describe() { // 共通の処理は親に書く
return String.format("%s の面積は %.1f", name, area());
}
}
class Circle extends Shape {
private final double r;
Circle(double r) { super("円"); this.r = r; }
@Override double area() { return Math.PI * r * r; }
}
class Rect extends Shape {
private final double w, h;
Rect(double w, double h) { super("長方形"); this.w = w; this.h = h; }
@Override double area() { return w * h; }
}
class Square extends Rect {
Square(double side) { super(side, side); }
@Override String describe() { return "[正方形] " + super.describe(); }
}
public class Poly {
public static void main(String[] args) {
List<Shape> shapes = List.of(new Circle(1), new Rect(2, 3), new Square(4));
for (Shape s : shapes) {
System.out.println(s.describe()); // 変数の型は Shape、動くのは実体のメソッド
}
Shape s = new Square(2);
System.out.println(s.getClass().getSimpleName());
System.out.println(s instanceof Rect);
if (s instanceof Rect r) { // パターンマッチングで安全にダウンキャスト
System.out.println("Rect として扱える: " + r.area());
}
try {
Circle c = (Circle) s; // 実体は Square なので変換できない
} catch (ClassCastException e) {
System.out.println("ClassCastException: " + e.getMessage());
}
}
}
実行結果(Java 21.0.10):
円 の面積は 3.1
長方形 の面積は 6.0
[正方形] 長方形 の面積は 16.0
Square
true
Rect として扱える: 4.0
ClassCastException: class Square cannot be cast to class Circle (Square and Circle are in unnamed module of loader 'app')
読み取れるポイントは3つです。
for文の中はs.describe()の1行だけですが、円・長方形・正方形でそれぞれのarea()が動いています。図形の種類が増えても、このループは書き換えずに済みます。Shape s = new Square(2);のように子のオブジェクトを親の型の変数に入れること(アップキャスト)は、キャストなしで書けます。- 逆向き(ダウンキャスト)は、実体が合わないと実行時に
ClassCastExceptionになります。instanceofのパターンマッチング(Java 16〜)を使うと、判定と変換を1行で安全に書けます。
変数の型と実体の型
| 決まるタイミング | 何を決めるか | |
|---|---|---|
変数の型(Shape s の Shape) |
コンパイル時 | 呼べるメソッドの範囲、どのフィールドを見るか |
実体の型(new Square(2) の Square) |
実行時 | 実際に動くインスタンスメソッド |
オーバーライドのルール(要点)
| 項目 | ルール |
|---|---|
| メソッド名・引数 | 親と同じにする(違うと別メソッド=オーバーロードになる) |
| 戻り値の型 | 親と同じか、その子クラス(共変戻り値) |
| アクセス修飾子 | 親と同じか、より広くする(protected → public は可) |
| 例外(検査例外) | 親より増やせない |
@Override |
付けておくと、綴りや引数の違いをコンパイラが知らせてくれる |
private・static・final メソッド |
オーバーライドの対象にならない |
オーバーロードとの違いや @Override の細かな働きは、別記事「Javaオーバーライドとオーバーロードの違い|@Overrideの役割」にまとめています。アクセス修飾子の範囲は「Javaアクセス修飾子一覧|privateとprotectedの違いを表とエラー例で」を参照してください。
フィールドの隠蔽:メソッドとは動きが違う
親と同じ名前のフィールドを子で宣言すると、オーバーライドではなく隠蔽(hiding)になります。メソッドは実体の型で決まり、フィールドは変数の型で決まる、という違いを実行して確かめます。
class Parent {
String name = "親のname";
String who() { return "Parent.who"; }
String showName() { return name; }
}
class Child extends Parent {
String name = "子のname"; // 同じ名前のフィールド=隠蔽
@Override String who() { return "Child.who"; } // インスタンスメソッドはオーバーライド
String both() { return name + " / " + super.name; }
}
public class Hide {
public static void main(String[] args) {
Child c = new Child();
Parent p = c; // 同じオブジェクトを親の型で見る
System.out.println("c.name = " + c.name);
System.out.println("p.name = " + p.name);
System.out.println("p.who() = " + p.who());
System.out.println("p.showName() = " + p.showName());
System.out.println("c.both() = " + c.both());
System.out.println("((Parent) c).name = " + ((Parent) c).name);
}
}
実行結果(Java 21.0.10):
c.name = 子のname
p.name = 親のname
p.who() = Child.who
p.showName() = 親のname
c.both() = 子のname / 親のname
((Parent) c).name = 親のname
c と p は同じオブジェクトを指していますが、p.name は「親のname」、p.who() は「Child.who」と、フィールドとメソッドで結果が分かれました。オブジェクトの中には name が2つあり、どちらを見るかは変数の型で決まります。親のメソッド showName() の中の name は、親の name を指します。
| 対象 | 同名で子に定義すると | どちらが使われるか |
|---|---|---|
| インスタンスメソッド | オーバーライド | 実体の型(実行時に決まる) |
| フィールド | 隠蔽 | 変数の型(コンパイル時に決まる) |
static メソッド |
隠蔽 | 変数の型・クラス名(コンパイル時に決まる) |
読みやすさのために、親と同じ名前のフィールドは子で宣言しないのがおすすめです。 値を子で変えたいときは、フィールドを private にしてゲッター(getName())をオーバーライドすると、メソッドと同じ動きにそろえられます。
親のコンストラクタからオーバーライドメソッドを呼ぶとどうなるか
コンストラクタの順番とオーバーライドが組み合わさると、意外な結果になる場面があります。
class Base {
Base() {
System.out.println("Base のコンストラクタ開始");
init(); // オーバーライドできるメソッドを呼んでいる
}
void init() { System.out.println("Base.init"); }
}
class Derived extends Base {
private String title = "初期値";
private final int size;
Derived() {
super();
size = 10;
System.out.println("Derived のコンストラクタ: title=" + title + ", size=" + size);
}
@Override
void init() { System.out.println("Derived.init: title=" + title + ", size=" + size); }
}
public class Trap {
public static void main(String[] args) { new Derived(); }
}
実行結果(Java 21.0.10):
Base のコンストラクタ開始
Derived.init: title=null, size=0
Derived のコンストラクタ: title=初期値, size=10
親のコンストラクタの時点では、子のフィールド初期化がまだ動いていないため、title は null、size は 0 です。 それでも init() は実体の型(Derived)のものが呼ばれます。
| 直す前 | 直した後 |
|---|---|
| 親のコンストラクタからオーバーライド可能なメソッドを呼ぶ | 親のコンストラクタから呼ぶのは private か final のメソッドだけにする |
子の初期化を init() に任せる |
子のコンストラクタで super(...) の後に自分で初期化する/ファクトリメソッドで生成後に呼ぶ |
final・abstractで継承の範囲を決める
継承を「止める」「必須にする」ためのキーワードが final と abstract です。4つのエラーを1つのファイルでまとめて再現します。
final class Token { }
class MyToken extends Token { }
class Report {
final String id() { return "R-001"; }
}
class MonthlyReport extends Report {
@Override String id() { return "M-001"; }
}
abstract class Job { abstract void run(); }
class EmptyJob extends Job { }
public class FinalErr {
public static void main(String[] a) { Job j = new Job(); }
}
コンパイル結果(Java 21.0.10):
FinalErr.java:2: error: cannot inherit from final Token
class MyToken extends Token { }
^
FinalErr.java:8: error: id() in MonthlyReport cannot override id() in Report
@Override String id() { return "M-001"; }
^
overridden method is final
FinalErr.java:11: error: EmptyJob is not abstract and does not override abstract method run() in Job
class EmptyJob extends Job { }
^
FinalErr.java:13: error: Job is abstract; cannot be instantiated
public static void main(String[] a) { Job j = new Job(); }
^
4 errors
| エラー文 | 原因 | 対応 |
|---|---|---|
cannot inherit from final Token |
final クラスを継承しようとした |
継承をやめて、フィールドに持つ(委譲) |
cannot override ... overridden method is final |
final メソッドをオーバーライドしようとした |
別名のメソッドを足す/親の設計を見直す |
is not abstract and does not override abstract method run() |
抽象メソッドを実装していない | run() を実装する、または子も abstract にする |
Job is abstract; cannot be instantiated |
抽象クラスを new した |
具象の子クラスを new する |
| キーワード | クラスに付けると | メソッドに付けると |
|---|---|---|
final |
そのクラスを継承できない(String、Integer、record など) |
子でオーバーライドできない |
abstract |
new できない。子に継承して使う |
本体を書かず、子に実装を求める |
final と abstract は正反対の意味なので、同じクラス・メソッドに両方は付けられません。
sealedで継承できる子クラスを限定する
final は「誰も継承できない」、何も付けなければ「誰でも継承できる」です。その中間として、Java 17 で正式機能になった sealed は「ここに書いたクラスだけが継承できる」と指定できます。
sealed abstract class Notice permits MailNotice, ChatNotice, PushNotice { }
final class MailNotice extends Notice { String to = "a@example.com"; }
final class ChatNotice extends Notice { String channel = "#general"; }
non-sealed class PushNotice extends Notice { } // ここから先は自由に継承できる
class SilentPush extends PushNotice { }
public class Sealed {
static String route(Notice n) {
return switch (n) { // default なしで網羅できる
case MailNotice m -> "メール: " + m.to;
case ChatNotice c -> "チャット: " + c.channel;
case PushNotice p -> "プッシュ: " + p.getClass().getSimpleName();
};
}
public static void main(String[] args) {
System.out.println(route(new MailNotice()));
System.out.println(route(new ChatNotice()));
System.out.println(route(new SilentPush()));
System.out.println(Notice.class.isSealed());
for (Class<?> c : Notice.class.getPermittedSubclasses()) System.out.println(" permits " + c.getSimpleName());
}
}
実行結果(Java 21.0.10):
メール: a@example.com
チャット: #general
プッシュ: SilentPush
true
permits MailNotice
permits ChatNotice
permits PushNotice
sealed にすると、switch で全種類を書いたかどうかをコンパイラが確認してくれます(default が不要になり、種類を増やしたときに書き忘れがコンパイルエラーで見つかります)。switch のパターンマッチングは別記事「Java switchパターンマッチング|record・sealedの書き方と実例」で詳しく扱っています。
sealed のルールに合わないときのエラー文も確認しました。
sealed abstract class Notice permits MailNotice { }
final class MailNotice extends Notice { }
final class FaxNotice extends Notice { } // permits にない
SealedErr.java:3: error: class is not allowed to extend sealed class: Notice (as it is not listed in its 'permits' clause)
sealed abstract class Notice permits MailNotice { }
class MailNotice extends Notice { } // final / sealed / non-sealed がない
SealedErr2.java:2: error: sealed, non-sealed or final modifiers expected
sealed クラスの子には、final・sealed・non-sealed のどれかを必ず付けます。
| 修飾子 | 継承できる範囲 | 使いどころ |
|---|---|---|
| (なし) | どのクラスからでも | 拡張される前提のクラス |
sealed ... permits |
permits に書いたクラスだけ |
種類が決まっている型(状態・結果・イベントの種類など) |
non-sealed |
制限を解除(その子から先は自由) | sealed 階層の一部だけ拡張を開けたいとき |
final |
誰も継承できない | 値を表すクラス、振る舞いを固定したいクラス |
継承か委譲かの判断基準
委譲(コンポジション)とは、別のクラスを継承するのではなく、フィールドとして持って処理を任せる書き方です。 「機能を再利用したい」という目的は同じでも、結果が変わる場面があります。
実行して比べる:ArrayListを継承した場合と持った場合
「追加した回数を数えるリスト」を、継承版と委譲版の両方で作ります。
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
// 継承版:ArrayList を継承して「追加した回数」を数える
class CountingListByInheritance<E> extends ArrayList<E> {
int added = 0;
@Override public boolean add(E e) { added++; return super.add(e); }
@Override public boolean addAll(Collection<? extends E> c) { added += c.size(); return super.addAll(c); }
}
// 委譲版:List を持って、必要なメソッドだけ公開する
class CountingList<E> {
private final List<E> list = new ArrayList<>();
private int added = 0;
void add(E e) { added++; list.add(e); }
void addAll(Collection<? extends E> c) { added += c.size(); list.addAll(c); }
int added() { return added; }
int size() { return list.size(); }
}
public class Deleg {
public static void main(String[] args) {
var a = new CountingListByInheritance<String>();
a.addAll(List.of("x", "y", "z"));
a.add(0, "w"); // オーバーライドしていない add(int, E) も呼べてしまう
a.remove("x");
System.out.println("継承版: added=" + a.added + ", size=" + a.size());
var b = new CountingList<String>();
b.addAll(List.of("x", "y", "z"));
b.add("w");
System.out.println("委譲版: added=" + b.added() + ", size=" + b.size());
// b.remove("x"); // 公開していないのでコンパイルエラーになる
}
}
実行結果(Java 21.0.10):
継承版: added=3, size=3
委譲版: added=4, size=4
継承版では4回追加したのに added=3 になりました。ArrayList には add(int, E) など追加の入口がいくつもあり、オーバーライドしていない入口を通ると数え漏れます。 さらに remove のような「数えるリスト」に不要な操作も、そのまま外から呼べます。
委譲版は、公開するメソッドを自分で決めているので、追加の入口は add と addAll の2つだけです。回数は必ず合い、親クラスの内部の作りが変わっても影響を受けません。
判断表
| 確認すること | はい → | いいえ → |
|---|---|---|
| 「子 は 親 である(is-a)」と自然に言えるか | 継承の候補 | 委譲(has-a:〜を持っている) |
親の public メソッドをすべて子でも公開してよいか |
継承の候補 | 委譲 |
| 親の型の変数に入れて、親と同じように扱いたいか(ポリモーフィズム) | 継承、またはインターフェース | 委譲 |
親クラスは継承される前提で作られているか(abstract、ドキュメントに拡張方法の記載がある) |
継承 | 委譲 |
| 親クラスを自分たちで管理しているか | 継承しやすい | 委譲が安心(ライブラリの変更の影響を受けにくい) |
| 目的が「コードの再利用」だけか | 委譲 | ― |
| 共通の「呼び出し方」だけをそろえたいか | インターフェース | ― |
迷ったら委譲から始め、is-a 関係とポリモーフィズムの両方が必要になったら継承にする、という順で考えると判断しやすくなります。 継承の深さは2〜3段までに収めると、コンストラクタの流れやメソッドの出どころを追いやすくなります。
| 継承 | 委譲 | インターフェース | |
|---|---|---|---|
| 書き方 | extends |
フィールドに持つ | implements |
| 関係 | is-a | has-a | can-do |
| 数 | 親は1つ | いくつでも | いくつでも |
| 公開されるメソッド | 親のものすべて | 自分で選ぶ | 宣言したもの |
| 親の変更の影響 | 受けやすい | 受けにくい | 小さい |
| ポリモーフィズム | できる | インターフェースと組み合わせる | できる |
継承を書くときのチェックリスト
| チェック項目 | 確認方法 |
|---|---|
| is-a 関係になっているか | 「(子)は(親)である」と声に出して自然か |
子のコンストラクタの先頭で super(...) を呼んでいるか |
親に引数なしコンストラクタがあるか確認 |
オーバーライドに @Override を付けたか |
付け忘れると、綴り違いが別メソッドとして通ってしまう |
| 親と同じ名前のフィールドを宣言していないか | 隠蔽になり、変数の型で値が変わる |
| 親のコンストラクタからオーバーライド可能なメソッドを呼んでいないか | 子のフィールドが null や 0 のまま参照される |
継承させないクラスに final、種類を限定したい階層に sealed を付けたか |
設計の意図がコードに残る |
| 階層が深くなりすぎていないか | 2〜3段を目安に、深い場合は委譲を検討 |
よくある質問
Q. 継承とインターフェースの実装(implements)は何が違いますか?
A. 継承(extends)は親クラスのフィールドとメソッドの中身ごと受け継ぎ、親は1つだけ指定できます。implements は「このメソッドを持っている」という約束を受け継ぐもので、いくつでも指定できます。共通の状態や処理を持たせたいなら継承(抽象クラス)、呼び出し方だけをそろえたいならインターフェースが向いています。
Q. privateなフィールドは継承されないのですか?
A. 子クラスのコードから直接は見えませんが、オブジェクトの中には存在します。冒頭の例でも Book から price は直接書けませんが、親の getPrice() を通して 2800 を取得できています。子から直接使わせたいときは protected にするか、ゲッターを用意します。
Q. super()は必ず書く必要がありますか?
A. 親に引数なしのコンストラクタがあれば、書かなくてもコンパイラが super(); を補います。親が引数ありのコンストラクタしか持たないときは、super(引数) を自分で書く必要があります。書かないと constructor ... cannot be applied to given types のエラーになります。
Q. Java 25では継承まわりで何が変わりましたか?
A. JEP 513(Flexible Constructor Bodies)が正式機能になり、super(...) の前に引数チェックなどの文を書けるようになりました(公式の JEP で確認。この記事の実行環境は Java 21 のため未実行です)。extends・super・final・abstract・sealed の基本ルールは Java 21 と同じです。
まとめ
- 継承は
class 子 extends 親と書き、親のフィールドとメソッドを受け継ぐ仕組み。 親は1つだけ指定できる super(...)は親のコンストラクタ、super.xxxは親のメンバーを指す。super(...)は Java 21 まではコンストラクタの先頭に書く- コンストラクタは「親が先、子が後」。各クラスの中では、フィールド初期化・初期化ブロック → コンストラクタ本体の順に動く
- 親に引数なしコンストラクタがないときは
super(引数)を書く。書かないとcannot be applied to given typesになる - メソッドは実体の型、フィールドは変数の型で決まる。親と同名のフィールドは宣言しないのがおすすめ
finalで継承を止め、abstractで実装を求め、sealedで継承できるクラスを限定できる- is-a 関係とポリモーフィズムが必要なら継承、機能の再利用だけなら委譲が向いている
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java継承とは|extends・superの使い方とオーバーライドを図解」
株式会社RJCでは、Javaの継承やクラス設計の考え方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。