Java バックエンド面接対策

Java 面接質問と回答 2026

言語の基礎から JVM (Java Virtual Machine) の動作、コレクション、Stream API、並行処理までを扱う、バックエンド開発者向けの Java 面接セット 30 問です。

Javaの模擬面接を開始クレジットカードは不要です。1回分の無料セッションがあります。
英語での技術面接練習非ネイティブ話者が技術面接の合格を目指して練習できるモードです。

Java と JVM

1Javaとは何ですか?また、なぜプラットフォーム非依存と見なされているのですか?

Javaは静的型付けの汎用プログラミング言語です。ソースコードはバイトコードへとコンパイルされ、そのバイトコードがJVM(Java Virtual Machine)上で実行されます。互換性のあるJVMが存在すれば、同一の .class ファイルや .jar ファイルを再コンパイルすることなく異なるOS上で通常実行できます。ただし、これは完全な非依存を意味するわけではありません。ネイティブライブラリ、OS固有のファイルパス、システムコマンド、OSごとの挙動の違いなどによって、アプリケーションが特定のプラットフォームに依存してしまうこともあります。

Java source -> javac -> bytecode -> JVM -> machine code
AI コーチを使ってこの質問に答えてみる

2JVM (Java Virtual Machine)、JRE (Java Runtime Environment)、JDK (Java Development Kit) の違いは何ですか?

JVMはバイトコードをロードして実行し、メモリ、スレッド、ガベージコレクションを管理します。歴史的にJREは、JVMにアプリケーションの実行に必要なライブラリを加えたものを指していました。JDKはランタイムに加えて、javac、java、デバッガ、javadoc、jar、診断ツールなどの開発ツールを含みます。近年のディストリビューションでは、カスタムランタイムイメージを作成してアプリケーションと一緒に配布することが一般的になったため、独立したJREが提供されることは少なくなっています。

JDK = development tools + runtime
Runtime = JVM + standard libraries
AI コーチを使ってこの質問に答えてみる

3JVM(Java Virtual Machine)はどのようにJavaコードを実行し、なぜJIT(Just-In-Time compilation)が必要とされるのですか?

javacはソースコードをバイトコードにコンパイルします。JVMはクラスをロードし、バイトコード命令の解釈実行を開始します。実行時に頻繁に実行される処理(ホットパス)が検出されると、JITコンパイラによって最適化された機械語コードにコンパイルされます。JITはメソッドのインライン展開、ループの最適化、エスケープ解析の適用、一部のメモリ割り当ての除去などを行います。これにより、Javaアプリケーションはウォームアップ後に実行速度が向上することがあり、JMHを使用しないマイクロベンチマークが誤解を招きやすい理由でもあります。

AI コーチを使ってこの質問に答えてみる

4ClassLoaderはどのように動作しますか?

ClassLoaderはクラス定義をJVM (Java Virtual Machine)にロードします。代表的なローダーには、Bootstrap、Platform、Application、およびカスタムローダーがあります。クラスは完全修飾名だけでなく、それをロードしたClassLoaderによっても識別されるため、異なるローダーによってロードされた同名のクラスは別個の型として扱われます。Javaでは通常「親委譲モデル(parent delegation)」が採用されており、ローダーがまず親ローダーに問い合わせることで、プラットフォームクラスが誤って置き換えられるリスクを低減しています。

Loading
Linking: verification, preparation, resolution
Initialization
AI コーチを使ってこの質問に答えてみる

5JVM (Java Virtual Machine) のメモリはどのように構成されていますか?

JVMの主要なメモリ領域には、オブジェクトや配列のためのヒープ、スレッドごとのコールフレームを保持するJavaスタック、クラスメタデータを格納するMetaspace、JITコンパイルされたコードのためのコードキャッシュ、PCレジスタ、ネイティブスタックが含まれます。「プリミティブ型は常にスタックに配置され、オブジェクトは常にヒープに配置される」という過度な単純化は避けるべきです。プリミティブ型のフィールドは所属するオブジェクトの内部に格納されますし、JITのエスケープ解析によってメモリ割り当てが省略されたり、オブジェクトの表現が変更されたりすることがあります。

