Java:継承
1学習の目的
- 継承で共通部分をまとめ、違う部分だけを書き足す設計ができるようになる。同じコードを何度も書かずに済む。
- ポリモーフィズム(多態性)を理解する。親の型でまとめて扱いながら、実際には子ごとに違う動きをする——オブジェクト指向の最も強力な仕組み。
2基礎解説
「本」「家具」「日用品」——どれも商品ですが、送料の計算だけが違うとします。3つのクラスを別々に書くと、名前や価格の処理を3回書くことになります。共通部分を親にまとめ、違いだけを子に書くのが継承です。
| 用語 | 意味 | 書き方 |
|---|---|---|
| 継承する | 親の機能を引き継ぐ | class Book extends Product |
| 親を呼ぶ | 親のコンストラクタ | super(name, price); |
| 上書き | 親のメソッドを変更 | @Override |
| 子クラスに公開 | 親のフィールド | protected |
- extends で親の機能をすべて引き継ぐ。子クラスには違う部分だけを書けばよく、共通処理は親に1回書くだけで済む。
- 子のコンストラクタでは super(…) を最初に呼ぶ。親の初期化を先に済ませてから、子の独自フィールドを設定する。
- @Override で親のメソッドを上書きする。この注釈は必須ではないが、綴りを間違えたときにエラーで教えてくれるので必ず書く。
- ⭐ 親の型で扱っても、実際の動きは子のもの(ポリモーフィズム)。Product[] items に本も家具も入れられ、shippingFee() はそれぞれの実装が呼ばれる。
- 親のフィールドを子から使うなら protected。private だと子クラスからも見えない。
現場使用例:商品の種類別処理、社員と管理職、図形(円・四角)の面積計算、支払い方法の違い。
// 親クラス(共通部分)
static class Product {
protected String name; // 子から使えるように protected
protected int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
// 子で上書きされる想定のメソッド
int shippingFee() {
return 600; // 標準の送料
}
@Override
public String toString() {
return name + ":" + price + "円(送料" + shippingFee() + "円)";
}
}
// 子クラス①(本は送料無料)
static class Book extends Product {
private String author; // 子だけが持つフィールド
Book(String name, int price, String author) {
super(name, price); // ① まず親を初期化
this.author = author; // ② 次に自分の分
}
@Override
int shippingFee() {
return 0; // 上書き
}
}
// 子クラス②(家具は送料が高い)
static class Furniture extends Product {
Furniture(String name, int price) {
super(name, price);
}
@Override
int shippingFee() {
return 3000;
}
}
// ⭐ ポリモーフィズム:親の型でまとめて扱える
Product[] items = {
new Product("マウス", 3200),
new Book("Java入門", 2800, "山田"),
new Furniture("デスク", 45000),
};
for (Product item : items) {
System.out.println(item.shippingFee()); // 600 / 0 / 3000
}
// 子の固有機能を使いたいとき
if (item instanceof Book bk) { // Java 16以降の書き方
System.out.println(bk.getAuthor());
}
3基本ドリル(10問)
出力 == Java入門
class Book extends Product { Book(...) { super(...); } } の形。
public class Main {
static class Product {
protected String name;
Product(String name) {
this.name = name;
}
}
static class Book extends Product {
Book(String name) {
super(name);
}
}
public static void main(String[] args) {
Book b = new Book("Java入門");
System.out.println(b.name);
}
}Book は Product の機能をすべて引き継ぐ。name フィールドを自分で宣言していないのに使えるのは、親から受け継いでいるため。
A. implements B. extends C. inherits D. super 選択★☆☆無料
解答 == B
「拡張する」を意味する英単語。
B
extends は「拡張する」という意味。親の機能に、子が独自の機能を足していくイメージ。
出力 == 0
@Override を付けて同じ名前・同じ引数のメソッドを定義する。
public class Main {
static class Product {
int shippingFee() {
return 600;
}
}
static class Book extends Product {
@Override
int shippingFee() {
return 0;
}
}
public static void main(String[] args) {
Book b = new Book();
System.out.println(b.shippingFee());
}
}子の実装が優先される。親のメソッドは残っているが、子で上書きしたほうが呼ばれる。
public class Main {
static class Parent {
String name;
Parent(String name) { this.name = name; }
}
static class Child extends Parent {
Child(String name) {
____(name);
}
}
public static void main(String[] args) {
System.out.println(new Child("テスト").name);
}
}出力 == テスト
「上位の・親の」を意味する5文字。
super
super は子のコンストラクタの最初に書く。親の初期化が済んでいないと、子の処理が正しく動かない可能性があるため。
A. private B. protected C. static D. final 選択★☆☆無料
解答 == B
private だと子からも見えない。
B
protected は「子クラスと同じパッケージ」から見える。private だと親のクラス内だけなので、子からアクセスできない。
出力3行。600 / 0 / 3000
Product[] に子クラスのオブジェクトも入れられる。呼ばれるのは実際の型のメソッド。
public class Main {
static class Product {
int shippingFee() { return 600; }
}
static class Book extends Product {
@Override
int shippingFee() { return 0; }
}
static class Furniture extends Product {
@Override
int shippingFee() { return 3000; }
}
public static void main(String[] args) {
Product[] items = {
new Product(),
new Book(),
new Furniture(),
};
for (Product item : items) {
System.out.println(item.shippingFee());
}
}
}変数の型は Product なのに、動きは実際の型のもの。これがポリモーフィズム——同じ呼び出しで、対象に応じた処理が行われる。
public class Main {
static class Parent {
String greet() { return "親"; }
}
static class Child extends Parent {
____
String greet() { return "子"; }
}
public static void main(String[] args) {
System.out.println(new Child().greet());
}
}出力 == 子
アットマークで始まる9文字の注釈。
@Override
@Override は必須ではないが、必ず書くべき。メソッド名を間違えたとき、「親にそんなメソッドは無い」とコンパイラが教えてくれる。
A. Product のもの B. Book のもの C. エラー D. 両方 選択★★☆広告解放
解答 == B
変数の型ではなく、実際のオブジェクトの型で決まる。
B
実行時の実際の型で決まる。変数の型(Product)は「何を入れられるか」を決めるだけで、動きは中身(Book)が決める。
出力 == 山田
if (item instanceof Book b) { b.getAuthor() } と書ける。
public class Main {
static class Product {
}
static class Book extends Product {
private String author;
Book(String author) {
this.author = author;
}
String getAuthor() {
return author;
}
}
public static void main(String[] args) {
Product[] items = {
new Product(),
new Book("山田"),
};
for (Product item : items) {
if (item instanceof Book b) {
System.out.println(b.getAuthor());
}
}
}
}判定と変換が1行で書ける(Java 16以降)。従来は ((Book) item).getAuthor() とキャストが必要だった。
出力 == Java入門(送料0円)
親の toString の中で shippingFee() を呼ぶと、子の実装が使われる。
public class Main {
static class Product {
protected String name;
Product(String name) {
this.name = name;
}
int shippingFee() {
return 600;
}
@Override
public String toString() {
return name + "(送料" + shippingFee() + "円)";
}
}
static class Book extends Product {
Book(String name) {
super(name);
}
@Override
int shippingFee() {
return 0;
}
}
public static void main(String[] args) {
System.out.println(new Book("Java入門"));
}
}親のコードが、子の実装を呼んでいる。親を書いた時点では Book の存在すら知らないのに、正しく動く——これが継承の強力さ。
4実践シナリオ(5問)
出力2行。300000 / 350000
親に salary() を定義し、子で @Override して手当を足す。
public class Main {
static class Employee {
protected String name;
protected int baseSalary;
Employee(String name, int baseSalary) {
this.name = name;
this.baseSalary = baseSalary;
}
int salary() {
return baseSalary;
}
}
static class Manager extends Employee {
Manager(String name, int baseSalary) {
super(name, baseSalary);
}
@Override
int salary() {
return baseSalary + 50000;
}
}
public static void main(String[] args) {
Employee e = new Employee("山田", 300000);
Manager m = new Manager("田中", 300000);
System.out.println(e.salary());
System.out.println(m.salary());
}
}基本給の管理は親、手当の計算は子。役職が増えても、親を変えずに新しい子クラスを足すだけで対応できる。
出力2行。28.3 / 20.0
親に double area() を定義し、子で計算方法を変える。円は Math.PI * r * r。
public class Main {
static class Shape {
double area() {
return 0;
}
}
static class Circle extends Shape {
private double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * radius * radius;
}
}
static class Rectangle extends Shape {
private double width;
private double height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
double area() {
return width * height;
}
}
public static void main(String[] args) {
Shape[] shapes = {
new Circle(3),
new Rectangle(4, 5),
};
for (Shape s : shapes) {
System.out.println(String.format("%.1f", s.area()));
}
}
}同じ area() の呼び出しで、円と四角が別々の計算をする。呼ぶ側は図形の種類を気にしなくてよい。
出力 == 3600
親の型でループを回し、shippingFee() を足していく。
public class Main {
static class Product {
int shippingFee() { return 600; }
}
static class Book extends Product {
@Override
int shippingFee() { return 0; }
}
static class Furniture extends Product {
@Override
int shippingFee() { return 3000; }
}
public static void main(String[] args) {
Product[] items = {
new Product(),
new Book(),
new Furniture(),
};
int total = 0;
for (Product item : items) {
total += item.shippingFee();
}
System.out.println(total);
}
}if文で種類を判定する必要がない。もし継承を使わなければ、「本なら0、家具なら3000…」という分岐を書き続けることになる。
出力 == 基本+拡張
super.describe() で親のメソッドを呼べる。
public class Main {
static class Parent {
String describe() {
return "基本";
}
}
static class Child extends Parent {
@Override
String describe() {
return super.describe() + "+拡張";
}
}
public static void main(String[] args) {
System.out.println(new Child().describe());
}
}完全に置き換えるのではなく、親の処理に足すことができる。共通処理を親に残しつつ、子で追加の処理を行いたいときに使う。
出力3行
instanceof で判定し、Book なら特別な処理をする。
public class Main {
static class Product {
protected String name;
Product(String name) {
this.name = name;
}
}
static class Book extends Product {
Book(String name) {
super(name);
}
}
static class Furniture extends Product {
Furniture(String name) {
super(name);
}
}
public static void main(String[] args) {
Product[] items = {
new Product("マウス"),
new Book("Java入門"),
new Furniture("デスク"),
};
for (Product item : items) {
if (item instanceof Book) {
System.out.println("【書籍】" + item.name);
} else {
System.out.println(item.name);
}
}
}
}instanceof は必要なときだけ使う。多用しているなら、そもそもメソッドを上書きして解決できないか考えたほうがよい設計になることが多い。
5仕上げ課題
親クラス Product
・protected String name、protected int price
・int shippingFee() … 標準600円
・int total() … 価格 + 送料
・String category() … 「一般」
・toString() … 「[一般] マウス:3,200円+送料600円 = 3,800円」
子クラス Book extends Product
・フィールド追加:private String author
・shippingFee() → 0円(送料無料)
・category() → 「書籍」
・getAuthor()
子クラス Furniture extends Product
・shippingFee() → 3000円
・category() → 「家具」
main の処理
① 3件を Product 配列に入れて全件出力
② 送料の合計を出力
③ instanceof で書籍だけ著者を出力
データ:マウス/3200、Java入門/2800/山田、デスク/45000
期待される出力(5行)
[一般] マウス:3,200円+送料600円 = 3,800円
[書籍] Java入門:2,800円+送料0円 = 2,800円
[家具] デスク:45,000円+送料3,000円 = 48,000円
送料合計:3,600円
書籍の著者:山田 総合★★★広告解放
出力5行が完全一致
toString は親に1回だけ書けばよい。中で category() と shippingFee() を呼べば、子の実装が自動的に使われる。金額は String.format("%,d", 値) で整形。
public class Main {
static class Product {
protected String name;
protected int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
int shippingFee() {
return 600;
}
int total() {
return price + shippingFee();
}
String category() {
return "一般";
}
@Override
public String toString() {
return "[" + category() + "] " + name + ":"
+ String.format("%,d", price) + "円+送料"
+ String.format("%,d", shippingFee()) + "円 = "
+ String.format("%,d", total()) + "円";
}
}
static class Book extends Product {
private String author;
Book(String name, int price, String author) {
super(name, price);
this.author = author;
}
@Override
int shippingFee() {
return 0;
}
@Override
String category() {
return "書籍";
}
String getAuthor() {
return author;
}
}
static class Furniture extends Product {
Furniture(String name, int price) {
super(name, price);
}
@Override
int shippingFee() {
return 3000;
}
@Override
String category() {
return "家具";
}
}
public static void main(String[] args) {
Product[] items = {
new Product("マウス", 3200),
new Book("Java入門", 2800, "山田"),
new Furniture("デスク", 45000),
};
int shippingTotal = 0;
for (Product item : items) {
System.out.println(item);
shippingTotal += item.shippingFee();
}
System.out.println("送料合計:" + String.format("%,d", shippingTotal) + "円");
for (Product item : items) {
if (item instanceof Book b) {
System.out.println("書籍の著者:" + b.getAuthor());
}
}
}
}toString() は親クラスに1回書いただけだ。それなのに3種類とも正しく表示される——category() と shippingFee() が、実際の型に応じて子の実装を呼んでいるからだ。親を書いた時点では Book も Furniture も存在していないのに、正しく動く。
これがポリモーフィズムの本質だ。もし継承を使わなければ、こう書くことになる。
if (種類.equals("書籍")) { 送料 = 0; } else if (種類.equals("家具")) { 送料 = 3000; } else { 送料 = 600; }
この分岐が、送料を計算するすべての場所に散らばる。新しい種類を追加するたび、全箇所を探して修正しなければならない——1箇所でも漏れれば不具合になる。
継承なら新しいクラスを1つ足すだけで済む。既存のコードには一切手を触れない。「追加には開いていて、変更には閉じている」——良い設計の指標としてよく語られる原則だ。
ただし継承にも限界がある。Javaでは親クラスを2つ持てない(多重継承の禁止)。「配送可能でもあり、割引可能でもある」といった複数の性質を持たせたいとき、継承だけでは表現できない。
次章のインターフェースが、その制約を解決する。