---
title: 'Variables and Scope'
source: 'https://academia.sh/en/courses/java-fundamentals/variables-and-scope'
course: 'Java Fundamentals'
language: en
updated: '2026-08-17T18:09:40+00:00'
license: 'CC BY-SA 4.0'
---

# Variables and Scope

The same int count declaration compiles to three separate things depending on where it sits: a field in the class body, a slot in a method body, statically the class's single-copy field; a method using the same name in two sibling blocks produces the same 20 instructions with one fewer slot.

The previous lesson measured which form a value is held in: the number itself, or an object
wrapping the number. The difference was one letter in the source, a call in the class file.
This lesson asks the same question of the name itself. The line `int count = 5` compiles to
three separate things depending on where it is written; the writing of all three in the
source is **identical**.

The Programming Fundamentals course established variables, binding, scope, and lifetime as
concepts — **those are not repeated here.** There, where a name is visible was explained.
Here, what is measured is **which place** the compiler gives that name in the class file: a
field sitting in the class body, or a slot in the local space set aside per call for the
method.

## Three Name Families

Java writes a name into one of three places, and the choice is decided by **where the
declaration sits.**

- A **local variable** is declared in a method's body. It does not appear as a name in the
  class file; it holds a numbered **slot** in the method's local space. A new copy forms
  every time the method is called and disappears when the call ends. Parameters are local
  variables too.
- An **instance variable** is declared in the class body, without `static`. It stands as a
  **field** in the class file and has a separate copy in every object.
- A **class variable** is declared in the class body with `static`. It is a field too, but
  its copy is **single**: it belongs to the class independent of object count.

The distinction depends on a keyword and an indentation in the source; in the class file it
depends on two separate structures. Fields stand by name in the class file's field table.
Local variables are not there at all — the method's code uses not names but **numbers.** This
lesson measures the three families' distinction on the `count` field of the record carried
through the course.

## The Measurement Core

The core is the previous lesson's core, with two measurements added: the **slot count** each
method uses, and the **field accesses** in that method. The class's field table is read too.
If compilation fails, no class file is produced, and the core reports this separately.

- **BS6** — Slot count is the size of the local space the class file sets aside for that
  method — not the number of names written in the source. In instance methods, **slot number
  zero is reserved for `this`**; in static methods, it starts from the first parameter.
- **BS7** — The field-access column carries the instruction name: `getfield` and `putfield`
  access an instance's field, `getstatic` and `putstatic` access the class's field.

```java
// Gauge.java — reads where a name lands in the class file: a field, or a slot
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.attribute.CodeAttribute;
import java.lang.classfile.instruction.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Gauge {
    record Method(String name, int instructions, int slots, List<String> fieldAccess) {}
    record ClassInfo(List<String> fields, List<Method> methods) {}

    static Path compile(String source, String className) throws Exception {
        Path d = Files.createTempDirectory("gauge");
        Files.writeString(d.resolve(className + ".java"), source);
        PrintWriter sink = new PrintWriter(Writer.nullWriter());
        int result = ToolProvider.findFirst("javac").orElseThrow()
                .run(sink, sink, "-d", d.toString(), d.resolve(className + ".java").toString());
        return result == 0 ? d.resolve(className + ".class") : null;
    }

    static ClassInfo read(String source, String className) throws Exception {
        Path c = compile(source, className);
        if (c == null) throw new IllegalStateException("did not compile: " + className);
        ClassModel cm = ClassFile.of().parse(c);
        List<String> fields = new ArrayList<>();
        for (FieldModel f : cm.fields()) fields.add(f.fieldName().stringValue());
        List<Method> methods = new ArrayList<>();
        for (MethodModel m : cm.methods()) {
            if (m.code().isEmpty()) continue;
            CodeAttribute code = (CodeAttribute) m.code().get();
            List<String> access = new ArrayList<>();
            int instructions = 0;
            for (CodeElement e : code) {
                if (e instanceof Instruction) instructions++;
                if (e instanceof FieldInstruction fi)
                    access.add(fi.opcode().name().toLowerCase(Locale.ROOT)
                            + " " + fi.name().stringValue());
            }
            methods.add(new Method(m.methodName().stringValue(), instructions,
                    code.maxLocals(), access));
        }
        return new ClassInfo(fields, methods);
    }
}
```

## Four Names, Three Separate Places

The measured class declares `totalCount` and `count` in the class body, and `incoming`,
`updated`, `n`, `t`, `temp`, and `second` in method bodies. `blockScope` and `noBlock` do
**the same work in the same order**; the only difference between them is whether the two
temporary names are opened inside curly braces.

```java
// Location.java — four names, three separate places
public class Location {
    static final String SOURCE = """
        class Warehouse {
            static int totalCount = 0;
            int count;
            void add(int incoming) {
                int updated = count + incoming;
                count = updated;
                totalCount += incoming;
            }
            static int blockScope(int n) {
                int t = 0;
                { int temp = n * 2; t += temp; }
                { int second = n * 3; t += second; }
                return t;
            }
            static int noBlock(int n) {
                int t = 0;
                int temp = n * 2; t += temp;
                int second = n * 3; t += second;
                return t;
            }
            static long wide(long weight, int extra) {
                long updated = weight + extra;
                return updated;
            }
            int visible(int count) { return count; }
            int accessesField(int count) { return this.count; }
        }
        """;

    public static void main(String[] args) throws Exception {
        Gauge.ClassInfo s = Gauge.read(SOURCE, "Warehouse");
        System.out.println("fields in class file: " + s.fields());
        System.out.printf("%-14s %6s %5s  %s%n", "method", "instr", "slots", "field access");
        for (Gauge.Method y : s.methods())
            System.out.printf("%-14s %6d %5d  %s%n", y.name(), y.instructions(), y.slots(),
                    y.fieldAccess().isEmpty() ? "-" : String.join(", ", y.fieldAccess()));
    }
}
```

```
fields in class file: [totalCount, count]
method          instr slots  field access
<init>              3     1  -
add                13     3  getfield count, putfield count, getstatic totalCount, putstatic totalCount
blockScope         20     3  -
noBlock            20     4  -
wide                7     5  -
visible             2     2  -
accessesField       3     2  getfield count
<clinit>            3     0  putstatic totalCount
```

The field table has **two** names: `totalCount` and `count`. Of the eight names declared in
the source, only two stand as names in the class file. The remaining six are not names
anywhere; they live only as slot numbers. A local variable's name **does not enter** the
class file — by the time the compiler's work is done, the name has been spent. If the
compiler is separately told to, names can be added to the class file as debug information,
but this is not part of the code: the method's instructions use the same slots without that
information either. Field names, by contrast, are not optional, because they are accessed
from outside the class and the accessing code has to know the name.

The `add` row shows all three families at once. In the same method, `count` appears twice
with an instance-field instruction (`getfield`, `putfield`), `totalCount` twice with a
class-field instruction (`getstatic`, `putstatic`); `incoming` and `updated` enter no field
instruction at all, because they are in slots. Three slots are counted: zero `this`, one
`incoming`, two `updated`.

The measurement's real row is the pair in the middle. `blockScope` and `noBlock` produce
**the same 20 instructions** — the work done at run time is identical, bit for bit. Slot
count is separate: **3** and **4**. Curly braces do nothing at run time; the work they do
finishes at compile time. Because the compiler knows the name `temp` dies at the end of the
first block, it gives the name `second` **the same slot.** Block scope is a concept that does
not exist at run time: all that is left of it in the class file is **one fewer slot.**

The `wide` row shows from another angle that a slot is not a name count: three names are
declared, **five** slots are counted. A slot is a fixed width, and 64-bit values do not fit
in it; `long weight` holds two slots, `int extra` one slot, `long updated` two slots again.
The local space's size depends not on how many names are written but on **which type** those
names are — this is the class-file counterpart of the width column in the previous lesson's
table.

The last two rows separate shadowing. `visible` and `accessesField` carry the same signature
and the name `count` appears in both; one makes **no** field access at all, the other does a
`getfield`. The `count` declared as a parameter **shadows** the `count` that is a field; the
only way to reach the field is to write `this.count`. Which one gets read is decided at
compile time and stands as two separate instructions in the class file.

Both corners of the table carry meaning too. `<init>` and `<clinit>` are methods not written
in the source; the first is the constructor preparing the `count` field, the second is the
class-initialization block carrying the `totalCount = 0` assignment. Both were measured in
the previous topic. `<clinit>`'s slot count being **zero** is meaningful here: there is no
`this` in a class-initialization block, because there is not yet any object in the picture.

## Scope's Visible Result

The distinction of place has a consequence visible not as a number but **as a value.** When
two objects are produced from the same class, the instance variable splits in two, the class
variable does not.

- **BS8** — Two objects are produced and the third call is made to the second object; because
  the class variable carries the total of all calls, the expected value is 3 plus 4 plus 10.
- **BS9** — The shadowing test is done at a point where previous calls have already changed
  the field, so it can be told apart from the value whether the parameter or the field is
  being read.

```java
// Scope.java — the visible result of a field's and a slot's lifetime
public class Scope {
    static class Warehouse {
        static int totalCount = 0;                 // class variable: single copy
        int count;                                  // instance variable: one copy per instance

        void add(int incoming) {
            int updated = count + incoming;         // local variable: one copy per call
            count = updated;
            totalCount += incoming;
        }

        int visible(int count) { return count; }     // parameter shadows the field
        int accessesField(int count) { return this.count; }
    }

    public static void main(String[] args) {
        Warehouse bolt = new Warehouse(), screw = new Warehouse();
        bolt.add(3);
        bolt.add(4);
        screw.add(10);
        System.out.println("bolt.count: " + bolt.count + " | screw.count: " + screw.count
                + " | Warehouse.totalCount: " + Warehouse.totalCount);
        System.out.println("visible(9): " + bolt.visible(9)
                + " | accessesField(9): " + bolt.accessesField(9));
    }
}
```