AI コーチを使ってこの質問に答えてみる

6ガベージコレクタ(Garbage Collector)はどのように動作しますか?

ガベージコレクタは、GCルートから到達不能になったオブジェクトが占有しているメモリを解放します。GCルートには、実行中のメソッドのローカル変数、静的フィールド、アクティブなスレッド、JNI(Java Native Interface)参照などが含まれます。多くのコレクタは「ほとんどの新規オブジェクトは短命である」という経験則(弱世代仮説)に基づいており、ヒープは頻繁にYoung世代とOld世代に分割されます。なお、GCはファイルや接続などのリソースを確定的なタイミングで閉じるわけではないため、外部リソースは `try-with-resources` を用いて確実に閉じる必要があります。

try (var stream = Files.newInputStream(path)) {
    // use stream
}
AI コーチを使ってこの質問に答えてみる

オブジェクト指向と型

7JavaがサポートしているOOP(Object-Oriented Programming: オブジェクト指向プログラミング)の4原則とは何ですか。

Javaは、カプセル化、抽象化、継承、ポリモーフィズムの4原則をサポートしています。カプセル化は、公開されたAPIの背後に内部状態を隠蔽します。抽象化は、本質的な振る舞いに着目します。継承は、既存の型に基づいて新しい型を作成します。ポリモーフィズムは、共有された規約を通じてコードが異なる実装を扱えるようにします。実際の開発では、結合度を低く抑えられるため、継承よりもコンポジションが好まれることがよくあります。

interface PaymentService {
    void pay(BigDecimal amount);
}

class CardPaymentService implements PaymentService {
    @Override
    public void pay(BigDecimal amount) {
        // payment implementation
    }
}
AI コーチを使ってこの質問に答えてみる

8メソッドのオーバーロードとオーバーライドの違いは何ですか?

オーバーロードは、同じ名前でパラメータが異なる複数のメソッドが存在することを意味し、コンパイラが引数の静的な型に基づいて呼び出すメソッドを選択します。オーバーライドは、サブクラスが親クラスのインスタンスメソッドの実装を置き換えることを意味し、実行時ディスパッチによって実際のオブジェクトの型に基づいて呼び出すメソッドが選択されます。なお、staticメソッドはオーバーライドされるのではなく隠蔽(hide)され、privateメソッドはアクセス可能なメンバーとして継承されないためオーバーライドされません。

void send(String message) {}
void send(String message, int priority) {}

class Animal {
    void speak() { System.out.println("Animal"); }
}

class Dog extends Animal {
    @Override
    void speak() { System.out.println("Dog"); }
}
AI コーチを使ってこの質問に答えてみる

9インターフェイスと抽象クラスの違いは何ですか?

インターフェイスは規約(コントラクト)を定義するものであり、抽象メソッド、デフォルトメソッド、静的メソッド、定数、およびプライベートヘルパーメソッドを含めることができます。一方、抽象クラスはこれらに加えて、インスタンスの状態、コンストラクタ、protectedフィールド、共通の実装、および抽象メソッドや具象メソッドを持つことができます。クラスが継承(extend)できるクラスは1つのみですが、インターフェイスは複数実装(implement)できます。独立した規約を定義する場合はインターフェイスを使用し、実装や状態を共有する密接に関連した型を定義する場合は抽象クラスを使用します。

interface Repository<T> {
    Optional<T> findById(long id);
}
AI コーチを使ってこの質問に答えてみる

10final キーワードにはどのような意味がありますか?

final 変数は一度しか代入できませんが、final な参照であっても参照先のオブジェクト自体が不変(イミュータブル)になるわけではありません。final メソッドはオーバーライドできず、final クラスは継承(拡張)できません。final は設計の意図を明確にし、参照の再代入や継承を制限しますが、それ単体で完全なイミュータブルモデルやスレッドセーフ性を提供するものではありません。

final int maxAttempts = 3;

final List<String> names = new ArrayList<>();
names.add("Alice"); // allowed

public final void validate() {}

