Java Scope
›Short versionOpen if you only have a minute.
- Scope is where a name is visible. Lifetime is how long the storage exists.
- A local is in scope from its declaration to the end of its block, and it must be assigned before it is read.
- A nested block can see outer locals. It cannot declare another local with the same name.
- A local may shadow a field. this.field still names the field.
- A lambda may capture a local only when it is final or effectively final. The counted loop index is assigned every pass, so that capture is rejected.
- Case labels share one switch block. A local that should exist in one case needs its own braces.
A discount is computed inside an if, then the receipt tries to print it after the closing brace. The variable was real a line earlier. The compiler now says it cannot find the symbol. The number did not leak out of the braces.
is that boundary. It answers where the source is allowed to use a name. answers how long the storage behind the name exists. The two line up for ordinary locals, and they split as soon as a method returns an object its local variable only pointed at.
From the declaration to the brace
discount is a . Its scope starts at the declaration inside the and ends at that block’s closing brace. The print after the if is outside that scope. Moving the declaration above the if, and assigning it in both branches, is what makes the later print legal. Declaring it inside and hoping the indent will carry it out does not.
static void printReceipt(int price, boolean member) {
if (member) {
int discount = price / 10;
System.out.println(discount);
}
// System.out.println(discount); // compile error: not in scope
}Scope also starts later than the brace. In a block, a use of ready above int ready = 1; does not see that declaration. The name is not yet in scope, and Java does not hoist locals to the top of the block. The compiler’s other check is definite assignment: even after the declaration, a read that some path can reach without a write is an error. Locals do not receive the default 0 or null that fields receive.
Parameters are locals whose declaration is the header, so their scope is the entire body. A parameter named price and a local in that method also named price collide. The inner declaration is rejected because the name is already in scope.
Names a block may reuse
An inner block sees the locals of the method around it. It uses them. It does not redeclare them. This method fails to compile, because the inner x hides a local that is still in scope, and Java forbids that hiding.
static void clash() {
int x = 1;
if (true) {
// int x = 2; // compile error: x is already defined
}
}A field follows a looser rule. A local may use the field’s name. That binds the bare name to the local. The field is still there, under this. This is legal, and it is also easy to misread, which is a reason to rename the local when the two values are easy to confuse.
class Order {
int quantity = 1;
void setQuantity(int quantity) {
System.out.println(quantity); // the parameter
System.out.println(this.quantity); // the field
this.quantity = quantity;
}
}A for header variable is scoped to that statement, including the condition, the update, and the body. After the loop, i is gone. Another loop in the same method may declare i again, because the first scope has ended. The same is true of a variable declared in a pair of braces that have already closed: a later pair may use the name.
for (int i = 0; i < 3; i++) {
System.out.println(i);
}
for (int i = 0; i < 3; i++) {
System.out.println(i);
}What outlives the name
When format returns, the local label is gone with the frame. That is its lifetime. The String it referred to can still be alive, because the caller stored the returned reference in title. Lifetime of the variable and lifetime of the object are different facts. Scope never kept label visible in the caller. The return value is a reference, not a leaked local.
static String format(String name) {
String label = "Order: " + name;
return label;
}
String title = format("Mira");Fields sit on the other side of that split. quantity on an Order remains readable by later instance methods for as long as that order is reachable. Access modifiers decide whether other classes may name it. Inside the class, you do not redeclare the field in each method to “bring it into scope.” It is already in scope, and a local of the same name is the special case you have to step around with this.
A name that surprises you is usually one of three situations. The use is above the declaration or outside the brace, so it is out of scope. The name repeats a local that is still in scope, so the new declaration is illegal. Or the name repeats a field, so the code compiles and the bare name means the local. Reading those three apart is the practical skill. The braces are the map.
A lambda adds a fourth. copies a local into the lambda object, and only when the local is final or effectively final. The counted loop assigns i on every pass, so the lambda is rejected. The program never reaches a run where every task prints the last index.
void queue(List<Runnable> tasks) {
for (int i = 0; i < 3; i++) {
tasks.add(() -> System.out.println(i));
}
}$ javac Tasks.javaTasks.java:3: error: local variables referenced from a lambda expression must be final or effectively finaltasks.add(() -> System.out.println(i));^1 error
Copy the number into a new local inside the body. That local is assigned once, so each lambda keeps its own copy. The tasks print 0, then 1, then 2, including after the method that created them has returned. The frame is gone. The copies sit on the lambdas. An enhanced for already has that fresh variable: for (String name : names) may capture name when the body does not assign it.
for (int i = 0; i < 3; i++) {
int step = i;
tasks.add(() -> System.out.println(step));
}When the captured value is a reference, the lambda and the method share one object. shared is never reassigned, so the lambda may mention it, and append changes the StringBuilder both names identify. That is the same split as pass-by-value: the variable was copied, and the object was not. A one-slot array used as a mutable box compiles for the same reason. The variable stays put. The object does not.
StringBuilder shared = new StringBuilder("a");
Runnable later = () -> shared.append("b");
later.run();
System.out.println(shared); // abclash declares int x in the method and int x again inside the if. Separately, setQuantity’s parameter is also named quantity, like the field. Which of those two programs compiles?
setQuantity compiles. A local may shadow a field, and this.quantity reaches the field. clash does not compile. The inner int x tries to hide a local that is still in scope, which Java rejects. Closing the outer x’s block, or using a different name, is what makes a second x legal. A later for loop may reuse i for that reason: the previous i’s scope has already ended.