Three keywords answer three different questions about ownership. this names the object a method was called on. static says a member belongs to the class rather than to any object. final says a name may be bound exactly once. They appear together constantly — private static final int MAX = 10; uses two of them in one line — and they are routinely half-understood.
This article takes each one on its own terms, then puts all three together in the initialisation order they produce. Every output line and every error message below was produced by compiling and running the code on OpenJDK 21.0.6.
![]()
One rule per keyword: this is the receiver, static belongs to the class, final binds once.
What this actually refers to
Inside an instance method or a constructor, this is a reference to the object the method was called on — the receiver. It is not a copy, not a special object, not a keyword the compiler rewrites away. Print it next to the reference you called through and you get the same value.
public class ThisIdentity {
String name;
ThisIdentity(String name) {
this.name = name;
}
void identify(ThisIdentity receiver) {
System.out.println(" this = " + this);
System.out.println(" receiver passed = " + receiver);
System.out.println(" this == receiver = " + (this == receiver));
System.out.println(" this.name = " + this.name);
}
public static void main(String[] args) {
ThisIdentity a = new ThisIdentity("alpha");
ThisIdentity b = new ThisIdentity("beta");
System.out.println("a = " + a);
a.identify(a);
System.out.println("b = " + b);
b.identify(b);
}
}
a = ThisIdentity@2a139a55
this = ThisIdentity@2a139a55
receiver passed = ThisIdentity@2a139a55
this == receiver = true
this.name = alpha
b = ThisIdentity@5cad8086
this = ThisIdentity@5cad8086
receiver passed = ThisIdentity@5cad8086
this == receiver = true
this.name = beta
The hexadecimal suffix is the object's identity hash; the values you see may differ, but the two lines within each call will always match. a.identify(...) and b.identify(...) run the same bytecode, and this is a different object each time.
That is the whole of it. Every unqualified field access inside an instance method is silently this.something; writing this explicitly changes nothing except readability — with one exception, which is the next section.
this.x = x makes the field and the parameter different slots
A parameter whose name matches a field shadows it: inside the method, the bare name means the parameter. This is the one place where writing this is not optional.

