C
Java/OOP/Lesson 04

オブジェクト指向の5原則 — クラス・カプセル化・継承・ポリモーフィズム・インターフェース

60分·theory

オブジェクト指向の5原則 — クラス・カプセル化・継承・ポリモーフィズム・インターフェース

🎯 このlessonを読み終えたらできること

このlessonをすべて読み終えると、以下の3つを自信を持って行えるようになります。

  • ✅ カプセル化・継承・ポリモーフィズム・抽象化の定義 + コード例
  • ✅ SOLID5原則(特にSRP・OCP・DIP)の面接回答
  • ✅ なぜ継承よりコンポジションなのかを一言で説明

学習目標をチェックリストとして持ち、すべて答えられるようになったらlessonを閉じてください。

オブジェクト指向とは — *現実世界のものように*コードを書く方法

コアとなる一行

オブジェクト指向(OOP) = 現実のものをオブジェクトとして表現し、それらのオブジェクトが互いにメッセージをやり取りして処理を行うプログラミングの方式。Javaの中心思想です。

なぜオブジェクト指向か

昔のC言語は関数とデータが別々でした。人の名前・年齢は変数として、「歩く・話す」は関数として — 分離されていたため、プログラムが大きくなると何が何だかわからなくなりました。

オブジェクト指向はデータと振る舞いをひとまとめにして扱います。「人」というクラスの中に、名前・年齢のようなフィールドと、歩く・話すのようなメソッドが一緒にあります。まるで現実の人間が身体(データ)行動を同時に持つように。

クラス vs オブジェクト — 型と焼き菓子

最も混乱しがちな2つの言葉です。比喩で解説します。

  • クラス = 。設計図。「こういう形のものを作る」という青写真
  • オブジェクト = 実際に作られたもの。1個・2個・100個作れて、それぞれ独立しています。
java
class 人間 {           // クラス = 設計図
    String 名前;
    int 年齢;
}

人間 a = new 人間();   // オブジェクト 1 = 最初のたい焼き
人間 b = new 人間();   // オブジェクト 2 = 2番目の (a とは完全に別物)

newキーワードが焼く行為です。メモリのどこかに新しい空間を確保し、その場所を変数aに記録します。

カプセル化 — 外部から勝手に触れないように

自分の銀行残高を誰でも自由に変えられるとしたら?大変なことになります。そのためOOPでは外部からの直接アクセスを遮断し、決められたメソッドを通じてのみデータを扱うようにします。

java
class 口座 {
    private int 残高;       // private → 外部からの直接アクセスX

    public void 入金(int 金額) {
        if (金額 <= 0) throw new IllegalArgumentException("0より大きくする必要があります");
        残高 += 金額;
    }

    public int 残高照会() { return 残高; }
}

privateキーワードで直接アクセスを遮断し、deposit()のような公式の窓口だけを開いておきます。その窓口の中で検証・ロギング・同期化などの付加作業をすべて処理できます。これがカプセル化の本質です。

継承 — 共通部分を再利用する

さまざまな動物クラスを作るとしましょう。犬・猫・鳥 — みんな移動して食べます。これを毎回別々に書くのは重複です。

java
class 動物 {
    void 食べる() { ... }
    void 動く() { ... }
}

class 犬 extends 動物 {
    void 吠える() { ... }   // 追加の行動のみ
}

犬は動物のすべての機能を自動的に引き継ぎ、自分だけの振る舞いを追加すればよいだけです。

ただし継承は危険な武器です。深く積み重ねると親を1行変えるだけで子がすべて影響を受け、変更が難しくなります。そのため現代のJavaではコンポジションを好みます。犬が動物を継承するのではなく、犬がMovableEatableインターフェースを持つ方式です。

ポリモーフィズム — 同じインターフェース、異なる動作

同じメソッド呼び出しがオブジェクトによって異なる結果を返すのがポリモーフィズムです。

java
動物 a = new 犬();
動物 b = new 猫();
a.鳴く();   // "ワンワン"
b.鳴く();   // "ニャーニャー"

