skip to content

How does InvocationHandler.invoke work, and what are the gotchas around Object methods, primitives, and exceptions?

level: seniorimportance: should knowfreq 42%

answer

  1. One method: invoke(proxy, method, args)
  2. args is null for zero-arg methods
  3. Primitive return → return wrapper, never null
  4. equals/hashCode/toString routed to invoke
  5. Undeclared checked → UndeclaredThrowableException
  6. Unwrap InvocationTargetException.getCause()

basics

~20 s

invoke(proxy, method, args) runs for every call on the proxy. You read the method name and arguments, do your logic (often forwarding to a real object), and return the result. Calls to equals, hashCode, and toString also reach invoke.

solid answer

~50 s

InvocationHandler has one method: Object invoke(Object proxy, Method method, Object[] args). The proxy routes every interface method — plus Object's equals, hashCode, and toString — into it. args is null when there are no parameters. Your return value must be assignment-compatible with the method's return type; for a primitive return you must return the boxed type (e.g. Integer for int) and never null, or you get a NullPointerException on unboxing. Exception handling is strict: you may throw any unchecked exception or a checked exception the method declares, but throwing an undeclared checked exception causes the proxy to wrap it in an UndeclaredThrowableException. A common pitfall is calling another interface method on the proxy from inside invoke — it re-enters invoke and can recurse infinitely. You typically implement equals/hashCode/toString explicitly so the proxy behaves sanely in collections and logs.

code

java · 15 lines
java
InvocationHandler h = (proxy, method, args) -> {
    if (method.getDeclaringClass() == Object.class) {
        return switch (method.getName()) {
            case "equals"   -> proxy == args[0];
            case "hashCode" -> System.identityHashCode(proxy);
            case "toString" -> "DynamicProxy@" + Integer.toHexString(System.identityHashCode(proxy));
            default -> throw new UnsupportedOperationException(method.getName());
        };
    }
    try {
        return method.invoke(target, args);      // forward to real object
    } catch (InvocationTargetException e) {
        throw e.getCause();                       // unwrap real exception
    }
};

go deeper

for a junior

Knows invoke is the callback that runs on each proxy call and can read method.getName() and args.

for a middle

Forwards to a real target via method.invoke, handles null args, and returns the right value type.

for a senior

Handles the primitive-return/boxing trap, routes equals/hashCode/toString, and understands UndeclaredThrowableException plus unwrapping InvocationTargetException.

for a principal

Designs robust handler base classes, reasons about re-entrancy, exception fidelity across proxy layers, and thread-safety of shared handler state.

## The single method that powers everything `InvocationHandler` is a functional interface: ```java public interface InvocationHandler { Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } ``` When you build a proxy with `Proxy.newProxyInstance(loader, interfaces, handler)`, the generated class implements each interface method with a body that essentially calls `handler.invoke(this, theMethodObject, theArgsArray)`. So **one** method handles **all** calls. ### The three parameters - **`proxy`**: the proxy instance itself (the same object the caller is holding). Useful if you need identity, but **be careful** — calling interface methods on it from within `invoke` re-enters `invoke`. - **`method`**: a `java.lang.reflect.Method` describing what was called — `method.getName()`, `getReturnType()`, `getParameterTypes()`, and `getDeclaringClass()` (which interface, or `Object`). - **`args`**: an `Object[]` of the call arguments. **It is `null`, not an empty array, when the method takes no parameters.** Primitive arguments arrive boxed (an `int` arg becomes an `Integer`). ### Return value rules (primitive trap) Whatever you return must be assignable to the method's declared return type. For a method returning `void`, return `null`. For a method returning a **primitive** like `int` or `boolean`, you must return the matching wrapper (`Integer`, `Boolean`); returning `null` triggers a `NullPointerException` when the proxy unboxes it. Returning the wrong type throws `ClassCastException`. ### Object methods flow through invoke The proxy does **not** auto-implement `equals`, `hashCode`, and `toString` from `Object`; these three are dispatched to your handler (other final `Object` methods like `getClass`/`wait`/`notify` are not). If you ignore them: `equals` falls back to identity if you forward to the target incorrectly, `hashCode` may be inconsistent, and `toString` prints something unhelpful. Best practice is to detect them in `invoke` and provide sane behavior: ```java if (method.getDeclaringClass() == Object.class) { switch (method.getName()) { case "equals": return proxy == args[0]; case "hashCode": return System.identityHashCode(proxy); case "toString": return "Proxy(" + ... + ")"; } } ``` ### Exception semantics `invoke` is declared `throws Throwable`, so you can throw anything. But the proxy enforces the called method's contract: - **Unchecked** exceptions (`RuntimeException`, `Error`) always propagate as-is. - A **checked** exception propagates only if the invoked interface method **declares** it. - Throwing an **undeclared checked** exception is illegal for the method's signature, so the proxy wraps it in an `java.lang.reflect.UndeclaredThrowableException` (an unchecked exception) before it reaches the caller. A subtle related point: when you forward with `method.invoke(target, args)` and the target throws, reflection wraps the real exception in an `InvocationTargetException` — you should unwrap it with `getCause()` and rethrow, or the caller sees the wrong exception type. ### Re-entrancy / recursion pitfall If inside `invoke` you call an interface method **on the `proxy` argument**, that call goes back into `invoke` — easy to cause infinite recursion. Forward to the **real target** object instead.

  • What happens if your handler throws a checked exception the interface method doesn't declare?
    The proxy wraps it in a java.lang.reflect.UndeclaredThrowableException (unchecked) with the original as its cause, because the thrown type would violate the method's declared signature.
  • Why might you see InvocationTargetException when forwarding a call?
    Method.invoke wraps any exception thrown by the underlying target method in an InvocationTargetException. You should catch it and rethrow getCause() so the original exception reaches the caller.

saying these in an interview costs you the question

  • Returning null from invoke for a primitive-returning method
  • Calling interface methods on the proxy arg inside invoke (infinite recursion)
  • Assuming args is an empty array for no-arg methods
  • Letting InvocationTargetException leak instead of unwrapping the cause
  • Thinking equals/hashCode/toString are auto-handled by the proxy

context