- JAVA
Java変数のスコープとは?初期値一覧と初期化エラーの直し方
Javaの変数のスコープとは、その変数を名前で使える範囲(有効範囲)のことです。基本のルールは「宣言した場所から、その宣言を囲む一番内側の波かっこ { } の終わりまで」。そして、フィールドには自動で初期値(0・false・null)が入りますが、ローカル変数には入りません。
この2つを押さえると、入門でよく出会う「variable might not have been initialized(変数は初期化されていない可能性があります)」と「cannot find symbol(シンボルを見つけられません)」の理由がすっきりわかります。この記事では、変数の種類ごとの有効範囲・初期値・未初期化時の挙動を1枚の早見表にまとめ、Java 21(OpenJDK 21.0.10)で実際に出したエラー全文と直し方をセットで示します。後半では、同名変数のシャドーイングと this、Java 10 以降の var、ラムダ式の「実質的 final」まで扱います。
変数のスコープと初期値の早見表
変数の種類別:有効範囲・初期値・未初期化時の挙動
| 変数の種類 | 宣言する場所 | 有効範囲(スコープ) | 自動の初期値 | 初期化しないまま使うと | 値が残る期間 |
|---|---|---|---|---|---|
| ローカル変数 | メソッドの中 | 宣言した行から、そのブロックの } まで |
なし | コンパイルエラー(might not have been initialized) | メソッドの呼び出し1回分 |
| ブロック内変数(for・if・try の中) | { } の中、for の ( ) の中 |
そのブロック(for 文)の中だけ | なし | コンパイルエラー。ブロックの外で使うと cannot find symbol | ブロックを抜けるまで |
| 引数(パラメータ) | メソッドの ( ) の中 |
そのメソッド全体 | 呼び出し側が渡した値 | 渡さないと呼び出し自体がコンパイルエラー | メソッドの呼び出し1回分 |
| インスタンスフィールド | クラスの直下(static なし) | クラス全体(static メソッドからは直接使えない) | あり(0・false・null) | エラーにならず初期値が使われる。参照型は null なので、メソッドを呼ぶと NullPointerException | そのインスタンスがある間 |
| static フィールド | クラスの直下(static あり) | クラス全体 | あり(0・false・null) | 同上 | クラスが読み込まれている間(通常はプログラム終了まで) |
型別の初期値一覧(フィールドと配列の要素)
| 型 | 初期値 | 実行結果での表示 |
|---|---|---|
| byte / short / int / long | 0 | 0 |
| float / double | 0.0 | 0.0 |
| char | ヌル文字(文字コード 0) | 画面には見えない文字(int に変換すると 0) |
| boolean | false | false |
| String、配列、Integer などの参照型 | null | null |
この初期値が入るのは、フィールド(インスタンス・static)と配列の要素だけです。ローカル変数には入りません。
エラー文から探す
| エラー文(英語/日本語) | よくある原因 | 直し方 |
|---|---|---|
variable x might not have been initialized/変数xは初期化されていない可能性があります |
ローカル変数に値を入れる前に使った。if の片方だけで代入した | 宣言と同時に初期値を入れる。else でも代入する |
cannot find symbol/シンボルを見つけられません |
ブロックの中で宣言した変数を外で使った。つづりが違う | 宣言をブロックの外(手前)に出す |
variable x is already defined in method .../変数 xはすでにメソッド ...で定義されています |
同じメソッドの中で同じ名前を2回宣言した | 名前を変える。型を付けずに代入だけにする |
non-static variable x cannot be referenced from a static context |
static メソッドからインスタンスフィールドを直接使った | インスタンスを作って使う |
local variables referenced from a lambda expression must be final or effectively final |
ラムダ式の中から、値を書き換えるローカル変数を使った | Stream の集計や for 文に置き換える |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java変数のスコープとは?初期値一覧と初期化エラーの直し方」
スコープの基本ルールを確認する
スコープは「波かっこ { } で作られる箱」で考えるとわかりやすくなります。内側の箱からは外側の変数が見えますが、外側の箱からは内側の変数が見えません。
class Lifetime { ← クラスの箱
static int staticCalls; … クラス全体で使える(static フィールド)
int instanceCalls; … クラス全体で使える(インスタンスフィールド)
void call(int step) { ← メソッドの箱(引数 step はここから下で使える)
int localCalls = 0; … この行から、メソッドの } まで使える
for (int i = 0; i < 3; i++) { ← for の箱(i はこの for の中だけ)
int doubled = i * 2; … この行から、for の } まで使える
}
// ここでは i も doubled も使えない
}
}
値が残る期間の違いを実行結果で見る
スコープ(名前が使える範囲)と合わせて、「値がいつまで残るか」も種類によって違います。3種類の変数を同じメソッドで数えてみます。
// Lifetime.java
public class Lifetime {
static int staticCalls;
int instanceCalls;
void call() {
int localCalls = 0;
localCalls++;
instanceCalls++;
staticCalls++;
System.out.println("local=" + localCalls + " instance=" + instanceCalls + " static=" + staticCalls);
}
public static void main(String[] args) {
Lifetime a = new Lifetime();
Lifetime b = new Lifetime();
a.call();
a.call();
b.call();
}
}
javac Lifetime.java
java Lifetime
実行結果(Java 21.0.10):
local=1 instance=1 static=1
local=1 instance=2 static=2
local=1 instance=1 static=3
| 変数 | 3回の結果 | 理由 |
|---|---|---|
| ローカル変数 localCalls | 1 → 1 → 1 | 呼び出すたびに新しく作られ、メソッドを抜けると消える |
| インスタンスフィールド instanceCalls | 1 → 2 → 1 | インスタンスごとに1つ。a は2回、b は1回 |
| static フィールド staticCalls | 1 → 2 → 3 | クラスに1つだけ。全インスタンスで共有 |
型別の初期値を実行して確かめる
フィールドと配列の要素に入る初期値を、全部の基本型で表示します。
// Defaults.java
public class Defaults {
byte b; short s; int i; long l; float f; double d; char c; boolean z;
String str; int[] arr; Integer boxed;
static int staticCount; static String staticName;
public static void main(String[] args) {
Defaults x = new Defaults();
System.out.println("byte = " + x.b);
System.out.println("short = " + x.s);
System.out.println("int = " + x.i);
System.out.println("long = " + x.l);
System.out.println("float = " + x.f);
System.out.println("double = " + x.d);
System.out.println("char = [" + x.c + "] (int値 " + (int) x.c + ")");
System.out.println("boolean = " + x.z);
System.out.println("String = " + x.str);
System.out.println("int[] = " + x.arr);
System.out.println("Integer = " + x.boxed);
System.out.println("static int = " + staticCount);
System.out.println("static String = " + staticName);
int[] nums = new int[3];
String[] names = new String[2];
boolean[] flags = new boolean[2];
double[] rates = new double[2];
System.out.println("new int[3] = " + java.util.Arrays.toString(nums));
System.out.println("new String[2] = " + java.util.Arrays.toString(names));
System.out.println("new boolean[2] = " + java.util.Arrays.toString(flags));
System.out.println("new double[2] = " + java.util.Arrays.toString(rates));
}
}
実行結果:
byte = 0
short = 0
int = 0
long = 0
float = 0.0
double = 0.0
char = [ ] (int値 0)
boolean = false
String = null
int[] = null
Integer = null
static int = 0
static String = null
new int[3] = [0, 0, 0]
new String[2] = [null, null]
new boolean[2] = [false, false]
new double[2] = [0.0, 0.0]
(char の [ ] の中は、実際には空白ではなく「ヌル文字」という見えない文字です。int に変換した値が 0 であることで確認できます。)
読み取れるポイントは3つです。
- int と Integer は初期値が違います。int は 0、Integer は参照型なので null です。
- 配列の変数そのもの(
int[] arr)は null、new int[3]で作った配列の要素は 0 です。「配列がまだない」のか「中身が 0」なのかを区別します。 - ローカル変数として宣言した配列でも、new した時点で要素には初期値が入ります。初期値が入らないのは「ローカル変数そのもの」です。
今回 Java 言語仕様(Java SE 21 版)の 4.12.5「Initial Values of Variables」のページを開いて確認したところ、クラス変数・インスタンス変数・配列の要素は作成時にデフォルト値で初期化されること、そして「すべてのローカル変数は、値が参照される前に確実に代入されていなければならない」ことが定められています。上の実行結果は、この仕様どおりの動きです。
フィールドの null をそのまま使うとどうなるか
フィールドは初期化しなくてもコンパイルが通りますが、参照型は null のままです。
// FieldNull.java
public class FieldNull {
private String title;
private int[] scores;
private int views;
public static void main(String[] args) {
FieldNull article = new FieldNull();
System.out.println(article.views + 1);
System.out.println(article.title.length());
}
}
javac -g FieldNull.java
java FieldNull
1
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "article.title" is null
at FieldNull.main(FieldNull.java:9)
int の views は 0 から計算できましたが、String の title は null のため、メソッドを呼んだところで実行時に NullPointerException になりました。
| 変数の種類 | 初期化を忘れたときに気づくタイミング |
|---|---|
| ローカル変数 | コンパイル時(エラーで教えてくれる) |
| フィールド(基本型) | 気づきにくい(0 や false のまま動く) |
| フィールド(参照型) | 実行時(NullPointerException) |
フィールドは、宣言と同時に値を入れるか、コンストラクタで必ず代入しておくと、あとで読む人にも意図が伝わります(例:private String title = "";)。
「variable might not have been initialized」を再現して直す
パターン1:値を入れずに使った
// NotInit.java
public class NotInit {
public static void main(String[] args) {
int total;
System.out.println(total);
}
}
javac NotInit.java
エラー全文:
NotInit.java:4: error: variable total might not have been initialized
System.out.println(total);
^
1 error
日本語表示の環境では、同じエラーが次のように出ます。
NotInit.java:4: エラー: 変数totalは初期化されていない可能性があります
直し方:宣言と同時に初期値を入れます。
int total = 0;
System.out.println(total);
パターン2:ifの片方だけで代入した
「代入しているのにエラーになる」ときは、ほとんどがこの形です。
// NotInitIf.java
public class NotInitIf {
public static void main(String[] args) {
int stock = args.length;
String label;
if (stock > 0) {
label = "在庫あり";
}
System.out.println(label);
}
}
NotInitIf.java:8: error: variable label might not have been initialized
System.out.println(label);
^
1 error
コンパイラは「if の条件が false だった場合、label には何も入っていない」と判断します。エラー文の「might(可能性があります)」は、「代入されない経路が1つでもある」 という意味です。
直し方:すべての経路で代入します。
// NotInitIfFixed.java
public class NotInitIfFixed {
public static void main(String[] args) {
int stock = args.length;
String label;
if (stock > 0) {
label = "在庫あり";
} else {
label = "在庫なし";
}
System.out.println(label);
}
}
在庫なし
| 直し方 | コード | 向いている場面 |
|---|---|---|
| else でも代入する | 上の例 | 分岐ごとに値が決まるとき |
| 宣言時に初期値を入れる | String label = "在庫なし"; |
「基本の値」があるとき |
| 三項演算子で1行にする | String label = stock > 0 ? "在庫あり" : "在庫なし"; |
2択のとき |
パターン3:ループ・try・配列
同じエラーになる形を3つまとめて確認します。
// NotInitLoop.java
public class NotInitLoop {
public static void main(String[] args) {
int sum;
for (int i = 1; i <= 3; i++) {
sum += i;
}
String name;
try {
name = args[0];
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("引数がありません");
}
System.out.println(name);
int[] scores;
scores[0] = 10;
}
}
NotInitLoop.java:5: error: variable sum might not have been initialized
sum += i;
^
NotInitLoop.java:13: error: variable name might not have been initialized
System.out.println(name);
^
NotInitLoop.java:15: error: variable scores might not have been initialized
scores[0] = 10;
^
3 errors
| 行 | なぜエラーか | 直し方 |
|---|---|---|
sum += i; |
sum = sum + i と同じで、最初の1回目に「まだ値のない sum」を読む |
int sum = 0; |
System.out.println(name); |
try の途中で例外が出ると、name に代入されないまま catch を通って進む | String name = ""; と初期値を入れる。または catch の中でも代入する |
scores[0] = 10; |
配列の変数 scores に、配列そのものがまだ入っていない | int[] scores = new int[3]; |
final フィールドも同じエラーになる
フィールドには自動で初期値が入りますが、final を付けたフィールドは例外で、自分で必ず1回代入する必要があります。
// FinalField.java
public class FinalField {
private final int id;
private final String name;
FinalField(int id) {
this.id = id;
}
}
FinalField.java:7: error: variable name might not have been initialized
}
^
1 error
エラーの位置がコンストラクタの閉じかっこ(7行目)になっているのは、「コンストラクタが終わる時点で name に代入されていない」という意味です。コンストラクタの中で this.name = "未設定"; のように代入すると解消します。
「cannot find symbol」を再現して直す
cannot find symbol は「その名前のものが、この場所からは見つかりません」という意味です。スコープの外で変数を使ったときに出ます。
// BlockScope.java
public class BlockScope {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
int doubled = i * 2;
}
System.out.println(i);
System.out.println(doubled);
if (args.length == 0) {
String message = "引数なし";
}
System.out.println(message);
try {
int parsed = Integer.parseInt("42");
} catch (NumberFormatException e) {
System.out.println(e.getMessage());
}
System.out.println(parsed);
}
static void helper() {
System.out.println(args.length);
}
}
エラー全文:
BlockScope.java:6: error: cannot find symbol
System.out.println(i);
^
symbol: variable i
location: class BlockScope
BlockScope.java:7: error: cannot find symbol
System.out.println(doubled);
^
symbol: variable doubled
location: class BlockScope
BlockScope.java:12: error: cannot find symbol
System.out.println(message);
^
symbol: variable message
location: class BlockScope
BlockScope.java:19: error: cannot find symbol
System.out.println(parsed);
^
symbol: variable parsed
location: class BlockScope
BlockScope.java:23: error: cannot find symbol
System.out.println(args.length);
^
symbol: variable args
location: class BlockScope
5 errors
日本語表示では、各エラーの1行目が BlockScope.java:6: エラー: シンボルを見つけられません になります。
読み方は「symbol:」の行です。symbol: variable i は「i という変数が見つからない」という意味で、どの名前が問題かをそのまま教えてくれます。
| 行 | 見つからない変数 | 理由 |
|---|---|---|
| 6 | i | for の ( ) で宣言した変数は、その for 文の中だけ |
| 7 | doubled | for のブロックの中で宣言した |
| 12 | message | if のブロックの中で宣言した |
| 19 | parsed | try のブロックの中で宣言した |
| 23 | args | main の引数は main の中だけ。別のメソッド helper からは見えない |
直し方:宣言をブロックの手前に出す
ブロックのあとでも使いたい変数は、ブロックの手前で宣言します。
// BlockScopeFixed.java
public class BlockScopeFixed {
public static void main(String[] args) {
int last = 0;
for (int i = 0; i < 3; i++) {
last = i * 2;
}
System.out.println(last);
String message = "引数あり";
if (args.length == 0) {
message = "引数なし";
}
System.out.println(message);
int parsed = 0;
try {
parsed = Integer.parseInt("42");
} catch (NumberFormatException e) {
System.out.println(e.getMessage());
}
System.out.println(parsed);
}
}
実行結果:
4
引数なし
42
別のメソッドで値を使いたいときは、引数として渡します(例:static void helper(String[] args) として helper(args); と呼ぶ)。
cannot find symbol のそのほかの原因
スコープ以外でも、同じエラー文になることがあります。symbol: の行に書かれた名前を手がかりに確認します。
symbol: の行 |
よくある原因 |
|---|---|
variable xxx |
スコープの外で使った。変数名のつづり・大文字小文字が違う |
method xxx(...) |
メソッド名のつづり、引数の型や個数が違う |
class Xxx |
import を書いていない。クラス名のつづりが違う |
同じ名前の変数を宣言したときのルール
同じメソッドの中では、同じ名前を2回宣言できない
// Redeclare.java
public class Redeclare {
public static void main(String[] args) {
int count = 1;
for (int i = 0; i < 2; i++) {
int count = 5;
}
for (int i = 0; i < 2; i++) { }
for (int i = 0; i < 2; i++) { }
Runnable r = () -> { int count = 9; };
}
static void print(String name) {
String name = "x";
}
}
Redeclare.java:5: error: variable count is already defined in method main(String[])
int count = 5;
^
Redeclare.java:9: error: variable count is already defined in method main(String[])
Runnable r = () -> { int count = 9; };
^
Redeclare.java:12: error: variable name is already defined in method print(String)
String name = "x";
^
3 errors
| 行 | 結果 | 理由 |
|---|---|---|
| 5 | エラー | 外側のローカル変数 count がまだ有効な範囲で、内側のブロックに同じ名前を宣言した |
| 7・8 | エラーなし | 1つ目の for を抜けた時点で i のスコープが終わるので、次の for で同じ名前を使える |
| 9 | エラー | ラムダ式の中も、外側のメソッドと同じスコープの続きとして扱われる |
| 12 | エラー | 引数 name と同じ名前のローカル変数を宣言した |
「値を入れ直したいだけ」なら、型を書かずに count = 5; と代入します。型を付けると「新しい変数の宣言」になります。
フィールドと同じ名前はOK(シャドーイング)とthis
ローカル変数や引数は、フィールドと同じ名前で宣言できます。このとき、そのメソッドの中では名前だけ書くとローカル変数(引数)のほうを指し、フィールドは隠れます。これをシャドーイング(隠ぺい)と呼びます。隠れたフィールドは this.名前 で指定できます。
// Shadow.java
public class Shadow {
private String name = "フィールドの値";
private int count;
void setNameWrong(String name) {
name = name;
}
void setName(String name) {
this.name = name;
}
void show() {
String name = "ローカルの値";
System.out.println("name = " + name);
System.out.println("this.name = " + this.name);
}
void countUp() {
int count = 0;
count++;
}
public static void main(String[] args) {
Shadow s = new Shadow();
s.show();
s.setNameWrong("新しい名前");
System.out.println("setNameWrong 後: " + s.name);
s.setName("新しい名前");
System.out.println("setName 後 : " + s.name);
s.countUp();
s.countUp();
System.out.println("count = " + s.count);
}
}
実行結果:
name = ローカルの値
this.name = フィールドの値
setNameWrong 後: フィールドの値
setName 後 : 新しい名前
count = 0
| メソッド | 起きていること | 結果 |
|---|---|---|
| show | 名前だけ書くとローカル変数、this.name でフィールド |
2つの値を区別して表示できる |
| setNameWrong | name = name; は「引数を引数自身に代入」しているだけ |
フィールドは変わらない(コンパイルエラーにもならない) |
| setName | this.name = name; で、左がフィールド、右が引数 |
フィールドが更新される |
| countUp | int count = 0; で新しいローカル変数を作ってしまった |
フィールド count は 0 のまま |
setNameWrong と countUp は、コンパイルも実行もエラーなく通るのに、結果だけが期待と違う形です。セッターやコンストラクタでは this. を付ける、フィールドを更新したいメソッドでは型を付けて宣言しない、の2点を習慣にすると防げます。
| 組み合わせ | 同じ名前を宣言できるか |
|---|---|
| ローカル変数 と 外側のローカル変数・引数 | できない(already defined) |
| ローカル変数・引数 と フィールド | できる(フィールドが隠れる。this. で指定) |
| 別々のメソッドのローカル変数どうし | できる(スコープが重ならない) |
続けて書いた2つの for 文の int i |
できる(スコープが重ならない) |
staticメソッドからインスタンスフィールドは直接使えない
フィールドのスコープは「クラス全体」ですが、static メソッドからインスタンスフィールドを名前だけで使うことはできません。
// StaticCtx.java
public class StaticCtx {
int price = 100;
static int taxRate = 10;
public static void main(String[] args) {
System.out.println(taxRate);
System.out.println(price);
}
}
StaticCtx.java:7: error: non-static variable price cannot be referenced from a static context
System.out.println(price);
^
1 error
インスタンスフィールドは「どのインスタンスの price か」が決まらないと使えないためです。new StaticCtx().price のようにインスタンスを作ってから使うと解消します。
Java 10以降のvarとスコープ
var は、ローカル変数の型を右辺から推論してくれる書き方です(Java 10 で追加)。スコープのルールは、型を書いたローカル変数とまったく同じです。
// VarDemo.java
import java.util.ArrayList;
import java.util.HashMap;
public class VarDemo {
public static void main(String[] args) {
var count = 10;
var name = "task";
var list = new ArrayList<String>();
var map = new HashMap<String, Integer>();
list.add(name);
map.put(name, count);
for (var entry : map.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
System.out.println(((Object) count).getClass().getSimpleName() + " / " + list.getClass().getSimpleName());
}
}
task=10
Integer / ArrayList
var は「何でも入る変数」ではなく、宣言した時点で型が1つに決まります。そのため、初期値のない宣言はできません。
// VarErr2.java
public class VarErr2 {
public static void main(String[] args) {
var a;
var b = null;
var d = 10;
d = "ten";
}
}
VarErr2.java:3: error: cannot infer type for local variable a
var a;
^
(cannot use 'var' on variable without initializer)
VarErr2.java:4: error: cannot infer type for local variable b
var b = null;
^
(variable initializer is 'null')
VarErr2.java:6: error: incompatible types: String cannot be converted to int
d = "ten";
^
3 errors
フィールドや引数に var を書いた場合は、次のエラーになりました。
VarErr.java:2: error: 'var' is not allowed here
var field = 1;
^
VarErr.java:12: error: 'var' is not allowed here
static void print(var x) { }
^
| 書き方 | 結果 | 理由 |
|---|---|---|
var count = 10; |
OK(int) | 右辺から型が決まる |
var a; |
エラー | 初期値がないと型を推論できない |
var b = null; |
エラー | null だけでは型が決まらない |
d = "ten";(d は int) |
エラー | 一度決まった型は変わらない |
| フィールド・メソッドの引数に var | エラー | var が使えるのはローカル変数(for 文の変数を含む) |
var を使うと「宣言と同時に初期化」が必須になるため、might not have been initialized のエラーが起きにくくなるという利点もあります。
ラムダ式から使えるのは「実質的final」のローカル変数
ラムダ式の中からは外側のローカル変数を読めますが、その変数は「実質的 final(一度も値を変えていない)」である必要があります。
// LambdaErr.java
import java.util.List;
public class LambdaErr {
public static void main(String[] args) {
List<Integer> prices = List.of(100, 250, 80);
int sum = 0;
prices.forEach(p -> sum += p);
System.out.println(sum);
String prefix = "No.";
prefix = "ID-";
Runnable r = () -> System.out.println(prefix + 1);
}
}
LambdaErr.java:7: error: local variables referenced from a lambda expression must be final or effectively final
prices.forEach(p -> sum += p);
^
LambdaErr.java:12: error: local variables referenced from a lambda expression must be final or effectively final
Runnable r = () -> System.out.println(prefix + 1);
^
2 errors
日本語表示では「ラムダ式から参照されるローカル変数は、finalまたは事実上のfinalである必要があります」です。
| 行 | なぜエラーか |
|---|---|
| 7 | ラムダ式の中でローカル変数 sum を書き換えようとした |
| 12 | prefix はラムダ式の中では読むだけだが、その前に prefix = "ID-"; と代入し直している(=実質的 final ではない) |
「実質的 final」とは、final と書いていなくても、宣言のあと一度も代入し直していない変数のことです。ローカル変数はメソッドを抜けると消えますが、ラムダ式はそのあとで実行されることもあるため、ラムダ式には「その時点の値のコピー」が渡されます。途中で値が変わるとコピーと食い違うので、変わらない変数だけを使える決まりになっています。
直し方の4パターン(実行結果つき)
// LambdaFixed.java
import java.util.List;
public class LambdaFixed {
private int fieldSum;
void addAll(List<Integer> prices) {
prices.forEach(p -> fieldSum += p);
}
public static void main(String[] args) {
List<Integer> prices = List.of(100, 250, 80);
int sum = prices.stream().mapToInt(Integer::intValue).sum();
System.out.println("stream: " + sum);
int total = 0;
for (int p : prices) {
total += p;
}
System.out.println("for : " + total);
int[] box = {0};
prices.forEach(p -> box[0] += p);
System.out.println("配列 : " + box[0]);
LambdaFixed f = new LambdaFixed();
f.addAll(prices);
System.out.println("field : " + f.fieldSum);
String base = "No.";
final String prefix = base.replace("No.", "ID-");
Runnable r = () -> System.out.println(prefix + 1);
r.run();
}
}
stream: 430
for : 430
配列 : 430
field : 430
ID-1
| 直し方 | コード | おすすめ度 |
|---|---|---|
| Stream の集計メソッドを使う | prices.stream().mapToInt(Integer::intValue).sum() |
合計・件数・最大などは、まずこれ |
| 拡張 for 文にする | for (int p : prices) { total += p; } |
読みやすく、制限もない |
| 要素1つの配列に入れる | int[] box = {0}; |
動くが、意図が伝わりにくい。並列処理には向かない |
| フィールドにする | fieldSum += p |
フィールドは実質的 final の制限を受けない。状態が増える点に注意 |
| 値を作り直す変数は、新しい変数に入れる | final String prefix = ...; |
「読むだけ」のケースはこれで解消 |
この制限があるのはローカル変数と引数だけで、フィールドには当てはまりません。上の fieldSum がコンパイルできているのがその例です。
もう一歩:パターン変数とswitchのスコープ
Java 16 以降の instanceof のパターン変数は、「その型だと確定している範囲」がスコープになります。また、昔からの switch 文は、switch 全体が1つのブロックです。
// PatternScope.java
public class PatternScope {
public static void main(String[] args) {
Object value = "hello";
if (!(value instanceof String s)) {
return;
}
System.out.println(s.length());
switch (args.length) {
case 0:
int n = 1;
System.out.println(n);
break;
case 1:
n = 2;
System.out.println(n);
break;
}
}
}
5
1
| コード | スコープ |
|---|---|
if (!(value instanceof String s)) { return; } のあと |
if の外でも s を使える(ここに来た時点で String だと確定しているため) |
switch 文の case 0: で宣言した n |
switch の } まで有効。case 1: では宣言なしで代入できる(値は入っていないので、代入してから使う) |
case ごとに変数を分けたいときは、case 0: { ... } のように波かっこで囲むか、アロー形式(case 0 -> { ... })を使うと、それぞれが独立したブロックになります。
スコープを上手に使うコツ
| コツ | 理由 |
|---|---|
| 変数は、使う直前で宣言する | スコープが短くなり、どこで値が変わるかを追いやすい |
| 宣言と同時に初期値を入れる | might not have been initialized を防げる |
ループの変数は for の ( ) の中で宣言する |
ループの外に名前が残らず、次のループで同じ名前を使える |
フィールドと同じ名前の引数には this. を付ける |
name = name; の取り違えを防げる |
| フィールドは必要なものだけにする | スコープが広い変数ほど、値を変える場所が増える |
| 値を変えない変数には final を付ける | 読む人に「変わらない」と伝わり、ラムダ式からも使える |
よくある質問
Q. ローカル変数にだけ初期値が自動で入らないのはなぜですか?
A. Java の言語仕様で、ローカル変数は「使う前に確実に代入されていること」が求められているためです。値を入れ忘れたまま 0 として計算が進むより、コンパイルの段階で知らせるほうが、早く気づけます。フィールドは、インスタンスが作られるときにまとめて初期値で埋められます。
Q. 「初期化」と「代入」は違うのですか?
A. 初期化は「変数に最初の値を入れること」、代入は「変数に値を入れること全般」です。int x = 0; は宣言と初期化を同時に行い、int x; x = 0; は宣言のあとの最初の代入で初期化しています。どちらの書き方でも、使う前に値が入っていればエラーになりません。
Q. フィールドの初期値 0 や null に頼ってもよいですか?
A. 動作としては問題ありません。ただし、「意図して 0 にしている」のか「入れ忘れ」なのかが読む人に伝わりにくいので、意味のある初期値は private int count = 0; のように書いておくと親切です。参照型は null のままメソッドを呼ぶと NullPointerException になるため、空文字や空のリストで初期化しておく方法もあります。
Q. グローバル変数は Java にありますか?
A. Java には、どこにも属さないグローバル変数はありません。近いものは public static のフィールドで、クラス名.フィールド名 でどこからでも使えます。スコープが広い分、値が変わる場所を追いにくくなるので、定数(public static final)として使うのが一般的です。
まとめ
- スコープは「宣言した場所から、それを囲む一番内側の
}まで」。内側から外側は見えるが、外側から内側は見えない - フィールドと配列の要素には初期値(数値は 0、boolean は false、参照型は null)が入る。ローカル変数には入らない
- 「might not have been initialized」は、代入されない経路が1つでもあると出る。宣言時に初期値を入れるか、else でも代入する
- 「cannot find symbol」は
symbol:の行を読む。ブロックの中で宣言した変数を外で使うなら、宣言を手前に出す - 引数とフィールドが同じ名前のときは
this.name = name;。name = name;はエラーにならないのにフィールドが変わらない - var はローカル変数専用で、初期値が必須。スコープのルールは通常のローカル変数と同じ
- ラムダ式から使えるローカル変数は実質的 final だけ。合計などは Stream の集計か for 文で書く
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java変数のスコープとは?初期値一覧と初期化エラーの直し方」
株式会社RJCでは、変数のスコープや初期化のしくみを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。