abはどちらもAnimal型ですが、sound()メソッドは実際のオブジェクトの型に応じて異なる動作をします。ランタイムに決定されます。

このおかげでList<Animal>という1つのリストに犬と猫を混ぜて格納し、1つのループで処理できます。動作はそれぞれが担当します。

インターフェース vs 抽象クラス — 混同されやすい2つ

どちらも直接インスタンス化できない抽象型です。違いは:

  • インターフェース役割。「比較できる(Comparable)」「反復できる(Iterable)」のような能力の仕様。多重実装が可能(implements A, B, C)。
  • 抽象クラス共通の骨格。「これらのメソッドは実装済みだが、一部はサブクラスが埋めるべき」。単一継承のみ(extends A)。

原則:役割だけを定義したいならインターフェース、共通コード + 一部抽象なら抽象クラス。Java 8以降はインターフェースもdefaultメソッドで実装を持てるようになり境界が曖昧になりましたが、役割 vs 骨格という直感は今も有効です。

まとめ

OOPは複雑なプログラムを小さな単位(クラス)に分けて管理する方式です。5つのコア原則:

原則一行まとめ
クラス・オブジェクト設計図・実体。newでオブジェクトを生成
カプセル化private + getter/setterで直接アクセスを遮断
継承extendsで共通部分を再利用(乱用禁止)
ポリモーフィズム同じメソッド、異なる動作。List<Animal>が可能
インターフェース役割の定義。多重実装が可能

> 💡 実務の現場:Javaバックエンドは95%がOOP。関数型・リアクティブは補助ツール。OOPが最も基本的な武器です。

例外処理 — エラーを*優雅に*扱う方法

例外とは何か

プログラムが予期しない状況に直面したとき — ファイルが存在しない・DBの接続が切れた・ゼロ除算が発生した — Javaは例外(Exception)というオブジェクトを投げます(throw)。誰かが受け取ら(catch)なければプログラムはクラッシュします

古いCのようにエラーコードを返すのではなく、別経路でエラーを伝播させるメカニズムです。

2種類の例外 — Checked vs Unchecked

これがJavaの特異な点です。他の言語にはほとんど存在しない区別。

  • Checked:コンパイラが必ず処理するよう強制しますIOExceptionSQLExceptionなど。try-catchまたはthrows宣言が必須。
  • Unchecked(RuntimeException):処理は推奨だが強制ではありませんNullPointerExceptionIllegalArgumentExceptionなど。

この区別は長年議論の的です。Springや最近のライブラリはCheckedをほとんど使いません — 強制処理がコードを汚すと見なされるからです。KotlinやC#はUncheckedのみです。

try-catch-finally

java
try {
    int x = Integer.parseInt(input);
} catch (NumberFormatException e) {
    log.error("パース失敗: {}", input, e);
    throw new BusinessException("不正な入力形式");
} finally {
    cleanup();    // 成功・失敗 *に関わらず* 常に実行
}

finallyはリソース解放に使われてきましたが、try-with-resources(Java 7+)のほうがすっきりしています:

java
try (BufferedReader r = new BufferedReader(new FileReader(f))) {
    return r.readLine();
}   // r.close() 自動呼び出し

AutoCloseableを実装したリソースはブロック終了時に自動でcloseされます。DB接続・ファイル・ネットワークソケットはすべてこのパターンが標準です。

よくある落とし穴4つ

1. catch (Exception e) { } — 空のcatch:最も恐ろしいアンチパターン。エラーを黙って飲み込みます。デバッグ地獄の始まり。せめてログだけでも残してください。

2. e.printStackTrace():標準出力にスタックトレースが散らばります。本番環境ではloggerを使いましょう:

java
catch (Exception e) {
    log.error("注文作成失敗: userId={}", userId, e);  // 構造化されたログ
}

3. 1つのtryブロックに2つの仕事異なる種類の処理をひとつのtryブロックにまとめると、どこでエラーが起きたかわかりません。小さなtryを複数使うほうが良いです。

