Java interview questions

Table of Contents

Lambdas vs Anonymous Classes

In Java, both anonymous classes and lambda expressions allow developers to define small blocks of behavior inline — typically to pass as callbacks or functional arguments. However, the way they are compiled and executed under the hood is quite different, with important implications for performance and memory efficiency.

An anonymous inner class is a full-fledged, unnamed class created at compile time. Each one generates a separate .class file (for example, OuterClass$1.class) that must be loaded and verified at runtime. This introduces additional overhead related to class loading, memory allocation, and object instantiation. Each use of an anonymous class typically results in a new object created on the heap.

On the other hand, lambda expressions are implemented more efficiently using theinvokedynamic instruction introduced in Java 7. Instead of creating a new class file, the JVM links the lambda to its corresponding functional interface at runtime, often reusing the same instance if no variables are captured from the enclosing scope. This means that lambdas typically have far less overhead in both memory and class-loading time.

Modern JVMs further optimize lambdas through techniques like method inlining — especially when a lambda is small or frequently invoked in a tight loop. In some cases, such as with method references, the compiler can even bypass object creation altogether, directly referencing the existing method.

Key differences:
• Anonymous classes create a separate .class file for each instance.
• Lambdas use invokedynamic for dynamic runtime binding, avoiding extra class files.
• Anonymous classes always allocate a new object on the heap.
• Stateless lambdas (that do not capture variables) can be reused and optimized by the JVM.
• Lambdas enable more efficient inlining and reduced method call overhead.

In short, while anonymous classes provide flexibility and full object-oriented semantics, lambdas are lighter, more efficient, and better aligned with functional programming styles. They are preferred in modern Java for concise, performant, and cleaner code.

What is Inlining

Inlining is one of the most fundamental and powerful optimizations performed by the JVM's Just-In-Time (JIT) compiler. Inlining means replacing a method call with the method's actual body of code. Instead of performing a separate function call — which involves stack setup, jumps, and returns — the JIT copies the method's body directly into the caller at runtime.

1int square(int x) {
2    return x * x;
3}
4
5int compute(int a, int b) {
6    return square(a) + square(b);
7}

Without inlining, the compiled bytecode performs:

• Two separate method invocations (square(a) and square(b))
• Two additional stack frames (one per call)
• Two jumps and returns

When the JIT compiler detects that square() is small and frequently called, it inlines the method. The resulting optimized code looks like this:

1int compute(int a, int b) {
2    return (a * a) + (b * b);
3}

By inlining, the JVM avoids unnecessary method calls and enables further optimizations. The benefits include:

Reduced overhead: Eliminates call/return and stack frame setup costs, especially in tight loops.
Deeper optimization opportunities: After inlining, the JIT can perform additional optimizations such as constant folding, loop unrolling, and dead code elimination.
Improved branch prediction: The resulting code becomes smaller and more predictable, allowing the CPU to optimize execution paths.

However, inlining is not always beneficial. Excessive inlining can lead to code bloat, which increases the size of compiled machine code. This can:

• Reduce instruction cache locality (larger code takes longer to fetch).
• Increase JIT compilation time.
• Occasionally degrade performance in very large applications.

In summary, inlining trades off a small increase in code size for a large potential gain in runtime performance. It's one of the JVM's most important optimizations for achieving the speed of native code while maintaining Java's flexibility and portability.

What is invokedynamic?

Traditionally, the JVM supported four main bytecode instructions for calling methods — invokestatic,invokevirtual,invokespecial, and invokeinterface. These worked perfectly for statically typed languages like Java.

But for dynamic languages on the JVM (like Groovy, JRuby, or Kotlin's lambdas), this model was too rigid. Each language had to generate complex, custom bytecode just to support dynamic method dispatch — that is, deciding which method to call only at runtime.

That's where invokedynamic comes in. It allows the JVM to defer method linkage — meaning *what method is actually called* — until runtime. This makes the JVM itself responsible for dynamic resolution, rather than forcing every language to reinvent the wheel.

• When the JVM encounters an invokedynamic instruction for the first time, it runs a bootstrap method.
• That bootstrap method acts like a factory — it decides how to link the call site and returns a CallSite object.
• The CallSite contains a MethodHandle — a direct reference to the target method.
• After this first setup, the JVM caches the linkage, so subsequent calls go straight to that MethodHandle — no extra overhead.
1Runnable r = () -> System.out.println("Hello");

When you write a lambda like the one above, the Java compiler doesn't generate a new class file for the lambda. Instead, it emits an invokedynamic instruction. At runtime, the JVM:

• Calls LambdaMetafactory.metafactory(...).
• Dynamically generates a hidden class that implements Runnable.
• Links that generated instance to the call site.
• Optionally returns a cached singleton lambda instance if possible.
• Future lambda calls go directly through the cached MethodHandle — no reflection, no overhead.

In essence, invokedynamic makes dynamic features — such as lambdas, method references, and even entire dynamic languages — run efficiently on the JVM. It bridges the gap between *statically compiled bytecode* and *runtime flexibility*.

Think of invokedynamic as the JVM's built-in way to say: “I'll figure out what method to call later — but once I know, I'll make it fast.”