☕ Java

final Keyword

The final keyword in Java means that something cannot be changed after it is first set. Applied to a variable, it means the variable cannot be reassigned after its initial assignment. Applied to a method, it means the method cannot be overridden by subclasses. Applied to a class, it means the class cannot be extended at all. final is the primary mechanism for communicating and enforcing immutability at three different granularities — variable, method, and class — and it plays a central role in writing safe, predictable, and thread-safe code.

final Variables — Assign Once, Never Change

A final variable can be assigned exactly once. After that single assignment, any attempt to reassign it is a compile error. This applies to local variables, method parameters, instance fields, and static fields — the semantics are the same in all cases: assign once, and the compiler guarantees no reassignment ever happens. For primitive final fields, the value is immutable — no operation can change the stored integer or double value. For reference final fields, the reference is immutable — the variable always points to the same object. But the object itself may still be mutable unless the object's class also enforces immutability. A final List is always the same List object, but the contents of that list can still change. This distinction between a final reference and a truly immutable value is one of the most important subtleties in Java's type system. Final fields in a class declare a strong design intent: this field will be set in the constructor and then never change for the lifetime of the object. This enables several optimisations and guarantees. The Java Memory Model provides a specific guarantee for final fields: once a constructor completes, all threads are guaranteed to see the correctly initialised values of all final fields, without any synchronisation. This is the foundation of immutable object thread safety. The compiler enforces that every final instance field is definitely assigned by the end of every constructor. If any constructor path could leave a final field unassigned, it is a compile error. This static enforcement means you can read a final field after construction with the certainty that it was always set.
Java
// ── final local variable — assigned once: ────────────────────────────
public static double calculateCircleArea(double radius) {
    final double PI = 3.14159265358979;  // assigned once
    // PI = 3.0;    // COMPILE ERROR — cannot reassign final variable
    return PI * radius * radius;
}

// ── final method parameter — cannot be reassigned inside method: ──────
public static int doubleValue(final int n) {
    // n = n * 2;  // COMPILE ERROR — n is final
    return n * 2;  // use n, don't reassign it
}

// ── final instance field — set in constructor, never changes: ─────────
public class Person {
    private final String name;      // must be set in every constructor path
    private final String id;
    private int age;                // not final — can change (birthday!)

    public Person(String name, int age) {
        this.name = name;           // ✓ first and only assignment
        this.id   = UUID.randomUUID().toString();  // ✓ set once
        this.age  = age;
    }

    public void haveBirthday() {
        age++;                  // ✓ age is not final — can change
        // name = "New Name";   // COMPILE ERROR — name is final
    }
}

// ── final reference vs mutable object — an important distinction: ─────
public class Team {
    private final List<String> members;   // final reference, mutable list

    public Team() {
        this.members = new ArrayList<>();  // assigned once in constructor
    }

    public void addMember(String name) {
        members.add(name);   // ✓ the LIST changes, but members still points
                             // to the same ArrayList — reference is unchanged
    }

    public void replaceRoster(List<String> newList) {
        // members = newList;  // COMPILE ERROR — cannot reassign final field
    }
}

// ── final static field — named constant: ─────────────────────────────
public class Config {
    public static final int    MAX_CONNECTIONS = 100;
    public static final String DEFAULT_HOST    = "localhost";
    public static final double VERSION         = 2.5;

    // Convention: static final constants use SCREAMING_SNAKE_CASE
}

System.out.println(Config.MAX_CONNECTIONS);  // 100
// Config.MAX_CONNECTIONS = 200;  // COMPILE ERROR

Blank final and Effectively final

