6.4
`static` and `final`
Use static for class-level data and final to prevent specific kinds of change.
Before you start
Topics used in this lesson
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.
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
mainrun 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.
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 = 2There 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 member | Static member | |
|---|---|---|
| Context | one object, available as this | the class-level context, with no this |
| Fields | one value per object | one associated value per defining class loader |
| Normal access | object.member | ClassName.member |
| Direct instance access | allowed | requires 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.
Optional internals
Going deeperOpen 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
3LIMIT 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:
| Declaration | Restriction | |
|---|---|---|
| final local or field | final int limit = 10; | assign that variable only once |
| final reference | final List<String> names | do not replace the reference; the object may still mutate |
| final method | final void close() | a subclass cannot override it |
| final class | final class Token | another 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}; // rejectedThe 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.
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.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
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 writeprintDetails()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.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.
Why can `final List<String> names = new ArrayList<>();` still accept `names.add("Atlas")`?
Show the answer
finalprevents another assignment to the variable. It does not make the referenced object immutable.names = new ArrayList<>();is rejected. Mutating the existing list throughnames.add(...)does not replace the reference value, so it is allowed.
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.
- Warm up: one idea with close guidance.
- Practice: combine the idea with earlier material.
- Challenge: investigate an edge case or implementation detail.
5 exercises105 pointsabout 120 minutes
One Copy or Many
ex-6-4-aA 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
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
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.Read Your Own Command Line
ex-6-4-bYou 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
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
java Args 10 20 30
got 3 arguments
[0] 10
[1] 20
[2] 30
total = 60
Hint 1
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
String. Typing 42 gives you the characters, and Integer.parseInt is what turns them into a number.Hint 3almost the answer
NumberFormatException around each parse and skip that one. Letting it escape means one typo throws away every number the user typed.Final Does Not Mean Frozen
ex-6-4-cEvery 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
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
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
List.copyOf for the collection. That is Section 8.2 arriving early.A Registry That Counts Itself
ex-6-4-dEvery 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
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
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
Trace Static and Instance Initialization
ex-6-2-dSix 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
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
Hint 2
Hint 3almost the answer
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
- staticEvery enum constant is a public static final object. That is the whole trick.8.4 - Enums
- staticShared state is exactly what makes threads dangerous.Phase XIV. Concurrency
- finalfinal is step one of four. On its own it is not enough.8.2 - Immutable Classes
- finalA lambda can only use local variables that never change.Phase XI. Functional Java
- finalfinal fields are safe to share between threads. Others are not.Phase XIV. Concurrency
static is next used in 7.4 - Static Nested, Inner, Local, and Anonymous Classes