Java Scope and Variable Lifetime
Give each value the scope and starting point its job needs. Learn which names a block can use, which assignments must happen before a read, and why shared working state can break an otherwise simple calculation.
Before you start: review methods and returned results, parameters and shared objects, and loop variables. All source examples use the Java 21 runner. Predict which declaration a name refers to before running.
What you will be able to do
- Identify where a local variable, parameter, loop variable, or field name can be used.
- Keep branch results in a suitable enclosing scope and assign every continuing path.
- Distinguish legal field shadowing from illegal overlapping local declarations.
- Explain fresh local state for each call and each executed block declaration.
- Separate a reference variable's scope from the lifetime of its referenced object.
- Replace an unnecessary shared accumulator with a local value and test call independence.
Scope answers where a name can be used
Scope is a rule about program text: where a declared name can refer to its variable. In the first example, planned is declared in main's block, so the later if body can read it. completed and remaining are declared inside that smaller block and are not available by those names after its closing brace.
The inner block can use an enclosing local, but the enclosing code cannot reach into the inner block's locals after the block finishes. That is why the final message prints planned, not remaining. Adding a println for remaining after the if would cause a cannot find symbol compilation error.
Declare a value in the narrowest scope that includes its real uses. Temporary work used only inside a branch belongs there. A result needed after the decision may need an enclosing declaration. Do not move every variable to the top of the class merely to make compiler errors disappear.
Main.java · An inner block can read an enclosing local
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Inside remaining: 40
Outside planned: 60
Try a useful out-of-scope compilation error
Change the last print to read remaining instead of planned. The variable exists only in the if block, so the later name cannot be resolved. Restore the example, then decide whether a real report needs an enclosing result declaration or simply a print inside the branch.
Being in scope is not the same as having a value
remaining is declared before the if, so both branch bodies and the final println can refer to it. Its declaration does not assign a value yet. The two branches deliberately cover every continuing path: one calculates positive remaining work, and the other assigns zero when the target has been reached or exceeded.
If the else assignment were removed, the name would still be in scope afterward, but Java could no longer establish that it has a value on every path reaching the print. The diagnostic would be about a variable that might not have been initialized, not about an unknown name.
Ordinary local variables do not receive a usable default merely because you declared them. Choose complete assignments that express the domain rule instead of adding an arbitrary initial zero to silence the compiler. The small nonnegative minute values here keep the subtraction in range; scope alone does not validate arithmetic inputs.
Main.java · Assign an enclosing result on both paths
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Remaining: 0
Compare scope failure with assignment failure
Remove the else assignment while leaving int remaining before the if. The name is now visible at the print, but the completed-at-least-planned path has no assigned value. The compiler reports that remaining might not have been initialized. Restore the meaningful zero result for that path.
Keep traversal variables and lasting results separate
A basic for initializer declares index for that loop's condition, update, and body. The counter is not in scope after the loop. processed is declared in the enclosing main block because the report needs it later. The two variables have different roles even though both change during processing.
The loop prints indexes 0, 1, and 2, while processed counts three completed iterations. Attempting to print index afterward would fail compilation. If the final counter value itself were genuinely needed, declaring that counter before the loop would extend its scope deliberately; it would not be necessary to make it a field.
The same principle applies to an enhanced-for element variable: its name belongs to the loop body, not to following report code. Keep aggregate results outside when they must survive across iterations, and keep per-element temporary values inside. Narrow scope makes it harder to accidentally reuse stale working data.
Main.java · The counter and the report have different scopes
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Processing index: 0
Processing index: 1
Processing index: 2
Processed: 3
A method has its own parameters and local working values
withBonus has a minutes parameter and a local bonus of five. main also has a local bonus, but that variable is not an implicit input to the helper. Methods do not gain access to their caller's local variables just because the call appears beneath those declarations in the file.
The first helper invocation receives 10, creates its own local bonus, and returns 15. The second invocation receives 20 and returns 25. The caller's unrelated local bonus remains 100. Parameters receive supplied values; local names within separate method bodies do not become one shared variable.
Pass inputs and return needed results, as the previous methods lessons established. A helper's local variable cannot be read directly from main, even if the helper is declared above it or uses the same spelling as a caller variable. Placement in the source file is not a channel for passing local state between methods.
Main.java · Caller locals are not helper inputs automatically
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
First helper result: 15
Second helper result: 25
Caller bonus: 100
Sibling blocks can reuse a name; overlapping locals cannot
The two explicit blocks declare separate variables called minutes. The first variable's scope has ended before the second declaration begins, so the reuse is legal. Each print reaches the local declared in its own block. The final line cannot refer to either minutes variable.
Do not generalize this into permission to redeclare a local inside a nested block while an outer local of the same name is still in scope. In an ordinary method body, int minutes = 10 followed by a nested int minutes = 20 is rejected. Java does not allow that overlapping local redeclaration here.
Redeclaring a method parameter as a local in that method's body is rejected for the same ordinary-code situation. Prefer names that communicate different roles, such as plannedMinutes and completedMinutes. An extra pair of braces is useful when it expresses a real limited operation, not when it obscures which value a reader should follow.
Main.java · Separate sibling blocks may reuse a local name
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
First block: 10
Second block: 20
Both blocks finished.
A local or parameter can hide a field name
A field is declared directly in the class body rather than inside a method. This example uses static int total as class-associated storage. A field is not automatically the right place for a temporary calculation just because several methods could refer to it. Instance fields belong to particular objects, covered later.
Here main's local total and adjustedTotal's parameter total each shadow the field when the simple name total is used in their respective bodies. Main.total explicitly names the static field and still reads 100. The parameter print reads 20, while the caller's own local print reads 10.
Field shadowing is legal in this example, unlike the overlapping local redeclaration discussed earlier. That difference often surprises students. Use qualification when it makes a genuine field reference clear, but prefer distinct meaningful names when reusing total would force readers to keep guessing which storage you intended.
Main.java · A parameter can shadow a field
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Caller local: 10
Field: 100
Parameter: 20
Qualified field: 100
Helper result: 25
Trace the three values called total
- Main.total identifies the static field containing 100.
- main's simple total identifies its local containing 10.
- adjustedTotal's simple total identifies the supplied parameter containing 20.
- The helper returns 25 without assigning to either of the other variables.
Rename the parameter to inputTotal and update its uses. Clearer naming can remove the need to keep disambiguating identical spellings.
static methods do not make their locals shared
The shared example intentionally uses a static field as an accumulator. It is initialized once for this class initialization, not reset automatically on each method call. The first sharedTotal call adds 30; the second starts from that retained 30 and returns 60. That behavior is wrong for a method promising an independent sum of its input.
localTotal is also a static method, but its total is a local variable initialized to zero on each call. The two local calls therefore each return 30. static on a method and static on a field are not interchangeable promises about the lifetime of every name used inside the method.
A field is appropriate when retained state genuinely belongs to the class or an object. It should not be a workaround for a result that needs to leave a method; return that result instead. Local working state gives independent calls a clear starting point and avoids accidental dependence on which report happened to run first.
Shared first: 30
Shared second: 60
Local first: 30
Local second: 30
Initialization inside a loop runs each time that declaration is reached
currentTotal is declared and initialized inside the loop body. Each entered iteration reaches that declaration again and starts a new local working value at zero. The three session totals are 10, 20, and 30. They are not 10, 30, and 60, because this particular local is not carrying a running sum across iterations.
overall is initialized before the loop and retains the accumulated result as the body repeats. Adding the three independent session totals gives 60. Moving its initialization into the body would reset it too, which would change the report's meaning rather than simply rearranging its formatting.
Choose the declaration's position from what the value measures: current item, current row, current method result, or genuinely retained state. Scope and initialization together express those roles. A variable being available in a block does not mean its initializer runs repeatedly unless execution actually reaches that declaration repeatedly.
Main.java · A body-local total starts fresh each iteration
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Session 1: 10
Session 2: 20
Session 3: 30
Overall: 60
Name visibility and object lifetime are different questions
Scope describes which source locations can refer to a declaration by name. Runtime lifetime concerns the variable or object existing during execution. A local variable belongs to a particular invocation or executed block, while an object it references can remain usable through other references after that local name is no longer accessible.
createScores returns the reference held by temporary. The caller stores it in scores, so the array remains usable after the helper returns. main cannot use the helper's local name temporary, but it can read and change the same array through its own variable. Losing one local name does not destroy every object that name once identified.
Do not infer exact memory reclamation or garbage-collection timing from a closing brace. Java manages object memory, and objects can become eligible for collection when no live references keep them reachable. Eligibility is not a promise of immediate cleanup, and being out of lexical scope is not itself proof that an object has disappeared.
Main.java · A returned array outlives its helper's local name
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Returned second score: 80
What ended when createScores returned?
The caller cannot use the helper's local name temporary. But the returned reference was stored in scores, and the array is still reachable through it. Changing scores[1] is therefore ordinary valid array access, not access to a destroyed local variable.
Fix a scope problem by clarifying ownership
Start a diagnosis with the compiler's category. cannot find symbol may mean the declaration is outside the current scope. might not have been initialized means the name exists but a continuing path may not assign it. variable is already defined points to an overlapping declaration, not missing storage.
Then decide who needs the result. Keep a branch-only temporary inside its branch, declare a shared decision result just outside the decision, return a helper result to its caller, and reserve fields for intentional retained state. These choices explain ownership more clearly than giving every method access to a wide collection of mutable names.
Finally test more than one call and more than one branch. A hidden field accumulator often produces a correct first result and an incorrect second one. A branch result may work on the path used in the sample but fail compilation when a different continuing path has no assignment. Good scope is part of correctness, not only style.
Repair exercise: give a branch result the right enclosing scope
Both branches declare a remaining variable, but the print after the decision cannot use either one. Repair the scope by declaring one result before the decision and assigning it on both paths. Keep the intended floor-at-zero policy.
This starter intentionally fails compilation. Read the cannot-find-symbol diagnostic before moving the declaration; then check that both continuing branches still assign a result.
Main.java · Repair branch result scope
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Reveal the explained repair
The two inner declarations belong to separate branch blocks. Neither name is in scope at the later print. One declaration in the enclosing block gives the decision and report a shared result, while assigning both paths satisfies definite assignment. Declaring a static field or replacing the meaningful branches with an arbitrary default is unnecessary. With completed 70, the same repaired program should print zero.
Main.java · Repaired branch result
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Remaining: 40
Independent lab: remove shared calculation state
Refactor an independent-total helper that incorrectly keeps a class field between calls. The starter deliberately runs and prints wrong repeated totals. Remove the unnecessary field and give each invocation its own local accumulator while keeping report values in main.
- Keep static boolean hasValidMinutes(int[] minutes): null or any negative value is invalid; an empty array is valid. The validator must not change data.
- Keep static long totalMinutes(int[] minutes). Its precondition is a valid nonnegative array. Return the sum without printing or mutating elements.
- Remove the accumulatedMinutes field entirely. Declare and initialize a local long total inside totalMinutes before its loop, so every invocation starts from zero.
- In main validate both arrays before calculation. On invalid input print Minutes must be present and nonnegative. and return without a normal report.
- Keep each returned report result in a local variable in main. Print the first total, second total, first total again, empty total, and an input-unchanged comparison in the order shown.
- Repeated calls must be independent, an empty array must total zero, and passing the same array for both inputs must not create shared accumulator behavior. Do not reset a field from main as a workaround.
The first array contains 20, 35, and 45 minutes. The second contains 5 and 10. main calls the first again, then an empty array. Edit source data; no standard input is required. Expected output after refactoring:
First total: 100
Second total: 15
First total again: 100
Empty total: 0
Input unchanged: true
Main.java · Refactor the retained accumulator
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Hints before the solution
Locate accumulatedMinutes directly in the class body. It is a field, not an invocation-local variable. Remove that declaration and create long total = 0 inside totalMinutes before its loop. Update and return this local instead of the field.
Do not reset a field from main before each call; that leaves independent callers responsible for hidden cleanup. Keep validation, input snapshots, and returned report locals as they are, then compare both the values and the input-unchanged result.
Reveal the explained lab solution
Main.java · Independent local-total solution
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
The starter returns 100, then 115, then 215, and even gives 215 for empty input, because its field retains all earlier additions. The refactor initializes local total for every invocation. main stores the returned values in its own report locals, so it needs no access to the helper's internal name. The long accumulator avoids int-sum overflow for the nonnegative int elements, and the array snapshots verify that neither validation nor calculation edited input. static still belongs on these helper methods; it no longer declares shared calculation storage.
Test changes and boundaries
Edit the two arrays in source, then run the complete call sequence. Reset restores the original starter or solution. Watch the repeated and empty totals: those expose retained state that the first total alone can conceal.
- Run the starter to observe 100, 115, 215, and 215. After refactoring, expect 100, 15, 100, and 0 with Input unchanged: true.
- Try empty arrays for both inputs. All four returned totals must be zero, without a field reset or special previous-call cleanup.
- Make second refer to first. The first, second, and repeated totals must all be 100, while the empty total remains 0. Aliasing inputs must not change an independent sum.
- Try a null first array or a second array containing -1. Expect only the validation message, with no normal report. Zero-minute entries are valid.
- Use two Integer.MAX_VALUE entries in the first array. Both calls on it must return 4294967294. Then change the second array and confirm its total cannot alter the repeated first result.
Common mistakes and how to diagnose them
cannot find symbol after a block or loop
Locate the declaration and its enclosing braces. A branch-local value or for-initializer variable is not available after its scope ends. Move a genuinely shared result to a suitable enclosing block or return it from a helper.
variable might not have been initialized
The name may be in scope but lack an assignment on a path that reaches its use. Assign every continuing branch according to the real policy. Local variables do not acquire field-style defaults automatically.
variable is already defined
An inner local may overlap an enclosing local or parameter with the same name. Ordinary nested blocks do not permit that redeclaration here. Use a distinct role name; separate sibling blocks are different because their scopes do not overlap.
A later call includes the previous result
Check for a field used as temporary working state. A static method's local accumulator is fresh per call, but a static field is retained. Initialize a local inside the calculation instead of resetting shared state from callers.
A field did not change when a same-named value changed
A local or parameter may shadow that field. Inspect the declaration used by the simple name and qualify a genuine static field as Main.fieldName when useful. Distinct names usually make the ownership clearer.
An array still works after its creating helper returned
This is expected when the reference was returned or retained elsewhere. The helper's local name is out of scope for the caller, but the object remains reachable through another reference. Scope is not a promise of immediate object destruction.
Frequently asked questions
Is scope the same as lifetime?
No. Scope is about where a declared name can be used in source. Runtime lifetime concerns a variable or object during execution. An object can remain reachable after a local name that referred to it is no longer available.
Do local variables receive default zero values?
Ordinary local variables must be definitely assigned before reading. Fields and newly created array elements have default initialization rules, but those do not supply a value to every local declaration.
Can I reuse a variable name in another block?
Separate sibling blocks can declare separate locals with the same name. An ordinary nested local cannot redeclare an enclosing local or parameter whose scope overlaps it. A local shadowing a field is a different, legal case.
Does a static method share its local variables between calls?
No. Each invocation has its own local working variables. A static field can retain state across calls, but static on the method does not turn its locals into fields.
Should I use a field so main can read a helper's total?
Not for an independent calculation. Return the total and store it in main's own local variable. Use fields when retained state genuinely belongs to a class or object, not simply to bypass a scope error.
Give temporary work a temporary home
Block boundaries, loop declarations, and method bodies determine which local names are available. Definite assignment separately checks whether a read has a value. Field shadowing differs from overlapping local redeclaration, and static methods still have fresh invocation-local variables.
Before moving on, explain why a returned array can remain usable, why both branches must assign an enclosing result, and why resetting a field from callers is not the right repair for an independent sum. Next: recursion, where separate invocations and their local state become especially important.
Can you identify the declaration and its working lifetime?
Answer all six questions and read the explanations. Results are temporary on this page and do not save course progress.
References and related learning
The scenarios, explanations, examples, and exercises are original SovranCode material. Official references support the Java 21 language rules.
- Java 21 specification: scope of declarations
- Java 21 specification: variable kinds and lifetimes
- Java 21 specification: shadowing
Review returned results · Review parameter variables and references · Review array lifetime and aliases · Java course roadmap.
Last updated: