Access modifiers control visibility — who can see a class, field, method, or constructor. There are four levels, from most to least restrictive:
| 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
String owner; // package-private (no keyword)
protected int id; // package + subclasses
public String getOwner() { return owner; } // anyone
}
"Default" (also called package-private) is what you get with no keyword — it is not a keyword itself. The guiding principle: declare everything as private as you can and widen only when there's a real need.
private restricts a member to its own class — not even subclasses can
see it. protected widens that to the same package plus any subclass,
including subclasses in a different package (the one case package-private
doesn't cover).
package base;
public class Animal {
private String dna; // hidden from everyone but Animal
protected String species; // visible to subclasses & same package
}
package zoo;
class Dog extends Animal {
void show() {
// System.out.println(dna); // won't compile — private
System.out.println(species); // OK — protected, inherited
}
}
Use private for true implementation detail and protected only when you
intend a member to be part of the inheritance contract.
static binds a member to the class itself rather than to any instance.
There is exactly one copy shared by all objects, and you access it through
the class name — no new required. It applies to fields, methods, nested
classes, and initializer blocks.
class MathUtil {
static final double PI = 3.14159; // one shared constant
static int square(int n) { // call without an instance
return n * n;
}
}
MathUtil.square(5); // 25 — no object created
MathUtil.PI; // shared field
A static method can't use this or access instance fields directly,
because there's no particular object in play. Statics load with the class and
live for the program's lifetime.
A static member belongs to the class — one shared copy for the whole
program. An instance member belongs to each object — every new gets
its own copy.
class Counter {
static int total; // one shared across all Counters
int id; // one per instance
Counter() { id = ++total; }
}
new Counter(); // id=1, total=1
new Counter(); // id=2, total=2 (total is shared)
Static methods can only touch static state; instance methods can touch both. You can call a static member before any object exists, whereas instance members require an object. Mutable static state is shared globally, which makes it a common source of threading bugs and memory leaks.
A static nested class is a class declared static inside another; it does
not hold a reference to an enclosing instance and can be created on its own.
A non-static nested class (an inner class) is tied to an outer instance and
can access its members.
class Outer {
int x = 10;
static class Nested { // no link to an Outer instance
int sum(int a) { return a + 5; }
}
class Inner { // bound to an Outer instance
int read() { return x; } // can use outer's x
}
}
Outer.Nested n = new Outer.Nested(); // no Outer needed
Outer.Inner i = new Outer().new Inner(); // needs an Outer
Prefer static nested unless the class genuinely needs the outer instance —
inner classes silently retain the enclosing object, a frequent memory-leak
cause.
A final variable can be assigned only once. For a primitive that locks the
value; for a reference it locks which object the variable points to — the
object itself can still be mutated.
final int MAX = 10; // MAX = 11; would not compile
final List<String> xs = new ArrayList<>();
xs.add("ok"); // mutating the object is allowed
xs = new ArrayList<>(); // reassigning the reference is not
final applies to local variables, fields, and method parameters, and it's
required (or "effectively final" status is) for any local variable captured by
a lambda or anonymous class.
A final method can't be overridden by a subclass — it fixes the
behavior. A final class can't be extended at all (e.g. String,
Integer). Both are tools for protecting invariants and enabling optimization.
class Base {
final void audit() { } // subclasses cannot override this
}
final class Money { } // cannot be subclassed
// class Cash extends Money {} // compile error
Making a class final is a common way to guarantee immutability stays
intact (no subclass can add mutable state or override behavior), and it lets the
compiler/JIT inline final methods more aggressively.
A blank final is a final field declared without an initializer; it
must then be assigned exactly once in every constructor (or an instance
initializer) before construction completes. A final parameter simply can't
be reassigned inside the method body.
class Point {
final int x; // blank final
Point(int x) { this.x = x; } // must be set here
int shift(final int by) {
// by = by + 1; // illegal — final parameter
return x + by;
}
}
Blank finals let you set an immutable field from constructor arguments rather
than a fixed literal — the backbone of immutable classes. Static blank finals
must be assigned in a static block.
No. final only freezes the reference, not the object's contents. A
final variable can't be reassigned, but if the object it points to is mutable,
its state can still change.
final StringBuilder sb = new StringBuilder("a");
sb.append("b"); // allowed — object is mutated
// sb = new StringBuilder(); // not allowed — reference is final
Immutability is a stronger, design-level property: it requires all fields be
final and private, no setters, defensive copies of mutable inputs/outputs,
and usually a final class. final is one ingredient of immutability, not the
whole recipe.
The idiom is 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";
}
Remember final on a reference only freezes the reference — a
static final List<String> X = new ArrayList<>() can still be mutated. Use
List.of(...) or Collections.unmodifiableList for a genuinely constant
collection.
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 mix abstract methods with concrete ones and state.
abstract class Shape {
abstract double area(); // no body — subclass must provide it
void describe() { // concrete method is allowed
System.out.println("area=" + area());
}
}
class Circle extends Shape {
double r;
double area() { return Math.PI * r * r; } // implementation
}
// new Shape(); // compile error — abstract class
Any class with even one abstract method must be declared abstract. Use it
when you want shared code plus mandatory hooks for subclasses to fill in.
abstract means "must be overridden by a subclass." The conflicting
modifiers all prevent overriding, so the combinations are contradictory and
rejected at compile time:
| 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 it can't be overridden |
abstract synchronized |
nothing to lock — there's no body |
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
}
The rule of thumb: abstract is only compatible with public/protected
(and package-private) — anything that keeps the method open to overriding.
transient marks an instance field to be skipped during serialization. When
a Serializable object is written out, transient fields are ignored; on
deserialization they come back as their default (0, false, null).
class Session implements Serializable {
String user; // serialized
transient String authToken; // NOT serialized — restored as null
transient int cacheSize; // restored as 0
}
Use it for sensitive data (passwords, tokens), values you can recompute,
or non-serializable fields you don't want to persist. transient has no effect
on static fields (statics aren't part of an instance's serialized state
anyway).
volatile tells the JVM a field may be changed by multiple threads, so every
read goes to main memory and every write is immediately visible to other
threads — no caching in a thread-local register. It also establishes a
happens-before ordering, preventing certain instruction reorderings.
class Worker {
private volatile boolean running = true; // visibility guaranteed
void stop() { running = false; } // seen by the run() thread
void run() {
while (running) { /* ... */ } // won't loop forever
}
}
What it does not give you is atomicity of compound actions:
volatile int x; x++; is still a race (read-modify-write isn't atomic). For
that you need synchronized or the Atomic* classes. Use volatile for simple
flags and the safe-publication of a single value.
synchronized provides mutual exclusion: only one thread at a time can hold
a given object's monitor lock, so a synchronized block/method runs without
interference. It also gives visibility guarantees (a happens-before edge on lock
release/acquire).
class Counter {
private int count;
synchronized void inc() { count++; } // locks on 'this'
private final Object lock = new Object();
void update() {
synchronized (lock) { // locks on a private monitor
count += 2;
}
}
}
A synchronized method locks this (or the Class object for a static
method); a synchronized block locks the object you name. Prefer a private
lock object over locking this to avoid outside code interfering with your lock.
this is a reference to the current object. Its three common uses:
disambiguating a field from a same-named parameter, passing the current object
to another method, and calling another constructor of the same class
(this(...)).
class Point {
int x, y;
Point(int x, int y) {
this.x = x; // field vs parameter
this.y = y;
}
Point() {
this(0, 0); // constructor chaining — must be first statement
}
Point self() { return this; } // return the current object
}
this is unavailable in a static context — there's no current instance.
The this(...) constructor call, if used, must be the first statement.
super refers to the parent class. It does two jobs: calling a superclass
constructor (super(...)) and accessing an overridden method or hidden
field of the parent (super.method()).
class Animal {
String sound() { return "..."; }
Animal(String name) { /* ... */ }
}
class Dog extends Animal {
Dog() {
super("Rex"); // parent constructor — must be first
}
@Override String sound() {
return super.sound() + "woof"; // call the parent's version
}
}
If you don't write super(...), the compiler inserts an implicit no-arg
super() — which is why a parent with only a parameterized constructor
forces every subclass to call super(...) explicitly.
instanceof tests whether an object is an instance of a given type (or a
subtype), returning a boolean. Since Java 16, pattern matching lets you
bind the result to a typed variable in one step, removing 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 a String
}
Key facts: instanceof on null is always false (so it's a built-in null
guard), and it's the standard way to make a downcast safe before performing
it.
Both are rarely written but show up in interviews. native marks a method
implemented in non-Java code (typically C/C++) via the JNI — it has no Java
body. strictfp forces floating-point math to follow strict IEEE-754
rules for fully portable, reproducible results across platforms.
class Bridge {
native void readSensor(); // body lives in a native library, no { }
}
strictfp class Calc { // all FP ops here are platform-independent
double scale(double x) { return x * 1.1; }
}
native is how the JDK itself reaches OS-level features. strictfp mattered
because older JVMs could use wider intermediate precision; as of Java 17 all
floating-point is strict by default, so the keyword is now effectively a no-op.
Two words are reserved keywords — so you can't use them as identifiers — yet
the language assigns them no function: goto and const.
// int goto = 5; // compile error — reserved word
// int const = 1; // compile error — reserved word
They were reserved deliberately so that programmers coming from C/C++ wouldn't
accidentally use them and so the language could repurpose them later (it never
did). Java uses final instead of const, and structured control flow plus
labeled break/continue instead of goto.
No — var (Java 10+) is a reserved type name, not a true keyword. That
distinction matters: because it isn't a keyword, you can still legally use var
as a variable, method, or package name (just not as a class name). It
provides local variable type inference: the compiler infers the static type
from the initializer.
var list = new ArrayList<String>(); // inferred ArrayList<String>
var count = 10; // int — still statically typed
int var = 5; // legal! 'var' as an identifier
// var var = 5; // illegal — can't infer here
Restrictions: local variables with an initializer only — never fields, method
parameters, return types, or var x; / var x = null;. It's syntactic sugar,
not dynamic typing.
Many modifiers stack freely (public static final), but some combinations are
contradictory and rejected by the compiler. The conflicts cluster around
abstract (which requires overriding) and the modifiers that forbid it.
| Combination | Legal? | Reason |
|---|---|---|
public static final |
yes | the classic constant |
private static |
yes | class-scoped helper |
abstract final |
no | one demands overriding, the other forbids it |
abstract static |
no | static methods can't be overridden |
abstract private |
no | private isn't inherited |
final abstract (class) |
no | a final class can't be subclassed/extended |
| two access modifiers | no | public private int x; is meaningless |
public static final int LIMIT = 5; // fine
// abstract final void f(); // error — see above
Rule of thumb: a member can have at most one access modifier, and abstract
is incompatible with anything that closes the door to overriding.
More Fundamentals interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.