OOP 面接の質問
オブジェクト指向プログラミング(OOP)のインタビューでは、カプセル化、継承、ポリモーフィズム、抽象化といった基本概念の理解と、デザインパターンを適用してクリーンで保守可能なコードを書く能力が評価されます。これらの質問は、ジュニアからシニアまで、ソフトウェアエンジニアリングの役割で一般的ですが、特に設計とアーキテクチャの決定が重要なシニアポジションでよく出題されます。面接官は、概念を明確に説明し、実際の例に関連付け、OOPの原則を示すコーディング問題を解くことを期待しています。
OOP 面接で問われる内容
コア原則
カプセル化、継承、ポリモーフィズム、抽象化に関する質問。例や実世界の類推を用います。
デザインパターン
シングルトン、ファクトリ、オブザーバー、ストラテジーなどの一般的なパターン、その実装と使用すべき場面。
クラス設計と関係
継承 vs コンポジション、抽象クラス vs インターフェース、結合度/凝集度、SOLID原則。
実践的なコーディング問題
クラス階層の実装、駐車場や図書館システムの設計、手続き型コードのOOPへのリファクタリング。
OOP 面接の質問例
- OOPの4つの柱を説明し、実世界の例を挙げてください。良い回答が押さえる点
- カプセル化: データと振る舞いを一つの単位にまとめ、外部から隠蔽する。
- 継承: 既存クラスを基に新しいクラスを作成し、コード再利用を促進。
- ポリモーフィズム: 同じインターフェースで異なる実装を提供。
- 抽象化: 複雑な実装を隠し、本質的な機能だけを公開。
サンプル回答を見る
OOPの4つの柱は、カプセル化、継承、ポリモーフィズム、抽象化です。カプセル化は、データとメソッドをクラスにまとめ、外部から直接アクセスできないようにすることで、例として車のエンジン内部を隠し、アクセルペダルだけを公開する挙動が挙げられます。継承は、既存のクラス(親)の特性を新しいクラス(子)が引き継ぐことで、例えば「車」クラスを継承した「スポーツカー」クラスは、車の基本的な機能を再利用しつつ、独自の機能を追加できます。ポリモーフィズムは、同じメソッド呼び出しでもオブジェクトに応じて異なる動作をする特性で、例として「運転する」メソッドが、普通の車とスポーツカーで異なる加速を示すことが挙げられます。抽象化は、複雑な内部実装を隠し、簡潔なインターフェースを提供することで、例えば車の運転操作(ハンドル、アクセル、ブレーキ)だけを知っていれば運転できることが該当します。これらの概念は、柔軟で保守性の高いコードを実現するための基盤です。
- 抽象クラスとインターフェースの違いは何ですか?それぞれ使用する場面は?良い回答が押さえる点
- 抽象クラス: インスタンス化不可、一部実装・状態を持てる。
- インターフェース: 純粋抽象、多重継承可能、状態は持てない。
- 抽象クラス使用場面: 共通の基底クラスから派生する関連クラス群。
- インターフェース使用場面: 異なるクラス間に共通の振る舞いを定義。
サンプル回答を見る
抽象クラスとインターフェースの主な違いは、抽象クラスはコンストラクタやフィールド(状態)を持ち、メソッドの一部実装を提供できるのに対し、インターフェースは抽象メソッドと定数のみを持ち、完全な抽象化を強制します。また、クラスは複数のインターフェースを実装できますが、抽象クラスは1つしか継承できません。使用場面として、抽象クラスは「動物」と「犬」「猫」のように、共通の基底を持つ関連クラス群に適しています。一方、インターフェースは「飛べる」という振る舞いを鳥、飛行機、ドローンなど異なるクラスに適用する場合に適しています。設計の指針として、is-a関係なら抽象クラス、can-do関係ならインターフェースを選ぶとよいでしょう。近年では、デフォルトメソッドを持つインターフェースも増え、抽象クラスの役割は縮小傾向にあります。
- Java(または任意の言語)でシングルトンパターンを実装し、スレッドセーフについて議論してください。良い回答が押さえる点
- シングルトンパターン: クラスのインスタンスが1つだけであることを保証。
- Javaでの実装: プライベートコンストラクタ、静的メソッドでインスタンス取得。
- スレッドセーフ問題: 複数スレッドから同時にアクセスされた場合に複数インスタンス生成のリスク。
- 解決策: synchronized、二重チェックロッキング、enum使用。
サンプル回答を見る
シングルトンパターンは、特定のクラスのインスタンスがシステム全体で1つだけであることを保証するデザインパターンです。Javaでは、プライベートコンストラクタと静的メソッド(getInstance)を提供します。スレッドセーフの問題は、複数スレッドが同時にgetInstanceを呼び出した際に、インスタンスが複数生成される可能性があることです。これを防ぐために、メソッド全体をsynchronizedにする方法(パフォーマンス低下)、二重チェックロッキング(volatileとsynchronizedを組み合わせる)、またはインスタンスをstaticフィールドでイニシャライズする方法があります。最もシンプルで安全なのは、enumを使ったシングルトンで、Javaはenumのインスタンスが常にユニークであることを保証します。以下のコードは二重チェックロッキングの例です。
参考コードjava public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } - 図形(Circle、Rectangle)のクラス階層を設計し、面積計算でポリモーフィズムを示してください。良い回答が押さえる点
- 抽象クラスShapeを定義し、抽象メソッドcalculateArea()を持つ。
- CircleクラスがShapeを継承し、半径から面積を計算。
- RectangleクラスがShapeを継承し、幅と高さから面積を計算。
- ポリモーフィズム: Shape型のリストに異なる図形を格納し、面積を一括計算。
サンプル回答を見る
図形のクラス階層を設計するには、まず抽象クラスShapeを作成し、抽象メソッドcalculateArea()を定義します。次に、CircleクラスとRectangleクラスがShapeを継承し、それぞれ独自の計算ロジックを実装します。ポリモーフィズムを示すために、Shape型のリストにCircleとRectangleのインスタンスを格納し、ループで各図形の面積を計算します。これにより、具体的な型を知らなくても共通のインターフェースで処理できます。以下のコード例では、各図形の面積計算とポリモーフィズムの使用を示します。時間計算量は面積計算がO(1)、リスト処理がO(n)です。
参考コードjava abstract class Shape { abstract double calculateArea(); } class Circle extends Shape { private double radius; Circle(double radius) { this.radius = radius; } @Override double calculateArea() { return Math.PI * radius * radius; } } class Rectangle extends Shape { private double width, height; Rectangle(double width, double height) { this.width = width; this.height = height; } @Override double calculateArea() { return width * height; } } public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(5), new Rectangle(4, 6) }; for (Shape s : shapes) { System.out.println("面積: " + s.calculateArea()); } } } - 継承がどのように密結合につながるかを説明し、例を使ってコンポジションを継承より優先する理由を説明してください。良い回答が押さえる点
- 継承は親クラスの変更が子クラスに影響し、密結合を生む。
- 例えば、親クラスにメソッドを追加すると、すべての子クラスに変更が波及。
- コンポジションはhas-a関係で、インターフェースを通じて疎結合。
- 例: Vehicleクラスを継承するより、Engineインターフェースを持つ方が柔軟。
サンプル回答を見る
継承は強力なコード再利用の手段ですが、親クラスと子クラスの間に密結合を生み出します。例えば、親クラスに新しいメソッドを追加すると、すべての子クラスがその影響を受け、場合によっては意図しない振る舞いを引き起こします。また、親クラスの実装の詳細が子クラスに漏れるため、変更が困難になります。コンポジション(委譲)は、オブジェクトが他のオブジェクトを保持し、インターフェースを通じて機能を委譲することで、疎結合を実現します。例として、「車」クラスが「エンジン」クラスを継承するのではなく、「エンジン」インターフェースを保持する設計にすると、エンジンの種類(ガソリン、電気)を動的に切り替えられます。このように、継承よりコンポジションを優先することで、柔軟性と保守性が向上します。
- 配送料金を計算するためにストラテジーパターンを実装するコードを書いてください。良い回答が押さえる点
- ストラテジーパターン: アルゴリズムをカプセル化し、実行時に切り替え可能。
- ShippingStrategyインターフェースを定義。
- StandardShippingとExpressShippingで実装。
- Orderクラスが戦略を持ち、配送料を計算。
サンプル回答を見る
ストラテジーパターンは、一連のアルゴリズムをカプセル化し、それらを交換可能にするデザインパターンです。配送料金計算の例では、ShippingStrategyインターフェースにcalculateCostメソッドを定義し、StandardShippingとExpressShippingの具象戦略クラスでそれぞれの料金計算ロジックを実装します。OrderクラスはShippingStrategyへの参照を保持し、注文時に戦略を設定することで、配送料金を動的に計算します。これにより、新しい配送方法が追加された場合でも、既存のコードを変更せずに戦略を追加できます。以下のコードは、ストラテジーパターンによる配送料金計算の実装例です。
参考コードjava interface ShippingStrategy { double calculateCost(double weight); } class StandardShipping implements ShippingStrategy { public double calculateCost(double weight) { return weight * 1.5; } } class ExpressShipping implements ShippingStrategy { public double calculateCost(double weight) { return weight * 3.0 + 10; } } class Order { private double weight; private ShippingStrategy strategy; Order(double weight) { this.weight = weight; } void setStrategy(ShippingStrategy strategy) { this.strategy = strategy; } double calculateShippingCost() { return strategy.calculateCost(weight); } } public class Main { public static void main(String[] args) { Order order = new Order(5.0); order.setStrategy(new StandardShipping()); System.out.println("通常配送料: " + order.calculateShippingCost()); order.setStrategy(new ExpressShipping()); System.out.println("速達配送料: " + order.calculateShippingCost()); } } - リスコフの置換原則とは何ですか?それを違反するコード例を示してください。良い回答が押さえる点
- リスコフの置換原則: 派生クラスは基底クラスの代わりに使用可能でなければならない。
- 基底クラスの事前条件は強化できず、事後条件は弱化できない。
- 違反例: SquareがRectangleを継承し、setWidthで高さも変更してしまう。
- 適切な設計: 不変条件を破らない継承、またはコンポジションを使用。
サンプル回答を見る
リスコフの置換原則(LSP)は、サブタイプのオブジェクトはその基底型のオブジェクトと置換可能でなければならないという原則です。つまり、基底クラスが期待する振る舞いを、派生クラスも満たす必要があります。違反の典型的な例は、Square(正方形)がRectangle(長方形)を継承する場合です。RectangleではsetWidthとsetHeightが独立して動作しますが、Squareでは幅と高さを常に等しく保つ必要があるため、setWidthをオーバーライドして高さも変更すると、Rectangleの仕様に違反します。LSPに従うには、SquareはRectangleの派生クラスとすべきではなく、代わりにShapeのような抽象クラスを共通の親とするか、コンポジションを使用します。以下のコードはLSP違反の例です。
参考コードjava class Rectangle { protected int width, height; public void setWidth(int w) { width = w; } public void setHeight(int h) { height = h; } public int getArea() { return width * height; } } class Square extends Rectangle { @Override public void setWidth(int w) { width = w; height = w; // LSP違反: 内部状態を変更 } @Override public void setHeight(int h) { height = h; width = h; // 同様に違反 } } // 利用コード Rectangle r = new Square(); r.setWidth(5); r.setHeight(4); System.out.println(r.getArea()); // 期待: 20, 実際: 16 - MenuItems、Order、Paymentのクラスを持つレストランの注文システムを設計し、適切なOOP設計を確保してください。良い回答が押さえる点
- MenuItemsクラス: 商品の名前、価格、カテゴリを保持。
- Orderクラス: 注文アイテムのリストと合計金額を管理。
- Paymentクラス: 支払い方法と処理ロジックをカプセル化。
- 適切な設計: 単一責任、疎結合、拡張性を考慮。
サンプル回答を見る
レストランの注文システムでは、まずMenuItemsクラスで各商品の情報(名前、価格、カテゴリなど)を保持します。Orderクラスは複数のMenuItemsをリストとして持ち、アイテムの追加・削除や合計金額の計算を行います。Paymentクラスは支払い方法(現金、クレジットカードなど)を抽象化し、支払い処理を委譲します。これにより、各クラスが単一責任を負い、変更の影響が局所化されます。例えば、新しい支払い方法を追加する場合はPaymentのサブクラスを作成するだけで、OrderやMenuItemsには影響しません。以下のコードは、この設計の簡略化した実装例です。時間計算量は合計金額計算がO(n)です。
参考コードjava class MenuItem { private String name; private double price; private String category; MenuItem(String name, double price, String category) { this.name = name; this.price = price; this.category = category; } double getPrice() { return price; } // その他のゲッター } class Order { private List<MenuItem> items = new ArrayList<>(); void addItem(MenuItem item) { items.add(item); } double getTotal() { return items.stream().mapToDouble(MenuItem::getPrice).sum(); } } interface Payment { boolean pay(double amount); } class CashPayment implements Payment { public boolean pay(double amount) { System.out.println("現金で " + amount + " 円支払い"); return true; } } class CreditCardPayment implements Payment { public boolean pay(double amount) { System.out.println("クレジットカードで " + amount + " 円支払い"); return true; } }
準備方法
- 各OOP原則の定義と例をマスターし、類推を使って説明できるようにしましょう。
- ホワイトボードやコードエディタでシステム設計を練習し、クラスの責務と関係に焦点を当てましょう。
- 一般的なデザインパターンを復習し、自分の言語でゼロから実装できるようにしましょう。
- トレードオフを理解する:継承 vs コンポジション、密結合 vs 疎結合、抽象クラス vs インターフェースをいつ使うか。
- 実践的なコーディングに備えて、小さなOOPプログラムを書き、設計の選択を説明できるようにしましょう。
よくある質問
最もよくあるOOPインタビューの質問は何ですか?
4つの柱の説明、抽象クラスとインターフェースの違い、クラス階層の設計、シングルトンなどのデザインパターンの実装がよく聞かれます。
OOPのコーディング問題にどう準備すればよいですか?
駐車場、図書館、Eコマースカートなどの小さなシステム設計を練習し、クラス構造、カプセル化、インターフェースの使用に焦点を当てましょう。
OOPインタビューでデザインパターンは必要ですか?
はい、特にシニアの役割では必要です。シングルトン、ファクトリ、ストラテジー、オブザーバー、デコレーターなど最も一般的なものを知っておきましょう。
継承とコンポジションの違いは何ですか?
継承は「is-a」関係、コンポジションは「has-a」関係を作ります。柔軟性と疎結合のためにコンポジションを優先しましょう。
OOPの概念を明確に説明するにはどうすればよいですか?
実世界の類推を使いましょう:車をオブジェクトとして(色、モデルなどのプロパティ、start、stopなどのメソッド)、または動物園の動物階層でポリモーフィズムを説明するなど。
OOP の質問をAIで練習、瞬時にフィードバック
履歴書をアップロードして、パーソナライズされた模擬面接を受け、改善点を確認 — 無料で始められます。