Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java dynamic proxies: define Object-method behavior explicitly

Last updated: 5 Oct 20265 min read
tutorial
AdvancedBy AITrove Editorial

A java.lang.reflect.Proxy routes interface calls through an InvocationHandler, including equals, hashCode, and toString declared by Object.

Operational contract

The adapter counts receipt reads and delegates the business call. It implements identity-based equals and hashCode for the proxy itself, and a fixed diagnostic toString. That avoids accidentally forwarding Object calls to an unrelated delegate, recursing through the proxy, or returning a value with the wrong type. A dynamic proxy only implements interfaces; a concrete class cannot be proxied this way. The method handler runs on the caller's thread, so a blocking delegate still blocks that caller.

Failure case

A metrics registry inserts a receipt-reader proxy into a hash map. If hashCode returns null or changes with a read count, lookup breaks even though the read method works. This adapter preserves stable identity semantics.

Java code

Java
import java.lang.reflect.Proxy;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;

public class ReceiptReaderProxy {
    public interface Reader { String read(long receiptId); }

    public static Reader counting(Reader delegate, AtomicLong reads) {
        Objects.requireNonNull(delegate);
        Objects.requireNonNull(reads);
        return (Reader) Proxy.newProxyInstance(
                Reader.class.getClassLoader(), new Class<?>[] { Reader.class },
                (proxy, method, arguments) -> {
                    if (method.getDeclaringClass() == Object.class) {
                        return switch (method.getName()) {
                            case "equals" -> proxy == arguments[0];
                            case "hashCode" -> System.identityHashCode(proxy);
                            case "toString" -> "ReceiptReaderProxy";
                            default -> throw new UnsupportedOperationException(method.getName());
                        };
                    }
                    if (!method.getName().equals("read"))
                        throw new UnsupportedOperationException(method.getName());
                    reads.incrementAndGet();
                    return delegate.read((long) arguments[0]);
                });
    }
}

Performance and ownership cost

Each call adds proxy dispatch and an atomic increment before the delegate's work. Storage is O(1) per proxy. A hot call path can make the shared counter contended, so measure it before placing a single counter on every high-volume invocation.

Common Mistakes

  • Do not forget equals, hashCode, and toString can reach the handler.
  • Do not return null for primitive-returning methods such as hashCode.
  • Do not expect Proxy to subclass a concrete class or make a blocking delegate asynchronous.

Connected lessons

java
runtime adapters
dynamic-proxy-object-method-contract
Storage details