A blank final is a final field that is not initialised in its declaration but is guaranteed to be initialised by every constructor path. This is useful when the initial value depends on constructor parameters or requires logic that cannot be expressed in a field initialiser. The compiler performs definite assignment analysis to verify that every constructor path assigns the final field exactly once. Effectively final is a different concept introduced in Java 8 alongside lambda expressions. A local variable is effectively final if it is never reassigned after its initial assignment — even if the final keyword is not explicitly written. Lambda expressions and anonymous classes can capture local variables only if those variables are final or effectively final. This rule exists because lambdas may execute in a different thread or at a different time than when the variable was created; if the variable could change, the lambda might see an inconsistent value.
Java
// ── Blank final — initialised in constructor: ────────────────────────
public class Triangle {
    private final double sideA;
    private final double sideB;
    private final double sideC;
    private final double perimeter;     // blank final — computed in constructor

    public Triangle(double a, double b, double c) {
        if (a <= 0 || b <= 0 || c <= 0)
            throw new IllegalArgumentException("Sides must be positive");
        if (a + b <= c || a + c <= b || b + c <= a)
            throw new IllegalArgumentException(
                "Triangle inequality violated");

        this.sideA     = a;
        this.sideB     = b;
        this.sideC     = c;
        this.perimeter = a + b + c;     // computed — must be set here
    }

    public double getPerimeter() { return perimeter; }
    public double area() {
        double s = perimeter / 2;
        return Math.sqrt(s * (s-sideA) * (s-sideB) * (s-sideC));
    }
}

// ── Compiler enforces blank final is set in ALL paths: ────────────────
public class Result {
    private final String status;

    public Result(boolean success) {
        if (success) {
            status = "SUCCESS";
            // If this was the only assignment and there was no else,
            // the compiler would error: variable 'status' might not be
            // initialised if success is false.
        } else {
            status = "FAILURE";
        }
        // Both paths set status — compiler is satisfied.
    }
}

// ── Effectively final — local variable never reassigned: ──────────────
int threshold = 10;         // effectively final — never reassigned below

List<Integer> numbers = List.of(1, 5, 15, 8, 12, 3);
long count = numbers.stream()
    .filter(n -> n > threshold)   // ✓ lambda captures threshold
    .count();
System.out.println(count);        // 3 (15, 12 are > 10... wait: 15,12 = 2, no: 15>10, 12>102)

// If threshold were reassigned, the capture would fail:
// threshold = 20;    // adding this line would make threshold NOT effectively final
// .filter(n -> n > threshold)  // COMPILE ERROR — threshold is not effectively final

// ── final parameter in lambda context: ────────────────────────────────
public void processItems(List<String> items, final String prefix) {
    items.forEach(item -> System.out.println(prefix + item));
    // prefix is final — lambda capture is safe
}

final Methods — Preventing Override

A final method cannot be overridden by any subclass. When a subclass attempts to override a final method, the compiler produces an error. This is a design statement from the class author: the behaviour of this method is fixed and is part of the class's contract that subclasses must honour, not change. final methods are appropriate in two situations. First, when a method's implementation must remain consistent for the correctness of other methods in the class — if a template method calls helper methods that are meant to be consistent with each other, making them final prevents subclasses from partially overriding the logic and breaking the invariants. Second, when security or safety depends on a known behaviour — in security-sensitive code, a method that performs authentication or access control might be final to prevent a subclass from weakening the check. The Java standard library uses final methods sparingly but deliberately. The getClass() method in Object is final because the identity of an object's class must be immutable and cannot be spoofed by a subclass. The wait() and notify() methods in Object are final because their semantics are bound to the intrinsic lock mechanism and cannot be replaced. Private methods are implicitly final — they cannot be overridden because subclasses cannot even see them. Similarly, static methods cannot be overridden (only hidden), so making them final has limited practical effect, though it is permitted.
Java
// ── final method prevents override: ──────────────────────────────────
public class Payment {
    private double amount;
    private String currency;

    public Payment(double amount, String currency) {
        this.amount   = amount;
        this.currency = currency;
    }

    // Template method — calls process() and then always audits:
    public final void execute() {
        validateAmount();       // check before processing
        process();              // subclass-specific processing
        audit();                // always audited — cannot be bypassed
    }

    // final — audit MUST happen this way for compliance:
    private final void audit() {
        System.out.printf("AUDIT: %s %.2f processed at %s%n",
            currency, amount, java.time.Instant.now());
    }