public final class Token {}
AI コーチを使ってこの質問に答えてみる

11Javaにおけるレコード(record)とは何ですか?

レコード(record)は、イミュータブル(不変)なデータキャリアを定義するための簡潔な構文です。コンパイラによって、privateかつfinalなフィールド、コンストラクタ、アクセサ、equals、hashCode、toStringが自動生成されます。レコードはDTO(Data Transfer Object)や値オブジェクト、メソッドの返却値に適しています。ただし、すべてのドメインエンティティを代替できるわけではありません。コンパクトコンストラクタでバリデーションを追加することは可能ですが、複雑な状態変更を伴う振る舞いや遅延ロード、継承を前提としたデータモデルにはレコードはあまり適していません。

public record User(long id, String name) {}

public record Order(List<String> items) {
    public Order {
        items = List.copyOf(items);
    }
}
AI コーチを使ってこの質問に答えてみる

12Javaで不変(イミュータブル)オブジェクトを作成するにはどうすればよいですか?

クラスを `final` にするか継承を厳密に制限し、フィールドを `private final` に設定した上で、セッターメソッドを設けずコンストラクタで入力値のバリデーションを行います。可変な入力オブジェクトに対しては防御的コピーを作成し、取得時にはコピーまたは変更不能なビューを返します。完全な不変性(ディープイミュータビリティ)を実現するには、すべてのネストされた内部状態も不変である必要があります。`final` フィールドは安全な公開(safe publication)を支援しますが、可変なコレクションや可変なネストオブジェクトをそのまま公開してしまうと、論理的に可変なオブジェクトになってしまいます。

public final class User {
    private final String name;
    private final List<String> roles;

    public User(String name, List<String> roles) {
        this.name = name;
        this.roles = List.copyOf(roles);
    }

    public List<String> getRoles() {
        return roles;
    }
}
AI コーチを使ってこの質問に答えてみる

参照、文字列、比較

13Javaにおける引数の受け渡しは値渡しですか、それとも参照渡しですか?

Javaは常に引数を値渡しします。プリミティブ型の場合、値そのものがコピーされます。オブジェクトの場合、参照の値がコピーされます。メソッド内ではコピーされた参照を通じてオブジェクトの状態を変更できますが、呼び出し元の参照自体を別のオブジェクトに置き換えることはできません。この違いにより、フィールドの変更は呼び出し元にも反映されますが、仮引数に新しいオブジェクトを代入しても呼び出し元には影響しません。

void change(int value) {
    value = 100;
}

void rename(User user) {
    user.setName("Kate");
}

void replace(User user) {
    user = new User("New");
}
AI コーチを使ってこの質問に答えてみる

14== と equals() の違いは何ですか?

プリミティブ型において、== は値を比較します。オブジェクト参照の場合、== は両方の参照が同一のオブジェクトを指しているかどうかを比較します。equals() はメソッドであり、クラスで適切に実装されていれば論理的な等価性を比較します。String、Integer、LocalDate などの多くの値クラスでは equals がオーバーライドされています。独自のドメインオブジェクトの場合、equals は意図された同一性または値としての等価性セマンティクスを反映するように実装する必要があります。

int a = 10;
int b = 10;
System.out.println(a == b); // true

String first = new String("java");
String second = new String("java");

System.out.println(first == second);      // false
System.out.println(first.equals(second)); // true
AI コーチを使ってこの質問に答えてみる

15equals/hashCodeの一般契約とは何ですか?

`equals` に従って2つのオブジェクトが等価である場合、それらは同じ `hashCode` を返さなければなりません。その逆は必須ではなく、異なるオブジェクトであっても同じ `hashCode` を持つことがあります。`equals` は反射性(reflexive)、対称性(symmetric)、推移性(transitive)、一貫性(consistent)を満たし、`null` に対しては `false` を返さなければなりません。この契約に違反すると、`HashMap` や `HashSet` などのハッシュベースのコレクションの動作が壊れ、オブジェクトがコレクション内に存在しているにもかかわらず検索できなくなるなどの不具合が生じます。

