オブジェクト指向の5原則 — クラス・カプセル化・継承・ポリモーフィズム・インターフェース
オブジェクト指向の5原則 — クラス・カプセル化・継承・ポリモーフィズム・インターフェース
🎯 このlessonを読み終えたらできること
このlessonをすべて読み終えると、以下の3つを自信を持って行えるようになります。
- ▸✅ カプセル化・継承・ポリモーフィズム・抽象化の定義 + コード例
- ▸✅ SOLID5原則(特にSRP・OCP・DIP)の面接回答
- ▸✅ なぜ継承よりコンポジションなのかを一言で説明
学習目標をチェックリストとして持ち、すべて答えられるようになったらlessonを閉じてください。
オブジェクト指向とは — *現実世界のものように*コードを書く方法
コアとなる一行
オブジェクト指向(OOP) = 現実のものをオブジェクトとして表現し、それらのオブジェクトが互いにメッセージをやり取りして処理を行うプログラミングの方式。Javaの中心思想です。
なぜオブジェクト指向か
昔のC言語は関数とデータが別々でした。人の名前・年齢は変数として、「歩く・話す」は関数として — 分離されていたため、プログラムが大きくなると何が何だかわからなくなりました。
オブジェクト指向はデータと振る舞いをひとまとめにして扱います。「人」というクラスの中に、名前・年齢のようなフィールドと、歩く・話すのようなメソッドが一緒にあります。まるで現実の人間が身体(データ)と行動を同時に持つように。
クラス vs オブジェクト — 型と焼き菓子
最も混乱しがちな2つの言葉です。比喩で解説します。
- ▸クラス = 型。設計図。「こういう形のものを作る」という青写真。
- ▸オブジェクト = 実際に作られたもの。1個・2個・100個作れて、それぞれ独立しています。
newキーワードが焼く行為です。メモリのどこかに新しい空間を確保し、その場所を変数aに記録します。
カプセル化 — 外部から勝手に触れないように
自分の銀行残高を誰でも自由に変えられるとしたら?大変なことになります。そのためOOPでは外部からの直接アクセスを遮断し、決められたメソッドを通じてのみデータを扱うようにします。
privateキーワードで直接アクセスを遮断し、deposit()のような公式の窓口だけを開いておきます。その窓口の中で検証・ロギング・同期化などの付加作業をすべて処理できます。これがカプセル化の本質です。
継承 — 共通部分を再利用する
さまざまな動物クラスを作るとしましょう。犬・猫・鳥 — みんな移動して食べます。これを毎回別々に書くのは重複です。
犬は動物のすべての機能を自動的に引き継ぎ、自分だけの振る舞いを追加すればよいだけです。
ただし継承は危険な武器です。深く積み重ねると親を1行変えるだけで子がすべて影響を受け、変更が難しくなります。そのため現代のJavaではコンポジションを好みます。犬が動物を継承するのではなく、犬がMovable・Eatableインターフェースを持つ方式です。
ポリモーフィズム — 同じインターフェース、異なる動作
同じメソッド呼び出しがオブジェクトによって異なる結果を返すのがポリモーフィズムです。
aとbはどちらもAnimal型ですが、sound()メソッドは実際のオブジェクトの型に応じて異なる動作をします。ランタイムに決定されます。
このおかげでList<Animal>という1つのリストに犬と猫を混ぜて格納し、1つのループで処理できます。動作はそれぞれが担当します。
インターフェース vs 抽象クラス — 混同されやすい2つ
どちらも直接インスタンス化できない抽象型です。違いは:
- ▸インターフェース — 役割。「比較できる(Comparable)」「反復できる(Iterable)」のような能力の仕様。多重実装が可能(
implements A, B, C)。 - ▸抽象クラス — 共通の骨格。「これらのメソッドは実装済みだが、一部はサブクラスが埋めるべき」。単一継承のみ(
extends A)。
原則:役割だけを定義したいならインターフェース、共通コード + 一部抽象なら抽象クラス。Java 8以降はインターフェースもdefaultメソッドで実装を持てるようになり境界が曖昧になりましたが、役割 vs 骨格という直感は今も有効です。
まとめ
OOPは複雑なプログラムを小さな単位(クラス)に分けて管理する方式です。5つのコア原則:
> 💡 実務の現場:Javaバックエンドは95%がOOP。関数型・リアクティブは補助ツール。OOPが最も基本的な武器です。
例外処理 — エラーを*優雅に*扱う方法
例外とは何か
プログラムが予期しない状況に直面したとき — ファイルが存在しない・DBの接続が切れた・ゼロ除算が発生した — Javaは例外(Exception)というオブジェクトを投げます(throw)。誰かが受け取ら(catch)なければプログラムはクラッシュします。
古いCのようにエラーコードを返すのではなく、別経路でエラーを伝播させるメカニズムです。
2種類の例外 — Checked vs Unchecked
これがJavaの特異な点です。他の言語にはほとんど存在しない区別。
- ▸Checked:コンパイラが必ず処理するよう強制します。
IOException・SQLExceptionなど。try-catchまたはthrows宣言が必須。 - ▸Unchecked(RuntimeException):処理は推奨だが強制ではありません。
NullPointerException・IllegalArgumentExceptionなど。
この区別は長年議論の的です。Springや最近のライブラリはCheckedをほとんど使いません — 強制処理がコードを汚すと見なされるからです。KotlinやC#はUncheckedのみです。
try-catch-finally
finallyはリソース解放に使われてきましたが、try-with-resources(Java 7+)のほうがすっきりしています:
AutoCloseableを実装したリソースはブロック終了時に自動でcloseされます。DB接続・ファイル・ネットワークソケットはすべてこのパターンが標準です。
よくある落とし穴4つ
1. catch (Exception e) { } — 空のcatch:最も恐ろしいアンチパターン。エラーを黙って飲み込みます。デバッグ地獄の始まり。せめてログだけでも残してください。
2. e.printStackTrace():標準出力にスタックトレースが散らばります。本番環境ではloggerを使いましょう:
3. 1つのtryブロックに2つの仕事:異なる種類の処理をひとつのtryブロックにまとめると、どこでエラーが起きたかわかりません。小さなtryを複数使うほうが良いです。
4. ドメインの意味を持たない例外:throw new RuntimeException("error")よりthrow new InsufficientBalanceException()のような意味のある名前のほうが、デバッグや文書化で大きな差を生みます。
まとめ
例外処理は防衛的なコードを書き散らすことではありません。正常フローと例外フローを明確に分離し、意味のあるメッセージ + ロギングで後から追跡できるようにすることが本質です。
ジェネリクス — *型を変数のように*扱う
なぜジェネリクスが必要か
Java 5以前、Listはあらゆる型を格納できました。便利に見えましたが、ランタイムにClassCastExceptionが多発しました。
ジェネリクスはコンパイル時に型を検証する仕組みです。
ランタイムの爆発 → コンパイラが事前にキャッチします。型安全性が核心的な価値です。
基本的な使い方
<T>は型パラメータ — メソッドのパラメータと似た概念です。コンパイラが呼び出し時にどの型かを推論します。
境界型(Bounded Type) — Tの条件
<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ははるかに強力で — 各値がオブジェクトであり、メソッド・フィールド・インターフェース実装まで可能です。
各enum値に追加情報を持たせたり、メソッドを定義したりもできます。ステートマシン・Strategyパターンによく使われます。
if-elseの羅列の代わりに、各enumが自分の振る舞いを持つパターンです。
アノテーション — コードにメタデータを付ける
@Override・@Deprecated・@SuppressWarnings — @で始まるものはすべてアノテーションです。コード自体ではなく、コードに関する情報を表します。
Springの核心はアノテーション
Javaバックエンドをやるなら、アノテーションがコードの80%を占めます。Springのほぼすべての仕組みはアノテーションベースです:
- ▸
@Component・@Service・@Repository— Bean登録 - ▸
@Autowired— 依存性の注入 - ▸
@RestController+@GetMapping("/users")— Webルーティング - ▸
@Transactional— トランザクション自動管理 - ▸
@Entity+@Id— JPAマッピング
アノテーションそれ自体は何もしません。Springのようなフレームワークがランタイムにアノテーションを読み取り、それに応じた動作(ルーティング・DI・トランザクション)を自動的に行います。
まとめ
- ▸enum = 型安全な定数 + 各値がオブジェクトのようにメソッド・フィールドを持てる
- ▸アノテーション = メタデータ。直接実行されません。フレームワーク・ツールが読み取って動作する
この2つがJavaの宣言型(declarative)スタイルを可能にします。「こう動け」と命令するのではなく、「これはこういうものだ」と宣言するだけでツールが処理してくれます。
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・継承・ポリモーフィズム
🤖 AIにこう依頼してみてください
このlessonの概念を知っていれば、AIに具体的に指示できます。漠然とした「直して」ではなく、語彙を持ったリクエスト — それがトークン節約の出発点です。
- ▸「このクラスをSOLID原則(特にSRP)違反がないか確認して分離してください」
- ▸「このオブジェクト生成コードにBuilderパターンを適用してください」
- ▸「この継承構造をインターフェース分離でリファクタリングしてください」
なぜトークンが節約されるのか
概念を知らないとAIの回答をもらっても「それは何ですか?」と再び聞く必要があります。その「再質問」がトークンを消費します。概念を一度理解しておけば会話が一度で終わります。