    // final — amount validation must not be weakened:
    private final void validateAmount() {
        if (amount <= 0) throw new IllegalArgumentException(
            "Payment amount must be positive: " + amount);
    }

    // Not final — subclasses define HOW to process:
    protected void process() {
        System.out.println("Generic payment processing");
    }

    public double getAmount()   { return amount; }
    public String getCurrency() { return currency; }
}

public class CreditCardPayment extends Payment {
    private String cardNumber;

    public CreditCardPayment(double amount, String currency, String card) {
        super(amount, currency);
        this.cardNumber = card;
    }

    @Override
    protected void process() {
        System.out.println("Charging card ending in " +
            cardNumber.substring(cardNumber.length() - 4));
    }

    // @Override
    // private void audit() { }   // COMPILE ERROR — audit() is final
    //
    // @Override
    // public void execute() { }  // COMPILE ERROR — execute() is final
}

CreditCardPayment payment = new CreditCardPayment(99.99, "GBP", "4242424242424242");
payment.execute();
// Charging card ending in 4242
// AUDIT: GBP 99.99 processed at 2025-05-30T12:00:00Z

final Classes — Preventing Inheritance

A final class cannot be extended. No class can declare extends FinalClass. This is the strongest immutability guarantee: not only is the class's own implementation fixed, but no subclass can change its behaviour through inheritance. Java's String class is the most important example — it is final precisely because the entire Java platform relies on the behaviour of String being predictable and unmodifiable. If String could be subclassed, a malicious or careless subclass could override equals(), hashCode(), or compareTo() and break the assumptions of every data structure that uses strings as keys. Making a class final is appropriate when the class is a complete, standalone value with well-defined semantics (String, Integer, BigDecimal), when the class provides a security guarantee that must not be weakened by inheritance, or when the class is an immutable record-like type where extension would not make semantic sense. The Java standard library's wrapper classes (Integer, Long, Double, Boolean, Character) are all final for these reasons. Final classes and immutable classes are related but distinct concepts. A final class prevents subclassing. An immutable class has state that cannot change after construction. Many classes are both (String is final and immutable), but they can exist independently — a final class can have mutable fields, and an immutable class can be non-final (though allowing subclasses of an immutable class is risky because the subclass might add mutable state or override methods to violate the parent class's immutability contract).
Java
// ── final class — cannot be subclassed: ──────────────────────────────
public final class Coordinate {
    private final double latitude;
    private final double longitude;

    public Coordinate(double latitude, double longitude) {
        if (latitude  < -90  || latitude  > 90)
            throw new IllegalArgumentException(
                "Latitude must be -90 to 90: " + latitude);
        if (longitude < -180 || longitude > 180)
            throw new IllegalArgumentException(
                "Longitude must be -180 to 180: " + longitude);
        this.latitude  = latitude;
        this.longitude = longitude;
    }

    public double getLatitude()  { return latitude;  }
    public double getLongitude() { return longitude; }

    public double distanceTo(Coordinate other) {
        // Haversine formula simplified:
        double dLat = Math.toRadians(other.latitude  - this.latitude);
        double dLon = Math.toRadians(other.longitude - this.longitude);
        double a = Math.sin(dLat/2) * Math.sin(dLat/2)
                 + Math.cos(Math.toRadians(this.latitude))
                 * Math.cos(Math.toRadians(other.latitude))
                 * Math.sin(dLon/2) * Math.sin(dLon/2);
        return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    }

    @Override
    public String toString() {
        return String.format("(%.6f, %.6f)", latitude, longitude);
    }
}

// public class FuzzyCoordinate extends Coordinate { }  // COMPILE ERROR

Coordinate london  = new Coordinate(51.5074, -0.1278);
Coordinate paris   = new Coordinate(48.8566,  2.3522);
System.out.printf("Distance: %.1f km%n", london.distanceTo(paris));  // 340.4 km

