CodeGym /コース /JAVA 25 SELF /抽象と継承階層の実装

抽象と継承階層の実装

JAVA 25 SELF
レベル 19 , レッスン 2
使用可能

1. 階層の構築: 抽象から詳細へ

抽象と階層の実装は、一般的な規則から具体的な詳細へとコードを構造化する方法です。まず、すべてのオブジェクトが何をできるべきか(抽象)を記述し、その後で各具象クラスがそれをどのように行うかを具体化します。

プログラミングでも日常と同じく、すべては問いから始まります。例えば: 「円と長方形の共通点は何でしょうか?」 答え: どちらも図形です。では図形の共通点は? たいてい面積があり、描画できます。

Java ではこれを abstract クラスで表現します:

public abstract class Shape {
    public abstract double area();
    public abstract void draw();
}

ここで言っていること:

  • どの図形も自分の面積を計算できなければなりません(area())。
  • どの図形も描画できなければなりません(draw())。
  • それをどのように実現するかは(今のところ)問いません。

では具体的な図形を作りましょう:

public class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public void draw() {
        System.out.println("半径 " + radius + " の円を描画します");
    }
}

public class Rectangle extends Shape {
    private double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }

    @Override
    public void draw() {
        System.out.println("長方形 " + width + "x" + height + " を描画します");
    }
}

何をしたのか?

  • 共通部分を抽象クラスに切り出しました。
  • 振る舞いをサブクラスで具体化しました。

概略図:


.          Shape
         /     \
     Circle   Rectangle

表: どこで何が実装されているか

クラス area() draw() 独自のフィールド
Shape
abstract
abstract
-
Circle 実装済み 実装済み
radius
Rectangle 実装済み 実装済み
width, height

2. なぜ便利なのか(そもそもなぜ動くのか)

異なるオブジェクトを同じインターフェースで扱える

たとえば、図形のコレクションがあるとします:

Shape[] shapes = {
    new Circle(5),
    new Rectangle(3, 4),
    new Circle(2.5)
};

型を意識せずに同じように反復できます:

for (Shape shape : shapes) {
    shape.draw();
    System.out.println("面積: " + shape.area());
}

どれが円でどれが長方形かは JVM に任せましょう! これがポリモーフィズム(多態性)です(すでに触れましたが、次のブロックでさらに詳しく扱います)。

拡張が容易

三角形を追加したくなったら? 新しい型を書く(既存コードは変更不要)だけです: Triangleextends Shape

public class Triangle extends Shape {
    private double base, height;

    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    public double area() {
        return 0.5 * base * height;
    }

    @Override
    public void draw() {
        System.out.println("三角形を描画します: 底辺 " + base + "、高さ " + height);
    }
}

他のコード(例えば図形リストの走査)は変更不要です。

コードの重複を避ける

もしすべての図形に共通のプロパティ(例えば色)が必要になったら、それを抽象クラスに切り出すと便利です:

public abstract class Shape {
    private String color = "黒";

    public String getColor() { return color; }
    public void setColor(String color) { this.color = color; }

    public abstract double area();
    public abstract void draw();
}

これで、円でも三角形でも、すべてのサブクラスが色を「継承」によって手に入れます。

3. 実践: ミニグラフィックエディタを開発する

すべてを組み合わせてみましょう。簡単なグラフィックエディタを作ると想像してください。

抽象クラス Figure

public abstract class Figure {
    private String color = "black";

    public String getColor() { return color; }
    public void setColor(String color) { this.color = color; }

    public abstract void draw();
    public abstract void resize(double factor);
}

具体的な図形

public class Line extends Figure {
    private double length;

    public Line(double length) {
        this.length = length;
    }

    @Override
    public void draw() {
        System.out.println("長さ " + length + " の線を色 " + getColor() + " で描画します");
    }

    @Override
    public void resize(double factor) {
        length *= factor;
        System.out.println("線の新しい長さ: " + length);
    }
}

public class Ellipse extends Figure {
    private double a, b;

    public Ellipse(double a, double b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public void draw() {
        System.out.println("楕円を描画します: 軸 " + a + " と " + b + "、色 " + getColor());
    }

    @Override
    public void resize(double factor) {
        a *= factor;
        b *= factor;
        System.out.println("楕円の新しい軸: " + a + "、" + b);
    }
}

新しいツール(例えば Polygon)を追加したいですか? 新しいクラスを作るだけで OK — エディタのコードは抽象の Figure 経由で動作します。

コードでの使用

Figure[] figures = {
    new Line(10),
    new Ellipse(5, 3)
};

for (Figure figure : figures) {
    figure.setColor("red");
    figure.draw();
    figure.resize(1.5);
}

出力:

長さ 10.0 の線を色 red で描画します
線の新しい長さ: 15.0
楕円を描画します: 軸 5.0 と 3.0、色 red
楕円の新しい軸: 7.5、4.5

階層の可視化


.            Figure
             /    \
          Line   Ellipse

4. 重複を避けるには: 共通のフィールドとメソッド

サブクラス間で共通なのはメソッドだけでなくフィールド(例: 中心座標)の場合もあります。抽象クラスはそれらを置くのに最適な場所です:

public abstract class Figure {
    private double x, y; // 中心の座標

