1. クラス階層とは?
プログラミング(に限らず)における階層は木構造です。木の上部に「共通」の実体(基底クラス)があり、その下により具体的な実体(サブクラス)が並びます。Java では継承を用いてクラス階層を構築します。各サブクラスはさらに別のサブクラスの親になれます。
たとえ:
- 動物(Animal)は共通クラス。
- 哺乳類(Mammal)は動物の特化。
- 犬(Dog)は哺乳類の特化。
- そして具体的な「Sharik」は、Dog クラスのオブジェクトです。
コードでは次のようになります:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
クラス階層では「上」で共通の性質と振る舞いを定義し、「下」で詳細を定義できます。これによりコードは論理的になり、重複を避けられます。
階層図
Animal
├── Mammal
│ ├── Dog
│ └── Cat
└── Bird
└── Sparrow
2. 階層の構築: 考え方と実践
共通と固有を見極める
基本ルール: 基底クラスは、そのすべての子孫に共通するものを含めます。一方で固有のものはサブクラスに持たせます。
例:
- すべての動物は呼吸して食べる — したがってメソッド breathe() と eat() は Animal に置きます。
- 鳥だけが飛べる — したがって fly() は Bird に置き、Animal には置きません。
- 犬だけが吠える — bark() は Dog に置きます。
例: 動物
// 基底クラス
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(name + " は食べる。");
}
void makeSound() {
System.out.println(name + " は音を出す。");
}
}
// サブクラス: 哺乳類
class Mammal extends Animal {
Mammal(String name) {
super(name);
}
void feedMilk() {
System.out.println(name + " は子に母乳を与える。");
}
}
// サブクラス: 犬
class Dog extends Mammal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " は吠える: ワンワン!");
}
void wagTail() {
System.out.println(name + " はしっぽを振る。");
}
}
// サブクラス: 猫
class Cat extends Mammal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " は鳴く: ニャー!");
}
void purr() {
System.out.println(name + " はゴロゴロ鳴く。");
}
}
// サブクラス: 鳥
class Bird extends Animal {
Bird(String name) {
super(name);
}
void fly() {
System.out.println(name + " は飛ぶ。");
}
@Override
void makeSound() {
System.out.println(name + " はさえずる: チュンチュン!");
}
}
main での呼び出し:
public class ZooDemo {
public static void main(String[] args) {
Dog sharik = new Dog("Sharik");
Cat murka = new Cat("Murka");
Bird sparrow = new Bird("Vorobey");
sharik.eat(); // Sharik は食べる。
sharik.makeSound(); // Sharik は吠える: ワンワン!
sharik.feedMilk(); // Sharik は子に母乳を与える。
sharik.wagTail(); // Sharik はしっぽを振る。
murka.eat(); // Murka は食べる。
murka.makeSound(); // Murka は鳴く: ニャー!
murka.feedMilk(); // Murka は子に母乳を与える。
murka.purr(); // Murka はゴロゴロ鳴く。
sparrow.eat(); // Vorobey は食べる。
sparrow.makeSound(); // Vorobey はさえずる: チュンチュン!
sparrow.fly(); // Vorobey は飛ぶ。
}
}
可視化: クラスのツリー
| クラス | 親クラス | 特徴 |
|---|---|---|
|
|
name, eat(), makeSound() |
|
|
feedMilk() |
|
|
makeSound(), wagTail() |
|
|
makeSound(), purr() |
|
|
fly(), makeSound() |
3. ほかの身近な例
幾何図形
クラス階層は幾何のモデリングにも適しています。
// 基底クラス
class Shape {
void draw() {
System.out.println("図形を描画します。");
}
}
// 円
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
void draw() {
System.out.println("半径 " + radius + " の円を描画します。");
}
}
// 長方形
class Rectangle extends Shape {
double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
void draw() {
System.out.println("長方形 " + width + "x" + height + " を描画します。");
}
}
使用例:
public class ShapeDemo {
public static void main(String[] args) {
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.draw(); // 半径 5.0 の円を描画します。
s2.draw(); // 長方形 3.0x4.0 を描画します。
}
}
交通
class Vehicle {
void move() {
System.out.println("乗り物が動いています。");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("車が道路を走っています。");
}
}
class Bicycle extends Vehicle {
@Override
void move() {
System.out.println("自転車の乗り手がペダルをこいでいます。");
}
}
main:
Vehicle v1 = new Car();
Vehicle v2 = new Bicycle();
v1.move(); // 車が道路を走っています。
v2.move(); // 自転車の乗り手がペダルをこいでいます。
ユーザー
class User {
String username;
User(String username) { this.username = username; }
void login() { System.out.println(username + " がシステムにログインしました。"); }
}
class Admin extends User {
Admin(String username) { super(username); }
void deleteUser(String user) {
System.out.println(username + " はユーザー " + user + " を削除しました");
}
}
class Customer extends User {
Customer(String username) { super(username); }
void buy() { System.out.println(username + " は購入しました。"); }
}
4. 役立つ注意点
継承の乱用は避けましょう。 継承は「is-a」関係のための道具です。「猫は動物である」と言いたいなら継承を使いましょう。一方「猫は尻尾を持つ」のような場合はコンポジション(has-a)を使います。
悪い例:
class Engine { /* ... */ }
class Car extends Engine { /* 車はエンジンか? いいえ、これはコンポジションです! */ }
良い例:
class Car {
Engine engine; // 車はエンジンを持つ
}
階層を深くしすぎない。 レベルが増えるほど、保守や理解が難しくなります。多くのケースでは 2~3 階層が上限です。
「平坦すぎる」階層にしない。 20 個ものクラスがすべて同じ基底クラスを直接継承しているなら、設計を見直すべきかもしれません。
「便利だから」というだけの継承。 ついメソッドやフィールドを「拝借」したくなることがありますが、これは混乱の元です。class A と class B が論理的に関連していないなら、たとえいくつかのメソッドのためでも class B を extends A にすべきではありません。
リスコフの置換原則に違反。 サブクラスが驚きなく親クラスの代わりとして使えないなら、その階層は誤っています。
コードの重複。 「このメソッド、あのクラスからコピペしたな」と感じたら、それは基底クラスへ抽出すべきサインです。
5. クラス階層を構築する際の典型的な誤り
誤り №1: 「is-a」ではないのに継承する。
メソッドやフィールドにアクセスしたいだけで継承を使うと、サブクラスが本当に「親の一種」ではないためにコードがすぐ混乱します。例えば「Car が Engine を継承する」は is-a ではなく has-a です。
誤り №2: サブクラス固有の特性を無視する。
すべてのサブクラスが見た目同じで何も新しいものを追加していないなら、そもそも階層は不要かもしれません — 単一クラスでよいでしょう。
誤り №3: サブクラス間でコードを重複させる。
同じ実装を複数のサブクラスにコピーしているなら、それは親クラスへ抽出すべきです。
誤り №4: 複雑すぎる、または深すぎる階層。
多段の階層は保守やテストが難しくなります。シンプルが最善です。
誤り №5: アノテーション @Override なしでメソッドをオーバーライドする。
アノテーションがないとシグネチャを間違えやすく、その結果メソッドはオーバーライドされず新規メソッドとして扱われます。常に @Override を使いましょう!
GO TO FULL VERSION