コンテンツにスキップ

1-2. 演習問題


1-1. コンパイル方式の説明として正しいのはどれか。

  • A. コードを1行ずつ読みながらその場で実行する
  • B. 実行前にソースコード全体を機械語に変換する
  • C. OSが直接ソースコードを実行する

1-2. Javaが「Write Once, Run Anywhere」を実現できる理由はどれか。

  • A. Javaのコードは機械語で書かれているから
  • B. JVMがバイトコードを各OSの機械語に変換して実行するから
  • C. Javaは全てのOSに対してコンパイルするから

1-3. スタックとヒープについて正しい説明はどれか。

  • A. ヒープは関数のローカル変数を保存し、スタックはオブジェクトを保存する
  • B. スタックは関数のローカル変数を保存し、ヒープは動的に生成されるオブジェクトを保存する
  • C. スタックとヒープは全く同じ用途で使われる

以下の文章の空欄を埋めてください。

  1. CPUが直接実行できるコードのことを (   ) という。
  2. 使われなくなったヒープ上のメモリを自動的に解放する仕組みを (   ) という。
  3. プロセスの中でさらに並行して動く実行単位を (   ) という。
  4. 関数が無限に自分自身を呼び出し続けることでスタックが溢れることを (   ) という。
解答欄

問題3:コードの実行順序を考えよう

Section titled “問題3:コードの実行順序を考えよう”

以下のJavaScriptコードを見て、出力される順番を答えてください。

console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
console.log("C");

出力される順番:、_____

また、なぜその順番になるのかを説明してください。

解答欄

4-1. コンパイル方式とインタプリタ方式のそれぞれの長所を1つずつ挙げてください。

解答欄

4-2. プロセスとスレッドの違いを説明してください。

解答欄

4-3. 以下のコードを見て、変数 xobj がそれぞれスタックとヒープのどちらに保存されるか答えてください(Java を想定)。

public void example() {
int x = 42;
String obj = new String("hello");
}
解答欄

あるプログラムを実行したところ、以下のエラーが発生した。

Exception in thread "main" java.lang.StackOverflowError

このエラーが発生した原因として考えられることを説明し、どのように修正すればよいかを述べてください。

解答欄

実際に Node.js を使って、実行順序とプロセスの概念を観察してみましょう。

① 非同期の実行順を確かめる

次のコマンドを実行し、A C B の順に表示されることを確認する。

Terminal window
node -e "console.log('A'); setTimeout(() => console.log('B'), 0); console.log('C')"

② 関数呼び出しの順番を確かめる

次のコマンドを実行し、main startgreetmain end の順に表示されることを確認する。

Terminal window
node -e "function greet(){ console.log('greet'); } function main(){ console.log('main start'); greet(); console.log('main end'); } main();"

③ プロセス ID を確かめる

次のコマンドを2回実行し、毎回違う数字が表示されることを確認する。

Terminal window
node -e "console.log(process.pid)"

問題1の解答(クリックで開く)

1-1. 正解:B. 実行前にソースコード全体を機械語に変換する

解説

  • A はインタプリタ方式の説明。PythonやRubyなどが代表例。
  • B がコンパイル方式。C・C++・Rustなどが代表例。事前に変換するため実行は速いが、コンパイル作業が必要。
  • C は誤り。OSはソースコードを直接実行できない。コンパイルまたはインタプリタを介する必要がある。

コンパイル vs インタプリタの判断基準

コンパイル方式: ソース → コンパイル → 実行ファイル → 実行
↑ここに時間がかかる ↑実行は速い
インタプリタ方式: ソース → 実行(行ごとに解釈しながら)
↑すぐ実行できる ↑実行はやや遅い

1-2. 正解:B. JVMがバイトコードを各OSの機械語に変換して実行するから

解説

  • A は誤り。Javaのソースコードは機械語ではなく、人間が読めるテキスト形式。
  • B が正しい。JavaはコンパイルするとOSに依存しない「バイトコード(.class)」が生成される。JVMがバイトコードをそれぞれのOSの機械語に変換して実行するため、1つのコードでWindowsでもMacでもLinuxでも動く。
  • C は誤り。JavaはすべてのOS向けに個別にコンパイルするわけではない。