```
bolt.count: 7 | screw.count: 10 | Warehouse.totalCount: 17
visible(9): 9 | accessesField(9): 7
```

`count` is separate in every object: **7** and **10**. `totalCount` is single: **17**, that
is, the sum of three calls. `updated` shows up nowhere, because it is destroyed at the end of
every call. The three families' lifetimes are separated this way too — a slot lives for the
call, an instance field for the object, a class field for as long as the class stays loaded.

The second line shows shadowing's cost. Two calls to the same object with the same argument
return **9** and **7**. There is no difference between the two methods' bodies in the source
except `this.`. A parameter carrying the same name as a field makes that field unreachable
throughout the method; what gets read is no longer the object's state, it is the argument
coming from the caller.

## The Bounding Measurement: Which Redeclaration Passes

Shadowing does not happen everywhere. Redeclaring the same name in nested blocks in Java is a
**compile error**; shadowing only happens between two names from separate families, that is,
between a field and a local. The five forms below are placed into the same class skeleton,
and only whether they compile is checked.

- **BS10** — All five cases are written into the same body, with the same class name; the
  measurement looks not at the text the compiler produces but at **whether the class file
  forms.** The compiler's message is not printed.

```java
// Declaration.java — which redeclaration passes the compile stage
public class Declaration {
    static final String[][] CASES = {
        {"same name in two sibling blocks",
         "int t = 0; { int g = n; t += g; } { int g = n; t += g; } return t;"},
        {"redeclaring the outer name in an inner block",
         "int t = 0; { int t = n; } return t;"},
        {"accessing an inner-block name from outside",
         "int t = 0; { int g = n; } return t + g;"},
        {"local name same as a field name",
         "int count = n; return count + this.count;"},
        {"local name same as a parameter name",
         "int n = 1; return n;"},
    };

    public static void main(String[] args) throws Exception {
        System.out.printf("%-34s %s%n", "declaration form", "compile");
        for (String[] d : CASES) {
            String source = "class D { int count = 5; int measure(int n) { " + d[1] + " } }";
            System.out.printf("%-34s %s%n", d[0],
                    Gauge.compile(source, "D") != null ? "passes" : "error");
        }
    }
}
```

```
declaration form                   compile
same name in two sibling blocks    passes
redeclaring the outer name in an inner block error
accessing an inner-block name from outside error
local name same as a field name    passes
local name same as a parameter name error
```

The two passing rows share something: in neither is there a point where **two names are
visible at the same time.** The `g` names in sibling blocks never see each other, because the
first has closed before the second opens — this is the source-side counterpart of slot
sharing. A field and a local, by contrast, are from separate families, and the compiler can
tell which one is meant by `this.`.

The three errors share something too: in every one, a single local name has been declared
twice or used outside its scope. In Java, a local variable's name **cannot be redeclared**
within its scope; an inner block cannot shadow an outer one. The fifth row is the narrowest
form of this — a parameter is a local variable too, so redeclaring the same name in the body
is an error. The boundary is drawn cleanly this way: shadowing exists only between a field
and a local, not between two locals, and the compiler rejects the second case before a class
file is ever produced.

It should also be written that this prohibition has a cost: because a short, meaningful name
in an inner block cannot be used a second time, names pile up in long methods. What is gained
in return is that a name has **a single declaration** at any point in a method body — the
reader who sees a name never has to search for which declaration it refers to. The
measurement shows this decision has no run-time counterpart at all: both passing forms
produce the same instructions in the class file.

## Summary

- Where a declaration sits decides where the name lands in the class file: names in the class
  body enter the **field** table, names in a method body only hold a numbered **slot** and
  never appear as a name in the class file.
- The `add` method shows all three families at once: `getfield`/`putfield` for `count`,
  `getstatic`/`putstatic` for `totalCount`, no field instruction at all for `incoming` and
  `updated`.
- Block scope ends at compile time: `blockScope` and `noBlock` produce **the same 20
  instructions**, the block form uses only **one fewer slot.**
- An instance variable lives per object, a class variable per class, a local variable per
  call; the measurement's values **7**, **10**, and **17** separate these three lifetimes.
- Shadowing happens only between a field and a local and is resolved with `this.`;
  redeclaring a local name in an inner block is a compile error.

## Next Step

This lesson measured where a name is written. Once the values names carry settle into their
places, next come the operators that combine them. The next lesson counts what one-character
forms like `+` and `+=` leave behind in the class file: the conversion the compiler inserts
when an `int` and a `long` are added, the compound-assignment form's silent addition of a
narrowing never written in the source, and its **visible** consequence; short-circuit
evaluation showing up in the class file as a branch instruction — and where two constants are
added, no step being added at all.
