π Bootcamp Note: This topic came up during Session 3 while we were debugging the WCC platform project. It's a foundational Java concept that even experienced engineers get asked about in interviews. In Java 21, understanding
finalalso means knowing when not to use it β because modern Java gives you better tools for some of the same problems.
The final keyword is about defining contracts β it tells the compiler and other developers: "this should not be changed, extended, or overridden." Where you use it changes exactly what is being locked down.
There are four places you can use final:
| Where | What it locks |
|---|---|
| Variable | Cannot be reassigned |
| Parameter | Cannot be changed inside the method |
| Method | Cannot be overridden by a subclass |
| Class | Cannot be extended (inherited) |
When you mark a variable as final, you're saying: you cannot reassign it after it's set.
final int year = 2026;
year = 2027; // β Compiler error β cannot reassign a final variableFor objects, final prevents you from pointing to a new object β but you can still change the object's internal state:
final List<String> participants = new ArrayList<>();
participants = new ArrayList<>(); // β Cannot reassign the reference
participants.add("Olena"); // β
Can still modify the contentsThink of it like this: final locks the address in memory, not the content at that address.
The most common use of final in real projects is paired with static to define constants β values that belong to the class itself and never change.
public class AppConfig {
public static final int MAX_RETRIES = 3;
public static final String API_VERSION = "v1";
}By convention, constants use UPPER_SNAKE_CASE. You'll see this pattern everywhere in Java code β including in the WCC platform project.
// Usage
if (retryCount > AppConfig.MAX_RETRIES) {
throw new RuntimeException("Too many retries");
}When a parameter is marked final, it cannot be reassigned inside the method.
public void greet(final String name) {
name = "Someone else"; // β Compiler error
System.out.println("Hello, " + name);
}It protects the caller's contract and prevents accidental mutation of an incoming value inside long methods. It's also a useful signal to readers: "I'm using this value as-is."
π‘ Best practice note: Using
finalon parameters is optional and sometimes considered noise by teams. Some style guides encourage it for clarity; others skip it to reduce verbosity. Check your team's conventions β in our WCC project, we tend to be explicit.
When a method is final, subclasses cannot override it. This is used to lock critical behaviour in place even when a class is extendable.
public class Member {
public final String getAccountId() {
return accountId; // No subclass can change how this works
}
}
public class Mentor extends Member {
@Override
public String getAccountId() { // β Compiler error β cannot override final method
return "custom-id";
}
}- When a method implements core logic that must stay consistent across all subclasses
- When the method is part of a security or integrity contract
- Java's own
Stringclass uses this extensively
When a class is final, no other class can extend it.
public final class Member {
// ...
}
public class Mentor extends Member { // β Compiler error β Member is final
// ...
}Java's own String class is final β that's why you can always trust how String behaves; no one can secretly subclass it and change its behaviour.
Java 21 introduced features that handle some traditional final use cases more elegantly. Understanding the relationship is important.
Introduced in Java 16, Records are data-carrying classes where all fields are automatically final. You don't need to write final yourself.
// Old way β manual final fields
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
// equals(), hashCode(), toString() β you'd write all of these too
}
// Java 21 way β Record does it all
public record Point(int x, int y) {}
// x and y are implicitly final
// Constructor, getters, equals(), hashCode(), toString() auto-generatedRecords are also implicitly final classes β they can't be extended. Use records when you need a simple, immutable data container (DTOs, API responses, value objects).
// Example from our bootcamp context
public record MentorSummary(String name, String socialNetwork, String profileUrl) {}Before Java 17, if you wanted to control inheritance, your only option was final (no subclasses at all) or leaving the class open to anyone. Sealed classes give you a middle ground: you choose exactly which classes can extend yours.
// final β blocks all inheritance
public final class Shape { } // No subclasses allowed at all
// sealed β controlled inheritance (Java 17+, stable in Java 21)
public sealed class Shape permits Circle, Rectangle, Triangle {
// Only Circle, Rectangle, and Triangle can extend Shape
}
public final class Circle extends Shape {
private final double radius;
// ...
}
public final class Rectangle extends Shape {
private final double width;
private final double height;
// ...
}Subclasses of a sealed class must declare one of:
finalβ no further subclassessealedβ further controlled subclassesnon-sealedβ open again for anyone to extend
Sealed classes work beautifully with pattern matching in switch (also finalised in Java 21). The compiler knows all possible subtypes, so it can check you haven't missed a case:
public double calculateArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
// No default needed β compiler knows these are ALL the shapes
};
}This is a pattern you'll start seeing in modern Spring Boot projects.
These are not the same thing, and confusing them is a common mistake.
final |
Immutability | |
|---|---|---|
| What it protects | The reference (the address) | The content (the values inside) |
| Can the internal state change? | Yes, if it's an object | No |
| Example | final List<String> names |
String, Integer, Records |
// final β the reference is locked, but contents can change
final List<String> names = new ArrayList<>();
names.add("Adriana"); // β
Allowed
names = new ArrayList<>(); // β Not allowed
// Immutable β the content itself cannot change
String name = "Adriana";
name.replace("A", "B"); // Creates a NEW string, original unchangedTo create a truly immutable class in Java, you need:
- Declare the class
final - Make all fields
private final - No setters
- Return copies (not references) of mutable objects from getters
Or simply β use a Record. It does all of the above for you.
- Constants β
public static final String BASE_URL = "..."Always. - Local variables you won't reassign β makes intent clear and code easier to follow
- Method parameters in complex methods β prevents accidental mutation
- Methods with critical logic β when a subclass should never change a specific behaviour
- Classes that must never be subclassed β utility classes, value objects
- You need testability β
finalclasses and methods cannot be mocked easily in unit tests. Be deliberate before making something final in production code. - You want controlled (not blocked) inheritance β use
sealedinstead - You're adding
finalto every parameter without reason β it becomes noise and reduces readability
| Old approach | Modern Java 21 approach |
|---|---|
Class with all private final fields + constructor |
record |
final class when you want some subclasses |
sealed class |
| Manual constants with verbose setup | static final (still the right tool here) |
// 1. Static constant
public static final int MAX_CONNECTIONS = 10;
// 2. Instance variable
private final String id;
// 3. Local variable
final String prefix = "WCC-";
// 4. Method parameter
public void process(final String input) { ... }
// 5. Method
public final String getId() { return id; }
// 6. Class
public final class BootcampConfig { ... }Experiment with each use case. Uncomment the lines to see what errors the compiler gives you β reading compiler errors is part of learning Java!
import java.util.ArrayList;
import java.util.List;
public class FinalExplorer {
// Constant β the Java convention for constants
public static final String BOOTCAMP_NAME = "WCC Java Bootcamp";
public static void main(String[] args) {
// 1. Final variable
final int year = 2026;
// year = 2027; // β Uncomment to see compiler error
// 2. Final with a list β reference is locked, content is not
final List<String> participants = new ArrayList<>();
participants.add("Olena");
participants.add("Lulu");
participants.add("Val");
// participants = new ArrayList<>(); // β Uncomment to see compiler error
System.out.println(BOOTCAMP_NAME + " - " + year);
System.out.println(participants);
}
}
// 3. Record β implicitly final fields + implicitly final class
record SessionNote(String topic, String presenter) {}
// 4. Sealed class β controlled inheritance
sealed class Participant permits Mentor, Mentee {}
final class Mentor extends Participant {}
final class Mentee extends Participant {}
// class RandomPerson extends Participant {} // β Uncomment to see compiler errorPush your experiment to your participant folder and note the errors! π
π‘ Interview tip: A strong answer to "Explain the
finalkeyword in Java" covers all four uses (variable, parameter, method, class), explains thefinalvs immutability distinction, and β in 2025+ interviews β mentions Records and Sealed Classes as modern alternatives. That's a senior-level answer.