    public Figure(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public void moveTo(double newX, double newY) {
        x = newX;
        y = newY;
        System.out.println("図形を点 (" + x + ", " + y + ") に移動しました");
    }

    public abstract void draw();
}

これで LineEllipse は、このメソッドを再実装することなく移動できます。

5. 別の例: 決済システム

抽象は図形だけの話ではありません。たとえば、決済処理システムを作っているとします.

抽象クラス Payment

public abstract class Payment {
    public abstract void process();
}

具体的な実装

public class CreditCardPayment extends Payment {
    @Override
    public void process() {
        System.out.println("クレジットカードでの支払いを処理します");
    }
}

public class PaypalPayment extends Payment {
    @Override
    public void process() {
        System.out.println("PayPal での支払いを処理します");
    }
}

使用方法

Payment[] payments = {
    new CreditCardPayment(),
    new PaypalPayment()
};

for (Payment payment : payments) {
    payment.process();
}

出力:

クレジットカードでの支払いを処理します
PayPal での支払いを処理します

6. このアプローチの利点

  • 統一インターフェース: 異なるオブジェクトを同じやり方で扱える。
  • 拡張性: 新しい種類のオブジェクトを追加しても既存コードの書き換えが不要。
  • 重複の最小化: 共通部分は基底の抽象クラスに集約。
  • 柔軟性: 詳細を気にせず抽象型のコレクションを扱える。

7. 実例: 交通

抽象は教科書だけの話ではありません。たとえば、輸送管理システムを設計するとします:

public abstract class Transport {
    public abstract void move();
    public abstract void fuelUp();
}

具体的な輸送手段が詳細を実装します:

public class Car extends Transport {
    @Override
    public void move() {
        System.out.println("車は道路を走ります");
    }

    @Override
    public void fuelUp() {
        System.out.println("ガソリンを給油します");
    }
}

public class Bicycle extends Transport {
    @Override
    public void move() {
        System.out.println("自転車はペダルをこぎます");
    }

    @Override
    public void fuelUp() {
        System.out.println("自転車に燃料は不要。必要なのはサイクリストのサンドイッチだけ!");
    }
}

8. 役に立つ図式: 抽象の階層の作り方

[抽象クラス]
        |
   [具体的なサブクラス]
        |
   [さらに具体的なサブクラス] (必要なら)

- 共通のものは上へ!
- 固有のものは下へ!

9. 抽象と階層を実装する際の典型的な誤り

エラー No. 1: サブクラス間のコード重複。
各サブクラスで同じフィールドやメソッドを書いていると気づいたら、それは抽象クラスに切り出すサインです。重複が減るなら、抽象を「広げる」ことを恐れないでください。

エラー No. 2: 「一般から個別へ」の原則に反する設計。
初心者はしばしば詳細から作り始め、共通部分を忘れがちです。その結果、RedCircleWithShadow のような、全体構造にうまく当てはまらないクラスが生まれます。まず抽象を定めてから詳細を詰めましょう。

エラー No. 3: 階層が深すぎる。
継承の連鎖が 3〜4 段以上になっているなら、継承の代わりに合成やインターフェースの利用を検討しましょう。

エラー No. 4: 不適切なメソッドの強制実装。
抽象クラスに、あるサブクラスには無関係な抽象メソッドが多すぎる場合、構造の見直しを検討しましょう。例えば、すべての交通手段に fuelUp() が必要なわけではありません(自転車には不要です)。

エラー No. 5: 抽象クラスとインターフェースの混同。
抽象クラスは共通状態や部分的な実装がある場合に使います。インターフェースはメソッドの存在だけを約束したい(データを保持したり振る舞いを実装したりしない)場合に使います。不必要にこれらを混同しないでください。

1
タスク
JAVA 25 SELF, レベル 19, レッスン 2
ロック未解除
交通シミュレータ: 車が走る 🛣️
交通シミュレータ: 車が走る 🛣️
1
タスク
JAVA 25 SELF, レベル 19, レッスン 2
ロック未解除
幾何図形ライブラリ 🔵⬛
幾何図形ライブラリ 🔵⬛
1
タスク
JAVA 25 SELF, レベル 19, レッスン 2
ロック未解除
基本的な人事管理システム 🧑‍💼
基本的な人事管理システム 🧑‍💼
1
タスク
JAVA 25 SELF, レベル 19, レッスン 2
ロック未解除
店舗向けの柔軟な支払いシステム 💰
店舗向けの柔軟な支払いシステム 💰
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION