Builder vs Factory, final methods, how overload resolution really works, inner/nested/local/anonymous classes (including the Java 16 static-member change), marker interfaces vs annotations, same-named classes, package-private access, final references, and overloading-plus-overriding pitfalls.
Published September 25, 2026
At 2–5 years, interviewers expect more than definitions. They want the mechanism (how the compiler picks an overload), the edge cases (static members in inner classes, the hidden reference to the outer instance), and design judgement (a marker interface or an annotation?). Each answer below leads with the short version, then goes deeper.
Short answer: Builder constructs a complex object step by step through a fluent API, and then produces the finished, usually immutable, object with build(). It solves the "many optional parameters" problem. Factory hides which concrete class gets created behind a creation method, and returns a ready object in one call.
HttpRequest request = HttpRequest.newBuilder() // Builder: step by step, readable, immutable result
.uri(URI.create("https://api.example.com/orders"))
.timeout(Duration.ofSeconds(5))
.header("Accept", "application/json")
.GET()
.build();
PaymentGateway gateway = PaymentGatewayFactory.forCountry("IN"); // Factory: picks the implementation class
| Builder | Factory | |
|---|---|---|
| Problem | Constructing one complex object | Choosing which object or class to create |
| Steps | Many fluent calls, then build() | A single call |
| Typical output | An immutable value object with optional fields | An implementation of an interface |
| Validation | In build(), across all fields | Inside the factory method |
Key points to cover:
@Builder and records with static factories are common in modern code.Learn it in depth → Builder Pattern
final on inheritance?Short answer: A final method can't be overridden, so every subclass inherits exactly that implementation. Use it to protect behaviour that invariants or security depend on. It's typical for template methods, where the overall algorithm is fixed and only certain steps are overridable.
public abstract class PaymentProcessor {
public final Receipt process(Payment p) { // the algorithm is locked
validate(p);
Receipt r = charge(p); // the step subclasses customise
audit(r);
return r;
}
protected abstract Receipt charge(Payment p);
}
Key points to cover:
final method, or hiding a method with the same name in a subclass through a different signature, is still possible.final methods. A @Transactional final method silently loses its transaction.Short answer: No. Choosing an overload is a compile-time decision, based on the static types of the arguments. At runtime, the JVM only performs dynamic dispatch on the receiver object, to pick an override of the method signature that was already chosen.
class Printer {
void print(Object o) { System.out.println("Object"); }
void print(String s) { System.out.println("String"); }
}
Object value = "hello";
new Printer().print(value); // "Object": the compiler only knows the static type Object
Key points to cover:
print into the types), pattern matching (switch (value) { case String s -> … }), or the Visitor pattern (double dispatch).Learn it in depth → Visitor Pattern
Short answer: The compiler:
void f(long x) { } // phase 1 (widening)
void f(Integer x) { } // phase 2 (boxing)
void f(int... x) { } // phase 3 (varargs)
f(5); // → f(long)
void g(Integer a, long b) { }
void g(long a, Integer b) { }
// g(1, 2); // ambiguous: each needs boxing for one argument → compile error
Short answer: The same compile-time process: match on the method name and the number, types and order of the arguments' static types, applying the three phases above and then choosing the most specific signature. The return type plays no part. Once the signature is fixed, runtime dispatch only selects which class's version of that exact signature runs.
Common trap: assuming null can always be passed. f(null), with overloads f(String) and f(StringBuilder), is ambiguous. With f(Object) and f(String), it picks f(String), because String is more specific than Object.
Short answer: Java has four kinds of nested class:
public class Order {
private final List<Line> lines = new ArrayList<>();
public static final class Builder { … } // static nested: no outer instance
public class LineIterator implements Iterator<Line> { // inner: uses Order.this.lines
private int i;
public boolean hasNext() { return i < lines.size(); }
public Line next() { return lines.get(i++); }
}
}
Order.LineIterator it = order.new LineIterator(); // an inner class needs an outer instance
Common trap: using a non-static inner class when you don't need the outer instance. Each inner object keeps its outer object alive, which is a classic memory leak (for example, listeners or tasks outliving their owner). Default to static nested classes.
Short answer: Since Java 16, yes. JEP 395 (which finalised records) relaxed the rule, so inner classes can now declare static fields, methods and nested types. Before Java 16, a non-static inner class could only declare static final compile-time constants. Static nested classes have always allowed any static members.
class Outer {
class Inner {
static int created = 0; // compiles on Java 16+, a compile error on Java 8–15
Inner() { created++; }
}
}
Common trap: repeating the old rule ("inner classes can't have static members") as current fact. It's worth mentioning which Java version your team uses.
Short answer: It lets you implement an interface, or extend a class, inline, for one-off use, with no named class. It's handy for callbacks and small strategies. Since Java 8, lambdas replace anonymous classes for functional interfaces. Anonymous classes remain useful when you need state (fields), several methods, or to extend an abstract class.
TimerTask cleanup = new TimerTask() { // an abstract class: a lambda can't do this
private int runs;
@Override public void run() { runs++; purgeExpiredSessions(); }
};
Key points to cover:
this is the anonymous object. In a lambda, this is the enclosing instance..class file. Lambdas use invokedynamic.Short answer: An interface with no methods, which marks a class as having some capability or permission. Code checks for it with instanceof, or with the type system. JDK examples: Serializable, Cloneable, RandomAccess, Remote.
Key points to cover:
void send(Transmittable t)). It's also inherited by subclasses.Short answer: When you want the compiler to enforce that only certain classes can be used somewhere. For example, only Auditable entities may be passed to an audit writer, or only Transmittable objects may be sent outside the network boundary.
public interface Transmittable { } // marker
public final class ExternalGateway {
public void send(Transmittable payload) { … } // non-transmittable objects are rejected at COMPILE time
}
record OrderSummary(String id, BigDecimal total) implements Transmittable { }
Key points to cover:
instanceof check at runtime, which fails later and less clearly. The type-based approach is the main advantage of a marker interface over an annotation.Short answer: Nothing breaks. The fully qualified names differ (java.util.Date and java.sql.Date). In a file that uses both, import one, and refer to the other by its fully qualified name. Importing both with single-type imports is a compile error.
import java.util.Date;
class Report {
Date created; // java.util.Date
java.sql.Date reportingDay; // fully qualified
}
Key points to cover:
banDuplicateClasses) catch it.Short answer: You can't, through normal code. That's the point of package-private access. The proper options:
public, if it really is part of the API.src/test/java).Reflection with setAccessible(true) technically works for classpath code, but the module system blocks it unless the package is opens. Don't use it in production code.
final variable?Short answer: Yes. final fixes the reference, not the object. You can't reassign the variable, but you can call mutating methods on the object it points to.
final List<String> tags = new ArrayList<>();
tags.add("java"); // ✅ the object changes
// tags = new ArrayList<>(); // ❌ compile error: the reference is final
final List<String> fixed = List.of("a", "b");
// fixed.add("c"); // UnsupportedOperationException: immutability comes from the object, not 'final'
Short answer: Package-private: visible only to classes in the same package. It's more restrictive than protected (which adds subclasses in other packages), and less restrictive than private.
Key points to cover:
public, and interface fields are public static final.Short answer: The overload is chosen at compile time, from the static type, but the override is chosen at runtime, from the object. When a subclass overloads where you meant it to override, calls through a parent reference silently skip the subclass method.
class Animal { void greet(Animal other) { System.out.println("animal greets animal"); } }
class Dog extends Animal {
void greet(Dog other) { System.out.println("dog greets dog"); } // OVERLOAD, not override
}
Animal a = new Dog();
a.greet(new Dog()); // "animal greets animal": greet(Animal) was chosen at compile time; Dog didn't override it
Key points to cover:
@Override, so the compiler rejects accidental overloads.equals(Money other) instead of equals(Object o).Q: Why should static nested classes be preferred over inner classes by default? A: They don't capture the outer instance, so they're lighter, can't leak the outer object, and can be instantiated without one. Use an inner class only when it genuinely needs the outer instance's state.
Q: How does an inner class access the outer class's private fields? A: Since Java 11, the JVM supports nestmates. Nested classes are allowed to access each other's private members directly. Before that, the compiler generated synthetic accessor methods.
Q: Can an anonymous class have a constructor?
A: Not an explicit one, because it has no name. Use an instance initialiser block, or pass arguments to the superclass constructor: new Base(arg) { … }.
Q: Why do Spring proxies ignore final and private methods?
A: CGLIB proxies are subclasses. A subclass can't override final or private methods, so the proxy can't add transactional or caching behaviour around them.