Core Java and OOP: the must-knows
A dense revision sheet of the core Java and OOP facts interviewers never let you miss: JDK/JRE/JVM, pass-by-value, the Integer cache, string pool, equals/hashCode, overloading vs overriding, abstract classes vs interfaces, immutability, copies, nested classes and access modifiers.
On this page
- Picture it first: the map of this round
- The platform: JDK, JRE, JVM and bytecode
- Values: pass-by-value, primitives and wrappers
- Identity and strings: == vs equals, the pool, StringBuilder
- OOP rules: the four pillars and what interviewers actually probe
- Abstract classes, interfaces and composition
- Keywords: static, final, this, super and access modifiers
- Class design: immutability, copies and nested classes
- How to say it in the interview
🧭 What is this sheet?
It's the core Java and OOP round compressed into one page you can revise in about 90 minutes. Every item here is something I've watched an interviewer ask in the first 15 minutes of a Java round: why Integer 127 equals 127 but 128 doesn't, whether Java passes objects by reference, why a static method can't be overridden. These questions are cheap to ask and expensive to fumble, because a wrong answer tells the interviewer your fundamentals are memorised rather than understood.
For this sheet, we'll walk the core language from the platform up: how code becomes bytecode, how values move between methods, how identity and equality work for wrappers and strings, then the OOP rules (overriding, interfaces, composition) and the keywords and class shapes that sit on top. Each item has the must-say fact, the one-line why, and a link to the full lesson when you want the deep version. Tick the boxes as you go; your progress is saved.
Below the line (out of scope)
- Exceptions get their own sheet: Exceptions: the must-knows
HashMapinternals and the collections round: Collections and HashMap: the must-knows- Records, sealed types, pattern matching and other Java 8 to 21 features: Java 8 to 21: the must-knows
- Heap, stack and GC in depth: JVM memory and garbage collection: the must-knows
- The one-page list of everything across all areas: The can't-skip Java interview checklist
The 30-second answer
javac compiles source to platform-neutral bytecode, and the JVM (part of the JRE, which is part of the JDK) loads, verifies, interprets and JIT-compiles it. Java is always pass-by-value: for objects, the value copied is the reference, so a method can mutate the object but can't make the caller's variable point elsewhere. == compares primitives by value and references by identity; equals compares meaning, and Integer.valueOf caches -128 to 127, which is why == on boxed 127 is true and on 128 is false. Strings are immutable and literals are pooled. Overloading is picked at compile time from static types; overriding is picked at runtime from the object's class, with covariant returns, no narrower access and no broader checked exceptions, and static methods are hidden, never overridden. I default to interfaces plus composition, make value classes immutable, and use static nested classes unless I need the outer instance.
What you’ll learn
- Explain JDK, JRE and JVM and prove that Java is always pass-by-value, even for objects
- Predict
==output puzzles using the Integer cache (-128 to 127) and the string pool - State the overriding rules (covariant returns, narrower checked exceptions, wider access, static methods hide) and how overloads are resolved
- Choose between an abstract class and an interface in Java 21, and defend composition over inheritance
- Write an immutable class from memory and explain deep vs shallow copy
- Get
static,final,this/super, nested classes and access modifiers right under pressure
Interview radar
Asked very often: Is Java pass-by-value or pass-by-reference? == vs equals? Why is String immutable? Overloading vs overriding? Abstract class vs interface? How do you make a class immutable?
Senior rounds probe: The Integer cache and its JLS guarantee, intern() and where the pool lives, overload resolution phases (widening, then boxing, then varargs), default-method diamond conflicts, calling an overridable method from a constructor, inner-class memory leaks.
Production angle: == on Long IDs that passes tests (small IDs) and fails in production (large IDs), a NullPointerException from unboxing a missing map value, a "constant" List that someone mutated because final only froze the reference.
01Picture it first: the map of this round
Think of the core Java round as six rooms an interviewer walks you through. They rarely go in order, but they always pull one question from each room, and the follow-ups connect them: an equals question leads to strings, which leads to immutability, which leads to final.
The map below groups every item on this sheet into those six rooms. Use it as your revision order.
A map of six groups of core Java must-knows: the platform (JDK, JRE, JVM, bytecode), values (pass-by-value, primitives vs wrappers, Integer cache), identity and strings (== vs equals, string pool, StringBuilder), OOP rules (four pillars, overloading vs overriding, abstract class vs interface, composition), keywords (static, final, this and super, access modifiers) and class design (immutability, deep vs shallow copy, nested classes)
02The platform: JDK, JRE, JVM and bytecode
This is usually the warm-up question. Answer it in three nested boxes and one pipeline, then stop talking.
- JDK ⊃ JRE ⊃ JVM: the JVM executes bytecode; the JRE is the JVM plus the core class libraries (
java.lang,java.util, ...); the JDK is the JRE plus developer tools (javac,jar,jshell,jlink,jcmd,jstack). Since Java 11, Oracle no longer ships a separate JRE download; you build a trimmed runtime withjlinkinstead. Full lesson - Source to bytecode to machine code:
javaccompiles.javainto.classfiles of bytecode (magic number0xCAFEBABE, major version 65 for Java 21). At runtime the class loader loads them lazily, the verifier checks them, the interpreter starts running them, and the JIT (C1, then C2 for hot code) compiles hot methods to native code. Full lesson - Why "write once, run anywhere" works: bytecode is platform-neutral, and the JVM is the platform-specific part. A
.classcompiled on a Mac runs on a Linux JVM of the same or newer version. - Why Java is "both compiled and interpreted": it's compiled ahead of time to bytecode, then interpreted and JIT-compiled at runtime. That's the honest answer to the trick question.
03Values: pass-by-value, primitives and wrappers
This room produces the most wrong answers, because the phrase "objects are passed by reference" is repeated in many tutorials. It's wrong in Java, and interviewers know it.
- Java is always pass-by-value: every argument is copied into the parameter. For a primitive, the value copied is the number. For an object, the value copied is the reference (a pointer-like handle), so both variables point to the same object. Mutating through the copy is visible to the caller; reassigning the copy is not. Full lesson
- Primitives vs wrappers: 8 primitives (
byte1 byte,short2,char2,int4,float4,long8,double8,booleanJVM-dependent) live directly in variables. Wrappers (Integer,Long, ...) are immutable heap objects, can benull, and are required for generics. A boxedIntegercosts about 16 bytes plus the 4 or 8 byte reference, versus 4 bytes for anint. Full lesson - Autoboxing calls
valueOf, unboxing callsintValue():Integer x = 5compiles toInteger.valueOf(5), andint y = xcompiles tox.intValue(). So unboxing anullthrowsNullPointerException. Full lesson - The Integer cache: the JLS (5.1.7) guarantees that boxing an
intin -128 to 127 returns the same cached object every time.Byte,Short,LongandCharacter(0 to 127) cache the same way;FloatandDoubledon't cache at all. TheIntegerupper bound can be raised with-XX:AutoBoxCacheMax, which is one more reason never to rely on it.
Here's the picture behind pass-by-value. The caller's variable and the parameter are two separate slots on two separate stack frames, holding copies of the same reference.
Two stack frames each hold their own copy of a reference: main's sb and mutate's sb both point to one StringBuilder on the heap, so append is visible to main, while reassign's copy is pointed at a new StringBuilder that main never sees
Try it yourself first
Before reading on, predict both lines of output.
static void mutate(StringBuilder sb) { sb.append(" world"); }
static void reassign(StringBuilder sb) { sb = new StringBuilder("new"); }
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("hello");
mutate(sb);
System.out.println(sb); // ?
reassign(sb);
System.out.println(sb); // ?
}
Both lines print hello world. mutate received a copy of the reference and used it to change the one shared object, so the caller sees the change. reassign pointed its own copy at a new object, which the caller's variable never knew about; that new StringBuilder becomes garbage when the method returns. This is also why the classic swap(a, b) method can't work in Java.
Many candidates say "primitives are passed by value and objects by reference." It sounds right, but it's the answer that fails the follow-up "then why can't I write swap?" Say it this way: Java passes references by value. In a true pass-by-reference language (C++ with &), assigning to the parameter would change the caller's variable.
04Identity and strings: == vs equals, the pool, StringBuilder
Once values are clear, the interviewer moves to identity. The trick is that two different mechanisms (the Integer cache and the string pool) make == look like it works on objects for small or literal values.
==vsequals:==compares primitive values, or for references, whether both point to the same object.equalscompares logical meaning, as defined by the class;Object.equalsdefaults to==. MixingInteger == intunboxes and compares numbers. Full lesson- The equals/hashCode contract:
equalsmust be reflexive, symmetric, transitive, consistent and false fornull; equal objects must have equal hash codes. Override one without the other andHashMap.getlooks in the wrong bucket and returnsnull. Full lesson - String is immutable, and why: the
finalclass holds a privatebyte[]it never exposes or changes. That makes pooling safe, letsStringcache itshashCode(great for map keys), makes strings thread-safe without locks, and stops someone changing a file path or class name after a security check. Full article - The string pool: string literals and compile-time constant expressions are interned in a JVM-wide table, so equal literals share one object. Since Java 7 the pool lives in the main heap (it was in PermGen before), so pooled strings can be garbage collected. The table has 65,536 buckets by default in recent JDKs, tunable with
-XX:StringTableSize. Full lesson new String("hi")andintern():new Stringalways creates a new heap object (plus the pooled literal, if it isn't there yet).s.intern()returns the pooled instance for that content, adding it if needed. Since Java 9, compact strings store Latin-1 text at 1 byte per character instead of 2. Full lesson- StringBuilder vs StringBuffer: same API, but
StringBuffer(Java 1.0) synchronizes every method andStringBuilder(Java 5) doesn't. UseStringBuilderfor local building;StringBufferis legacy. Default capacity is 16 characters, and it roughly doubles when full. Full lesson
Try it yourself first
This is the output puzzle I see most often. Predict all five lines before reading the answer below.
Integer a = 127, b = 127;
Integer c = 128, d = 128;
String s1 = "java";
String s2 = "ja" + "va"; // two literals
String part = "ja";
String s3 = part + "va"; // a non-final variable
System.out.println(a == b);
System.out.println(c == d);
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s1 == s3.intern());
The answer is true, false, true, false, true. Boxed 127 comes from the cache, while each boxed 128 is a new object. "ja" + "va" is a compile-time constant, so javac folds it into the literal "java" and it's pooled. part + "va" is computed at runtime (through invokedynamic string concatenation since Java 9), producing a new object; if part were declared final String part = "ja", it would become a constant and that line would print true. intern() returns the pooled instance, so the last line is true.
Many candidates compare Long or Integer IDs with == because it passed every test. Tests use small IDs like 1 to 100, which sit inside the cache; production IDs like 48,213 don't. Always compare wrappers with equals or Objects.equals, and watch for int total = map.get(key) unboxing a missing value into an NPE.
String concatenation with + inside a loop creates a new String (and copies all previous characters) on every iteration. Building a 10,000-piece string that way copies on the order of n²/2 characters, around 50 million character copies for 10,000 pieces of 1 character. One StringBuilder with an initial capacity does the same job with one growing buffer. Outside loops, + is fine; javac and the JVM already optimise single expressions.
05OOP rules: the four pillars and what interviewers actually probe
Nobody gets hired for reciting "encapsulation, abstraction, inheritance, polymorphism". The marks are in the follow-up, so here's the probe behind each pillar.
| Pillar | One-line definition | What the interviewer actually probes |
|---|---|---|
| Encapsulation | Hide state behind methods that protect invariants | "Is a class with private fields and a getter and setter for each encapsulated?" (Not really: setters let anyone break invariants.) |
| Abstraction | Expose what an object does, hide how | "Abstraction vs encapsulation?" (Abstraction is the design view, a contract; encapsulation is the mechanism, access control.) |
| Inheritance | A subclass is-a parent and reuses its code | "When would you avoid it?" (Fragile base class, Liskov violations; prefer composition.) |
| Polymorphism | One call, behaviour chosen by the object's type | "Compile-time vs runtime?" (Overloading is resolved by javac; overriding by the JVM's virtual dispatch.) |
- Runtime polymorphism mechanics: an overridable instance method call compiles to
invokevirtual(orinvokeinterface), and the JVM picks the implementation from the object's runtime class, typically through a per-class method table. Fields, static methods and private methods are not dispatched this way. Full lesson - Overloading rules: same name, different parameter list (count, types or order). A different return type alone is a compile error.
javacpicks the overload from the static (declared) types of the arguments, in three phases: exact match or widening first, then boxing/unboxing, then varargs. Full lesson - Overriding rules: same name and parameter types; the return type may be the same or a subtype (covariant return, since Java 5); access can't be narrower (a
protectedmethod can becomepublic, neverprivate); the override can't declare new or broader checked exceptions (fewer, narrower or any unchecked are fine).finalmethods can't be overridden andprivatemethods aren't inherited, so they can't be either. Use@Overrideso the compiler checks you. Full lesson - Static methods hide, they don't override: a static method with the same signature in a subclass hides the parent's; which one runs depends on the declared type of the reference, decided at compile time. Fields hide the same way.
Try it yourself first
p is declared as Parent but holds a Child. Predict the three printed values.
class Parent {
String name = "parent";
static String kind() { return "P"; }
String who() { return "Parent"; }
}
class Child extends Parent {
String name = "child";
static String kind() { return "C"; }
@Override String who() { return "Child"; }
}
Parent p = new Child();
System.out.println(p.name + " " + p.kind() + " " + p.who());
It prints parent P Child. Only who() is an instance method, so it's the only one dispatched on the runtime class. The field and the static method are both resolved from the declared type Parent at compile time (IDEs and javac -Xlint:static warn that p.kind() should be written Parent.kind()).
Overload resolution has its own classic: given f(long x), f(Integer x) and f(int... x), the call f(5) picks f(long), because widening (phase 1) beats boxing (phase 2), which beats varargs (phase 3). And with f(Object) and f(String), f(null) picks f(String), the most specific match.
Many candidates call an overridable method from a constructor. The parent constructor runs first, so it dispatches to the child's override before the child's fields are initialised: the child sees null and 0. Keep constructors to private or final helper calls. This one shows up as a senior follow-up because it combines constructor order with dynamic dispatch.
06Abstract classes, interfaces and composition
Since Java 8, interfaces can carry code, so the old answer "interfaces have no implementation" is out of date. Interviewers check whether your answer moved with the language.
| Abstract class | Interface (Java 21) | |
|---|---|---|
| Instance state | Any fields | Only public static final constants |
| Constructors | Yes (run via super(...)) |
No |
| Method kinds | Anything | abstract, default and static (Java 8), private (Java 9) |
| Method access | Any | public unless private |
| How many per class | One (extends) |
Many (implements) |
| Use it for | Shared state and a template for closely related classes | A capability or contract that unrelated classes can share |
- Abstract class: can't be instantiated, may have constructors, state and a mix of abstract and concrete methods. Good for the template method pattern, where the base class owns the algorithm and subclasses fill in steps. Full lesson
- Interface: a contract a class can add regardless of its hierarchy.
defaultmethods let you evolve an interface without breaking implementers (that's why they were added, for methods likeCollection.stream()), andprivatemethods share code between defaults. Full lesson - The default-method diamond: if a class inherits the same default method from two interfaces, the rules are: a class method wins over any default; a more specific (sub)interface wins over its parent; otherwise it's a compile error and you must override it, optionally calling
A.super.hello(). Full lesson - Composition over inheritance: inheritance couples you to the parent's implementation details, not only its contract. The textbook case is a counting
HashSetsubclass that overrides bothaddandaddAlland double-counts, becauseaddAllinternally callsadd. Wrapping aSet(has-a) and delegating avoids that coupling. Use inheritance only for a true is-a relationship you control. Full lesson - Sealed types (Java 17) sit in between: a class or interface lists exactly which subclasses are permitted, which makes
switchpattern matching exhaustive. Full lesson
My default: start with an interface for the contract and compose behaviour. Reach for an abstract class when several implementations genuinely share state and constructor logic.
07Keywords: static, final, this, super and access modifiers
These are short questions, but they come in rapid fire, and hesitation costs more than the question is worth.
static: belongs to the class, not an instance. Static fields exist once per class (per class loader), static blocks run once at class initialisation, and static methods have nothis, so they can't touch instance members directly. Shared mutable static state is a common source of thread-safety and test-isolation bugs. Full lessonfinal: a final variable is assigned once (a final reference can still point to a mutable object), a final method can't be overridden, a final class can't be extended. Final fields also get a Java Memory Model guarantee: other threads see their constructor-assigned values once the constructor finishes, as long asthisdidn't escape. Full lessonfinalvsfinallyvsfinalize: a modifier, a block that always runs aftertry, and a deprecated GC hook (deprecated for removal by JEP 421 in Java 18; usetry-with-resourcesorCleaner).thisandsuper:thisis the current object (disambiguate fields, chain withthis(...), pass yourself along);superreaches the parent's members and constructor. In Java 21,this(...)orsuper(...)must be the first statement of a constructor, and if you write neither,javacinsertssuper(), which fails to compile if the parent has no no-arg constructor. (Java 22 began previewing statements beforesuper(...).) this, super- Access modifiers: four levels, from narrowest to widest, in the table below. A top-level class can only be
publicor package-private. Full lesson
| Modifier | Same class | Same package | Subclass in another package | Everywhere |
|---|---|---|---|---|
private |
✓ | |||
| (none: package-private) | ✓ | ✓ | ||
protected |
✓ | ✓ | ✓ (through inheritance) | |
public |
✓ | ✓ | ✓ | ✓ |
The protected detail interviewers like: a subclass in another package can use the inherited member on this or on references of its own type, but not on an arbitrary instance of the parent class.
08Class design: immutability, copies and nested classes
The last room is about building classes that are safe to share. This is where the earlier rooms pay off: immutability uses final, private access and copies, and nested classes connect back to how references are held.
- Immutable class recipe: make the class
final(or the constructor private with factories), all fieldsprivate final, no setters, defensive copies in the constructor and in getters for mutable inputs likeListorDate, and don't letthisescape during construction. Immutable objects are thread-safe, safe as map keys and cacheable. Full lesson - Records (Java 16) generate the constructor, accessors,
equals,hashCodeandtoStringfrom final fields, but they're only shallowly immutable: copy collection components withList.copyOfin a compact constructor. Full lesson - Shallow vs deep copy: a shallow copy duplicates the outer object and shares everything it references; a deep copy also duplicates the referenced mutable objects.
Object.clone()and arrayclone()are shallow. Prefer copy constructors or static factories overCloneable, which is a marker interface with a broken design (noclonemethod on the interface, and it ignores constructors). Full lesson - Nested classes: a static nested class has no link to an outer instance; an inner class holds a hidden
Outer.thisreference and needs an outer instance to be created; a local class is declared in a method; an anonymous class is declared and instantiated in one expression. Local and anonymous classes (and lambdas) can only capture effectively final locals. Full lesson - The inner-class leak: because an inner or anonymous class instance holds its outer instance, registering one as a long-lived listener or caching it keeps the whole outer object (and everything it references) alive. Default to
staticnested classes. A lambda only capturesthisif its body uses instance members.
Here's the immutable recipe in code, with the two defensive copies that most candidates forget:
public final class Order {
private final String id;
private final List<String> items;
public Order(String id, List<String> items) {
this.id = Objects.requireNonNull(id);
this.items = List.copyOf(items); // copy in: the caller's list can change later
}
public String id() { return id; }
public List<String> items() {
return items; // already unmodifiable, safe to hand out
}
public Order withItem(String item) { // "modification" returns a new object
List<String> next = new ArrayList<>(items);
next.add(item);
return new Order(id, next);
}
}
Notice three things. List.copyOf makes an unmodifiable copy (and rejects null elements), so the getter doesn't need a second copy; with a plain ArrayList field you would return Collections.unmodifiableList or a copy. The withItem method is how immutable types "change": they return a new instance, the same way String.concat does. And List.copyOf is itself shallow: if the elements were mutable objects, you'd need to copy those too.
A pattern I've seen more than once: a team declares public static final List<String> ALLOWED = new ArrayList<>(...) as a "constant" allow-list. Some request handler appends to it during a feature path, and every later request accepts the new value until the next deploy. final only freezes the reference. List.of(...) would throw UnsupportedOperationException on the first write.
09How to say it in the interview
When I ask these, I'm listening for precision on three points: that you say "references are passed by value" rather than "objects are passed by reference", that you can explain why == sometimes works on wrappers and strings (cache and pool), and that you know overriding is a runtime decision while overloading, static methods and fields are compile-time. Senior candidates also bring up the constructor-calls-overridable-method trap, the default-method diamond, and the defensive copies in an immutable class without being asked.
In the interview
"javac turns source into bytecode, and the JVM inside the JRE inside the JDK loads, verifies, interprets and JIT-compiles it. Java is always pass-by-value; for objects the value is the reference, so a method can mutate the caller's object but can't reassign the caller's variable. == checks identity for references, equals checks meaning, and boxed values from -128 to 127 are cached, which is why == on 127 is true and on 128 is false; same idea with pooled string literals. Overloads are chosen at compile time from declared types, widening before boxing before varargs. Overrides are chosen at runtime, can return a subtype, can't narrow access or add broader checked exceptions, and static methods and fields are hidden, not overridden. I default to interfaces and composition, make value types immutable with final fields and defensive copies, and use static nested classes unless I really need the outer instance."
Here's how this might look if you sketch it on the whiteboard:
On the whiteboard
Core Java and OOP
- JDK ⊃ JRE ⊃ JVM ·
.java→ javac → bytecode → load, verify, interpret, JIT - pass-by-value: references copied · mutate visible, reassign not
==identity ·equalsmeaning · Integer cache -128..127 (JLS) · unbox null → NPE- String: final, immutable, pooled literals (heap since 7) ·
intern()· constants folded - StringBuilder (fast) vs StringBuffer (synchronized, legacy)
- overload: compile time, widen > box > varargs · override: runtime, covariant return, access ≥, checked ≤
- static methods and fields: hidden, not overridden
- interface: default/static (8), private (9) · abstract class: state + constructors
- composition > inheritance · no overridable calls in constructors
- immutable: final class, private final, no setters, copy in and out
- clone = shallow · inner class holds
Outer.this→ prefer static nested
Interview questions for this topicComing soon
Further reading (8)
- The Java Language Specification, Java SE 21 Edition
- JLS 5.1.7: Boxing Conversion (the cached range)
- JLS 8.4.8: Inheritance, Overriding, and Hiding
- JLS 15.12.2: Compile-Time Step 2, Determine Method Signature
- java.lang.String (Java SE 21 API)
- java.lang.Integer.valueOf (Java SE 21 API)
- JEP 254: Compact Strings
- Effective Java, 3rd ed. (Items 17, 18, 20: immutability, composition, interfaces)