Layers of Logic

6.4

`static` and `final`

Use static for class-level data and final to prevent specific kinds of change.

Core21 min read5 exercises

Before you start

Topics used in this lesson

01

Prerequisites

The previous lessons defined instance fields and methods. Every Student object receives its own instance fields, and an instance method runs with one current object available as this.

Some state and behaviour do not belong to one Student. A count of all created students concerns the class as a whole. A validation helper may need no object state. Java represents that class-level context with static.

02

Why this matters

This counter cannot count all students:

class Student {
    int created = 0;

    Student() {
        created++;
    }
}

Each new object receives a separate created field. After creating three students, every object reports 1.

We also need to answer two questions that the usual slogans hide:

  • Why can main run before an application object exists?
  • Why can a static method call an instance method in some programs but not others?

Then we need a separate promise: a value or reference that cannot be assigned again. That is the job of final, not static.

03

Core explanation

Shared state and per-object state

public class StudentDemo {
    static class Student {
        private static int created;

        private final int id;
        private String name;

        Student(String name) {
            this.id = ++created;
            this.name = name;
        }

        int id() {
            return id;
        }

        String name() {
            return name;
        }

        static int createdCount() {
            return created;
        }
    }

    public static void main(String[] args) {
        Student a = new Student("Atlas");
        Student b = new Student("Beacon");

        System.out.println(a.id() + ": " + a.name());
        System.out.println(b.id() + ": " + b.name());
        System.out.println("created = " + Student.createdCount());
    }
}

Output:

1: Atlas
2: Beacon
created = 2

There is one created field associated with Student in this runtime context. Each object still has its own id and name.

Access a static member through the declaring class, as in Student.createdCount(). Java permits access through an object expression in some cases, but that spelling hides the fact that no particular object is used.

Instance memberStatic member
Contextone object, available as thisthe class-level context, with no this
Fieldsone value per objectone associated value per defining class loader
Normal accessobject.memberClassName.member
Direct instance accessallowedrequires an explicit object receiver

Why main is static

The standard Java launcher looks for an entry method with the familiar signature:

public static void main(String[] args)

Because the method is static, the launcher does not need to choose a constructor or create an application object first. It can invoke the entry point in the class-level context.

The standard launcher supplies an array of command-line arguments. With java Demo red 42, the array contains "red" and "42". With no arguments, the launcher supplies an empty array. Code elsewhere can call main(null) directly, so non-null is a launcher contract, not a universal property of every possible call.

04

Optional internals

Going deeper
Open optional internalsYou can finish the core lesson without this section.

A static method has no implicit receiver

class Student {
    String name;

    void printDetails() {
        System.out.println(name);
    }

    static void print(Student student) {
        student.printDetails();
    }

    static void broken() {
        // printDetails();
        // System.out.println(name);
    }
}

print(student) is valid because student is the explicit receiver for printDetails(). The commented lines fail because broken() has no this, and no other Student object was named.

An instance method can access static members too. Prefer Student.createdCount() over this.createdCount() so the ownership remains visible.

Loading is not initialization

The JVM can load and link a class before it initializes the class. Static field initializers and static blocks run during class initialization, in textual order.

public class InitDemo {
    static class Config {
        static final int LIMIT = 10;
        static int retries = announce("field");

        static {
            System.out.println("block");
        }

        static int announce(String text) {
            System.out.println(text);
            return 3;
        }
    }

    public static void main(String[] args) {
        System.out.println(Config.LIMIT);
        System.out.println("before retries");
        System.out.println(Config.retries);
    }
}

Typical output:

10
before retries
field
block
3

LIMIT is a compile-time constant, so its value may be inserted into InitDemo by the compiler. Reading it need not initialize Config. Reading the non-constant retries is an active use, so initialization runs first.

Initialization happens at most once for a particular class initialization state. A static field is commonly described as one per class, but separate class loaders can define separate runtime classes with the same name and separate static state.

Four meanings of final

The effect depends on the declaration:

DeclarationRestriction
final local or fieldfinal int limit = 10;assign that variable only once
final referencefinal List<String> namesdo not replace the reference; the object may still mutate
final methodfinal void close()a subclass cannot override it
final classfinal class Tokenanother class cannot extend it