// ── Why String is final: ──────────────────────────────────────────────
// If String were NOT final, an attacker could write:
// public class EvilString extends String {
//     @Override
//     public boolean equals(Object o) { return true; }  // always equal!
//     @Override
//     public int hashCode() { return 0; }  // all in same hash bucket!
// }
// This would break HashMap, HashSet, security checks, and comparisons.
// By making String final, Java prevents this attack.

// ── final class is still inheritable from Object: ────────────────────
// All classes implicitly extend Object, including final classes.
// final prevents OTHER classes from extending it, but
// the final class itself still inherits from Object.
Coordinate c = new Coordinate(0, 0);
System.out.println(c.getClass());    // uses Object's getClass() — fine
System.out.println(c.toString());    // uses Coordinate's toString()

final, Immutability, and Thread Safety

The connection between final and thread safety is one of the most important and least-understood aspects of the Java Memory Model. The Java Language Specification provides a specific guarantee: a thread that obtains a reference to an object after its constructor has completed is guaranteed to see the correctly initialised values of all final fields of that object, without any explicit synchronisation. This guarantee is what makes immutable objects inherently thread-safe. If all fields of an object are final (and the constructor does not let this escape), then the object can be freely shared between threads without any synchronisation — no volatile, no synchronized, no locks required. Multiple threads can read the fields simultaneously and will all see the correct values. Mutable objects, even with final references, do not receive this guarantee. A final List reference guarantees that the variable always points to the same List, but changes to the list's contents require explicit synchronisation to be visible across threads. The Java Memory Model is complex, but the practical takeaway is straightforward: make all fields final, never let this escape from the constructor, and your object is automatically thread-safe — a powerful property that requires no additional effort beyond good design.
Java
// ── Immutable class — all final fields, thread-safe by design: ────────
public final class ImmutableUser {
    private final long     id;
    private final String   username;
    private final String   email;
    private final Set<String> roles;    // defensive copy — see below

    public ImmutableUser(long id, String username, String email,
                          Set<String> roles) {
        this.id       = id;
        this.username = Objects.requireNonNull(username, "username required");
        this.email    = Objects.requireNonNull(email,    "email required");
        // Defensive copy + wrap in unmodifiable — protects against
        // caller modifying the set after passing it in:
        this.roles    = Collections.unmodifiableSet(
            new HashSet<>(Objects.requireNonNull(roles, "roles required")));
    }

    public long      getId()       { return id;       }
    public String    getUsername() { return username; }
    public String    getEmail()    { return email;    }
    public Set<String> getRoles()  { return roles;    }  // unmodifiable view

    // "Wither" methods — return new object rather than mutating:
    public ImmutableUser withEmail(String newEmail) {
        return new ImmutableUser(id, username, newEmail, roles);
    }

    public ImmutableUser withRole(String newRole) {
        Set<String> newRoles = new HashSet<>(roles);
        newRoles.add(newRole);
        return new ImmutableUser(id, username, email, newRoles);
    }
}

// ── Thread safety without synchronisation: ────────────────────────────
ImmutableUser user = new ImmutableUser(1L, "alice", "alice@example.com",
                                        Set.of("READ", "WRITE"));

// Share this object with 1000 threads — perfectly safe:
// Every thread will see the correct values of id, username, email, roles.
// No locks, no volatile, no synchronised — the final fields guarantee it.

ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
    pool.submit(() -> {
        System.out.println(user.getUsername());  // always "alice" — safe
    });
}

// ── The Java Memory Model guarantee for final fields: ─────────────────
// "An object is considered to be completely initialized when its
//  constructor finishes. A thread that can only see a reference to an
//  object after that object has been completely initialized is guaranteed
//  to see the correctly initialized values for that object's final fields."
//
// Practical consequence:
//   - All final fields = thread-safe without synchronisation
//   - Any non-final field = requires synchronisation for safe sharing
//   - Even one non-final field breaks the guarantee for the whole object

Related Topics in Object-Oriented Programming

OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.