4. ドメインの意味を持たない例外throw new RuntimeException("error")よりthrow new InsufficientBalanceException()のような意味のある名前のほうが、デバッグや文書化で大きな差を生みます。

まとめ

例外処理は防衛的なコードを書き散らすことではありません。正常フローと例外フローを明確に分離し意味のあるメッセージ + ロギングで後から追跡できるようにすることが本質です。

ジェネリクス — *型を変数のように*扱う

なぜジェネリクスが必要か

Java 5以前、Listはあらゆる型を格納できました。便利に見えましたが、ランタイムにClassCastExceptionが多発しました。

java
// 昔
List names = new ArrayList();
names.add("ホン・ギルドン");
names.add(42);            // 問題発生!Integer が入る
String x = (String) names.get(1);   // ランタイム ClassCastException

ジェネリクスコンパイル時に型を検証する仕組みです。

java
List<String> names = new ArrayList<>();
names.add("ホン・ギルドン");
names.add(42);            // コンパイルエラー!✅

ランタイムの爆発 → コンパイラが事前にキャッチします。型安全性が核心的な価値です。

基本的な使い方

java
public <T> T findById(Class<T> type, Long id) {
    // T が何であっても同じロジック
    return em.find(type, id);
}

User u = findById(User.class, 42L);
Order o = findById(Order.class, 100L);

<T>型パラメータ — メソッドのパラメータと似た概念です。コンパイラが呼び出し時にどの型かを推論します。

境界型(Bounded Type) — Tの条件

java
public <T extends Number> double sum(List<T> list) {
    double total = 0;
    for (T n : list) total += n.doubleValue();
    return total;
}

<T extends Number> = TはNumberまたはそのサブクラスのみ可能。Integer・Long・Doubleのような数値型だけが通過できます。

ワイルドカード — ?の意味

最も混乱しやすい部分です。

  • List<? extends Number> — Numberの何らかのサブクラス(正確には不明)。読み取りのみ可能、書き込みは不可。
  • List<? super Integer> — Integerの何らかのスーパークラス(正確には不明)。書き込みは可能(Integerのみ)、読み取りはObjectとしてのみ。

PECS原則 — Producer Extends, Consumer Super:

  • データを取り出して使う側(プロデューサー) → extends
  • データを入れるだけの側(コンシューマー) → super

> 覚える必要はありません。ジェネリクスライブラリを作るときのみ深く見ます。利用者側ではList<User>のような単純な使い方が99%です。

型消去(Type Erasure) — ランタイムには消える

Javaジェネリクスの奇妙な特性。コンパイル後はすべての<T>が消え、Objectに変換されます。そのため:

  • new T()は不可(ランタイムにTが何かわからない) → Class<T>引数として受け取る必要がある
  • T[]配列の生成は不可
  • instanceof List<String>は不可 → instanceof List<?>のみ

これはJavaが一度決めて戻れないデザイン上の負債です。C#のReified Genericsのようになっていればよかったのですが、互換性のため変更できません。

まとめ

ジェネリクスはコンパイル時の型安全性のためのツールです。List<User>のような基本的な使い方は必須。ワイルドカード・境界型はライブラリ作成時に深く掘り下げればよいです。

enum + アノテーション — Javaの*メタツール*

enum — ただの定数ではない

他の言語ではenumは単なる定数の集まりです。Javaのenumははるかに強力で — 各値がオブジェクトであり、メソッド・フィールド・インターフェース実装まで可能です。

java
public enum 決済状態 {
    PENDING("決済待機", 0),
    PAID("決済完了", 1),
    REFUNDED("払い戻し完了", 2);

    private final String 名前;
    private final int コード;

    決済状態(String 名前, int コード) {
        this.名前 = 名前;
        this.コード = コード;
    }

    public String 名前() { return 名前; }
}

各enum値に追加情報を持たせたり、メソッドを定義したりもできます。ステートマシン・Strategyパターンによく使われます。

java
public enum 割引 {
    NONE { public int 適用(int 価格) { return 価格; } },
    TEN  { public int 適用(int 価格) { return (int)(価格 * 0.9); } },
    VIP  { public int 適用(int 価格) { return (int)(価格 * 0.7); } };
    public abstract int 適用(int 価格);
}