A blank final field may be assigned in every constructor path:

class Unit {
    final int id;

    Unit(int id) {
        this.id = id;
    }
}

The compiler checks that each constructor assigns id once and that no path reads it first.

static final and compile-time constants

Class constants are commonly written this way:

static final int MAX_UNITS = 500;
static final String REGION = "asia";
static final List<String> ROLES = List.of("reader", "writer");

All three variables are static and final. Only the first two qualify as constant variables because their types and initializers meet Java’s compile-time constant rules. ROLES is not a compile-time constant, even though the reference cannot be reassigned and List.of returns an unmodifiable list.

The distinction shows up in class initialization and binary compatibility. A client can inline a compile-time constant. Changing the library value may therefore require recompiling the client.

Final reference is not immutable object

final int[] scores = {70, 80};
scores[0] = 99;              // allowed
// scores = new int[]{1, 2}; // rejected

The array mutation changes an element. It does not assign a new reference to scores. Immutability needs control over every mutation path, often including defensive copies and immutable member types.

05

Tradeoffs

Static mutable state has a long lifetime and hidden consumers. One test can leave a value for the next test. Two threads can race on created++. A method can depend on a static field without showing that dependency in its parameters.

Use static state when the ownership is genuinely class-wide. Constants and stateless helper methods are common cases. For replaceable services, request-scoped data, and mutable collaborators, objects passed explicitly are usually easier to test and reason about.

final gives the compiler a narrow guarantee. It does not automatically give thread safety, deep immutability, or a constant expression. State the exact guarantee you need, then check whether final supplies it.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. Can a static method call an instance method?

    Show the answer

    Yes, when it has an explicit object to use as the receiver. For example, static void print(Student s) { s.printDetails(); } is valid.

    A static method has no implicit this, so it cannot write printDetails() when that call requires a Student receiver. The accurate rule is not "static calls only static." The rule is that instance access needs an object.

  2. When does a static initializer run?

    Show the answer

    It runs when the JVM initializes that class or interface. Initialization follows loading, linking, and initialization of the superclass.

    Defined active uses include creating an instance, invoking a static method, and reading or writing a non-constant static field. Merely obtaining a class literal does not initialize the class. Reading an inlined compile-time constant may not initialize it either.

  3. Why can `final List<String> names = new ArrayList<>();` still accept `names.add("Atlas")`?

    Show the answer

    final prevents another assignment to the variable. It does not make the referenced object immutable.

    names = new ArrayList<>(); is rejected. Mutating the existing list through names.add(...) does not replace the reference value, so it is allowed.

07

Exercises

Start with the warm-ups. Continue to practice exercises when the basic idea works. Challenges are optional: skip them on a first pass and return after the next lesson.

  1. Warm up: one idea with close guidance.
  2. Practice: combine the idea with earlier material.
  3. Challenge: investigate an edge case or implementation detail.

5 exercises105 pointsabout 120 minutes

Coding exercises provide starter code and concrete success checks. Exercises marked checked can also be run through the VS Code extension. Concept questions are selectable quizzes that give immediate feedback on this page.
A

One Copy or Many

Warm up·20 min·15 points

checkedex-6-4-a

A counter and a serial number. One is shared, one is not, and the code depends on that being exactly right.

Predict all four printed values first. Then break it, twice, on purpose.

Remove static from the counter and then add it to the serial. Trace how each change alters the ownership and output.

Finish by defining a static field as state associated with the runtime class rather than any one instance. In advanced systems, separate defining class loaders can create separate runtime classes and static state.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class Counting {
    static void reset()
    static int[] serialsOfThree()
    static int createdAfterThree()
    static boolean allObjectsSeeTheSameCount()
    static int copiesOf(String field)
    static boolean staticNeedsAnObject()
}

Unit has a static created counter and a per object serial. serialsOfThree resets, makes three units, and returns their serials. copiesOf takes "created" or "serial" and says how many copies exist when three objects have been made.