[通常のコンパイル言語(C言語など)]
ソースコード → Windows用実行ファイル ← Windows専用
→ Linux用実行ファイル ← Linux専用(別々に必要)
[Java]
ソースコード → バイトコード(.class) → JVM(Windows) → 実行
↑1つだけでOK → JVM(Linux) → 実行
→ JVM(macOS) → 実行

1-3. 正解:B. スタックは関数のローカル変数を保存し、ヒープは動的に生成されるオブジェクトを保存する

解説

  • A は逆の説明のため誤り。
  • B が正しい。スタックは後入れ先出し(LIFO)構造で関数の呼び出し情報やローカル変数を管理する。ヒープは new などで動的に生成されるオブジェクトを保存する。
  • C は誤り。スタックとヒープは管理方法・用途・サイズが全く異なる。
比較スタックヒープ
管理自動(関数の出入りで自動確保・解放)プログラムまたはGCが管理
速度高速スタックより遅い
サイズ小さい(数MB)大きい(GBオーダーも可)
用途ローカル変数、関数の呼び出し情報オブジェクト、動的なデータ
問題2の解答(クリックで開く)
  1. 機械語(マシン語)

CPUが直接解釈・実行できる0と1の命令列。アーキテクチャ(CPUの種類)によって異なる。たとえばIntelのCPU用の機械語はARMのCPUでは動かない。ソースコードを書くのは人間のためであり、実際にCPUが動かす命令は機械語に変換されたもの。

  1. ガベージコレクション(GC: Garbage Collection)

ヒープ上に確保されたオブジェクトのうち、どこからも参照されなくなったものを「不要なゴミ」と判断して自動解放する仕組み。JavaやJavaScript、PythonなどはGCを持つ。 CやC++はGCがないため、開発者が free()delete で手動解放する必要がある。解放し忘れると「メモリリーク」が発生し、長時間動作するサーバーで徐々にメモリが枯渇する問題につながる。

  1. スレッド

プロセスの中でさらに並行して動く処理の単位。同じプロセス内のスレッドはメモリを共有するため、通信コストが低くデータ共有が容易な反面、複数スレッドが同じデータを同時に書き換える「競合状態(Race Condition)」に注意が必要。

  1. スタックオーバーフロー(StackOverflow)

関数が自分自身を終了条件なしに呼び続けると、スタックにフレームが積み上がり続けてスタック領域の上限を超えた状態。プログラムはクラッシュする。エラーメッセージは Java では java.lang.StackOverflowError、Cでは「Segmentation fault」など。

問題3の解答(クリックで開く)

出力順:A → C → B

詳細解説:JavaScriptのイベントループ

JavaScriptは「シングルスレッド」で動くため、同時に複数の処理を並列実行することはできない。しかし「イベントループ」という仕組みで非同期処理を実現している。

[コールスタック] [タスクキュー]
1. console.log("A") 実行 → "A" が出力される
2. setTimeout(fn, 0) 呼び出し
→ コールバック fn はタスクキューに追加される
(0ms後でも「後で実行」のキューに入る)
3. console.log("C") 実行 → "C" が出力される
4. コールスタックが空になる
5. イベントループがタスクキューから fn を取り出して実行
→ console.log("B") が実行される → "B" が出力される

重要なポイントsetTimeout(..., 0) は「即時実行」ではなく「現在の処理が全部終わった後に実行」を意味する。0ミリ秒という指定は「最低でも0ms後にキューに追加する」という意味であり、実行のタイミングはイベントループが決める。

問題4の解答例(クリックで開く)

4-1. コンパイルとインタプリタの長所

コンパイル方式の長所:実行速度が速い ソースコードは実行前に機械語へ変換済みなので、実行時に変換コストがかからない。C++で書かれたゲームエンジンや、Rustで書かれたシステムソフトウェアがコンパイル方式で高速な理由はここにある。

インタプリタ方式の長所:すぐに実行でき、開発サイクルが短い コンパイル手順が不要なので、コードを書いてすぐ実行して結果を確認できる。PythonやJavaScriptがデータ分析・Webフロントエンドで広く使われる理由の一つ。また対話的な実行(REPLなど)にも向いている。