public class Shadowing {
int balance;
void setBroken(int balance) {
balance = balance; // parameter assigned to itself; the field never moves
}
void setFixed(int balance) {
this.balance = balance; // field on the left, parameter on the right
}
public static void main(String[] args) {
Shadowing acc = new Shadowing();
acc.setBroken(500);
System.out.println("after setBroken(500): balance = " + acc.balance);
acc.setFixed(500);
System.out.println("after setFixed(500): balance = " + acc.balance);
}
}
after setBroken(500): balance = 0
after setFixed(500): balance = 500
setBroken compiles, runs, and does nothing. It assigns the parameter to itself and leaves the field at its default 0. javac -Xlint:all reports no warning for it — this is a bug that ships silently, and an IDE inspection is the only thing that will catch it for you.
The two names live in different places, which is why they can coexist:
| Name | Where the slot lives | Lifetime |
|---|---|---|
balance (parameter) | the method's frame on the stack | one call |
this.balance (field) | inside the object on the heap | as long as the object |
The alternative to this.balance = balance is renaming the parameter — void setFixed(int newBalance). Java code overwhelmingly prefers the first, because keeping the parameter name equal to the field name is a useful signal that the method just stores it.
this(...) calls another constructor of the same class
A constructor can delegate to another constructor of the same class with this(...). It is the standard way to express default arguments, since Java has no default parameter values.
public class ThisChaining {
String host;
int port;
boolean tls;
ThisChaining(String host, int port, boolean tls) {
System.out.println(" full constructor: host=" + host + " port=" + port + " tls=" + tls);
this.host = host;
this.port = port;
this.tls = tls;
}
ThisChaining(String host, int port) {
this(host, port, true); // must be the first statement
System.out.println(" two-arg constructor finished");
}
ThisChaining(String host) {
this(host, 443);
System.out.println(" one-arg constructor finished");
}
public static void main(String[] args) {
System.out.println("new ThisChaining(\"api.example.com\")");
ThisChaining c = new ThisChaining("api.example.com");
System.out.println("result: " + c.host + ":" + c.port + " tls=" + c.tls);
}
}
new ThisChaining("api.example.com")
full constructor: host=api.example.com port=443 tls=true
two-arg constructor finished
one-arg constructor finished
result: api.example.com:443 tls=true
The chain runs inward first and unwinds outward: the one-argument constructor calls the two-argument one, which calls the full one, which is the only constructor that actually assigns anything. Add a field later and there is exactly one place to change.
On JDK 21 the delegating call must be the first statement in the constructor body:
ChainNotFirst() {
System.out.println("before delegating");
this(443);
}
ChainNotFirst.java:10: error: call to this must be first statement in constructor
this(443);
^
1 error
Java 25 relaxed this with flexible constructor bodies, which allow statements before the delegating call as long as they do not read the object under construction. On JDK 21 the rule is absolute.
Passing this out, and returning this for chaining
this is an ordinary reference, so it can be passed to another method and returned from one. Returning it is what makes a fluent API work: each setter hands the same object back, so the calls can be written in a chain.
public class FluentThis {
private String host = "localhost";
private int port = 80;
private int timeoutMs = 1000;
FluentThis host(String host) {
this.host = host;
return this; // hand the same object back
}
FluentThis port(int port) {
this.port = port;
return this;
}
FluentThis timeoutMs(int timeoutMs) {
this.timeoutMs = timeoutMs;
return this;
}
String describe() {
return host + ":" + port + " timeout=" + timeoutMs + "ms";
}
// passing `this` to a method that needs the whole object
void log() {
Audit.record(this, "configured");
}
public static void main(String[] args) {
FluentThis cfg = new FluentThis();
FluentThis same = cfg.host("api.example.com").port(8443).timeoutMs(250);
System.out.println("cfg.describe() = " + cfg.describe());
System.out.println("same == cfg = " + (same == cfg));
cfg.log();
}
}
class Audit {
static void record(FluentThis source, String event) {
System.out.println("audit: " + event + " -> " + source.describe());
}
}
cfg.describe() = api.example.com:8443 timeout=250ms
same == cfg = true
audit: configured -> api.example.com:8443 timeout=250ms
same == cfg is true, which is the point worth noticing: the chain never created a second object. cfg.host(...) mutated cfg and returned it, and every subsequent call in the chain mutated the same object again. That is convenient, and it is also why a chained setter API is a poor fit when you wanted an unshared copy — every holder of that reference sees every change.
this does not exist in a static context
A static method is not called on an object, so there is no receiver for this to name.
public class ThisInStatic {
int value = 7;
static void report() {
System.out.println(this.value);
}
}
ThisInStatic.java:5: error: non-static variable this cannot be referenced from a static context
System.out.println(this.value);
^
1 error
The message is oddly worded — this is described as a variable — but it is precise: this is an implicit parameter that instance methods receive and static methods do not. The same message appears whenever a static method reaches for anything that would need one.
static: one copy that belongs to the class
A static member belongs to the class itself. There is exactly one copy of a static field no matter how many objects exist, and it exists before the first object is created. An instance field is the opposite: one slot per object.