Steps and success checks

  • Make three units and show each has its own serial
  • Show that the shared counter reaches three
  • Say how many copies of each field exist
  • Show that a static field can be read with no object at all
Counting.java
public class Counting {

    static class Unit {
        static int created = 0;      // shared
        int serial;                  // one per object

        Unit() {
            created++;
            serial = created;
        }
    }

    static void reset() { }  // TODO: put the counter back to zero

    static int[] serialsOfThree()   { return new int[0]; }  // TODO
    static int createdAfterThree()  { return -1; }          // TODO

    static boolean allObjectsSeeTheSameCount() { return false; }  // TODO

    // With three objects made, how many copies of each field exist?
    static int copiesOf(String field) { return -1; }  // TODO

    // Do you need an object to read a static field?
    static boolean staticNeedsAnObject() { return true; }  // TODO

    public static void main(String[] args) {
        // TODO: make three units and print their serials and the shared count
    }
}
Hint 1
serial is assigned inside the constructor from the shared counter, so each object captures a different value at the moment it was made.
Hint 2
There is one created, however many units exist. Reading it through any object gives the same answer as reading it through the class.
Hint 3almost the answer
Unit.created needs no Unit object. Reading this non-constant static field is an active use that initializes Unit if necessary.
Why this exercise exists

This exercise checks whether you can predict which fields belong to each object and which are associated with the runtime class.

B

Read Your Own Command Line

Warm up·20 min·15 points

checkedex-6-4-b

You have written String[] args in every program in this course and never once used it. Use it.

Run your program with arguments after the class name and watch them arrive. It is an ordinary array, exactly like every array from Section 4.1, filled in by the JVM before main starts.

Handle both awkward cases properly. No arguments at all, and an argument that is not a number.

Under the standard launcher, no command-line arguments means an empty array rather than null, so args.length is safe. That statement describes launcher invocation. A direct call such as Args.main(null) remains possible Java code.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class Args {
    static int count(String[] args)
    static String listThem(String[] args)
    static int sumNumbers(String[] args)
    static int countUnparseable(String[] args)
    static String describe(String[] args)
    static boolean standardLauncherPassesNull()
}

listThem returns index=value pairs separated by single spaces, empty for no arguments. sumNumbers skips anything that will not parse rather than failing. describe returns "nothing was typed" for an empty array.

Steps and success checks

  • Report how many arguments arrived and print each with its index
  • Add them up as numbers
  • Handle nothing being typed, and something that is not a number
  • Say what type everything on the command line arrives as
Args.java
public class Args {

    static int count(String[] args) { return -1; }  // TODO

    // "0=10 1=20 2=30", empty string for no arguments.
    static String listThem(String[] args) { return ""; }  // TODO

    // Add them up. Skip anything that is not a number rather than failing.
    static int sumNumbers(String[] args) { return -1; }  // TODO
    static int countUnparseable(String[] args) { return -1; }  // TODO

    // "nothing was typed", or a summary.
    static String describe(String[] args) { return ""; }  // TODO

    // Does the standard java launcher pass null when no arguments were typed?
    static boolean standardLauncherPassesNull() { return true; }  // TODO

    public static void main(String[] args) {
        // TODO: report how many arguments arrived, and print each with its index
        // TODO: run it with numbers, with no arguments, and with a word
    }
}

Sample run

You type
java Args 10 20 30
It prints
got 3 arguments
[0] 10
[1] 20
[2] 30
total = 60
Hint 1
The standard java launcher supplies a zero-length array when no arguments are typed. Code that calls main like an ordinary method could still pass null.
Hint 2
Everything arrives as a String. Typing 42 gives you the characters, and Integer.parseInt is what turns them into a number.
Hint 3almost the answer
Catch NumberFormatException around each parse and skip that one. Letting it escape means one typo throws away every number the user typed.
Why this exercise exists

This exercise checks whether you can treat String[] args as the array supplied by the standard Java launcher and separate that launcher contract from a direct method call.

C

Final Does Not Mean Frozen

Practice·25 min·25 points

checkedex-6-4-c

Every field is final. Two lines still change the object.

Predict all four lines before trying any of them. The question to ask each time is the same: am I replacing the arrow, or changing what the arrow points at?