@Override
public boolean equals(Object object) {
    if (this == object) return true;
    if (!(object instanceof User other)) return false;
    return id == other.id;
}

@Override
public int hashCode() {
    return Long.hashCode(id);
}
AI コーチを使ってこの質問に答えてみる

16なぜStringは不変(immutable)なのですか?

Stringが不変(immutable)である理由は、安全性、予測可能性、およびパフォーマンスの向上にあります。不変性により、文字列のインターン化(String Interning)、スレッド間での安全な共有、信頼性の高いhashCodeのキャッシュ、さらにはファイルパス、URL、クラス名、セキュリティ上重要な値などでの安全な利用が可能になります。concat、replace、substringなどの操作は、既存の文字列を変更するのではなく新しい文字列を生成します。Java 9以降、Stringは内部的にコンパクト文字列表現として`byte[]`を使用する場合がありますが、公開されている不変性の規約(コントラクト)は変わりません。

String value = "Java";
value.toUpperCase();

System.out.println(value); // Java

String a = "java";
String b = "java";

System.out.println(a == b); // often true due to interning
AI コーチを使ってこの質問に答えてみる

17String、StringBuilder、StringBufferの違いは何ですか?

Stringは変更不能(イミュータブル)であるため、コンパイラが単純なケースを最適化できるものの、ループ内で文字列結合を繰り返すと多くの中間オブジェクトが生成される可能性があります。StringBuilderは変更可能(ミュータブル)であり、単一スレッド内で文字列を構築する場合に適しています。StringBufferは似ていますが同期化(スレッドセーフ)されているため、通常は低速であり、主にレガシーなスレッドセーフAPIとの互換性のために利用されます。フォーマット処理にはString.formatやテンプレート形式のAPIの方が意図が明確になる場合がありますが、必ずしも高速であるとは限りません。

StringBuilder builder = new StringBuilder();

for (String item : items) {
    builder.append(item).append(',');
}

String result = builder.toString();

String message = "Hello, " + name;
AI コーチを使ってこの質問に答えてみる

ジェネリクスとコレクション

18ジェネリクスとは何ですか?また、型消去とは何ですか?

ジェネリクスを使用すると、`List<String>` のように型安全なコンテナやAPIを記述できます。コンパイル時にコンパイラが型をチェックし、必要に応じてキャストを挿入します。実行時にはほとんどのジェネリクス型情報が消去(型消去)されるため、`List<String>` と `List<Integer>` は実行時において同一のクラスを共有します。このため、`new T()` の直接作成、プリミティブ型の型引数としての使用、または `instanceof List<String>` による確実な型チェックを行うことはできません。

List<String> names = new ArrayList<>();
names.add("Alice");

List<? extends Number> producer;
List<? super Integer> consumer;
AI コーチを使ってこの質問に答えてみる

19Java Collections Frameworkとは何ですか?

Collections Frameworkは、オブジェクトのグループを保持および処理するためのインターフェイスと実装のセットです。主要なインターフェイスには `List`、`Set`、`Queue`、`Deque`、`Map` があります。実装には `ArrayList`、`LinkedList`、`HashSet`、`TreeSet`、`HashMap`、`TreeMap`、`PriorityQueue` などがあります。コレクションを選択する際は、アクセスパターン、順序付けの要件、一意性、検索の計算量、メモリオーバーヘッド、および並行性のニーズに基づいて判断します。

List<String> list = new ArrayList<>();
Set<Long> ids = new HashSet<>();
Map<Long, User> usersById = new HashMap<>();
Deque<String> queue = new ArrayDeque<>();
AI コーチを使ってこの質問に答えてみる

20HashMapはどのように動作しますか?

HashMapは、キーのハッシュ値に基づいて選択されたバケットにエントリを格納します。検索時にはハッシュ値を計算してバケットを特定し、equalsメソッドでキーを比較します。衝突はバケット内部で処理され、モダンなJavaでは一定の条件を満たすと長いリンクリストが平衡木へとツリー化されます。HashMapは1つのnullキーと複数のnull値を許容し、スレッドセーフではなく、負荷係数(load factor)の閾値を超えるとサイズが拡張されます。