4-2. プロセスとスレッドの違い

プロセス:OSが管理するプログラムの実行単位。それぞれ独立したメモリ空間を持つ。

プロセスA(Chrome) プロセスB(VSCode)
┌────────────────┐ ┌────────────────┐
│ メモリ空間A │ │ メモリ空間B │
└────────────────┘ └────────────────┘
互いにアクセスできない(OSが隔離)

スレッド:プロセスの中で並行して動く処理の単位。同じメモリ空間を共有する。

プロセスA(Chrome)
┌───────────────────────────────┐
│ メモリ空間A(共有) │
│ スレッド1(タブ描画) │
│ スレッド2(ネットワーク処理)│
│ スレッド3(JS実行) │
└───────────────────────────────┘
スレッド間はメモリを共有できる
ただし競合状態(Race Condition)に注意が必要

4-3. スタックとヒープの保存先

public void example() {
int x = 42; // (1)
String obj = new String("hello"); // (2)
}

(1) int x = 42:スタック int はJavaのプリミティブ型(基本型)。ローカル変数はスタックに直接値が積まれる。example() が終了すると自動的に消える。

(2) String obj = new String("hello"):ヒープ(オブジェクト本体)とスタック(参照) new で生成したオブジェクト本体(“hello” というデータ)はヒープに確保される。変数 obj はそのオブジェクトの「住所(参照)」を持つだけで、参照自体はスタックに積まれる。

スタック ヒープ
┌───────────┐ ┌──────────────┐
│ x = 42 │ │ "hello" │
│ obj ─────┼──────▶│ (Stringオブジェクト) │
└───────────┘ └──────────────┘
↑関数終了時に消える ↑GCが回収するまで残る
問題5の解答例(クリックで開く)

StackOverflowError の原因と修正

原因:終了条件のない再帰呼び出し

以下のような再帰関数が典型例。

// 問題のあるコード(無限再帰)
public int countDown(int n) {
return countDown(n - 1); // 終了条件がない!
}

関数を呼ぶたびにスタックにフレームが積まれ続け、スタック領域の上限を超えるとエラーになる。

スタック
┌──────────────────┐
│ countDown(-999) │
│ countDown(-998) │
│ ... │
│ countDown(0) │
│ countDown(1) │ ← ここから積み上がり始める
└──────────────────┘ ← スタック上限に達してクラッシュ

修正方法①:正しい終了条件(ベースケース)を追加する

public int countDown(int n) {
if (n <= 0) return 0; // ← ベースケース(終了条件)
return countDown(n - 1);
}

修正方法②:再帰をループに書き換える

public int countDown(int n) {
while (n > 0) {
n--; // スタックを消費しない
}
return 0;
}

ループはスタックを消費しないため、スタックオーバーフローは発生しない。非常に深い再帰が必要な場合はループへの書き換えを検討する。

問題6の解答例(クリックで開く)

① 非同期の実行順を確かめる

Terminal window
$ node -e "console.log('A'); setTimeout(() => console.log('B'), 0); console.log('C')"
A
C
B

setTimeout(..., 0) は即時実行ではなく、「現在の処理が終わったあとで実行待ちキューに回す」という意味である。 そのため、まず同期的に AC が出て、そのあとで B が出る。


② 関数呼び出しの順番を確かめる

Terminal window
$ node -e "function greet(){ console.log('greet'); } function main(){ console.log('main start'); greet(); console.log('main end'); } main();"
main start
greet
main end

main() の実行中に greet() が呼ばれると、コールスタックに greet のフレームが積まれて先に処理される。 greet() が終わると、元の main() に戻って main end が表示される。 これがスタックの「後から入ったものが先に終わる」という動きである。


③ プロセス ID を確かめる

Terminal window
$ node -e "console.log(process.pid)"
12345
$ node -e "console.log(process.pid)"
12398

数字そのものは毎回異なるが、実行のたびに別の PID が割り当てられる ことが重要である。 これは、node コマンドを1回実行するたびに新しいプロセスが作られ、終了するとそのプロセスも終わるためである。