Course contents

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.

FreeMust know for interviewsLesson21 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: the map of this round
  2. The platform: JDK, JRE, JVM and bytecode
  3. Values: pass-by-value, primitives and wrappers
  4. Identity and strings: == vs equals, the pool, StringBuilder
  5. OOP rules: the four pillars and what interviewers actually probe
  6. Abstract classes, interfaces and composition
  7. Keywords: static, final, this, super and access modifiers
  8. Class design: immutability, copies and nested classes
  9. 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)

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.

Fig. 1

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)

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 with jlink instead. Full lesson
  • Source to bytecode to machine code: javac compiles .java into .class files of bytecode (magic number 0xCAFEBABE, 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 .class compiled 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 (byte 1 byte, short 2, char 2, int 4, float 4, long 8, double 8, boolean JVM-dependent) live directly in variables. Wrappers (Integer, Long, ...) are immutable heap objects, can be null, and are required for generics. A boxed Integer costs about 16 bytes plus the 4 or 8 byte reference, versus 4 bytes for an int. Full lesson
  • Autoboxing calls valueOf, unboxing calls intValue(): Integer x = 5 compiles to Integer.valueOf(5), and int y = x compiles to x.intValue(). So unboxing a null throws NullPointerException. Full lesson
  • The Integer cache: the JLS (5.1.7) guarantees that boxing an int in -128 to 127 returns the same cached object every time. Byte, Short, Long and Character (0 to 127) cache the same way; Float and Double don't cache at all. The Integer upper 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.

Fig. 2

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

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.

Where candidates slip

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.

  • == vs equals: == compares primitive values, or for references, whether both point to the same object. equals compares logical meaning, as defined by the class; Object.equals defaults to ==. Mixing Integer == int unboxes and compares numbers. Full lesson
  • The equals/hashCode contract: equals must be reflexive, symmetric, transitive, consistent and false for null; equal objects must have equal hash codes. Override one without the other and HashMap.get looks in the wrong bucket and returns null. Full lesson
  • String is immutable, and why: the final class holds a private byte[] it never exposes or changes. That makes pooling safe, lets String cache its hashCode (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") and intern(): new String always 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 and StringBuilder (Java 5) doesn't. Use StringBuilder for local building; StringBuffer is 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.

Where candidates slip

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.

Performance angle

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 (or invokeinterface), 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. javac picks 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 protected method can become public, never private); the override can't declare new or broader checked exceptions (fewer, narrower or any unchecked are fine). final methods can't be overridden and private methods aren't inherited, so they can't be either. Use @Override so 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.

Where candidates slip

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. default methods let you evolve an interface without breaking implementers (that's why they were added, for methods like Collection.stream()), and private methods 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 HashSet subclass that overrides both add and addAll and double-counts, because addAll internally calls add. Wrapping a Set (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 switch pattern 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 no this, so they can't touch instance members directly. Shared mutable static state is a common source of thread-safety and test-isolation bugs. Full lesson
  • final: 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 as this didn't escape. Full lesson
  • final vs finally vs finalize: a modifier, a block that always runs after try, and a deprecated GC hook (deprecated for removal by JEP 421 in Java 18; use try-with-resources or Cleaner).
  • this and super: this is the current object (disambiguate fields, chain with this(...), pass yourself along); super reaches the parent's members and constructor. In Java 21, this(...) or super(...) must be the first statement of a constructor, and if you write neither, javac inserts super(), which fails to compile if the parent has no no-arg constructor. (Java 22 began previewing statements before super(...).) this, super
  • Access modifiers: four levels, from narrowest to widest, in the table below. A top-level class can only be public or 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 fields private final, no setters, defensive copies in the constructor and in getters for mutable inputs like List or Date, and don't let this escape during construction. Immutable objects are thread-safe, safe as map keys and cacheable. Full lesson
  • Records (Java 16) generate the constructor, accessors, equals, hashCode and toString from final fields, but they're only shallowly immutable: copy collection components with List.copyOf in 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 array clone() are shallow. Prefer copy constructors or static factories over Cloneable, which is a marker interface with a broken design (no clone method 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.this reference 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 static nested classes. A lambda only captures this if 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.

In the real world

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 · equals meaning · 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)