Then answer the design question from the mutation tests. Final fields alone do not make the class immutable.

Finish by making it genuinely immutable, and list every change you needed. There will be four, and final is only one of them.

Section 8.2 expands these requirements into a complete immutable-class design.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class FinalTrap {
    static Squad sample()
    static boolean canChangeArrayContents()
    static boolean canAddToFinalList()
    static boolean canRepointFinalField()
    static boolean frozenResistsArrayChange()
    static String frozenListRejectsAdd()
}

Squad has three final fields. FrozenSquad is a second nested class that is genuinely frozen: copies in the constructor, copies in the getter, and an unmodifiable list. frozenListRejectsAdd returns "added" or the exception's simple name.

Steps and success checks

  • Change the contents of a final array and a final list
  • Try to repoint a final field and record what happens
  • Write a version that really is frozen
  • Say in one line what final actually promises
FinalTrap.java
import java.util.*;

public class FinalTrap {

    static class Squad {
        final String name;
        final int[] memberIds;
        final List<String> notes;

        Squad(String name, int[] memberIds) {
            this.name = name;
            this.memberIds = memberIds;
            this.notes = new ArrayList<>();
        }
    }

    static Squad sample() { return null; }  // TODO: "Alpha", {1, 2, 3}

    // Try to change things through the FINAL fields. Predict each first.
    static boolean canChangeArrayContents() { return false; }  // TODO
    static boolean canAddToFinalList()      { return false; }  // TODO

    // Try assigning a new array to the final field. Does it compile?
    static boolean canRepointFinalField() { return true; }  // TODO

    // TODO: a FrozenSquad that really is frozen
    static class FrozenSquad {
    }

    static boolean frozenResistsArrayChange() { return false; }  // TODO
    static String frozenListRejectsAdd()      { return ""; }     // TODO

    public static void main(String[] args) {
        // TODO: change a final field's contents three ways before fixing anything
    }
}
Hint 1
final means the variable cannot be pointed at anything else. It says nothing about whether that thing can change.
Hint 2
So memberIds[0] = 99 is fine and memberIds = new int[3] is not. Only the second one is what final is about.
Hint 3almost the answer
Freezing takes three things: copy on the way in, copy on the way out, and List.copyOf for the collection. That is Section 8.2 arriving early.
Why this exercise exists

This exercise checks whether you understand that final locks the arrow and not the thing at the end of it.

D

A Registry That Counts Itself

Practice·30 min·25 points·The Registry

checkedex-6-4-d

Every member of this class is either shared or per-unit, and each one has a right answer. Decide each deliberately and write the reason as a comment.

Two are constants, two are shared totals, and four belong to one unit. Trace how an incorrect ownership choice produces matching ids or a total that resets.

The automatic id assignment is worth noticing. A shared counter, read and incremented in a constructor, is genuinely useful and it is also exactly the shape that breaks under threads. Phase XIV will come back to this precise line of code, so leave a comment for your future self.

Finish with the static block. Print at the top of main, then create the first Unit. The order separates starting main from the later active use that initializes Unit.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class RegistryStats {
    static void reset()
    static boolean shouldBeStatic(String field)
}

Unit is a nested class. It needs static constants MAX_READINESS and ORGANISATION, static counters totalCreated and totalDeployments, a shared nextId, per unit name, id, readiness and deployments, a constructor that assigns the next id automatically, a deploy() that increases both counts, a static report(), and a static block. shouldBeStatic takes a field name and answers whether it belongs to the class. Also expose a static bannerCount that the static block increases, so the checks can see it ran once.

Steps and success checks

  • Decide static or not for every field and comment why
  • Assign ids automatically from a shared counter
  • Make deploy increase both this unit's count and the shared total
  • Show the static block running during Unit initialization, not once per Unit object
RegistryStats.java
public class RegistryStats {

    static int bannerCount = 0;
    static void banner() { bannerCount++; }

    static class Unit {
        // TODO: decide static or not for each of these, and comment why:
        //   MAX_READINESS = 100.0, ORGANISATION = "Layers of Logic Registry"
        //   totalCreated, totalDeployments, nextId
        //   name, id, readiness, deployments

