ローカル変数のスコープの全体像
「スコープ」は、ローカル変数が「どこから見えて、どこまで使えるか」の範囲です。Java のローカル変数は「宣言されたブロック({ })の内側」でだけ有効で、ブロックを出た瞬間に見えなくなります。同名の再宣言や未初期化の使用はコンパイルで弾かれ、ラムダや匿名クラスに渡すには「実質的 final(再代入しない)」である必要があります。
ブロックスコープの基本
ブロックごとに見える/見えないが決まる
ローカル変数は宣言されたブロック(メソッド、if/else、for、while、try/catch など)内でのみ参照できます。ブロック外へは見えません。
void demo() {
int x = 10; // このメソッドブロック内で有効
if (x > 0) {
int y = 20; // if ブロック内でのみ有効
System.out.println(x + y); // OK
}
System.out.println(x); // OK
// System.out.println(y); // NG: y は見えない(コンパイルエラー)
}
Javaライフタイムとスコープの違い
- スコープ: 参照可能なコード範囲(静的)
- ライフタイム: 実行時に値が生きている期間(動的) ローカル変数はスコープがブロックに限定され、ライフタイムもその実行中に限られます。
名前衝突とシャドーイング
同一ブロック内での再宣言は不可
同じスコープ(見える範囲が重なる)で同名のローカル変数は宣言できません。入れ子でも外側の変数のスコープに重なるため、基本的に再宣言は不可です。
void bad() {
int a = 1;
// { int a = 2; } // NG: a が同スコープ内に既に存在
}
Java非重複スコープなら再使用可能
別のブロックでスコープが切れていれば、同名を再利用できます。
void ok() {
{ int i = 0; System.out.println(i); } // ここで i のスコープが終わる
{ int i = 1; System.out.println(i); } // 別ブロックなので OK
}
Javaフィールドとのシャドーイング
ローカル変数がクラスのフィールド名を隠すことがあります。区別したいときは this.field を使います。
class C {
int value = 10;
void f() {
int value = 99; // フィールドをシャドー
System.out.println(value); // 99(ローカル)
System.out.println(this.value); // 10(フィールド)
}
}
Javaループとスコープ
for のカウンタはループ内のみ
初期化で宣言した変数は、ループの本体内でのみ有効で、ループ後は見えません。
for (int i = 0; i < 3; i++) {
System.out.println(i);
}
// System.out.println(i); // NG: i はスコープ外
Java拡張 for(foreach)の変数もブロック限定
for (String name : names) {
System.out.println(name); // name はループ内でのみ有効
}
Javaループ内の再宣言と境界
- 宣言位置: ループ本体の
{ }内で宣言すれば、各反復で新しい変数が作られます。 - 境界の型: ループ変数を外へ持ち出す必要があるなら、外側で宣言してから使います。
try/catch とリソースのスコープ
try-with-resources のリソースは try の中だけ
リソース変数は try ブロックのスコープ内でのみ参照でき、ブロック終端で自動クローズされます。
import java.nio.file.*;
void read(Path p) throws java.io.IOException {
try (var br = java.nio.file.Files.newBufferedReader(p)) {
System.out.println(br.readLine());
}
// br はここでは見えない(スコープ外)
}
Javacatch の例外変数もブロック限定
try { /* ... */ }
catch (Exception e) {
System.err.println(e.getMessage()); // e は catch 内のみ
}
// e はスコープ外
Javaラムダ・匿名クラスと「実質的 final」
キャプチャできるのは再代入しない変数
ラムダや匿名クラスが外側のローカル変数を参照する場合、その変数は「実質的に final(以後再代入しない)」でなければなりません。
void g() {
int base = 10; // 実質的 final(再代入しなければ)
java.util.function.IntUnaryOperator f = x -> x + base; // OK
// base = 20; // NG: ラムダがキャプチャした後に再代入は不可
}
Javaパラメータも同様の制約
メソッド引数も、ラムダでキャプチャするなら「実質的 final」にしておくと安全です。
switch とパターン変数のスコープ
古典 switch の変数スコープ
switch 文内で宣言した変数のスコープは、宣言位置から switch の終わりまで。各 case ごとに独立させたいならブロック { } を作ってください。
switch (kind) {
case 1: { int v = 10; System.out.println(v); break; }
case 2: { int v = 20; System.out.println(v); break; }
}
Javainstanceof のパターン変数
パターンマッチの変数は、「判定が真である範囲」でのみ有効です。
Object obj = "hello";
if (obj instanceof String s) { // このブロックの中でだけ s が見える
System.out.println(s.toUpperCase());
}
// s はスコープ外
Java初期化(definite assignment)とすぐ役立つルール
未初期化の使用はコンパイルで弾かれる
ローカル変数は「使用までに必ず値が入っている」必要があります。if/else の両枝で代入しておかないと、コンパイルエラーになります。
int x;
if (flag) x = 1;
// System.out.println(x); // NG: 代入が保証されない
// 修正例
int y;
if (flag) y = 1; else y = 0;
System.out.println(y); // OK
Java再宣言よりスコープの縮小で管理する
大きなスコープに不要な変数を置かず、「必要な最小のブロック」に宣言します。名前の衝突と見通しの悪さを防げます。
よくあるつまずきと対策
スコープ外参照のコンパイルエラー
- 原因: 変数が見えない位置で参照している。
- 対策: 参照したい位置までスコープを広げる(外側で宣言)、または参照位置をスコープ内に移す。
同名変数の重複宣言
- 原因: 外側に同名が既にあり、内側で再宣言している。
- 対策: 名前を変える、ブロックを分ける、変数の寿命を短くする。
ラムダでの再代入禁止に引っかかる
- 原因: キャプチャ後に外側の変数へ再代入。
- 対策: 代入をやめる/配列やホルダ(AtomicInteger など)に値を入れて中身だけ更新する。
java.util.concurrent.atomic.AtomicInteger base = new java.util.concurrent.atomic.AtomicInteger(10);
java.util.function.IntUnaryOperator f = x -> x + base.get(); // キャプチャは参照、値は更新可能
base.set(20); // 後から更新しても OK
Java例題で身につける
例 1: ブロックごとの見える範囲
void scopeDemo() {
int total = 0;
{ // 小さな作業ブロック
int add = 5;
total += add;
}
// System.out.println(add); // NG: add はスコープ外
System.out.println(total); // 5
}
Java例 2: for 変数のスコープと再利用
for (int i = 0; i < 2; i++) System.out.println(i);
// int i = 100; // ループのスコープが終わった後なら宣言できる
Java例 3: try-with-resources のスコープ
import java.nio.file.*;
import java.io.*;
void printFirstLine(Path p) throws IOException {
try (var br = Files.newBufferedReader(p)) {
System.out.println(br.readLine());
}
// br はここでは参照不可
}
Java例 4: 実質的 final とラムダ
void h() {
int base = 10;
var f = (java.util.function.IntUnaryOperator) x -> x + base; // OK
// base = 20; // NG: 実質的 final を破る
System.out.println(f.applyAsInt(5)); // 15
}
Java例 5: switch の独立スコープ
int kind = 1;
switch (kind) {
case 1: { String label = "ONE"; System.out.println(label); break; }
case 2: { String label = "TWO"; System.out.println(label); break; }
}
Java仕上げのアドバイス(重要部分のまとめ)
ローカル変数は「宣言したブロックの内側だけで見える」。同名再宣言は重なるスコープでは不可、非重複なら再使用可。for のカウンタ・catch の例外・try 资源はそれぞれのブロック限定。ラムダ・匿名クラスで外側を参照するなら「実質的 final」を守る。未初期化の使用はコンパイルで弾かれるため、代入の保証を意識して書く——この型が身につけば、スコープによるバグ(見えない/衝突/未初期化)を根こそぎ防げます。
