6.4
`static` and `final`
Static members belong to a class-level context. Final restricts reassignment, overriding, or inheritance, depending on where it appears.
Previously on
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.
The problem
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.
The idea
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.
Under the hood
Going deeperA 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.
This distinction matters for 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.
What it costs
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
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises80 pointsabout 95 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.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
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.
What your program must do
- 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.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
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.
What your program must do
- 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.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
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.
What your program must do
- 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.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
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.
What your program must do
- 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
After the credits
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.
Threads you opened in this section
- 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 will return in 7.4 - Static Nested, Inner, Local, and Anonymous Classes