        // TODO: a constructor taking a name and a readiness, assigning the id automatically
        // TODO: void deploy()  -> increases this unit's count AND the shared total
        // TODO: static String report()  -> a line naming the organisation and both totals
        // TODO: a static block that calls banner() during Unit initialization
    }

    static void reset() { }  // TODO: put the shared counters back

    static boolean shouldBeStatic(String field) { return false; }  // TODO

    public static void main(String[] args) {
        // TODO: make two units, deploy a few times, and print the report
    }
}
Hint 1
Ask what the field is ABOUT. readiness is about one unit. totalCreated is about all of them. That question decides every one.
Hint 2
this.id = ++nextId reads the shared counter and stores a per unit value, which is the two kinds of field working together.
Hint 3almost the answer
The static block runs when Unit is initialized by its first active use. Making more Unit objects does not repeat class initialization.
Why this exercise exists

This exercise checks whether you can choose class-level or per-instance ownership for each member and predict class initialization separately from object construction.

E

Trace Static and Instance Initialization

Challenge·25 min·25 points

checkedex-6-2-d

Six numbered things, printing as they run. Predict the exact output before running anything.

There are two sequences mixed together here, and separating them is the exercise. One sequence happens once for the whole class. The other happens on every new. Work out which of the six belongs to which before you look.

Then extend it. Add a field that has both an initialiser and a constructor assignment, and predict which value survives.

Finish by answering the practical question. If a constructor reads a field before assigning it, what does it see? The ordering you just worked out gives you the answer, and this is a real bug shape rather than a curiosity.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class Order {
    static int report(String what)
    static List<String> firstObject()
    static List<String> secondObject()
    static List<String> viaChainedConstructor()
    static boolean staticsRunPerObject()
}

Noisy is a nested class with a static field initialiser, a static block, an instance field initialiser, an instance block and two constructors, each calling report with its number. report records into a shared list so the checks can read the order. Each of the three methods clears the log at the right moment and returns what happened.

Steps and success checks

  • Predict the full order for the first object before running
  • Show that the static parts do not run for the second object
  • Say where the constructor body sits in the order
  • Show what this() does to the order
Order.java
import java.util.*;

public class Order {

    static final List<String> log = new ArrayList<>();

    static class Noisy {
        static int staticCounter = report("1. static field initialiser");
        static { report("2. static block"); }

        int instanceField = report("3. instance field initialiser");
        { report("4. instance block"); }

        Noisy() { report("5. constructor body"); }

        Noisy(String tag) {
            this();
            report("6. second constructor, after this()");
        }
    }

    static int report(String what) { return 0; }  // TODO: record it

    // PREDICT each of these before running.
    static List<String> firstObject()          { return List.of(); }  // TODO
    static List<String> secondObject()         { return List.of(); }  // TODO
    static List<String> viaChainedConstructor(){ return List.of(); }  // TODO

    static boolean staticsRunPerObject() { return true; }  // TODO

    public static void main(String[] args) {
        // TODO: write down your predicted order, then run all three
    }
}
Hint 1
Creating the first Noisy object is an active use. The JVM initializes Noisy before constructing that object, so its static field initializer and static block run first.
Hint 2
Clear the log at the right moment. For the second object you have to make one first, or you will catch the static work as well.
Hint 3almost the answer
The constructor body is LAST, after every field initialiser and instance block. That is why a field initialiser cannot depend on something the constructor sets.
Why this exercise exists

This exercise checks whether you can separate one-time class initialization from the field, block, and constructor steps repeated for each new object.

08

Where this leads

Section 7.4 uses the same receiver distinction for static nested classes and inner classes. A static nested object does not automatically retain an enclosing object; an inner object does.

Section 8.2 builds immutable classes and adds the protections that final alone cannot provide. Section 8.4 shows enum constants, which are static final objects created during enum initialization.

Phase XIV returns to the counter. created++ is a read, an addition, and a write. Shared static state makes that sequence visible to every thread, so safe concurrency needs more than one keyword.

Later connections