Map<Long, User> users = new HashMap<>();
users.put(user.id(), user);

User found = users.get(user.id());
AI コーチを使ってこの質問に答えてみる

21HashMap、LinkedHashMap、TreeMap、ConcurrentHashMapの違いは何ですか?

HashMapは順序を保証しない高速なアクセスを提供し、スレッドセーフではありません。LinkedHashMapは挿入順またはアクセス順を保持し、LRU(Least Recently Used)のようなキャッシュに有用です。TreeMapは自然順序付けまたはComparatorによってキーをソートした状態に保ち、操作の計算量はO(log n)となります。ConcurrentHashMapはセグメント化または内部的な同期機構によって並行アクセス向けに設計されており、nullのキーや値を許可せず、高負荷環境ではsynchronizedMapよりも推奨されます。

map.computeIfAbsent(key, ignored -> loadValue());
AI コーチを使ってこの質問に答えてみる

22`ArrayList`と`LinkedList`の違いは何ですか?

`ArrayList`は配列を内部構造として持ちます。インデックス指定による高速なアクセス、コンパクトなメモリ配置、効率的な反復処理(イテレーション)を提供しますが、中間への要素挿入には要素のシフトが必要です。`LinkedList`は双方向連結リストです。ノードの位置がすでに分かっている場合の挿入コストは低いものの、インデックス指定によるアクセスはO(n)であり、メモリオーバーヘッドが大きく、キャッシュ局所性も劣ります。多くの実際のアプリケーションでは、一部の挿入処理が含まれていても`ArrayList`の方が高速に動作します。

AI コーチを使ってこの質問に答えてみる

23ComparableとComparatorの違いは何ですか?

ComparableはcompareToを通じてクラス内部で自然順序付けを定義します。Comparatorは外部から順序付けを定義する別個のオブジェクトまたはラムダ式です。Comparableは型に明確なデフォルトの順序が1つある場合に適しています。Comparatorは名前順、日付順、優先度順など、複数の順序が存在する場合に適しています。ソートされたセットやマップで使用する場合、compareとcompareToは一貫している必要があり、equalsと整合していることが望まれます。

class User implements Comparable<User> {
    private int age;

    @Override
    public int compareTo(User other) {
        return Integer.compare(age, other.age);
    }
}

Comparator<User> byName =
    Comparator.comparing(User::getName);

Comparator<User> byAgeThenName =
    Comparator.comparingInt(User::getAge)
              .thenComparing(User::getName);
AI コーチを使ってこの質問に答えてみる

例外とリソース

24検査例外(checked exception)と非検査例外(unchecked exception)の違いは何ですか?

検査例外はExceptionを継承しますがRuntimeExceptionは継承せず、コンパイラによって呼び出し元で処理するかメソッドシグネチャで宣言することが強制されます。非検査例外はRuntimeExceptionを継承し、メソッドシグネチャでの宣言は不要です。検査例外はAPIの境界で回復可能な状況に適しています。非検査例外はプログラミングエラー、不正な状態、契約違反に適しています。近年のサービス開発では、レイヤーを越えて過剰に伝播しコードを煩雑にするため、検査例外を過度に使用することを避ける傾向があります。

void load() throws IOException {
    // ...
}

throw new IllegalArgumentException("Invalid id");
AI コーチを使ってこの質問に答えてみる

25throw、throws、try-with-resourcesの違いは何ですか?

throwはその場で即座に例外をスローします。throwsはメソッドシグネチャで発生し得る例外を宣言します。try-with-resourcesはAutoCloseableなリソースを逆順で自動的にクローズします。メインブロックとcloseの両方で例外がスローされた場合、後者の例外は抑制された例外(suppressed exception)として保持されます。複数のリソースを扱う場合、手動でfinallyブロックを書くよりも安全で明瞭です。

throw new IllegalArgumentException("Invalid id");

void readFile() throws IOException {}

try (var reader = Files.newBufferedReader(path)) {
    return reader.readLine();
}
AI コーチを使ってこの質問に答えてみる

Stream API と関数型スタイル

