Keywords and modifiers are the dialect of Java
Most of Java's grammar is built from a small vocabulary of keywords, and a handful of
them — the modifiers — decide who can see a member, how many copies exist, whether
it can change, and how it behaves across threads. Interviewers love this material
because each keyword carries a precise, often surprising rule: final doesn't make objects
immutable, static methods can't touch this, and var isn't actually a keyword. This
guide threads them into a single story so the rules stick instead of floating as
flashcards.
The four access modifiers
Access modifiers control visibility — who is allowed to reference a class, field, method, or constructor. Java has four levels, and the fourth is the one you get by writing nothing:
| Modifier | Same class | Same package | Subclass (other pkg) | Everywhere |
|---|---|---|---|---|
private | yes | no | no | no |
| (default) | yes | yes | no | no |
protected | yes | yes | yes | no |
public | yes | yes | yes | yes |
public class Account {
private double balance; // only this class can see it
String owner; // package-private — no keyword at all
protected int id; // package + any subclass
public String getOwner() { return owner; } // anyone
}
The no-keyword level is called package-private (or just "default"); it is not a
keyword you can type. The key distinction between private and protected is the
inheritance gap: private is locked to its own class — even subclasses can't see it —
while protected reaches subclasses including ones in a different package, the one case
package-private doesn't cover. The guiding principle is to make everything as private
as possible and widen only when a real need appears: protected signals "this is part of
the inheritance contract," not just "I needed to reach it from over there."
static: bound to the class, not the object
The static keyword binds a member to the class itself rather than to any instance.
There is exactly one shared copy, reachable through the class name with no new
required. It applies to fields, methods, nested classes, and initializer blocks.
class Counter {
static int total; // ONE copy shared by every Counter
int id; // one per instance
Counter() { id = ++total; }
static int square(int n) { return n * n; } // call without an object
}
Counter.square(5); // 25 — no instance needed
new Counter(); // id=1, total=1
new Counter(); // id=2, total=2 (total is shared)
A static member belongs to the class for the program's lifetime; an instance
member is born with each object. The crucial constraint: a static method can't use
this or read instance fields directly, because no particular object is in play — static
code can only see static state, while instance code sees both. That shared-by-everyone
nature also makes mutable static state a classic source of threading bugs and memory leaks,
so reserve it for genuinely global, ideally immutable, values.
static nested classes vs inner classes
Marking a nested class static changes its relationship to the enclosing object. A
static nested class holds no reference to an outer instance and can be created
standalone; a non-static inner class is tied to an outer instance and can read its
members.
class Outer {
int x = 10;
static class Nested { int sum(int a) { return a + 5; } } // no Outer link
class Inner { int read() { return x; } } // uses Outer's x
}
Outer.Nested n = new Outer.Nested(); // no Outer instance required
Outer.Inner i = new Outer().new Inner(); // needs an enclosing Outer
Prefer static nested unless the class genuinely needs the outer instance: an inner class silently retains a reference to its enclosing object, which is a frequent and hard-to-spot memory-leak cause.
final: assign once, then frozen
final means assign exactly once. For a variable that locks the value (primitive) or
which object the reference points to (object) — though the object itself can still mutate.
For a method it bans overriding; for a class it bans extension entirely (think
String, Integer).
final int MAX = 10; // MAX = 11; would not compile
final List<String> xs = new ArrayList<>();
xs.add("ok"); // mutating the object is fine
// xs = new ArrayList<>(); // reassigning the reference is not
final class Money { } // cannot be subclassed
class Base { final void audit() {} } // subclasses cannot override audit()
final applies to locals, fields, and parameters, and any local captured by a lambda must
be final or effectively final. A blank final is a final field declared without
an initializer; it must be set exactly once in every constructor (a static blank final, in
a static block) — this is how immutable classes set fields from constructor arguments. A
final parameter simply can't be reassigned in the body. On methods and classes, final
both protects invariants and lets the JIT inline more aggressively.
final does not mean immutable
This is the trap interviewers reach for: final freezes the reference, never the
object's contents.
final StringBuilder sb = new StringBuilder("a");
sb.append("b"); // allowed — the object is mutated
// sb = new StringBuilder(); // not allowed — the reference is final
Immutability is a stronger, design-level property: all fields private final, no
setters, defensive copies of mutable inputs and outputs, and usually a final class so no
subclass can sneak mutable state in. final is one ingredient of immutability, not the
whole recipe.
static final: the constant idiom
Combine the two and you get the canonical constant: static final with an
UPPER_SNAKE_CASE name. static gives one shared copy; final makes it unassignable. The
compiler can inline static final primitive and String constants for efficiency.
public class Config {
public static final int MAX_RETRIES = 3;
public static final String APP_NAME = "Interviews";
// a "constant" reference is still mutable underneath:
public static final List<String> ROLES = List.of("admin", "user"); // truly fixed
}
Remember the reference caveat: static final List<String> X = new ArrayList<>() can still
be .add()-ed to. For a genuinely constant collection use List.of(...) or
Collections.unmodifiableList(...).
abstract: a contract demanding an override
An abstract method declares a signature with no body — subclasses must implement it. An abstract class can't be instantiated; it exists to be extended and may freely mix abstract methods, concrete methods, and state.
abstract class Shape {
abstract double area(); // no body — subclass must supply it
void describe() { System.out.println("area=" + area()); } // concrete is fine
}
class Circle extends Shape {
double r;
double area() { return Math.PI * r * r; }
}
// new Shape(); // compile error — abstract class
Any class containing even one abstract method must itself be declared abstract. Use it
when you want shared code plus mandatory hooks for subclasses to fill in.
Why abstract clashes with final, static, and private
abstract means "must be overridden." So it's contradictory with every modifier that
prevents overriding, and the compiler rejects those combinations outright:
| Combination | Why it's illegal |
|---|---|
abstract final | final forbids overriding; abstract requires it |
abstract static | static methods aren't polymorphic — can't be overridden |
abstract private | private isn't inherited, so nothing can override it |
abstract synchronized | there's no body, so nothing to lock |
abstract native | native has an external body; abstract has none |
abstract class C {
// abstract final void a(); // error
// abstract static void b(); // error
// abstract private void c(); // error
}
More broadly: a member can carry at most one access modifier (public private int x;
is meaningless), and abstract is only compatible with public/protected/package-private
— anything that keeps the door to overriding open.
transient and volatile: two field-level signals to the JVM
These both modify how a field is treated by the runtime rather than its visibility.
transient marks a field to be skipped during serialization; on deserialization it
returns as its default (0, false, null). volatile tells the JVM a field may be
touched by multiple threads, so every read hits main memory and every write is
immediately visible — plus it establishes a happens-before ordering.
class Session implements Serializable {
String user; // serialized normally
transient String authToken; // skipped — restored as null
}
class Worker {
private volatile boolean running = true; // visibility across threads
void stop() { running = false; } // immediately seen by run()
void run() { while (running) { /* ... */ } } // won't loop forever
}
Use transient for sensitive data (passwords, tokens), recomputable caches, or
non-serializable fields — it has no effect on static fields, which aren't part of an
instance's serialized state anyway. Use volatile for simple flags and safe publication of
a single value, but remember it does not give atomicity: volatile int x; x++; is
still a race because read-modify-write isn't atomic.
synchronized: mutual exclusion
For compound atomicity, you reach for synchronized, which provides mutual exclusion:
only one thread at a time holds an object's monitor lock, so the guarded code runs
without interference (and gets the same happens-before visibility guarantees).
class Counter {
private int count;
synchronized void inc() { count++; } // locks on 'this'
private final Object lock = new Object();
void update() {
synchronized (lock) { count += 2; } // locks a private monitor
}
}
A synchronized method locks this (or the Class object if static); a synchronized
block locks the object you name. Prefer a private lock object over locking this, so
unrelated outside code can't interfere with your lock.
this and super: the current and parent objects
this references the current object — used to disambiguate a field from a same-named
parameter, to pass the current object along, and to chain to another constructor via
this(...). super references the parent class — to call its constructor super(...)
or reach an overridden method super.method().
class Animal {
String sound() { return "..."; }
Animal(String name) { /* ... */ }
}
class Dog extends Animal {
Dog() { super("Rex"); } // parent ctor — must be first statement
@Override String sound() { return super.sound() + "woof"; }
}
Both this(...) and super(...) must be the first statement in a constructor, so you
use at most one. If you omit super(...), the compiler inserts an implicit no-arg
super() — which is exactly why a parent with only a parameterized constructor forces
every subclass to call super(...) explicitly. And this is unavailable in any static
context, since there's no current instance.
instanceof and pattern matching
instanceof tests whether an object is an instance of a type (or subtype), returning a
boolean. Since Java 16, pattern matching binds the result to a typed variable in one
step, eliminating the manual cast.
Object o = "hello";
if (o instanceof String) { // classic form
String s = (String) o; // explicit cast needed
System.out.println(s.length());
}
if (o instanceof String s) { // pattern matching (Java 16+)
System.out.println(s.length()); // 's' is already typed
}
Two facts worth memorizing: instanceof on null is always false (a built-in null
guard), and it's the standard way to make a downcast safe before performing it.
var, and the keywords Java never used
var (Java 10+) provides local variable type inference — but it is a reserved type
name, not a true keyword. That distinction is testable: because it isn't a keyword you can
still use var as a variable, method, or package name (just not a class name). Meanwhile
two genuine reserved keywords, goto and const, exist but do nothing — reserved so C/C++
programmers wouldn't trip on them and so the language could repurpose them later (it never
did).
var list = new ArrayList<String>(); // inferred ArrayList<String>
var count = 10; // int — still statically typed
int var = 5; // legal! 'var' as an identifier
// int goto = 5; // compile error — reserved but unused keyword
// int const = 1; // compile error — reserved but unused keyword
var works only on locals with an initializer — never fields, parameters, return types,
or var x; / var x = null;. Java uses final where C uses const, and labeled
break/continue where C uses goto. (Two more rarities round out the trivia: native
marks a method implemented in non-Java code via JNI, and strictfp forced IEEE-754 math —
now a no-op since Java 17 made all floating-point strict.)
Recap
Java's modifiers each answer one precise question. Access modifiers —
private < default < protected < public — control visibility, narrowest by default.
static binds a member to the class with one shared copy and no this. final
means assign-once: it freezes references, methods (no override), and classes (no extend),
but it does not make objects immutable — that's a whole-design discipline. abstract
demands an override, which is why it clashes with final, static, and private.
Thread-aware keywords — transient, volatile, and synchronized — govern
serialization and concurrency, while this/super navigate the object hierarchy,
instanceof (with pattern matching) makes downcasts safe, and var infers local
types without ever being a real keyword. Learn the reason behind each rule and the
combinations sort themselves out.