The static keyword in Java means “belonging to the class itself, not to any particular instance.” When you declare a variable, method, block, or inner class as static, it is associated with the class as a whole rather than with individual objects created from that class.
Real-world analogy: Think of a school classroom. Every student (instance) has their own notebook (instance variable), but the whiteboard on the wall (static variable) is shared by everyone in the class. There is only one whiteboard, and if the teacher writes something on it, every student can see it. You do not need a specific student to access the whiteboard — you just walk into the classroom and look at it.
This is exactly how static works in Java. A static member belongs to the classroom (the class), not to any individual student (object). It exists in memory once, it is loaded when the class is first loaded by the JVM, and it can be accessed without creating any object.
Here are the key characteristics of the static keyword:
this referenceThe static keyword can be applied to four things in Java:
| Applied To | What It Creates | Example |
|---|---|---|
| Variable | Class variable (shared by all instances) | static int count; |
| Method | Class method (callable without an object) | static void printInfo() |
| Block | Static initializer (runs once at class loading) | static { ... } |
| Inner Class | Static nested class (no outer instance needed) | static class Entry { } |
A static variable (also called a class variable) is a variable declared with the static keyword inside a class but outside any method or constructor. Unlike instance variables, which get their own copy for each object, a static variable has exactly one copy shared across all instances of the class.
Static variables are stored in the method area (or metaspace in modern JVMs), not on the heap with individual objects. They are initialized when the class is first loaded, and they persist for the entire lifetime of the application.
When to use static variables:
final)Let us start with a simple example that demonstrates the difference between static and instance variables:
public class Employee {
// Instance variable -- each Employee gets its own copy
private String name;
// Static variable -- shared across ALL Employee objects
private static int employeeCount = 0;
// Static variable -- company name is the same for all employees
private static String companyName = "TechCorp";
public Employee(String name) {
this.name = name;
employeeCount++; // Increment the shared counter
}
public String getName() {
return name;
}
public static int getEmployeeCount() {
return employeeCount;
}
public static String getCompanyName() {
return companyName;
}
public static void main(String[] args) {
System.out.println("Employees before: " + Employee.getEmployeeCount());
// Output: Employees before: 0
Employee emp1 = new Employee("Alice");
Employee emp2 = new Employee("Bob");
Employee emp3 = new Employee("Charlie");
System.out.println("Employees after: " + Employee.getEmployeeCount());
// Output: Employees after: 3
// All employees share the same company name
System.out.println(emp1.getName() + " works at " + Employee.getCompanyName());
// Output: Alice works at TechCorp
System.out.println(emp2.getName() + " works at " + Employee.getCompanyName());
// Output: Bob works at TechCorp
}
}
In the example above, employeeCount is incremented every time a new Employee is created. Because it is static, all three objects share the same counter. If it were an instance variable, each object would have its own counter stuck at 1.
Here is a memory visualization to make this concrete:
| Memory Area | Variable | Value |
|---|---|---|
| Method Area (shared) | Employee.employeeCount |
3 |
| Method Area (shared) | Employee.companyName |
“TechCorp” |
| Heap (emp1 object) | name |
“Alice” |
| Heap (emp2 object) | name |
“Bob” |
| Heap (emp3 object) | name |
“Charlie” |
A common real-world use of static variables is generating unique IDs:
public class Order {
private static int nextOrderId = 1000; // Shared counter for ID generation
private final int orderId;
private final String product;
private final double amount;
public Order(String product, double amount) {
this.orderId = nextOrderId++; // Assign and increment atomically
this.product = product;
this.amount = amount;
}
@Override
public String toString() {
return "Order{id=" + orderId + ", product='" + product + "', amount=$" + amount + "}";
}
public static void main(String[] args) {
Order order1 = new Order("Laptop", 999.99);
Order order2 = new Order("Mouse", 29.99);
Order order3 = new Order("Keyboard", 79.99);
System.out.println(order1); // Order{id=1000, product='Laptop', amount=$999.99}
System.out.println(order2); // Order{id=1001, product='Mouse', amount=$29.99}
System.out.println(order3); // Order{id=1002, product='Keyboard', amount=$79.99}
}
}
Important caveat: In a multi-threaded application, the nextOrderId++ pattern above is not thread-safe because the increment operation is not atomic. In production code, you would use AtomicInteger instead:
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadSafeOrder {
// Thread-safe static counter
private static final AtomicInteger nextOrderId = new AtomicInteger(1000);
private final int orderId;
public ThreadSafeOrder() {
this.orderId = nextOrderId.getAndIncrement();
}
public int getOrderId() {
return orderId;
}
}
A static method belongs to the class, not to any instance. You call it using the class name, and it does not have access to this or any instance variables/methods. Static methods can only directly access other static members.
Rules for static methods:
ClassName.methodName()this or super keywordsYou use static methods every day in Java without realizing it. Math.sqrt(), Integer.parseInt(), Collections.sort(), Arrays.asList() — these are all static methods. You never create a Math object to calculate a square root.
One of the most common uses of static methods is the utility class — a class that contains only static methods and has no state. Utility classes are never instantiated.
public final class StringUtils {
// Private constructor prevents instantiation
private StringUtils() {
throw new UnsupportedOperationException("Utility class cannot be instantiated");
}
/**
* Checks if a string is null or empty (after trimming whitespace).
*/
public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}
/**
* Capitalizes the first letter of a string.
*/
public static String capitalize(String str) {
if (isBlank(str)) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase();
}
/**
* Reverses a string.
*/
public static String reverse(String str) {
if (str == null) {
return null;
}
return new StringBuilder(str).reverse().toString();
}
/**
* Truncates a string to the specified length, adding "..." if truncated.
*/
public static String truncate(String str, int maxLength) {
if (str == null || str.length() <= maxLength) {
return str;
}
return str.substring(0, maxLength) + "...";
}
public static void main(String[] args) {
System.out.println(StringUtils.isBlank("")); // true
System.out.println(StringUtils.isBlank(" ")); // true
System.out.println(StringUtils.isBlank("hello")); // false
System.out.println(StringUtils.capitalize("hello")); // Hello
System.out.println(StringUtils.reverse("Java")); // avaJ
System.out.println(StringUtils.truncate("Hello, World!", 5)); // Hello...
}
}
Notice the key patterns in the utility class above: the class is final (cannot be extended), the constructor is private (cannot be instantiated), and all methods are static.
Another important use of static methods is the factory method pattern. Instead of calling new directly, you call a static method that creates and returns an object. This has several advantages over constructors: you can give the method a descriptive name, you can return a cached instance, and you can return a subclass.
public class DatabaseConnection {
private final String host;
private final int port;
private final String database;
private final boolean ssl;
// Private constructor -- clients cannot call new DatabaseConnection()
private DatabaseConnection(String host, int port, String database, boolean ssl) {
this.host = host;
this.port = port;
this.database = database;
this.ssl = ssl;
}
// Static factory method -- descriptive name tells you what it creates
public static DatabaseConnection forProduction(String database) {
return new DatabaseConnection("prod-db.company.com", 5432, database, true);
}
// Another static factory method
public static DatabaseConnection forDevelopment(String database) {
return new DatabaseConnection("localhost", 5432, database, false);
}
// Another static factory method
public static DatabaseConnection forTesting() {
return new DatabaseConnection("localhost", 5432, "test_db", false);
}
@Override
public String toString() {
return String.format("Connection{host='%s', port=%d, db='%s', ssl=%s}",
host, port, database, ssl);
}
public static void main(String[] args) {
DatabaseConnection prod = DatabaseConnection.forProduction("users_db");
DatabaseConnection dev = DatabaseConnection.forDevelopment("users_db");
DatabaseConnection test = DatabaseConnection.forTesting();
System.out.println(prod);
// Connection{host='prod-db.company.com', port=5432, db='users_db', ssl=true}
System.out.println(dev);
// Connection{host='localhost', port=5432, db='users_db', ssl=false}
System.out.println(test);
// Connection{host='localhost', port=5432, db='test_db', ssl=false}
}
}
This is one of the most common compiler errors beginners encounter. A static method exists without any object, so there is no this -- and therefore no way to know which object's instance variables to access.
public class StaticAccessDemo {
private String instanceVar = "I belong to an instance";
private static String staticVar = "I belong to the class";
// STATIC method
public static void staticMethod() {
System.out.println(staticVar); // OK -- accessing static from static
// System.out.println(instanceVar); // COMPILE ERROR: non-static variable
// // cannot be referenced from a static context
// System.out.println(this); // COMPILE ERROR: 'this' cannot be
// // referenced from a static context
}
// INSTANCE method -- can access everything
public void instanceMethod() {
System.out.println(instanceVar); // OK -- accessing instance from instance
System.out.println(staticVar); // OK -- accessing static from instance
staticMethod(); // OK -- calling static from instance
}
public static void main(String[] args) {
// Static method -- called on the class
StaticAccessDemo.staticMethod();
// Instance method -- requires an object
StaticAccessDemo obj = new StaticAccessDemo();
obj.instanceMethod();
}
}
A static block (also called a static initializer) is a block of code enclosed in static { } that runs exactly once when the class is first loaded by the JVM. It runs before any constructor, before any static method is called, and before any object is created.
Static blocks are used for complex initialization of static variables -- things that cannot be done in a single line, such as loading configuration files, registering database drivers, populating lookup tables, or handling exceptions during initialization.
Key facts about static blocks:
import java.util.*;
public class CountryLookup {
// Static map that will be populated in the static block
private static final Map COUNTRY_CODES;
private static final Map COUNTRY_NAMES;
// Static block -- runs once when CountryLookup class is loaded
static {
System.out.println("Static block 1: Initializing country codes...");
Map codes = new HashMap<>();
codes.put("US", "United States");
codes.put("GB", "United Kingdom");
codes.put("DE", "Germany");
codes.put("FR", "France");
codes.put("JP", "Japan");
codes.put("AU", "Australia");
codes.put("BR", "Brazil");
codes.put("IN", "India");
COUNTRY_CODES = Collections.unmodifiableMap(codes);
}
// Second static block -- runs after the first one
static {
System.out.println("Static block 2: Building reverse lookup...");
Map names = new HashMap<>();
for (Map.Entry entry : COUNTRY_CODES.entrySet()) {
names.put(entry.getValue().toLowerCase(), entry.getKey());
}
COUNTRY_NAMES = Collections.unmodifiableMap(names);
}
public static String getCountryName(String code) {
return COUNTRY_CODES.getOrDefault(code.toUpperCase(), "Unknown");
}
public static String getCountryCode(String name) {
return COUNTRY_NAMES.getOrDefault(name.toLowerCase(), "Unknown");
}
public static void main(String[] args) {
// The static blocks run BEFORE main executes
System.out.println("--- main() starts ---");
System.out.println(getCountryName("US")); // United States
System.out.println(getCountryName("JP")); // Japan
System.out.println(getCountryCode("Germany")); // DE
System.out.println(getCountryCode("Brazil")); // BR
}
}
// Output:
// Static block 1: Initializing country codes...
// Static block 2: Building reverse lookup...
// --- main() starts ---
// United States
// Japan
// DE
// BR
Notice in the output above that both static blocks ran before main() started executing. This is because main() is a static method of the class, and the class must be loaded before any of its methods can be called.
Here is a practical example showing a common real-world use case -- loading a JDBC driver and reading a configuration file:
import java.io.*;
import java.util.Properties;
public class AppConfig {
private static final Properties CONFIG = new Properties();
private static final String APP_VERSION;
static {
// Load configuration file
try (InputStream input = AppConfig.class
.getClassLoader()
.getResourceAsStream("application.properties")) {
if (input == null) {
System.err.println("Warning: application.properties not found, using defaults");
CONFIG.setProperty("db.host", "localhost");
CONFIG.setProperty("db.port", "5432");
CONFIG.setProperty("db.name", "myapp");
CONFIG.setProperty("app.version", "1.0.0-default");
} else {
CONFIG.load(input);
}
} catch (IOException e) {
throw new ExceptionInInitializerError("Failed to load configuration: " + e.getMessage());
}
APP_VERSION = CONFIG.getProperty("app.version", "unknown");
}
public static String get(String key) {
return CONFIG.getProperty(key);
}
public static String get(String key, String defaultValue) {
return CONFIG.getProperty(key, defaultValue);
}
public static String getVersion() {
return APP_VERSION;
}
}
Understanding the execution order is critical for debugging initialization issues:
public class InitializationOrder {
// 1. Static variable initialization (in order)
private static String staticVar = initStaticVar();
// 2. Static block (in order with other static blocks)
static {
System.out.println("2. Static block runs");
}
// 4. Instance variable initialization (in order)
private String instanceVar = initInstanceVar();
// 5. Instance initializer block
{
System.out.println("5. Instance initializer block runs");
}
// 6. Constructor
public InitializationOrder() {
System.out.println("6. Constructor runs");
}
private static String initStaticVar() {
System.out.println("1. Static variable initialized");
return "static";
}
private String initInstanceVar() {
System.out.println("4. Instance variable initialized");
return "instance";
}
public static void main(String[] args) {
System.out.println("3. main() starts");
System.out.println("--- Creating first object ---");
new InitializationOrder();
System.out.println("--- Creating second object ---");
new InitializationOrder();
}
}
// Output:
// 1. Static variable initialized
// 2. Static block runs
// 3. main() starts
// --- Creating first object ---
// 4. Instance variable initialized
// 5. Instance initializer block runs
// 6. Constructor runs
// --- Creating second object ---
// 4. Instance variable initialized
// 5. Instance initializer block runs
// 6. Constructor runs
Notice that the static members (steps 1 and 2) run only once, while the instance members (steps 4, 5, and 6) run every time a new object is created.
A static inner class (formally called a static nested class) is a class defined inside another class with the static modifier. Unlike a regular inner class, it does not need an instance of the outer class to be created. It is essentially a top-level class that is logically grouped inside another class for organizational purposes.
Key differences between static and non-static inner classes:
| Feature | Static Nested Class | Inner Class (Non-Static) |
|---|---|---|
| Outer instance required? | No | Yes |
| Can access outer instance members? | No (only static members) | Yes (all members) |
| Creation syntax | new Outer.Inner() |
outer.new Inner() |
| Has reference to outer class? | No | Yes (hidden reference) |
| Memory leak risk? | Low | Higher (holds outer reference) |
The most famous example in the Java standard library is Map.Entry. You do not need a Map instance to create an Entry -- it is a static nested interface inside Map.
One of the most common uses in application code is the Builder pattern:
public class HttpRequest {
private final String url;
private final String method;
private final Map headers;
private final String body;
private final int timeoutMs;
// Private constructor -- only the Builder can create HttpRequest objects
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = Collections.unmodifiableMap(builder.headers);
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
}
// Static nested class -- does NOT need an HttpRequest instance to exist
public static class Builder {
// Required
private final String url;
// Optional with defaults
private String method = "GET";
private Map headers = new HashMap<>();
private String body = null;
private int timeoutMs = 30000;
public Builder(String url) {
this.url = url;
}
public Builder method(String method) {
this.method = method;
return this;
}
public Builder header(String key, String value) {
this.headers.put(key, value);
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeout(int ms) {
this.timeoutMs = ms;
return this;
}
public HttpRequest build() {
return new HttpRequest(this);
}
}
@Override
public String toString() {
return method + " " + url + " (timeout=" + timeoutMs + "ms, headers=" + headers + ")";
}
public static void main(String[] args) {
// Builder is created without an HttpRequest instance
HttpRequest request = new HttpRequest.Builder("https://api.example.com/users")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"name\": \"Alice\"}")
.timeout(5000)
.build();
System.out.println(request);
// POST https://api.example.com/users (timeout=5000ms, headers={Authorization=Bearer token123, Content-Type=application/json})
}
}
Here is another practical example -- a linked list node as a static nested class:
public class LinkedList{ private Node head; private int size; // Static nested class -- Node does not need access to LinkedList's instance members // The type parameter is independent of LinkedList's type parameter private static class Node { E data; Node next; Node(E data) { this.data = data; this.next = null; } } public void addFirst(T element) { Node newNode = new Node<>(element); newNode.next = head; head = newNode; size++; } public T getFirst() { if (head == null) { throw new NoSuchElementException("List is empty"); } return head.data; } public int size() { return size; } public static void main(String[] args) { LinkedList list = new LinkedList<>(); list.addFirst("Charlie"); list.addFirst("Bob"); list.addFirst("Alice"); System.out.println("First: " + list.getFirst()); // First: Alice System.out.println("Size: " + list.size()); // Size: 3 } }
A static import allows you to use static members (fields and methods) of a class without qualifying them with the class name. Instead of writing Math.PI and Math.sqrt(), you can write PI and sqrt() directly.
Static imports were introduced in Java 5. They are useful for reducing verbosity when you frequently use static constants or utility methods from a specific class.
Syntax:
import static java.lang.Math.PI; -- imports a single static memberimport static java.lang.Math.*; -- imports all static members from the class// Without static import
public class CircleCalcVerbose {
public static void main(String[] args) {
double radius = 5.0;
double area = Math.PI * Math.pow(radius, 2);
double circumference = 2 * Math.PI * radius;
double diagonal = Math.sqrt(Math.pow(radius, 2) + Math.pow(radius, 2));
System.out.println("Area: " + area);
System.out.println("Circumference: " + circumference);
System.out.println("Diagonal: " + diagonal);
}
}
// With static import -- much cleaner for math-heavy code
import static java.lang.Math.PI;
import static java.lang.Math.pow;
import static java.lang.Math.sqrt;
public class CircleCalcClean {
public static void main(String[] args) {
double radius = 5.0;
double area = PI * pow(radius, 2);
double circumference = 2 * PI * radius;
double diagonal = sqrt(pow(radius, 2) + pow(radius, 2));
System.out.println("Area: " + area);
System.out.println("Circumference: " + circumference);
System.out.println("Diagonal: " + diagonal);
}
}
Static imports are also very common in unit tests with frameworks like JUnit and AssertJ:
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class CalculatorTest {
@Test
void testAddition() {
Calculator calc = new Calculator();
// Without static import: Assertions.assertEquals(5, calc.add(2, 3));
assertEquals(5, calc.add(2, 3));
assertTrue(calc.add(2, 3) > 0);
}
@Test
void testDivisionByZero() {
Calculator calc = new Calculator();
assertThrows(ArithmeticException.class, () -> calc.divide(10, 0));
}
}
Readability trade-offs: Static imports improve readability when the origin of the method is obvious (like PI, assertEquals), but they hurt readability when it is unclear where a method comes from. Use them sparingly and avoid wildcard imports (*) in production code.
| Good Use | Bad Use |
|---|---|
import static java.lang.Math.PI |
import static com.utils.StringHelper.* |
import static org.junit.Assert.* (in tests) |
import static java.util.stream.Collectors.* |
| Constants everyone recognizes | Methods that could come from multiple places |
The final keyword in Java means "this cannot be changed." It is a restriction that prevents modification, and it can be applied to three things: variables, methods, and classes. Each application has a different meaning, but they all share the same core idea -- once something is declared final, it is locked down.
Real-world analogy: Think of writing in permanent marker versus pencil. A pencil (non-final) lets you erase and rewrite. A permanent marker (final) means once you write something, it stays. You cannot change the text -- you can only read it.
| Applied To | What It Prevents | Example |
|---|---|---|
| Variable | Reassignment (value cannot change) | final int MAX = 100; |
| Method | Overriding (subclasses cannot override) | final void process() |
| Class | Inheritance (cannot be extended) | final class String |
The final keyword is one of the most important tools for writing robust, predictable, and thread-safe Java code. Experienced developers use it liberally because it communicates intent, catches bugs at compile time, and enables compiler optimizations.
A final variable can only be assigned once. After the assignment, any attempt to reassign it results in a compile-time error. There are several forms of final variables, each with its own rules.
A local variable declared final must be assigned exactly once before it is used. It does not need to be assigned at declaration -- it just cannot be reassigned.
public class FinalLocalVariables {
public static void main(String[] args) {
// Assigned at declaration -- cannot be changed
final int maxRetries = 3;
// maxRetries = 5; // COMPILE ERROR: cannot assign a value to final variable
// Blank final -- assigned later, but only once
final String greeting;
boolean isMorning = true;
if (isMorning) {
greeting = "Good morning!";
} else {
greeting = "Good afternoon!";
}
System.out.println(greeting); // Good morning!
// greeting = "Hello!"; // COMPILE ERROR: variable greeting might already have been assigned
// Final in a loop -- a new variable is created each iteration
for (int i = 0; i < 3; i++) {
final int value = i * 10; // This is fine -- each iteration creates a new scope
System.out.println(value); // 0, 10, 20
}
}
}
A final instance variable must be assigned by the time the constructor finishes. It can be assigned in the declaration, in an instance initializer block, or in the constructor -- but exactly once.
public class ImmutableUser {
// Final instance variables -- set once, never changed
private final String username;
private final String email;
private final long createdAt;
// Assigned at declaration
private final String role = "USER";
public ImmutableUser(String username, String email) {
this.username = username;
this.email = email;
this.createdAt = System.currentTimeMillis();
// After the constructor finishes, these can NEVER be reassigned
}
// Only getters -- no setters for final fields
public String getUsername() { return username; }
public String getEmail() { return email; }
public long getCreatedAt() { return createdAt; }
public String getRole() { return role; }
public static void main(String[] args) {
ImmutableUser user = new ImmutableUser("alice", "alice@example.com");
System.out.println(user.getUsername()); // alice
System.out.println(user.getEmail()); // alice@example.com
System.out.println(user.getRole()); // USER
}
}
Method parameters can be declared final to prevent reassignment inside the method body. This is considered good practice by many developers because it avoids accidental modification of input values.
public class FinalParameters {
// The 'final' keyword prevents reassigning the parameter
public static double calculateDiscount(final double price, final double discountRate) {
// price = price * 0.9; // COMPILE ERROR: final parameter cannot be assigned
// You must create a new variable instead
double discountedPrice = price * (1 - discountRate);
return discountedPrice;
}
// Common in constructors -- ensures you do not accidentally swap or modify parameters
public static class Product {
private final String name;
private final double price;
public Product(final String name, final double price) {
// These assignments are fine -- we are assigning TO the fields, not reassigning params
this.name = name;
this.price = price;
}
}
public static void main(String[] args) {
double result = calculateDiscount(100.0, 0.20);
System.out.println("Discounted price: $" + result); // Discounted price: $80.0
}
}
The combination of static and final creates a constant -- a value that belongs to the class and can never be changed. Java convention is to name constants using UPPER_SNAKE_CASE.
public class HttpStatus {
// Constants -- public static final with UPPER_SNAKE_CASE
public static final int OK = 200;
public static final int CREATED = 201;
public static final int NO_CONTENT = 204;
public static final int BAD_REQUEST = 400;
public static final int UNAUTHORIZED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
public static final int INTERNAL_SERVER_ERROR = 500;
public static final String DEFAULT_CONTENT_TYPE = "application/json";
public static final long REQUEST_TIMEOUT_MS = 30_000L;
public static String getStatusMessage(int code) {
switch (code) {
case OK: return "OK";
case CREATED: return "Created";
case BAD_REQUEST: return "Bad Request";
case UNAUTHORIZED: return "Unauthorized";
case NOT_FOUND: return "Not Found";
case INTERNAL_SERVER_ERROR: return "Internal Server Error";
default: return "Unknown";
}
}
public static void main(String[] args) {
System.out.println(HttpStatus.OK + " " + getStatusMessage(OK));
// 200 OK
System.out.println(HttpStatus.NOT_FOUND + " " + getStatusMessage(NOT_FOUND));
// 404 Not Found
// HttpStatus.OK = 201; // COMPILE ERROR: cannot assign a value to final variable
}
}
A final method cannot be overridden by subclasses. When you declare a method as final, you are saying: "This is the definitive implementation. No subclass is allowed to change this behavior."
When to use final methods:
Performance note: In older Java versions, declaring a method as final allowed the JVM to inline the method for better performance. Modern JVMs (HotSpot) are smart enough to do this optimization automatically, so performance is no longer a reason to use final methods. Use them for design correctness, not performance.
public abstract class DataProcessor {
// FINAL method -- defines the algorithm template. Subclasses CANNOT override this.
public final void process(String data) {
System.out.println("=== Starting data processing ===");
String validated = validate(data);
String transformed = transform(validated);
save(transformed);
System.out.println("=== Processing complete ===\n");
}
// These are the "hooks" that subclasses MUST implement
protected abstract String validate(String data);
protected abstract String transform(String data);
protected abstract void save(String data);
}
public class CsvProcessor extends DataProcessor {
// Cannot override process() -- it is final
// public void process(String data) { } // COMPILE ERROR
@Override
protected String validate(String data) {
System.out.println("Validating CSV format...");
if (!data.contains(",")) {
throw new IllegalArgumentException("Not valid CSV: " + data);
}
return data;
}
@Override
protected String transform(String data) {
System.out.println("Transforming CSV to uppercase...");
return data.toUpperCase();
}
@Override
protected void save(String data) {
System.out.println("Saving CSV: " + data);
}
}
public class JsonProcessor extends DataProcessor {
@Override
protected String validate(String data) {
System.out.println("Validating JSON format...");
if (!data.startsWith("{")) {
throw new IllegalArgumentException("Not valid JSON: " + data);
}
return data;
}
@Override
protected String transform(String data) {
System.out.println("Transforming JSON -- adding timestamp...");
return data.replace("}", ", \"processed\": true}");
}
@Override
protected void save(String data) {
System.out.println("Saving JSON: " + data);
}
}
public class Main {
public static void main(String[] args) {
DataProcessor csv = new CsvProcessor();
csv.process("name,age,city");
DataProcessor json = new JsonProcessor();
json.process("{\"name\": \"Alice\"}");
}
}
// Output:
// === Starting data processing ===
// Validating CSV format...
// Transforming CSV to uppercase...
// Saving CSV: NAME,AGE,CITY
// === Processing complete ===
//
// === Starting data processing ===
// Validating JSON format...
// Transforming JSON -- adding timestamp...
// Saving JSON: {"name": "Alice", "processed": true}
// === Processing complete ===
In the example above, the process() method is final. This guarantees that every subclass follows the same three-step algorithm: validate, transform, save. A subclass can customize each step, but it cannot skip a step, reorder them, or bypass the logging. This is the Template Method Pattern, and the final keyword is what makes it work.
A final class cannot be extended (subclassed). When you declare a class as final, no other class can inherit from it. This is the strongest form of the final keyword -- it locks down the entire class hierarchy.
Why would you make a class final?
String, Integer, Double, and all wrapper classes are final.Many well-known Java classes are final:
| Final Class | Reason |
|---|---|
java.lang.String |
Immutability and security (used in class loading, security managers) |
java.lang.Integer, Double, Boolean, etc. |
Wrapper classes must be immutable |
java.lang.Math |
Utility class with only static methods |
java.lang.System |
Core system operations must not be overridden |
// This class is final -- nobody can extend it
public final class Money {
private final long cents; // Store as cents to avoid floating-point issues
private final String currency;
public Money(long cents, String currency) {
if (cents < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
if (currency == null || currency.length() != 3) {
throw new IllegalArgumentException("Currency must be a 3-letter ISO code");
}
this.cents = cents;
this.currency = currency.toUpperCase();
}
// Factory methods for convenience
public static Money dollars(double amount) {
return new Money(Math.round(amount * 100), "USD");
}
public static Money euros(double amount) {
return new Money(Math.round(amount * 100), "EUR");
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies: "
+ this.currency + " and " + other.currency);
}
return new Money(this.cents + other.cents, this.currency);
}
public Money multiply(int factor) {
return new Money(this.cents * factor, this.currency);
}
public double toDouble() {
return cents / 100.0;
}
@Override
public String toString() {
return String.format("%s %.2f", currency, toDouble());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return cents == money.cents && currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(cents, currency);
}
public static void main(String[] args) {
Money price = Money.dollars(29.99);
Money tax = Money.dollars(2.40);
Money total = price.add(tax);
System.out.println("Price: " + price); // Price: USD 29.99
System.out.println("Tax: " + tax); // Tax: USD 2.40
System.out.println("Total: " + total); // Total: USD 32.39
Money bulk = Money.euros(9.99).multiply(5);
System.out.println("Bulk: " + bulk); // Bulk: EUR 49.95
}
}
// This would cause a COMPILE ERROR:
// class ExtendedMoney extends Money { } // Cannot inherit from final 'Money'
Starting in Java 8, a local variable is considered effectively final if it is never reassigned after initialization, even if it is not explicitly declared with the final keyword. This matters because lambda expressions and anonymous inner classes can only capture local variables that are final or effectively final.
Before Java 8, you had to explicitly write final on every variable you wanted to use inside an anonymous class. Java 8 relaxed this rule -- if you never reassign the variable, the compiler treats it as final automatically.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class EffectivelyFinalDemo {
public static void main(String[] args) {
// This variable is "effectively final" -- never reassigned
String prefix = "Hello, ";
List names = Arrays.asList("Alice", "Bob", "Charlie");
// Lambda can use 'prefix' because it is effectively final
List greetings = names.stream()
.map(name -> prefix + name) // 'prefix' captured here
.collect(Collectors.toList());
System.out.println(greetings);
// [Hello, Alice, Hello, Bob, Hello, Charlie]
// ------------------------------------
// This variable is NOT effectively final -- it is reassigned
String suffix = "!";
suffix = "!!"; // Reassignment makes it NOT effectively final
// This would cause a COMPILE ERROR:
// names.stream().map(name -> name + suffix);
// Error: local variables referenced from a lambda expression
// must be final or effectively final
}
}
Why does Java enforce this rule? When a lambda captures a local variable, it creates a copy of that variable's value. If the original variable could change after the lambda captured it, the lambda would have a stale copy, leading to confusing bugs. By requiring final or effectively final, Java guarantees consistency.
Here is a more practical example showing effectively final variables in event handling and callbacks:
import java.util.*;
import java.util.concurrent.*;
public class EffectivelyFinalPractical {
public static void main(String[] args) throws Exception {
// All of these are effectively final (never reassigned)
String apiUrl = "https://api.example.com/data";
int maxRetries = 3;
long timeoutMs = 5000L;
// They can all be used inside the lambda
ExecutorService executor = Executors.newSingleThreadExecutor();
Future future = executor.submit(() -> {
// All three variables are captured as effectively final
System.out.println("Fetching from: " + apiUrl);
System.out.println("Max retries: " + maxRetries);
System.out.println("Timeout: " + timeoutMs + "ms");
return "data from " + apiUrl;
});
System.out.println("Result: " + future.get());
executor.shutdown();
// ------------------------------------
// Workaround when you need to modify a "captured" variable:
// Use an array or AtomicInteger
List names = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
// WRONG -- this would not compile:
// int count = 0;
// names.forEach(name -> count++);
// RIGHT -- use an array (the array reference is effectively final,
// even though its contents change)
int[] count = {0};
names.forEach(name -> {
if (name.length() > 3) {
count[0]++;
}
});
System.out.println("Names longer than 3 chars: " + count[0]);
// Names longer than 3 chars: 3
}
}
The table below summarizes the differences between final, effectively final, and non-final variables:
| Type | Explicitly declared final? | Reassigned? | Usable in lambdas? |
|---|---|---|---|
final int x = 10; |
Yes | No (compile error) | Yes |
int x = 10; (never reassigned) |
No | No | Yes (effectively final) |
int x = 10; x = 20; |
No | Yes | No (compile error) |
The combination of static and final is the standard way to define constants in Java. A static final variable belongs to the class (not any instance) and cannot be reassigned. It is the Java equivalent of const in other languages.
The Java compiler performs constant folding for static final primitive values and String literals -- it replaces references to the constant with the actual value at compile time. This means changing a constant requires recompiling all classes that reference it.
Before Java 5 introduced enums, developers used static final int constants to represent fixed sets of values (like days of the week, status codes, etc.). This pattern, known as the int enum pattern, has significant drawbacks that enums solve. Here is a comparison:
// OLD WAY: static final int constants (fragile, not type-safe)
public class PaymentStatusOld {
public static final int PENDING = 0;
public static final int COMPLETED = 1;
public static final int FAILED = 2;
public static final int REFUNDED = 3;
// Problem 1: Any int is accepted -- no type safety
public static void processPayment(int status) {
if (status == COMPLETED) {
System.out.println("Payment completed");
}
}
public static void main(String[] args) {
processPayment(COMPLETED); // OK
processPayment(42); // Compiles fine! No error. Bug hiding in plain sight.
processPayment(-1); // Also compiles fine.
}
}
// BETTER WAY: enum (type-safe, readable, extensible)
public enum PaymentStatus {
PENDING("Awaiting processing"),
COMPLETED("Payment successful"),
FAILED("Payment declined"),
REFUNDED("Payment refunded");
private final String description;
PaymentStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
public class PaymentProcessor {
// Problem solved: Only valid PaymentStatus values are accepted
public static void processPayment(PaymentStatus status) {
System.out.println("Status: " + status + " - " + status.getDescription());
}
public static void main(String[] args) {
processPayment(PaymentStatus.COMPLETED); // OK
// processPayment(42); // COMPILE ERROR -- not a PaymentStatus
}
}
When to use static final constants vs. enums:
Use static final |
Use enum |
|---|---|
Mathematical constants (PI, E) |
Fixed set of related values (status, type, direction) |
| Configuration values (timeout, max retries) | Values that need methods or behavior |
| String constants (API keys, URLs, messages) | Values used in switch statements |
| Numeric limits (MAX_SIZE, MIN_AGE) | Values that benefit from type safety |
Even experienced developers fall into these traps. Understanding these pitfalls will save you hours of debugging.
This is the single most common static-related error for beginners. A static method has no this reference, so it cannot access instance variables or methods.
public class StaticMistake1 {
private String name = "Alice"; // Instance variable
private static String company = "TechCorp"; // Static variable
public static void main(String[] args) {
// COMPILE ERROR: non-static variable 'name' cannot be referenced from a static context
// System.out.println(name);
// This works -- 'company' is static
System.out.println(company);
// Fix: Create an instance first
StaticMistake1 obj = new StaticMistake1();
System.out.println(obj.name); // Now we can access 'name' through the object
}
}
This is the most dangerous misconception about final. When you declare a reference variable as final, it means the reference cannot point to a different object. But the object itself can still be modified.
import java.util.*;
public class FinalNotImmutable {
public static void main(String[] args) {
// final reference -- cannot reassign the variable
final List names = new ArrayList<>();
// But we CAN modify the contents of the list!
names.add("Alice");
names.add("Bob");
names.add("Charlie");
System.out.println(names); // [Alice, Bob, Charlie]
names.remove("Bob");
System.out.println(names); // [Alice, Charlie]
names.clear();
System.out.println(names); // []
// This is what final prevents -- reassigning the reference
// names = new ArrayList<>(); // COMPILE ERROR: cannot assign a value to final variable
// To make truly immutable, use Collections.unmodifiableList or List.of()
final List immutableNames = List.of("Alice", "Bob", "Charlie");
// immutableNames.add("Diana"); // RUNTIME ERROR: UnsupportedOperationException
// immutableNames.remove("Alice"); // RUNTIME ERROR: UnsupportedOperationException
System.out.println(immutableNames); // [Alice, Bob, Charlie]
// Same applies to final maps, sets, arrays...
final int[] numbers = {1, 2, 3};
numbers[0] = 99; // This is allowed! Array contents can change.
System.out.println(Arrays.toString(numbers)); // [99, 2, 3]
// numbers = new int[5]; // COMPILE ERROR: cannot reassign final array reference
}
}
Mutable static variables are shared across all threads. Without proper synchronization, concurrent modifications can cause data corruption, race conditions, and bugs that are extremely hard to reproduce.
import java.util.concurrent.atomic.AtomicInteger;
public class StaticThreadSafety {
// DANGEROUS: Mutable static variable without synchronization
private static int unsafeCounter = 0;
// SAFE: Using AtomicInteger for thread-safe operations
private static final AtomicInteger safeCounter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
// Simulate 1000 threads incrementing the counter
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
unsafeCounter++; // NOT thread-safe (race condition)
safeCounter.incrementAndGet(); // Thread-safe
}
});
threads[i].start();
}
// Wait for all threads to finish
for (Thread t : threads) {
t.join();
}
System.out.println("Expected: 100000");
System.out.println("Unsafe counter: " + unsafeCounter); // Often LESS than 100000
System.out.println("Safe counter: " + safeCounter.get()); // Always exactly 100000
}
}
If your static blocks depend on each other, order matters. A static variable or block cannot reference another static member that is declared later in the file.
public class StaticOrderMistake {
// These run in order: first staticA, then static block, then staticB
static int staticA = 10;
static {
// staticA is 10 here -- already initialized
System.out.println("Static block: staticA = " + staticA);
// staticB is 0 here -- default value, not yet initialized to 20
// This is a subtle bug if you expect staticB to be 20
System.out.println("Static block: staticB = " + staticB); // Prints 0, not 20!
}
static int staticB = 20; // This assignment happens AFTER the static block
public static void main(String[] args) {
System.out.println("main: staticA = " + staticA); // 10
System.out.println("main: staticB = " + staticB); // 20
}
}
// Output:
// Static block: staticA = 10
// Static block: staticB = 0 <-- Surprise! Not 20!
// main: staticA = 10
// main: staticB = 20
Here are the guidelines that experienced Java developers follow when working with static and final:
| Practice | Reason |
|---|---|
Use static for utility methods that do not depend on object state |
Clearer intent, no unnecessary object creation |
| Prefer static factory methods over constructors for complex creation logic | Descriptive names, can return cached instances or subclasses |
| Minimize mutable static state | Shared mutable state causes threading bugs and makes testing harder |
Make utility classes final with a private constructor |
Prevents instantiation and subclassing |
| Access static members via the class name, not an object reference | Math.PI instead of mathObj.PI -- clearer and less misleading |
| Use static inner classes instead of inner classes when the inner class does not need the outer instance | Avoids hidden reference to outer class, prevents memory leaks |
| Practice | Reason |
|---|---|
Use final for all fields that should not change after construction |
Enforces immutability, enables safe sharing between threads |
Use static final for constants with UPPER_SNAKE_CASE |
Standard Java naming convention, recognized by all developers |
Prefer enum over static final int for type-safe sets of values |
Compile-time type safety, readable in debugger and logs |
Mark classes as final if they are not designed for inheritance |
Prevents accidental subclassing that could break invariants |
Use final on method parameters in complex methods |
Prevents accidental reassignment, especially in long methods |
Use effectively final variables in lambdas instead of adding final everywhere |
Reduces noise; the compiler checks for you |
final fields and no setters means you never need synchronization for reads.Money, Color, Point).static final and wrap it with Collections.unmodifiableList() or use List.of().final liberally. Many coding standards (including Google Java Style Guide) recommend making every variable final unless there is a reason not to.
Let us bring everything together with a real-world example. This AppSettings class demonstrates static final constants, static utility methods, static blocks for initialization, final parameters, effectively final variables in lambdas, a static nested class, and immutable design -- all in one cohesive example.
This is the kind of code you would see in a production Spring Boot or enterprise Java application:
import java.util.*;
import java.util.stream.Collectors;
/**
* Application settings manager demonstrating static, final, and combined patterns.
* This class is final (cannot be subclassed) and uses static members extensively.
*/
public final class AppSettings {
// ========== STATIC FINAL CONSTANTS ==========
// These are true constants -- known at compile time, never change
public static final String APP_NAME = "MyApplication";
public static final String APP_VERSION = "2.5.1";
public static final int DEFAULT_PORT = 8080;
public static final int MAX_CONNECTIONS = 100;
public static final long SESSION_TIMEOUT_MS = 30 * 60 * 1000L; // 30 minutes
public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
// ========== STATIC FINAL COLLECTIONS (immutable) ==========
public static final List SUPPORTED_LANGUAGES;
public static final Map DEFAULT_HEADERS;
// ========== MUTABLE STATIC STATE (minimized, thread-safe) ==========
private static final Map settings = new HashMap<>();
private static boolean initialized = false;
// ========== STATIC BLOCKS ==========
// Complex initialization that cannot be done in a single line
static {
// Initialize supported languages
SUPPORTED_LANGUAGES = List.of("en", "es", "fr", "de", "ja", "pt", "zh");
}
static {
// Initialize default HTTP headers
Map headers = new LinkedHashMap<>();
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
headers.put("X-App-Name", APP_NAME);
headers.put("X-App-Version", APP_VERSION);
DEFAULT_HEADERS = Collections.unmodifiableMap(headers);
}
static {
// Load default settings
settings.put("db.host", "localhost");
settings.put("db.port", "5432");
settings.put("db.name", "myapp");
settings.put("log.level", "INFO");
settings.put("cache.enabled", "true");
settings.put("cache.ttl.seconds", "300");
initialized = true;
System.out.println("[AppSettings] Initialized with " + settings.size() + " default settings");
}
// ========== PRIVATE CONSTRUCTOR ==========
// Prevents instantiation -- this is a utility/configuration class
private AppSettings() {
throw new UnsupportedOperationException("AppSettings cannot be instantiated");
}
// ========== STATIC METHODS ==========
/**
* Gets a setting value by key.
*
* @param key the setting key (final -- cannot be reassigned in the method)
* @return the value, or null if not found
*/
public static String get(final String key) {
Objects.requireNonNull(key, "Setting key cannot be null");
return settings.get(key);
}
/**
* Gets a setting value with a default fallback.
*/
public static String get(final String key, final String defaultValue) {
String value = settings.get(key);
return value != null ? value : defaultValue;
}
/**
* Gets a setting as an integer.
*/
public static int getInt(final String key, final int defaultValue) {
String value = settings.get(key);
if (value == null) {
return defaultValue;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
System.err.println("[AppSettings] Warning: '" + key + "' is not a valid integer: " + value);
return defaultValue;
}
}
/**
* Gets a setting as a boolean.
*/
public static boolean getBoolean(final String key, final boolean defaultValue) {
String value = settings.get(key);
return value != null ? Boolean.parseBoolean(value) : defaultValue;
}
/**
* Sets a configuration value.
*/
public static void set(final String key, final String value) {
Objects.requireNonNull(key, "Setting key cannot be null");
Objects.requireNonNull(value, "Setting value cannot be null");
settings.put(key, value);
}
/**
* Returns all settings whose keys start with the given prefix.
* Demonstrates effectively final variables in lambda expressions.
*/
public static Map getByPrefix(final String prefix) {
// 'prefix' is effectively final (also explicitly final here)
// so it can be used inside the lambda
return settings.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(prefix))
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> v1,
LinkedHashMap::new
));
}
/**
* Prints a formatted summary of all settings.
*/
public static void printSummary() {
// 'separator' is effectively final -- used in lambda below
String separator = "=".repeat(50);
System.out.println(separator);
System.out.println(APP_NAME + " v" + APP_VERSION + " Configuration");
System.out.println(separator);
settings.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> System.out.printf(" %-25s = %s%n",
entry.getKey(), entry.getValue()));
System.out.println(separator);
System.out.println("Supported languages: " + SUPPORTED_LANGUAGES);
System.out.println("Default headers: " + DEFAULT_HEADERS);
System.out.println(separator);
}
/**
* Creates a snapshot of current settings.
* Returns a SettingsSnapshot (static nested class).
*/
public static SettingsSnapshot snapshot() {
return new SettingsSnapshot(new HashMap<>(settings));
}
// ========== STATIC NESTED CLASS ==========
// Does not need an AppSettings instance (which cannot be created anyway)
/**
* An immutable snapshot of settings at a point in time.
* This is a static nested class -- it does not hold a reference to AppSettings.
*/
public static final class SettingsSnapshot {
private final Map data;
private final long timestamp;
private SettingsSnapshot(final Map data) {
this.data = Collections.unmodifiableMap(data);
this.timestamp = System.currentTimeMillis();
}
public String get(final String key) {
return data.get(key);
}
public int size() {
return data.size();
}
public long getTimestamp() {
return timestamp;
}
@Override
public String toString() {
return "SettingsSnapshot{size=" + data.size() + ", timestamp=" + timestamp + "}";
}
}
// ========== MAIN METHOD -- Demonstrates everything ==========
public static void main(String[] args) {
// Static final constants -- accessed via class name
System.out.println("App: " + AppSettings.APP_NAME + " v" + AppSettings.APP_VERSION);
System.out.println("Default port: " + AppSettings.DEFAULT_PORT);
System.out.println("Session timeout: " + AppSettings.SESSION_TIMEOUT_MS + "ms");
System.out.println();
// Static methods -- called without creating an object
System.out.println("DB Host: " + AppSettings.get("db.host"));
System.out.println("DB Port: " + AppSettings.getInt("db.port", 3306));
System.out.println("Cache enabled: " + AppSettings.getBoolean("cache.enabled", false));
System.out.println();
// Modify settings at runtime
AppSettings.set("db.host", "prod-db.company.com");
AppSettings.set("log.level", "DEBUG");
AppSettings.set("feature.dark-mode", "true");
// Get settings by prefix (uses lambda with effectively final variable)
Map dbSettings = AppSettings.getByPrefix("db.");
System.out.println("Database settings: " + dbSettings);
System.out.println();
// Static nested class -- created without AppSettings instance
AppSettings.SettingsSnapshot snap = AppSettings.snapshot();
System.out.println("Snapshot: " + snap);
System.out.println("Snapshot db.host: " + snap.get("db.host"));
// The snapshot is immutable -- changes to AppSettings do not affect it
AppSettings.set("db.host", "new-host.example.com");
System.out.println("Live db.host: " + AppSettings.get("db.host"));
System.out.println("Snapshot db.host: " + snap.get("db.host")); // Still old value
System.out.println();
// Print full summary
AppSettings.printSummary();
}
}
// Output:
// [AppSettings] Initialized with 6 default settings
// App: MyApplication v2.5.1
// Default port: 8080
// Session timeout: 1800000ms
//
// DB Host: localhost
// DB Port: 5432
// Cache enabled: true
//
// Database settings: {db.host=prod-db.company.com, db.port=5432, db.name=myapp}
//
// Snapshot: SettingsSnapshot{size=7, timestamp=1709136000000}
// Snapshot db.host: prod-db.company.com
// Live db.host: new-host.example.com
// Snapshot db.host: prod-db.company.com
//
// ==================================================
// MyApplication v2.5.1 Configuration
// ==================================================
// cache.enabled = true
// cache.ttl.seconds = 300
// db.host = new-host.example.com
// db.name = myapp
// db.port = 5432
// feature.dark-mode = true
// log.level = DEBUG
// ==================================================
// Supported languages: [en, es, fr, de, ja, pt, zh]
// Default headers: {Content-Type=application/json, Accept=application/json, X-App-Name=MyApplication, X-App-Version=2.5.1}
// ==================================================
Here is a quick reference of everything covered in this tutorial:
| Keyword | Applied To | Effect |
|---|---|---|
static |
Variable | One copy shared by all instances (class variable) |
static |
Method | Belongs to the class; callable without an object |
static |
Block | Runs once when the class is loaded (static initializer) |
static |
Inner Class | Nested class that does not require an outer instance |
static import |
Import | Allows using static members without class name qualification |
final |
Variable | Cannot be reassigned after initialization |
final |
Method | Cannot be overridden by subclasses |
final |
Class | Cannot be extended (subclassed) |
static final |
Variable | Class-level constant (convention: UPPER_SNAKE_CASE) |
| Effectively final | Local variable | Never reassigned; usable in lambdas without final keyword |
Key takeaways for your career:
static for things that belong to the class, not to instances -- utility methods, constants, counters, factory methodsfinal to prevent change -- immutable fields, constants, template methods, sealed classesfinal prevents reassignment, not mutation -- a final List can still have elements added to itenum over static final int for type-safe sets of valuesstatic final with UPPER_SNAKE_CASE for constants -- this is the universal Java conventionfinal declarations