int 最終 = 割引.VIP.適用(10000);   // 7000

if-elseの羅列の代わりに、各enumが自分の振る舞いを持つパターンです。

アノテーション — コードにメタデータを付ける

@Override@Deprecated@SuppressWarnings@で始まるものはすべてアノテーションです。コード自体ではなく、コードに関する情報を表します。

java
@Override         // 親メソッドのオーバーライド確認 (コンパイラ検証)
public String toString() { return ...; }

@Deprecated       // 使用非推奨 (IDE警告)
public void oldApi() { ... }

Springの核心はアノテーション

Javaバックエンドをやるなら、アノテーションがコードの80%を占めます。Springのほぼすべての仕組みはアノテーションベースです:

  • @Component@Service@Repository — Bean登録
  • @Autowired — 依存性の注入
  • @RestController + @GetMapping("/users") — Webルーティング
  • @Transactional — トランザクション自動管理
  • @Entity + @Id — JPAマッピング
java
@RestController
@RequestMapping("/api/users")
public class UserController {
    @Autowired
    private UserService service;

    @GetMapping("/{id}")
    public User get(@PathVariable Long id) {
        return service.findById(id);
    }
}

アノテーションそれ自体は何もしません。Springのようなフレームワークがランタイムにアノテーションを読み取り、それに応じた動作(ルーティング・DI・トランザクション)を自動的に行います。

まとめ

  • enum = 型安全な定数 + 各値がオブジェクトのようにメソッド・フィールドを持てる
  • アノテーション = メタデータ。直接実行されません。フレームワーク・ツールが読み取って動作する

この2つがJavaの宣言型(declarative)スタイルを可能にします。「こう動け」と命令するのではなく、「これはこういうものだ」と宣言するだけでツールが処理してくれます。

💻 📌 OOP実践コード — 暗記不要、パターンだけ覚える
// ============================================
// 1. ビルダーパターン — 検証可能なオブジェクトの生成
// ============================================
public class ユーザー {
    private final String メール;
    private final String 名前;
    private int ポイント;

    private ユーザー(Builder b) {
        if (!b.メール.contains("@"))
            throw new IllegalArgumentException("メール形式 X");
        this.メール = b.メール;
        this.名前 = b.名前;
        this.ポイント = b.ポイント;
    }

    public static Builder builder() { return new Builder(); }

    public static class Builder {
        private String メール, 名前;
        private int ポイント = 0;

        public Builder メール(String x) { this.メール = x; return this; }
        public Builder 名前(String x) { this.名前 = x; return this; }
        public Builder ポイント(int x) { this.ポイント = x; return this; }
        public ユーザー build() { return new ユーザー(this); }
    }
}

// 使用
ユーザー u = ユーザー.builder().メール("[email protected]").名前("ホンギルドン").build();

// ============================================
// 2. Strategy パターン — enum で自然に
// ============================================
public enum 配送方法 {
    一般 { public int 料金(int 重さ) { return 3000; } },
    当日 { public int 料金(int 重さ) { return 10000 + 重さ * 100; } },
    無料 { public int 料金(int 重さ) { return 0; } };
    public abstract int 料金(int 重さ);
}

int 費用 = 配送方法.当日.料金(2);    // 10200

// ============================================
// 3. ジェネリック Repository — 再利用可能なベース
// ============================================
public abstract class Repository<T, ID> {
    public abstract T findById(ID id);
    public abstract List<T> findAll();
    public abstract T save(T entity);
}

public class UserRepository extends Repository<User, Long> {
    @Override public User findById(Long id) { /* ... */ }
    // ...
}

// ============================================
// 4. 例外処理 — 意味のある
// ============================================
public class 残高不足Exception extends RuntimeException {
    public 残高不足Exception(String msg) { super(msg); }
}

public void 出金(Long 口座Id, int 金額) {
    var 口座 = 口座Repo.findById(口座Id)
        .orElseThrow(() -> new EntityNotFoundException("口座なし"));
    if (口座.残高() < 金額) {
        throw new 残高不足Exception("不足: 現在 " + 口座.残高());
    }
    口座.出金(金額);
}

