Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Object-Oriented Programming (OOP)
Java is an OOP language at its core. Everything is an object (except primitives). OOP helps organize complex software by modeling it after real-world entities — but the real value is not “modeling the real world” (a common oversimplification). The real value is managing complexity. OOP gives you tools to hide implementation details (encapsulation), share behavior (inheritance), and write flexible code that works with families of related types (polymorphism). Think of it as building with LEGO: each brick (object) has a well-defined shape (interface), you can snap them together without knowing what is inside, and you can swap one brick for another of the same shape.1. Classes & Objects
A Class is a blueprint or template. An Object is a specific instance created from that blueprint.- Class:
Car(Has wheels, engine, color) - Object:
Tesla Model 3(Red, Electric)
Access Modifiers
Control who can see and use your code. This is Encapsulation.| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes | No |
default | Yes | Yes | No | No |
private | Yes | No | No | No |
2. Inheritance
Inheritance allows a class to acquire properties of another. It promotes code reuse. Java supports single inheritance (a class can extend only one parent).3. Polymorphism
Polymorphism (“many forms”) allows you to treat objects of different subclasses as objects of their superclass. This makes your code flexible.Animal, Java knows at runtime that the actual object is a Dog, so it calls the Dog version of makeSound. This is called Dynamic Dispatch.
Abstract Classes
An abstract class cannot be instantiated. It’s a partial blueprint that forces subclasses to complete the implementation.4. Interfaces
Interfaces define a contract. They say “I promise to do X”, but they don’t say how. A class can implement multiple interfaces.5. Records (Java 14+)
For years, Java developers wrote boilerplate code for classes that just held data (getters,equals, hashCode, toString). Records solve this. They are immutable data carriers.
Before (Boilerplate):
6. Sealed Classes (Java 17+)
Sealed classes give you control over your hierarchy. You can specify exactly which classes are allowed to extend your class.Summary
- Classes: Encapsulate state and behavior.
- Inheritance:
extendsfor code reuse. - Polymorphism: Treat subclasses as superclasses.
- Interfaces: Define contracts (
implements). - Records: Concise, immutable data classes. Use them for DTOs.
- Sealed Classes: Restricted inheritance hierarchies.
Interview Deep-Dive
Explain how dynamic dispatch works at the JVM level. When you call an overridden method, what exactly happens in memory?
Explain how dynamic dispatch works at the JVM level. When you call an overridden method, what exactly happens in memory?
- When a class defines or inherits virtual methods (in Java, all non-static, non-final, non-private instance methods are virtual by default), the JVM creates a vtable (virtual method table) for that class. The vtable is an array of method pointers, one entry per virtual method. Each object instance contains a hidden pointer (sometimes called the klass pointer) in its object header that points to the class metadata, which includes the vtable.
- When you write
animal.makeSound()whereanimalis typed asAnimalbut the actual object is aDog, the JVM performs these steps at runtime: (1) load the klass pointer from the object header, (2) look up the vtable for the class, (3) index into the vtable at the slot assigned tomakeSound(), (4) call the function pointer found there. BecauseDogoverridesmakeSound(), the vtable entry forDogpoints toDog.makeSound()rather thanAnimal.makeSound(). This isinvokevirtualat the bytecode level. - The JIT compiler aggressively optimizes this. If profiling data shows that a particular call site always sees
Dogobjects (monomorphic), the JIT replaces the vtable lookup with a direct call toDog.makeSound()and inlines the method body. It inserts a type guard check, and if aCatsuddenly appears at that call site, it deoptimizes and falls back to the full vtable dispatch. For bimorphic sites (exactly two types), the JIT can generate an if-else chain. For megamorphic sites (many types), it falls back to the full vtable lookup. - The practical implication: polymorphism in Java is not free, but the JIT makes it close to free for the common case. The cost only becomes measurable in tight loops processing millions of objects with many different concrete types at the same call site, which causes megamorphic dispatch and defeats inlining.
invokestaticcalls a static method. No receiver object, no dispatch — the target method is fully resolved at compile time. This is the cheapest call.invokespecialcalls constructors (init), private methods, andsuper.method()calls. The target is known at compile time because these are not subject to overriding. It uses the receiver’s compile-time type, not its runtime type.invokevirtualcalls instance methods on classes. It performs vtable dispatch as described above. The vtable index for a given method name is fixed within a class hierarchy, so dispatch is a single array lookup — O(1).invokeinterfacecalls methods declared in an interface. This is slightly more expensive thaninvokevirtualbecause a class can implement multiple interfaces, and the vtable offsets for interface methods are not guaranteed to be at fixed positions across unrelated classes. The JVM uses an itable (interface method table) or a search mechanism. In practice, the JIT optimizes monomorphic and bimorphicinvokeinterfacesites just as aggressively asinvokevirtual, so the difference is only measurable in cold code.- There is also
invokedynamic(added in Java 7 for JVM-based languages, used heavily by lambdas since Java 8), which defers method linking to runtime via a bootstrap method. The first invocation calls a bootstrap method that returns aCallSitebound to aMethodHandle, and subsequent invocations go through the bound handle. This is how lambdas avoid generating a new class for every lambda expression.
When should you prefer composition over inheritance? Give a concrete example where inheritance caused a real problem.
When should you prefer composition over inheritance? Give a concrete example where inheritance caused a real problem.
- The rule of thumb is: use inheritance for “is-a” relationships where substitutability makes semantic sense, and use composition for “has-a” relationships or when you want to reuse behavior without coupling to a type hierarchy. In practice, I reach for composition first because inheritance creates tight coupling — the subclass depends on the implementation details of the superclass, not just its interface.
- The classic real-world problem is the fragile base class. Joshua Bloch describes this in Effective Java with the
InstrumentedHashSetexample: you extendHashSetand overrideadd()andaddAll()to count insertions. ButHashSet.addAll()internally callsadd()for each element. Your overriddenaddAll()increments the count, then callssuper.addAll(), which calls your overriddenadd()for each element, double-counting. Your code is correct against the public API contract, but broken because of an implementation detail of the superclass. If the superclass changes its internal delegation pattern in a future release, your subclass silently breaks. This is not hypothetical — I have seen this pattern cause counting and auditing bugs in production systems. - The composition solution is a wrapper (decorator) pattern: create a class that holds a
Setas a field, delegates allSetmethods to it, and adds the counting logic in the wrapper. The wrapper depends only on theSetinterface, not onHashSet’s internal implementation. IfHashSetchanges its internals, the wrapper is unaffected. - A broader example from enterprise Java: many legacy codebases built deep inheritance hierarchies for domain entities —
BaseEntitytoAuditableEntitytoTenantEntitytoCustomer. Any change toBaseEntityrisks breaking all descendants. Modern frameworks like Spring prefer composition (inject behaviors via services and interceptors) and flat class hierarchies. The rise of interfaces with default methods in Java 8 further reduced the need for abstract base classes.
- Sealed classes address one of the main objections to inheritance: uncontrolled extension. With
sealed, you declare the complete set of permitted subtypes. This gives you the exhaustiveness guarantees of an algebraic data type (the compiler knows all possible subtypes for pattern matching) while keeping the inheritance hierarchy intentionally tight. - The key shift is that sealed classes make inheritance appropriate for closed type hierarchies — scenarios like an AST (Abstract Syntax Tree) with
sealed interface Expr permits Literal, BinaryOp, UnaryOp, or a state machine withsealed class State permits Running, Paused, Stopped. In these cases, the set of subtypes is part of the design and should not be extended by arbitrary external code. - For open extension points (where you want arbitrary third-party implementations), interfaces without
sealedremain the right choice. For behavior reuse without type coupling, composition is still better. Sealed classes add a middle ground that did not exist before: controlled inheritance with compiler-verified exhaustiveness. - The practical benefit combines with switch pattern matching. When you switch on a sealed type and handle all permitted subtypes, the compiler verifies exhaustiveness — no
defaultcase needed, and adding a new subtype forces you to update every switch. This eliminates the “forgot to handle the new case” class of bugs that plagues open inheritance.
How does the equals/hashCode contract work, and what breaks in production if you violate it?
How does the equals/hashCode contract work, and what breaks in production if you violate it?
- The contract states: if two objects are equal according to
equals(), they must return the samehashCode(). The reverse is not required — different objects can have the same hash code (collisions are expected). Additionally,equals()must be reflexive (a.equals(a)is true), symmetric (a.equals(b)impliesb.equals(a)), transitive, and consistent across multiple invocations. - If you override
equals()without overridinghashCode(), objects that are logically equal will have different hash codes (inherited fromObject, which returns the memory address or an identity-based hash). This means: you insertnew Customer("Alice")into aHashSet, then checkset.contains(new Customer("Alice"))and it returnsfalse— even though the customer is logically in the set. TheHashSetused the hash code to determine which bucket to check, found a different bucket, and never compared viaequals(). - I have seen this cause a production data loss incident. A caching layer keyed on a custom
OrderKeyclass that overrodeequals()but nothashCode()resulted in cache misses on every lookup. The cache was functionally useless — it consumed memory but never returned hits. The system fell through to the database on every request, causing a cascading overload during peak traffic. The fix was a one-linehashCode()override, but the incident cost hours of downtime. - Best practice: use your IDE or Lombok’s
@EqualsAndHashCodeto generate both together, or use Records (which auto-generate correct implementations). If you must implement manually, useObjects.hash(field1, field2)forhashCode()and compare the same fields inequals(). Never include mutable fields inhashCode()— if the hash changes after the object is stored in aHashMap, the object becomes unretrievable (it is in the bucket for the old hash, but lookups use the new hash).
- A
HashMapcomputes the hash of the key at insertion time and uses it to determine the bucket (array index). The key-value entry is stored in that bucket. When you callget(key), theHashMapcomputes the hash of the lookup key, goes to the corresponding bucket, and then walks the chain (or tree, in Java 8+) in that bucket, comparing each entry’s key viaequals(). - If you mutate the key object after insertion such that its
hashCode()now returns a different value, the entry is still physically in the old bucket, but anyget()orcontains()call with the mutated key computes the new hash, goes to the new bucket, and finds nothing. The entry is effectively orphaned — it exists in the map, contributes to the size, and is visible during iteration, but is invisible to hash-based lookups. - Even
remove()will not find it, becauseremove()also uses the hash to locate the bucket. The orphaned entry will remain until theHashMapis garbage collected. In a long-running server with aHashMapused as a cache, this manifests as a slow memory leak — the map grows but entries are never retrieved or removed. I have diagnosed this with heap dumps showingHashMap$Nodeentries with keys that do not match any current lookup patterns.
What are the practical differences between abstract classes and interfaces in modern Java? When would you choose one over the other?
What are the practical differences between abstract classes and interfaces in modern Java? When would you choose one over the other?
- Before Java 8, the distinction was clear: abstract classes could have state (fields) and implemented methods, while interfaces could only declare method signatures. Java 8 blurred this by adding default methods and static methods to interfaces. Java 9 added private methods to interfaces. Now the practical differences are narrower: abstract classes can have constructors, mutable instance state (fields), and non-public methods, while interfaces can only have public abstract methods, default methods, static methods, private methods, and constants (
public static finalfields). - The decisive factor is single vs. multiple inheritance. A class can extend only one abstract class but implement many interfaces. This means interfaces are the only way to mix in multiple capabilities. If you need a type to be both
SerializableandComparableandCloseable, interfaces are the only option. - I choose abstract classes when: there is genuine shared state (fields that subclasses need), the hierarchy represents a true “is-a” relationship with shared implementation that goes beyond default method logic (e.g., a template method pattern with multiple interdependent protected methods), or I need to enforce invariants via a constructor.
- I choose interfaces when: defining a capability or contract (Flyable, Printable, Repository), building APIs that external code will implement, or when multiple inheritance of type is needed. In modern Java, interfaces with default methods cover 90% of cases that used to require abstract classes.
- A senior-level nuance: interfaces evolved so much that the remaining reason for abstract classes is essentially mutable state and constructors. If your shared code is stateless, put it in an interface with default methods. If it manages state, you need an abstract class. But be cautious with default methods in interfaces — if two interfaces declare the same default method and a class implements both, you get a compile error that requires manual resolution. This “diamond problem” is Java’s version of the C++ multiple inheritance headache, though Java forces you to deal with it explicitly rather than silently choosing one.
- Suppose you have an interface
Printerin a library (v1) with one abstract methodprint(). A consumer classMyPrinter implements Printercompiles fine. In v2 of the library, you add a default methodprintBatch(List items)with a default implementation that callsprint()in a loop. This compiles and runs without the consumer changing anything — this is the whole point of default methods, to evolve interfaces without breaking existing implementations. - The problem arises if the consumer class already had its own
printBatch(List items)method. Now the class has an implicit override of the interface’s default method. This might be fine, or the semantics might diverge — the consumer’sprintBatchmight do something completely different. The consumer’s code still compiles because their method matches the interface signature, but the behavior changes silently when the interface adds the default method. - A worse scenario: if
MyPrinter implements Printer, BatchProcessorand bothPrinterandBatchProcessoradd aprintBatch()default method in their respective v2 releases,MyPrinterfails to compile with a “class inherits unrelated defaults” error. The consumer did not change any code but now has a compilation failure caused by two upstream libraries independently evolving their interfaces. This is rare but real — it is why library authors should add default methods conservatively.
Records are immutable data carriers. What are their limitations, and when should you not use them?
Records are immutable data carriers. What are their limitations, and when should you not use them?
- Records are final (cannot be extended), their components are implicitly final (immutable), they cannot declare instance fields beyond the record components, and they cannot extend other classes (though they can implement interfaces). The compiler generates
equals(),hashCode(),toString(), canonical constructor, and component accessor methods. - You should not use records when you need mutability (e.g., a builder pattern or an entity that changes state over its lifecycle), when you need inheritance (records cannot participate in a class hierarchy beyond implementing interfaces), or when you need JPA/Hibernate entities (JPA requires a no-arg constructor and mutable fields for proxying and lazy loading, which records cannot provide — though some JPA providers are adding experimental record support).
- A common mistake: records give you shallow immutability, not deep immutability. If a record component is a
List<String>, the record does not copy or wrap the list. A caller can pass a mutable list, and then mutate it externally, modifying the record’s “immutable” state. Defensive copying in the canonical constructor is required for true immutability:public MyRecord(List<String> items) { this.items = List.copyOf(items); }. - Where records excel: DTOs (Data Transfer Objects), API response/request objects, value objects in Domain-Driven Design, map keys (they generate correct
equals/hashCodeautomatically), and pattern matching (records can be deconstructed in pattern matching expressions in Java 21). They eliminate the most tedious boilerplate in Java and make the code’s intent clearer — when you seerecord, you immediately know this is a transparent, immutable data carrier.
- Records have a fundamentally different serialization mechanism than regular classes. When a record is serialized, only the component values are written. When it is deserialized, the canonical constructor is invoked with those values. This is a major security improvement over regular Java serialization, where deserialization bypasses constructors entirely (using
sun.misc.UnsafeorReflectionFactoryto create objects without calling any constructor). With regular classes, a malicious serialized payload can create objects in invalid states because validation in the constructor is never executed. - Because record deserialization goes through the canonical constructor, any validation logic you put there (range checks, null checks, invariant enforcement) is guaranteed to run on deserialization. This closes a class of deserialization vulnerabilities that have plagued Java for decades.
- The practical caveat: records are not automatically
Serializable. You must explicitlyimplements Serializable. And the general guidance still applies — Java’s built-in serialization mechanism has enough security concerns that most modern systems use JSON (Jackson, Gson) or Protocol Buffers instead. Records work exceptionally well with Jackson —@JsonPropertyannotations on components work as expected, and Jackson can construct records via the canonical constructor.