Java | 基礎文法:ローカル変数のスコープ

Java Java
スポンサーリンク

ローカル変数のスコープの全体像

「スコープ」は、ローカル変数が「どこから見えて、どこまで使えるか」の範囲です。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 はここでは見えない(スコープ外)
}
Java

catch の例外変数もブロック限定

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; }
}
Java

instanceof のパターン変数

パターンマッチの変数は、「判定が真である範囲」でのみ有効です。

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」を守る。未初期化の使用はコンパイルで弾かれるため、代入の保証を意識して書く——この型が身につけば、スコープによるバグ(見えない/衝突/未初期化)を根こそぎ防げます。

タイトルとURLをコピーしました