The cleanest proof is a counter incremented in the constructor and read through the class name.
public class InstanceCounter {
static int created = 0; // one slot, owned by the class
int id; // one slot per object
InstanceCounter() {
created++; // shorthand for InstanceCounter.created++
this.id = created;
}
public static void main(String[] args) {
System.out.println("before any object: InstanceCounter.created = " + InstanceCounter.created);
InstanceCounter a = new InstanceCounter();
InstanceCounter b = new InstanceCounter();
InstanceCounter c = new InstanceCounter();
System.out.println("a.id = " + a.id + " b.id = " + b.id + " c.id = " + c.id);
System.out.println("InstanceCounter.created = " + InstanceCounter.created);
System.out.println("a sees created = " + a.created + ", b sees " + b.created + ", c sees " + c.created);
}
}
before any object: InstanceCounter.created = 0
a.id = 1 b.id = 2 c.id = 3
InstanceCounter.created = 3
a sees created = 3, b sees 3, c sees 3
Two things are proved here. created was readable and equal to 0 before any object existed, so it does not live inside an object. And after three constructions all three objects report 3, because they are not reporting three fields — they are all reading the one field that belongs to the class. id, by contrast, is 1, 2 and 3, one value per object.
static int created | int id | |
|---|---|---|
| How many slots | one, for the whole class | one per object |
| Created when | the class is initialised | the object is constructed |
| Reachable without an object | yes | no |
| Accessed as | InstanceCounter.created | a.id |
Calling a static member: through the class, not through an object
A static member should be qualified by the class name. Java also lets you write it through an instance reference, which compiles but says something untrue about the code.
public class StaticCallSite {
static int square(int n) {
return n * n;
}
public static void main(String[] args) {
System.out.println("via the class : " + StaticCallSite.square(5));
StaticCallSite obj = new StaticCallSite();
System.out.println("via an instance : " + obj.square(5));
StaticCallSite nothing = null;
System.out.println("via a null ref : " + nothing.square(5));
}
}
via the class : 25
via an instance : 25
via a null ref : 25
The third line is the one to remember: calling a static method through a null reference does not throw NullPointerException. The compiler resolved the call from the static type of nothing, discarded the reference, and emitted an ordinary static call. Nothing was ever dereferenced.
javac will tell you about all of this if you ask:
StaticCallSite.java:10: warning: [static] static method should be qualified by type name, StaticCallSite, instead of by an expression
System.out.println("via an instance : " + obj.square(5));
^
StaticCallSite.java:13: warning: [static] static method should be qualified by type name, StaticCallSite, instead of by an expression
System.out.println("via a null ref : " + nothing.square(5));
^
2 warnings
The same warning fires for fields — a.created in the previous section produces static variable should be qualified by type name under -Xlint:static. Turn that lint on and write ClassName.member everywhere.
What a static method cannot do
A static method has no receiver, so it cannot touch anything that needs one: no instance fields, no instance methods, no this.
public class StaticTouchesField {
int instanceCount = 0;
static int total = 0;
static void bump() {
instanceCount++; // no object to bump it on
total++; // fine: static field, static method
}
}
StaticTouchesField.java:6: error: non-static variable instanceCount cannot be referenced from a static context
instanceCount++; // no object to bump it on
^
1 error
total++ on the next line is fine. The restriction is one-directional: a static method cannot reach instance state, but an instance method can freely read and write static state, because an instance method has a receiver and a class.
The two ways out of that error are to make the method non-static, or to pass the object in as a parameter and use it explicitly.
Static initialiser blocks and lazy class initialisation
A static { ... } block runs once, when the class is initialised. It exists for setup that a single field initialiser expression cannot express — filling a lookup table, reading a resource, wiring several fields together.
Static field initialisers and static blocks run in source order, as one combined sequence.
public class StaticBlocks {
static int a = report("1. static field a");
static {
System.out.println("2. static block one");
b = 20; // legal: assigning a field declared below
}
static int b = report("3. static field b overwrites the 20 above");
static {
System.out.println("4. static block two, b = " + b);
}
static int report(String label) {
System.out.println(label);
return 99;
}
public static void main(String[] args) {
System.out.println("5. main starts, a = " + a + ", b = " + b);
}
}
1. static field a
2. static block one
3. static field b overwrites the 20 above
4. static block two, b = 99
5. main starts, a = 99, b = 99
Note step 2 writing to b before b is declared, and step 3 then overwriting it. A static block may assign a field declared later in the file, but may not read one — that is an illegal forward reference. Ordering static state so that it depends on declaration position is a reliable way to produce a bug nobody can see.
Class initialisation is lazy
The interesting part is when that sequence runs. A class is initialised on first active use, not when the program starts and not when the class is merely named. The article on compiling and running a Java program described the class loader resolving names lazily; this is the same mechanism, one step further along.
class Heavy {
static final int LIMIT = 100; // compile-time constant
static final String NAME = makeName(); // not a compile-time constant
static {
System.out.println(" >>> Heavy is being initialised");
}
static String makeName() {
return "heavy";
}
static int twice(int n) {
return n * 2;
}
}
public class LazyInit {
public static void main(String[] args) {
System.out.println("1. main started");
Heavy ref = null; // naming the type
System.out.println("2. declared a Heavy variable, still nothing");
System.out.println("3. Heavy.LIMIT = " + Heavy.LIMIT);
System.out.println("4. about to call Heavy.twice(21)");
System.out.println("5. Heavy.twice(21) = " + Heavy.twice(21));
System.out.println("6. Heavy.NAME = " + Heavy.NAME);
}
}
1. main started
2. declared a Heavy variable, still nothing
3. Heavy.LIMIT = 100
4. about to call Heavy.twice(21)
>>> Heavy is being initialised
5. Heavy.twice(21) = 42
6. Heavy.NAME = heavy
Three separate facts fall out of that trace:
- Declaring a variable of type
Heavydid not initialise the class. A type name in a declaration is not a use. - Reading
Heavy.LIMITdid not initialise the class either. That is the constant-inlining rule of the next section, visible from the outside. - The first real use — calling
Heavy.twice(21)— triggered initialisation, and the static block ran before the method it was blocking.
Running the same program under -verbose:class confirms the class is not even loaded until step 4. The excerpt below keeps only the relevant lines, with the leading timestamp and the absolute path trimmed:
$ java -verbose:class LazyInit
4. about to call Heavy.twice(21)
[info][class,load] Heavy source: file:.../java-a25/
>>> Heavy is being initialised
static final constants and compile-time inlining
The constant convention is static final with a SCREAMING_SNAKE_CASE name: static because a constant does not vary per object, final because it must not be reassigned.
public class Limits {
public static final int MAX_RETRIES = 3; // compile-time constant
public static final String PREFIX = "v1-"; // compile-time constant
public static final int[] SIZES = {1, 2, 3}; // NOT a constant
}
A static final field of a primitive type or String, initialised with a constant expression, is a compile-time constant. The compiler does not emit a field read for it — it copies the value straight into every class that uses it. javap -c shows exactly that:
public class UsesLimits {
static int budget() {
return Limits.MAX_RETRIES * 10;
}
static String tag() {
return Limits.PREFIX + "beta";
}
static int firstSize() {
return Limits.SIZES[0];
}
}
$ javap -c UsesLimits.class
static int budget();
Code:
0: bipush 30
2: ireturn
static java.lang.String tag();
Code:
0: ldc #9 // String v1-beta
2: areturn
static int firstSize();
Code:
0: getstatic #11 // Field Limits.SIZES:[I
3: iconst_0
4: iaload
5: ireturn
budget() is one instruction pushing 30. Limits does not appear in it at all — the multiplication was folded at compile time. tag() is a single ldc of the finished string "v1-beta". Only firstSize(), which reads the array, emits a real getstatic against Limits.
That is efficient, and it has a consequence people meet the hard way. Change the constant, recompile only the class that declares it, and the users keep the old value:
$ java Run
Limits.MAX_RETRIES = 3
UsesLimits.budget()= 30
UsesLimits.tag() = v1-beta
# MAX_RETRIES changed from 3 to 5; only Limits.java recompiled
$ javac Limits.java && java Run
Limits.MAX_RETRIES = 3
UsesLimits.budget()= 30
UsesLimits.tag() = v1-beta
# everything recompiled
$ javac Limits.java UsesLimits.java Run.java && java Run
Limits.MAX_RETRIES = 5
UsesLimits.budget()= 50
UsesLimits.tag() = v1-beta
Even Run itself printed the stale 3, because Run had inlined the constant too. Changing a public compile-time constant is a binary-incompatible change: every dependant must be recompiled, which is why a constant published in a library is much harder to change than a method. Build tools recompile the whole module and hide this; a partial rebuild does not.
Not every static final is a compile-time constant. The initialiser has to be a constant expression:
public class Kinds {
public static final int A = 3; // constant expression -> inlined
public static final int B; // assigned in a static block
public static final int C = Integer.parseInt("3"); // method call
public static final String S = "v" + 1; // constant expression -> inlined
static { B = 3; }
}
$ javap -c UsesKinds.class
static int a();
Code:
0: iconst_3
1: ireturn
static int b();
Code:
0: getstatic #9 // Field Kinds.B:I
3: ireturn
static int c();
Code:
0: getstatic #13 // Field Kinds.C:I
3: ireturn
static java.lang.String s();
Code:
0: ldc #16 // String v1
2: areturn
A and S were inlined; B and C produce a real field read. If you need a public constant you can change without recompiling the world, that is the trick: initialise it in a static block, or through a method call, so it stops being a compile-time constant.
Utility classes, and when static is the wrong tool
When a method needs no object state — its answer depends only on its arguments — static is the right choice. A class of such methods is a utility class, and the conventional shape stops anyone from instantiating it:
final class Text {
private Text() { // nobody can call new Text()
throw new AssertionError("no instances of Text");
}
static String repeat(String s, int times) {
return s.repeat(times);
}
static boolean isBlank(String s) {
return s == null || s.isBlank();
}
}
public class TextUtil {
public static void main(String[] args) {
System.out.println(Text.repeat("ab", 3));
System.out.println(Text.isBlank(" "));
}
}
ababab
true
The private constructor is what makes it a utility class rather than a class that merely has static methods. java.lang.Math and java.util.Arrays are both built this way.
Where static goes wrong is shared mutable state. A field written as static because it felt convenient becomes one value for the entire program:
class Cart {
static int total = 0; // meant to be per cart, written as per class
int items = 0; // correctly per cart
void add(int price) {
total += price;
items++;
}
}
public class SharedState {
public static void main(String[] args) {
Cart alice = new Cart();
Cart bob = new Cart();
alice.add(30);
bob.add(12);
bob.add(8);
System.out.println("alice: items = " + alice.items + ", total = " + Cart.total);
System.out.println("bob : items = " + bob.items + ", total = " + Cart.total);
}
}
alice: items = 1, total = 50
bob : items = 2, total = 50
Alice's cart holds one item worth 30, and reports a total of 50. The item counts are right because items is an instance field; the totals are wrong because total is not. In a single-threaded example the damage is merely wrong numbers. Add threads and the same field becomes a data race.
The rule that holds up: static for behaviour that depends only on its arguments and for genuinely immutable constants; instance fields for anything that describes one particular object.
A nested class can also be declared static, which means something different again — that the nested class does not hold a reference to an enclosing instance. That belongs with inner classes in the advanced course.
final on a local variable and on a parameter
On a local variable or a parameter, final means the name may be assigned exactly once and never re-pointed.
public class FinalLocals {
static int rounded(final double raw, final int places) {
final double factor = Math.pow(10, places);
places = 2; // reassigning a final parameter
return (int) Math.round(raw * factor);
}
public static void main(String[] args) {
final int limit;
limit = 10; // legal: a blank final assigned once
limit = 20; // second assignment
System.out.println(limit);
}
}
FinalLocals.java:4: error: final parameter places may not be assigned
places = 2; // reassigning a final parameter
^
FinalLocals.java:11: error: variable limit might already have been assigned
limit = 20; // second assignment
^
2 errors
Two different messages, and the difference is worth reading. A parameter arrives already assigned, so any assignment at all is rejected: final parameter places may not be assigned. A local declared without an initialiser is a blank final — it may be assigned once, anywhere, and the compiler tracks whether that has definitely happened yet. A blank final assigned twice gives might already have been assigned.
Assigning a final that was initialised at its declaration gives the third message, the one most people recognise:
final int limit = 10;
limit = 20;
FinalLocalInit.java:4: error: cannot assign a value to final variable limit
limit = 20;
^
1 error
final on a local is not required for a lambda or an anonymous class to capture it, but the variable must at least be effectively final — assigned once and never touched again:
int factor = 3;
factor = 4; // makes `factor` not effectively final
List.of(1, 2).forEach(n -> System.out.println(n * factor));
EffectivelyFinal.java:7: error: local variables referenced from a lambda expression must be final or effectively final
List.of(1, 2).forEach(n -> System.out.println(n * factor));
^
1 error
Delete the second assignment and it compiles. Writing final on the declaration makes the requirement explicit and turns a confusing error at the lambda into an obvious one at the reassignment.
final fields: assigned exactly once
A final field must be assigned exactly once before the constructor finishes, either by its initialiser or by every constructor. The compiler checks both halves of that: not fewer than once, and not more.
Leave a path that does not assign it and you get:
public class FinalFieldMissing {
private final String name;
private final int size;
FinalFieldMissing(String name) {
this.name = name; // size never assigned on this path
}
}
FinalFieldMissing.java:7: error: variable size might not have been initialized
}
^
1 error
The caret points at the closing brace of the constructor, because that is where the omission becomes final. Assign it twice within one constructor and you get the blank-final message again:
FinalFieldReassign(int size) {
this.size = size;
this.size = size * 2; // second assignment in the same constructor
}
FinalFieldReassign.java:6: error: variable size might already have been assigned
this.size = size * 2; // second assignment in the same constructor
^
1 error
And a field that already has an initialiser cannot be assigned by a constructor at all, because the initialiser runs first and has used up the one assignment:
public class FinalFieldTwice {
private final int size = 10;
FinalFieldTwice(int size) {
this.size = size; // the initialiser already assigned it
}
}
FinalFieldTwice.java:5: error: cannot assign a value to final variable size
this.size = size; // the initialiser already assigned it
^
1 error
The practical consequence: a field that varies by constructor argument must be a blank final — declared with no initialiser and assigned in every constructor. That is exactly what the this.x = x idiom does, and it is why final fields and constructor chaining fit together so well. Delegate with this(...) to one constructor that assigns everything, and there is only one path for the compiler to check.
final on a reference does not make the object immutable
This is the most misunderstood thing about final. final constrains the variable: the reference stored in it cannot be re-pointed. It says nothing whatsoever about the object at the other end.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class FinalReference {
public static void main(String[] args) {
final int[] counts = {1, 2, 3};
final StringBuilder sb = new StringBuilder("Java");
final List<String> tags = new ArrayList<>(List.of("a", "b"));
final String label = "fixed";
// the OBJECTS are still fully mutable
counts[0] = 99;
sb.append(" 21");
tags.add("c");
System.out.println("counts = " + Arrays.toString(counts));
System.out.println("sb = " + sb);
System.out.println("tags = " + tags);
System.out.println("label = " + label + " (String has no mutator at all)");
}
}
counts = [99, 2, 3]
sb = Java 21
tags = [a, b, c]
label = fixed (String has no mutator at all)
Every one of those variables is final, and three of the four objects changed. What final actually blocks is re-pointing:
final StringBuilder sb = new StringBuilder("Java");
sb.append(" 21");
// re-pointing the reference, not mutating the object
sb = new StringBuilder("Kotlin");
FinalRepoint.java:6: error: cannot assign a value to final variable sb
sb = new StringBuilder("Kotlin");
^
1 error
label did not change for a completely different reason: String has no method that modifies it. Immutability is a property of the class — no mutating methods and no writable state — and no keyword at a variable can grant it.
The same trap in constant form is public static final on an array:
import java.util.Arrays;
public class PublicConstantArray {
public static final String[] LEVELS = {"DEBUG", "INFO", "WARN"};
public static final int MAX = 3;
public static void main(String[] args) {
System.out.println("before: " + Arrays.toString(PublicConstantArray.LEVELS));
PublicConstantArray.LEVELS[0] = "TRACE"; // no compiler complaint at all
System.out.println("after : " + Arrays.toString(PublicConstantArray.LEVELS));
System.out.println("MAX = " + PublicConstantArray.MAX);
}
}
before: [DEBUG, INFO, WARN]
after : [TRACE, INFO, WARN]
MAX = 3
LEVELS looks like a constant, is named like a constant, and any caller anywhere can overwrite its elements — permanently, for the whole program. javac -Xlint:all says nothing. MAX really is constant, because int has no interior to modify.
⚠️ A
public static finalarray or mutable collection is a public mutable field with a misleading name. PublishList.of(...)orCollections.unmodifiableList(...)instead, or hand out a copy from a method.
The distinction, in one line: static final gives you one slot, assigned once, shared by everyone; whether the value in that slot can be changed from the inside depends entirely on its type. For int, String and other immutable types the two coincide, which is why the trap survives.
final methods and final classes
final on a method means no subclass may override it. final on a class means no class may extend it at all — which implies every one of its methods is effectively final too.
final class Config {
final String describe() {
return "config";
}
}
class TunedConfig extends Config {
}
FinalShapes.java:7: error: cannot inherit from final Config
class TunedConfig extends Config {
^
1 error
FinalMethodOverride.java:8: error: describe() in Derived cannot override describe() in Base
String describe() {
^
overridden method is final
1 error
java.lang.String, Integer and the other wrapper types are all final classes, which is part of how they guarantee immutability — no subclass can add mutable state or override a method to lie about the value. When and why you would mark your own classes and methods final is a design question that only makes sense once overriding is on the table; that is articles 27 and 28.
What final means in each position
final is one keyword doing four jobs, and mixing them up is what produces the "but I made it final" bug.
| Position | What is fixed | What is not fixed | Error if you try |
|---|---|---|---|
| local variable | the variable, after one assignment | the object it refers to | cannot assign a value to final variable x |
| blank local (no initialiser) | after the first assignment | the object it refers to | variable x might already have been assigned |
| parameter | the parameter, from entry | the object passed in | final parameter x may not be assigned |
| instance field | the field, after the constructor | the object it refers to | cannot assign a value to final variable x |
| blank final field | must be assigned in every constructor | the object it refers to | variable x might not have been initialized |
static final primitive or String | the value, program-wide, and inlined into callers | nothing left to change | cannot assign a value to final variable X |
static final array or object | the reference only | every element and every field of it | nothing — mutation compiles silently |
| method | the implementation, against overriding | anything the method reads or writes | overridden method is final |
| class | the class, against extension | the objects, if they have mutable fields | cannot inherit from final C |
The row that matters is the seventh, and it is the only one with no compiler diagnostic.
Initialisation order, run and proved
Put all three keywords together and the order everything runs in is fully determined. Class-level initialisation happens once, on first use. Object-level initialisation happens on every new.

class Widget {
static int serial = trace("1. static field initialiser", 100);
static {
System.out.println("2. static block");
}
int id = trace("3. instance field initialiser", ++serial);
{
System.out.println("4. instance initialiser block, id = " + id);
}
Widget(String label) {
System.out.println("5. constructor body, label = " + label + ", id = " + id);
}
static int trace(String message, int value) {
System.out.println(" " + message + " -> " + value);
return value;
}
}
public class InitOrder {
public static void main(String[] args) {
System.out.println("main starts, Widget not touched yet");
System.out.println("--- new Widget(\"first\") ---");
new Widget("first");
System.out.println("--- new Widget(\"second\") ---");
new Widget("second");
System.out.println("--- Widget.serial = " + Widget.serial + " ---");
}
}
main starts, Widget not touched yet
--- new Widget("first") ---
1. static field initialiser -> 100
2. static block
3. instance field initialiser -> 101
4. instance initialiser block, id = 101
5. constructor body, label = first, id = 101
--- new Widget("second") ---
3. instance field initialiser -> 102
4. instance initialiser block, id = 102
5. constructor body, label = second, id = 102
--- Widget.serial = 102 ---
Read it twice. Steps 1 and 2 printed exactly once, at the first new, not at program start. Steps 3, 4 and 5 printed once per object, in that fixed order: instance field initialisers and instance initialiser blocks first, in source order, then the constructor body. serial kept its value between the two constructions, because it is the class's field.
| Phase | What runs | How often |
|---|---|---|
| Class initialisation | static field initialisers and static { } blocks, in source order | once, at first active use |
| Object initialisation | instance field initialisers and { } blocks, in source order | on every new |
| Constructor | the constructor body | on every new, after the block above |
The instance initialiser block { ... } is rarely used, because a constructor can do the same work more clearly. Its one real use is sharing setup across several constructors that do not chain — and chaining with this(...) is usually better.
Common mistakes and the errors they produce
| Mistake | What happens | Message |
|---|---|---|
x = x; instead of this.x = x; | compiles, field stays at its default | none, not even under -Xlint:all |
Using this in a static method | compile error | non-static variable this cannot be referenced from a static context |
| Reading an instance field from a static method | compile error | non-static variable x cannot be referenced from a static context |
A statement before this(...) on JDK 21 | compile error | call to this must be first statement in constructor |
Reassigning a final parameter | compile error | final parameter x may not be assigned |
Assigning a final local twice | compile error | variable x might already have been assigned |
A constructor leaving a final field unassigned | compile error | variable x might not have been initialized |
Assigning a final field that has an initialiser | compile error | cannot assign a value to final variable x |
Overriding a final method | compile error | overridden method is final |
Extending a final class | compile error | cannot inherit from final C |
| Reading a static field before the class is used | returns the default, or an inlined constant | none |
Mutating the object behind a final reference | compiles, object changes | none |
Mutating a public static final array | compiles, the constant changes for everyone | none |
| Changing a public compile-time constant without recompiling dependants | callers keep the old value | none |
Per-object state declared static | compiles, all objects share one value | none |
The last five produce no diagnostic at all. Four of the five are the same misunderstanding wearing different clothes: final and static are about the slot, never about the value inside it.
FAQ
Do I have to write this in front of every field?
No. Inside an instance method the bare name already resolves to the field, and this. in front of it changes nothing. It is required only when a parameter or a local shadows the field — the this.x = x case — and useful when you want to pass or return the object itself. Some teams write it everywhere for consistency; that is a style choice, not a correctness one.
Why can main not call my method?
Because main is static and your method is not. javac says non-static method f() cannot be referenced from a static context. Either make the method static too, if it does not use any fields, or create an object in main and call the method on it. The second is what real programs do: main builds one object and hands control to it.
Is a static field shared between threads?
Yes, and that is exactly why it is dangerous. One slot, visible to every thread, with no synchronisation of any kind implied by the keyword. A static counter incremented from several threads can lose increments, and usually will under load. If a static field must be mutable and touched concurrently, it needs AtomicInteger, a lock, or volatile, depending on what you are doing — none of which static gives you.
What is the difference between static final and final?
static final is one slot for the whole class, assigned once, and — for primitives and String with a constant initialiser — copied into every class that reads it. A plain final instance field is one slot per object, assigned once per object, and may legitimately differ between objects. Use static final for a value that is the same everywhere, and a final instance field for a value fixed at construction that varies by object.
Does final make my object thread-safe?
Not by itself, but a final field does carry a real guarantee: after a constructor returns normally, every thread that sees the object sees the correctly initialised value of its final fields, without extra synchronisation. That guarantee covers the field, not what it points at — a final reference to a mutable ArrayList is exactly as unsafe as a non-final one. A class whose fields are all final and all point at immutable objects is genuinely thread-safe.
Does final make code faster?
Treat it as no. The JIT compiler determines what is effectively constant from the whole program, and does not need the keyword to inline a method or fold a value. The one measurable effect is the compile-time inlining of static final constants shown earlier, which is a compilation behaviour rather than a speed-up you can rely on. Use final to state an intent, and let the JIT do performance.
Why did my constant not change after I edited it?
Because it was a compile-time constant and the classes that read it had already inlined the old value. Recompile everything that depends on it — mvn clean, gradle clean, or delete the output directory — and the new value appears. If a constant genuinely has to be changeable without recompiling its users, stop it being a compile-time constant by initialising it in a static block or from a method call.
Conclusion
this is the receiver: the object the instance method was called on, available as an ordinary reference you can pass, return, and use to disambiguate this.x = x. It does not exist in a static method, because a static method has no receiver.
static moves a member from the object to the class: one slot, created at class initialisation, reachable without an object, and shared by every object there will ever be. That makes it right for stateless helpers and true constants, and wrong for anything describing one particular object. Class initialisation is lazy and runs once, in source order across static field initialisers and static blocks, and a static final primitive or String is inlined into every caller — which is why editing one requires recompiling everything that reads it.
final binds a name once. On a local or parameter it forbids reassignment; on a field it forces exactly one assignment, by the initialiser or by every constructor; on a method it forbids overriding; on a class it forbids extension. What it never does is freeze the object at the other end of a reference, which is the single most common thing people believe about it.
Next in this series: encapsulation, getters, setters and access modifiers — what private, public and package-private actually control, why exposing a field is a decision you cannot take back, and what a getter is genuinely for.