SOLID5原則 — bad / goodコードで比較

SOLIDとは何か5つのオブジェクト指向設計原則の頭文字。Robert C. Martin(Uncle Bob)がまとめたもの。面接では必ず一度は聞かれます。### S — Single Responsibility(単一責任)1つのクラスは1つのことだけ。変更理由が1つであるべき。``java// ❌ 悪い例 — ユーザー情報 + メール送信 + DB保存が1つのクラスにclass User { void save() { / DB / } void sendWelcomeEmail() { / SMTP / }}// ✅ 良い例 — 責任分離class User { / データのみ / }class UserRepository { void save(User u) { } }class EmailService { void sendWelcome(User u) { } }`### O — Open/Closed(開放/閉鎖)拡張に対しては開いており、修正に対しては閉じている。新機能の追加は既存コードを変更せずにできるべき。`java// ❌ 新しい決済手段ごとにif-elseを追加if (type.equals("CARD")) { ... } else if (type.equals("KAKAO")) { ... }// ✅ インターフェース + 実装クラス — 新しい手段は新しいクラスでinterface PaymentMethod { void pay(int amount); }class CardPayment implements PaymentMethod { ... }class KakaoPayment implements PaymentMethod { ... }`### L — Liskov Substitution(リスコフの置換)子クラスは親クラスを完全に代替できるべき。親を使うコードに子を渡しても問題なく動作する`java// ❌ 正方形が長方形を継承すると壊れるclass Rectangle { void setWidth(int w); void setHeight(int h); }class Square extends Rectangle { void setWidth(int w) { super.setWidth(w); super.setHeight(w); } // 親の動作を変更}`継承の代わりにインターフェースの分離が正解になることが多いです。### I — Interface Segregation(インターフェース分離)大きなインターフェースは細かく分割せよ。使わないメソッドの実装を強制しない。`java// ❌ 巨大なインターフェースinterface Worker { void work(); void eat(); void sleep(); }// ✅ 役割別に分離interface Workable { void work(); }interface Eatable { void eat(); }`### D — Dependency Inversion(依存性逆転)具体クラスではなくインターフェースに依存せよ。SpringのDIがこの原則の実践です。`java// ❌ 具体クラスに依存class UserService { MySQLRepository repo = new MySQLRepository(); }// ✅ インターフェースに依存 — テスト・DB交換が自由class UserService { private final UserRepository repo; UserService(UserRepository repo) { this.repo = repo; }}``## 暗記不要 — 原則は結果です良いコードを書いていると自然とついてくる原理です。名前を覚えようとするのではなく、各原則の意図だけ覚えてください。

☕ 直接実行 — class・継承・ポリモーフィズム

カプセル化・継承・ポリモーフィズムを一度に。動物クラス階層のサンプルです。
☕ Java
✏️ 코드 편집기
📟 출력 결과
▶ 実行ボタンを押してください
💡 코드를 직접 수정하고 실행해보세요. 변수값을 바꾸거나 println을 추가해 결과를 확인하세요!
☁️ Judge0 API로 서버에서 실행 — Java / Python / JS / C++ 지원

🤖 AIにこう依頼してみてください

このlessonの概念を知っていれば、AIに具体的に指示できます。漠然とした「直して」ではなく、語彙を持ったリクエスト — それがトークン節約の出発点です。

  • 「このクラスをSOLID原則(特にSRP)違反がないか確認して分離してください」
  • 「このオブジェクト生成コードにBuilderパターンを適用してください」
  • 「この継承構造をインターフェース分離でリファクタリングしてください」

なぜトークンが節約されるのか

概念を知らないとAIの回答をもらっても「それは何ですか?」と再び聞く必要があります。その「再質問」がトークンを消費します。概念を一度理解しておけば会話が一度で終わります

オブジェクト指向の5原則 — クラス・カプセル化・継承・ポリモーフィズム・インターフェース - Java