Skip to content

Java · Fundamentals

Java Keywords & Modifiers — Access, static, final & abstract Explained

12 min read Updated 2026-06-20 Share:

Practice Keywords & Modifiers interview questions

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:

ModifierSame classSame packageSubclass (other pkg)Everywhere
privateyesnonono
(default)yesyesnono
protectedyesyesyesno
publicyesyesyesyes
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:

CombinationWhy it's illegal
abstract finalfinal forbids overriding; abstract requires it
abstract staticstatic methods aren't polymorphic — can't be overridden
abstract privateprivate isn't inherited, so nothing can override it
abstract synchronizedthere's no body, so nothing to lock
abstract nativenative 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 modifiersprivate < 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.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel