Java 面接の質問
Javaのインタビューでは、コア言語機能、オブジェクト指向設計、並行性の理解、問題解決能力が深く問われます。シニアエンジニアは構文を知っているだけでなく、パフォーマンス、メモリ、ベストプラクティスについても考察できることが期待されます。このガイドでは、最も一般的で難しいJavaインタビューの質問をカバーします。
Java 面接で問われる内容
コアJavaとOOP
継承、ポリモーフィズム、カプセル化、抽象化、インターフェース vs 抽象クラス、オブジェクトのライフサイクル。
コレクションフレームワーク
HashMapの内部、ConcurrentHashMap、ArrayList vs LinkedList、Comparator vs Comparable、fail-fast vs fail-safe イテレータ。
並行性とマルチスレッディング
スレッドのライフサイクル、synchronized、volatile、ロック(ReentrantLock、ReadWriteLock)、エグゼキュータ、fork/join、並行コレクション。
JVM内部とパフォーマンス
クラスローディング、メモリモデル(ヒープ、スタック、メタスペース)、ガベージコレクションアルゴリズム(G1、CMS、ZGC)、プロファイリング、チューニング。
Java 面接の質問例
- Javaにおける抽象クラスとインターフェースの違いを説明し、それぞれを使用する場面を教えてください。良い回答が押さえる点
- 抽象クラスは単一継承、インスタンス変数、コンストラクタを持つことができる
- インターフェースは多重継承、デフォルトメソッド、staticメソッドを持つことができる
- 抽象クラスは共通の基底実装を提供する際に使用
- インターフェースは機能の契約を定義する際に使用
サンプル回答を見る
抽象クラスは1つまたは複数の抽象メソッドを持ち、インスタンス化できません。一方、インターフェースは全てのメソッドが抽象(Java 8以降はデフォルトメソッドやstaticメソッドも可能)です。抽象クラスは単一継承のみですが、インターフェースは多重継承をサポートします。抽象クラスはインスタンス変数やコンストラクタを持てますが、インターフェースは定数(static final)のみです。使用場面として、抽象クラスは関連するクラス間で共通のコードや状態を共有する場合に適しています(例:動物の抽象クラスから犬や猫が継承)。インターフェースは異なるクラスに共通の振る舞いを強制する場合に使用します(例:Serializableインターフェース)。抽象クラスとインターフェースの選択は、設計の柔軟性と継承の制約を考慮する必要があります。抽象クラスは「is-a」関係、インターフェースは「can-do」関係を表すことが多いです。Java 8以降、インターフェースのデフォルトメソッドによって抽象クラスとの境界が曖昧になりましたが、状態を持つ必要がある場合は抽象クラスが適しています。
- HashMapは内部的にどのように動作しますか?リサイズはパフォーマンスにどのように影響しますか?良い回答が押さえる点
- HashMapはバケット(配列)とリンクリスト/赤黒木で構成
- putメソッドでキーのハッシュコードを計算し、バケットインデックスを決定
- 衝突時にはリンクリストまたは赤黒木でエントリを格納
- リサイズ(rehash)は容量が閾値を超えたときに発生、パフォーマンスに影響
サンプル回答を見る
HashMapは内部でエントリの配列(テーブル)を持ち、各要素はリンクリストまたは赤黒木の先頭です。putメソッドでは、キーのhashCode()からハッシュ値を計算し、テーブルサイズでマスクしてインデックスを取得します。同じインデックスに既存のエントリがある場合(衝突)、リンクリストに追加します。Java 8以降、リンクリストの長さが8以上になると赤黒木に変換され、検索性能がO(n)からO(log n)に向上します。getメソッドも同様にハッシュを計算し、該当バケット内を線形検索または木検索します。リサイズは、エントリ数がテーブルサイズ×負荷係数(デフォルト0.75)を超えたときに発生し、新しいテーブルを作成して全エントリを再配置(rehash)します。リサイズはコストが高く、O(n)の時間がかかり、頻繁に発生するとパフォーマンスが低下します。そのため、初期容量を適切に設定することでリサイズ回数を減らせます。また、リサイズ中はエントリのハッシュ値が変化するため、テーブルサイズが2の累乗である利点を活かしてビット演算で高速に処理します。
- スレッドセーフなシングルトンをJavaで書いてください(ダブルチェックロッキング、Bill Pugh方式)。良い回答が押さえる点
- ダブルチェックロッキングはvolatileとsynchronizedを組み合わせる
- Bill Pugh方式は内部静的クラスを使用
サンプル回答を見る
スレッドセーフなシングルトン実装として、ダブルチェックロッキング(DCL)とBill Pugh方式があります。DCLでは、インスタンスをvolatileフィールドとして宣言し、nullチェックの外側と内側でsynchronizedブロックを使用します。これにより、一度インスタンスが生成された後は同期のオーバーヘッドを回避します。ただし、volatileがないと、インスタンス生成時の命令の並べ替えにより不完全なオブジェクトが公開される可能性があります。Bill Pugh方式は、内部静的クラスにインスタンスを保持する方法で、JVMのクラスロード機構を利用してスレッドセーフを実現します。この方法はコードが簡潔で、同期オーバーヘッドがありません。以下にDCLとBill Pugh方式のコード例を示します。
参考コードjava // ダブルチェックロッキング(DCL) public class SingletonDCL { private static volatile SingletonDCL instance; private SingletonDCL() {} public static SingletonDCL getInstance() { if (instance == null) { synchronized (SingletonDCL.class) { if (instance == null) { instance = new SingletonDCL(); } } } return instance; } } // Bill Pugh方式 public class SingletonBillPugh { private SingletonBillPugh() {} private static class SingletonHolder { private static final SingletonBillPugh INSTANCE = new SingletonBillPugh(); } public static SingletonBillPugh getInstance() { return SingletonHolder.INSTANCE; } } - synchronized、ReentrantLock、ReadWriteLockの違いは何ですか?それぞれのユースケースを挙げてください。良い回答が押さえる点
- synchronizedはJVMレベルで自動的にロックを管理
- ReentrantLockは明示的なロックで柔軟性が高い
- ReadWriteLockは読み取り/書き込みを分離して並行性を向上
サンプル回答を見る
synchronizedはJavaの組み込みの同期機構で、メソッドまたはブロックに使用します。ロックの取得と解放が自動的に行われ、コードが簡潔ですが、フェアネス(公平性)の設定やタイムアウト、割り込み可能なロックといった高度な機能はありません。ReentrantLockはjava.util.concurrent.locksパッケージに属し、明示的にlock()とunlock()を呼び出します。フェアネスポリシーの指定、tryLock()によるタイムアウト、lockInterruptibly()による割り込み応答が可能です。また、同じスレッドが複数回ロックを取得できる再入可能(reentrant)です。ReadWriteLockは、読み取り専用と書き込みのロックを分離し、読み取り同士は競合しないため、読み取りが多いシナリオでスループットを向上させます。ユースケースとして、synchronizedは単純な排他制御(例:カウンタの更新)、ReentrantLockは高度な制御が必要な場合(例:キューからのポーリング)、ReadWriteLockはキャッシュやデータ構造への読み取り集中アクセスに適しています。ただし、ReadWriteLockは書き込みスレッドが少ない場合に効果的で、書き込みが多いと性能が低下します。
- Javaメモリモデルについて説明してください。volatileはどのように可視性を保証しますか?良い回答が押さえる点
- JMMはスレッド間の可視性と順序を定義
- volatileは変数の読み書きが主メモリを直接アクセスすることを保証
- happens-before関係によって可視性を保証
サンプル回答を見る
Javaメモリモデル(JMM)は、スレッドがどのようにメモリを共有するか、またコンパイラやプロセッサの最適化によって引き起こされるメモリの順序問題を規定します。JMMはhappens-before関係に基づいて、あるスレッドの操作が別のスレッドからどのように見えるかを定義します。volatileキーワードは変数に対して、読み取りと書き込みが主メモリに対して直接行われることを保証し、スレッド間の可視性を確保します。具体的には、volatile変数への書き込みは、その後の同じ変数の読み取りに対してhappens-before関係を確立します。これにより、書き込みスレッドがvolatile変数に書き込むと、その値が他のスレッドから即座に見えるようになります。volatileはアトミック性を保証しないため、複合操作(例:count++)には適しません。その場合はAtomicクラスやsynchronizedを使用します。JMMの理解は、正しいスレッドセーフなコードを書くために重要であり、特にvolatileの誤用はデッドロックやデータ競合を引き起こす可能性があります。
- 文字列内で最初に繰り返されない文字を見つけるメソッドを、Java Streamsを使用してまたは使用せずに書いてください。良い回答が押さえる点
- 文字列を走査して各文字の出現回数をカウント
- Streamsを使用する場合と使用しない場合の2つの実装
サンプル回答を見る
最初に繰り返されない文字を見つける方法として、まず文字列を1回走査して各文字の出現回数をマップに記録します(LinkedHashMapを使用すると挿順を維持できます)。その後、再度文字列を走査し、最初にカウントが1の文字を返します。Streamsを使用しない場合、forループで実装します。Streamsを使用する場合、chars()でIntStreamを取得し、collectでMapにまとめ、その後filterとfindFirstで最初のユニーク文字を見つけます。ただし、Streams版は効率面で2回の走査が必要です(1回のcollectとその後のfilter)。時間計算量はO(n)、空間計算量はO(文字セット)です。以下に両方の実装を示します。
参考コードjava import java.util.*; import java.util.stream.*; public class FirstNonRepeating { // Streamsを使用しない実装 public static Character firstNonRepeatingChar(String s) { Map<Character, Integer> countMap = new LinkedHashMap<>(); for (char c : s.toCharArray()) { countMap.put(c, countMap.getOrDefault(c, 0) + 1); } for (Map.Entry<Character, Integer> entry : countMap.entrySet()) { if (entry.getValue() == 1) { return entry.getKey(); } } return null; } // Streamsを使用した実装 public static Character firstNonRepeatingCharStream(String s) { Map<Character, Long> countMap = s.chars() .mapToObj(c -> (char) c) .collect(Collectors.groupingBy(Function.identity(), LinkedHashMap::new, Collectors.counting())); return countMap.entrySet().stream() .filter(entry -> entry.getValue() == 1) .map(Map.Entry::getKey) .findFirst() .orElse(null); } } - Javaのガベージコレクションはどのように動作しますか?G1GCとZGCを比較してください。良い回答が押さえる点
- GCはヒープ上の不要なオブジェクトを自動的に解放
- 世代別GC(Young、Old、Permanent/Metaspace)
- G1GCはリージョンベースで予測可能な停止時間
- ZGCは極低レイテンシ向けで大規模ヒープにスケール
サンプル回答を見る
Javaのガベージコレクション(GC)は、ヒープメモリ上の到達不能になったオブジェクトを自動的に回収し、メモリを再利用します。基本的に世代別GCを採用し、新しいオブジェクトはYoung領域(Eden、Survivor)に割り当てられ、Minor GCで不要オブジェクトを回収し、生き残ったオブジェクトはOld領域へ昇格します。Full GCはOld領域が満杯になると発生し、全ヒープを対象に停止時間が長くなります。G1GC(ガベージファースト)は、ヒープを複数のリージョンに分割し、最も多くのガベージを含むリージョンから優先的に回収します。これにより、停止時間を予測可能にし、アプリケーションのスループットとレイテンシのバランスを取ります。一方、ZGCは低レイテンシを重視し、リージョンベースかつカラーポインタ技術を用いて、ほぼ停止時間なし(サブミリ秒)で動作します。ZGCは大規模ヒープ(数TB)でもスケーラブルですが、G1GCに比べてCPUオーバーヘッドがやや高く、適用対象はレイテンシに敏感なアプリケーションに限定されます。G1GCはJava 9以降のデフォルトGCであり、多くのアプリケーションで適切なパフォーマンスを発揮します。ZGCはJava 15以降で本番対応となりました。
- wait-notify(またはLock conditions)を使用してカスタムブロッキングキューを実装してください。良い回答が押さえる点
- BlockingQueueのput()とtake()を実装
- 内部にLinkedListとReentrantLockのConditionを使用
- put()はキューが満杯の場合に待機、take()は空の場合に待機
サンプル回答を見る
カスタムブロッキングキューは、スレッドセーフなキューで、要素の追加時に容量制限に達している場合は追加するスレッドをブロックし、要素の取得時にキューが空の場合は取得するスレッドをブロックします。実装にはLinkedListとReentrantLock、および2つのCondition(notFull、notEmpty)を使用します。put()メソッドでは、ロックを取得し、キューが満杯の間はnotFull.await()で待機します。その後要素を追加し、notEmpty.signal()で待機中の取得スレッドを起こします。take()メソッドでは逆の処理を行います。注意点として、signal()ではなくsignalAll()を使用すると、全ての待機スレッドが起きるため効率が悪くなりますが、ここではシンプルにsignal()を使用します。また、割り込みに応答するために、await()の代わりにawaitInterruptibly()を使用することもできます。以下に実装例を示します。
参考コードjava import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class CustomBlockingQueue<T> { private final Queue<T> queue = new LinkedList<>(); private final int capacity; private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public CustomBlockingQueue(int capacity) { this.capacity = capacity; } public void put(T element) throws InterruptedException { lock.lock(); try { while (queue.size() == capacity) { notFull.await(); // 満杯なら待機 } queue.add(element); notEmpty.signal(); // 空でなくなったことを通知 } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); // 空なら待機 } T item = queue.poll(); notFull.signal(); // 空きができたことを通知 return item; } finally { lock.unlock(); } } }
準備方法
- ホワイトボードやオンラインエディタでクリーンなスレッドセーフコードを書く練習をしましょう。
- JVMの内部を理解し、メモリとGCの動作について説明できるようにしましょう。
- コレクションフレームワークをマスターしましょう:各実装のトレードオフを知る。
- Java 8以降の機能(ストリーム、ラムダ、Optional、CompletableFuture、新しいDate/Time API)を復習しましょう。
- 「HashMapとConcurrentHashMapの違いは何ですか?」という質問に対して、内部詳細を含む深い回答を用意しましょう。
よくある質問
Java 8の機能はインタビューで重要ですか?
はい、ほとんどのインタビューではラムダ、ストリーム、Optional、CompletableFutureの知識を前提としています。これらは現代のJava開発で頻繁に使用されます。
JVMチューニングフラグを暗記する必要がありますか?
一般的なGCフラグ(-XX:+UseG1GC、-Xms、-Xmxなど)を知り、ヒープサイズの調整方法を理解しておくべきです。暗記よりもその理由を理解することが重要です。
Javaインタビューでデザインパターンはどのくらい頻繁に出題されますか?
よく出ます。特にシングルトン、ファクトリ、ビルダー、オブザーバーです。スレッドセーフなシングルトンの実装や、Java 8のラムダを使ったストラテジーパターンの説明ができるように準備しましょう。
fail-fastとfail-safeイテレータの違いは何ですか?
fail-fastイテレータは、反復中にコレクションが構造的に変更されるとConcurrentModificationExceptionをスローします(例:ArrayList)。fail-safeイテレータはクローンに対して動作します(例:ConcurrentHashMap、CopyOnWriteArrayList)。
Java 9~17の機能もインタビューで必要ですか?
はい、モジュール(Jigsaw)、sealedクラス、レコード、instanceofのパターンマッチング、テキストブロックについて質問される可能性があります。少なくとも主要な機能を把握しておきましょう。
Java の質問をAIで練習、瞬時にフィードバック
履歴書をアップロードして、パーソナライズされた模擬面接を受け、改善点を確認 — 無料で始められます。