26Stream APIとは何ですか?

Stream APIを使用すると、データシーケンスを宣言的に処理できます。`filter`、`map`、`sorted` などの中間操作は遅延評価され、`toList`、`collect`、`reduce`、`forEach` などの終端操作によって実行がトリガーされます。ストリームはコレクションではなく、通常は一度しか消費されず、副作用のない純粋関数と組み合わせることで最も効果を発揮します。`parallelStream` は高速な実行を保証するものではなく、パフォーマンスの低下や正確性の問題を引き起こす可能性があります。

List<String> names = users.stream()
    .filter(User::isActive)
    .sorted(Comparator.comparing(User::getName))
    .map(User::getName)
    .toList();
AI コーチを使ってこの質問に答えてみる

27Optionalとは何ですか?また、どのような場合に使用すべきですか?

`Optional<T>` は値が存在しない可能性を明示的に表す型であり、メソッドの戻り値の型として適しています。有用なメソッドには `map`、`flatMap`、`filter`、`orElse`、`orElseGet`、`orElseThrow` などがあります。`orElse` は引数を先行評価しますが、`orElseGet` に渡された Supplier は値が存在しない場合にのみ実行されます。特段の理由がない限り、`Optional` をエンティティのフィールド、メソッドの引数、またはシリアライズ対象の DTO (Data Transfer Object) に使用することは通常推奨されません。

Optional<User> user = repository.findById(id);

String name = user
    .map(User::getName)
    .orElse("Unknown");

optional.orElse(expensiveOperation());
optional.orElseGet(() -> expensiveOperation());
AI コーチを使ってこの質問に答えてみる

並行処理

28synchronized、volatile、およびAtomicクラスは何を保証しますか?

`synchronized` は相互排他と happens-before 関係を提供します。`volatile` は最新の書き込みに対する可視性を保証し命令の並べ替えを抑制しますが、複合操作をアトミックにするわけではありません。Atomic クラスは `incrementAndGet` などの個別の操作をアトミックに提供します。複数の関連する変更にはロックまたは `synchronized` を、単純な状態フラグには `volatile` を、競合が発生するカウンターには `AtomicInteger` や `LongAdder` を使用します。

synchronized (lock) {
    counter++;
}

private volatile boolean running = true;

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
AI コーチを使ってこの質問に答えてみる

29レースコンディション(競合状態)、デッドロック、およびスレッドセーフティとは何ですか?

レースコンディションは、予測できないスレッドの実行順序に処理結果が依存する場合に発生します。デッドロックは、複数のスレッドが互いのリソースを解放待ちし合って永久に停止することで発生します。スレッドセーフティを確保するには、共有される変更可能な状態を減らし、一貫したロック順序を維持し、クリティカルセクションを短縮し、ロック取得中に外部コードを呼び出すのを避け、変更不能なオブジェクト、並行コレクション、タイムアウト、またはメッセージパッシングを活用します。スレッドセーフティは、状態アクセス手順全体の設計によって成り立つ特性です。

AI コーチを使ってこの質問に答えてみる

30ExecutorService、CompletableFuture、および仮想スレッドの違いは何ですか?

`ExecutorService` はスレッドプールとタスクの実行を管理します。`CompletableFuture` は非同期処理の合成を容易にします。仮想スレッドは JVM (Java Virtual Machine) 上の軽量スレッドであり、大量のブロッキング I/O タスクを扱うのに適しています。スレッド単位のリクエストモデルを簡素化しますが、CPUバウンドな処理を高速化したり、データベースや外部リソースの制限を解消したり、変更可能な状態を自動的にスレッドセーフにしたりするわけではありません。

try (var executor = Executors.newFixedThreadPool(4)) {
    Future<Integer> future = executor.submit(() -> calculate());
    int result = future.get();
}

CompletableFuture<User> userFuture =
    CompletableFuture.supplyAsync(this::loadUser)
        .thenApply(this::enrichUser)
        .exceptionally(error -> fallbackUser());

try (var executor =
         Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> handleRequest());
}
AI コーチを使ってこの質問に答えてみる