Let's get started with a Microservice Architecture with Spring Cloud:
Overriding a Method in an Instantiated Java Object
Last updated: February 14, 2026
1. Overview
We can override an instantiated object‘s behavior via runtime behavior modifications, which we typically achieve through design patterns and frameworks rather than by directly altering the object’s class at runtime.
Let’s discuss four approaches to modify the behavior of an existing object.
2. Adding Logging to a Calculator
Let’s say that we have a Calculator interface and implementation that defines two basic methods, add and subtract:
public interface Calculator {
int add(int a, int b);
int subtract(int a, int b);
}
public class SimpleCalculator implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int subtract(int a, int b) {
return a - b;
}
}
Let’s further say that we’d like to track method call count or add logging to these methods, but we either can’t or don’t want to modify the class itself. In other words, when someone calls add(3, 5), or subtract(10, 3), we also want to track a count of method calls, or log some logging messages using the SLF4J Logback framework.
There are at least four ways we could consider doing this:
- Subclass
- The Decorator Pattern
- JDK Dynamic Proxy
- Spring’s ProxyFactory
3. Subclassing
Subclassing is a straightforward approach. Simply put, we can extend SimpleCalculator and override its methods add and subtract to include logging:
public class LoggingCalculator extends SimpleCalculator {
@Override
public int add(int a, int b) {
log.debug("LOG: Before addition.");
int result = super.add(a, b);
log.debug("LOG: After addition. Result: {}", result);
return result;
}
@Override
public int subtract(int a, int b) {
log.debug("LOG: Before subtraction.");
int result = super.subtract(a, b);
log.debug("LOG: After subtraction. Result: {}", result);
return result;
}
}
Now, if we construct a LoggingCalculator instead of a SimpleCalculator, we’ll get the logging behavior that we want. Thereupon, we verify using JUnit 5 test assertions that we can use this subclass:
@Test
void givenACalculatorClass_whenSubclassingToAddLogging_thenLoggingCalculatorCanBeUsed() {
Calculator calculator = new LoggingCalculator();
assertEquals(8, calculator.add(5, 3));
assertEquals(2, calculator.add(5, 3));
}
There’s a limitation, though. For example, what do we do if we already have an instance of SimpleCalculator at runtime? Therefore, we need to find runtime approaches to overriding method behavior, which is what we discuss next.
4. Using the Decorator Pattern
Decorator is a design pattern that provides the benefits of subclassing while also addressing its limitations. It’s a structural pattern that enables behavior to be added to an individual object. Further, we can add behavior either statically or dynamically. Moreover, we can add behavior without affecting the class itself or the behavior of other objects from the same class.
Let’s define a decorator class called MeteredCalculatorDecorator that implements the Calculator interface, and overrides its two methods to track method call count:
public class MeteredCalculatorDecorator implements Calculator {
private final Calculator wrappedCalculator;
private final Map<String, Integer> methodCalls;
public MeteredCalculatorDecorator(Calculator calculator) {
this.wrappedCalculator = calculator;
this.methodCalls = new HashMap<>();
methodCalls.put("add", 0);
methodCalls.put("subtract", 0);
}
@Override
public int add(int a, int b) {
methodCalls.merge("add", 1, Integer::sum);
return wrappedCalculator.add(a, b);
}
@Override
public int subtract(int a, int b) {
methodCalls.merge("subtract", 1, Integer::sum);
return wrappedCalculator.subtract(a, b);
}
public int getCallCount(String methodName) {
return methodCalls.getOrDefault(methodName, 0);
}
}
Accordingly, this decorator wraps behavior, and thus, when we pass it a Calculator object, it wraps its behavior to add tracking of method call count. As before, we can verify that it does extend the methods’ behavior with a test method:
@Test
void givenACalculator_whenUsingMeteredDecorator_thenMethodCallsAreCountedCorrectly() {
Calculator simpleCalc = new SimpleCalculator();
MeteredCalculatorDecorator decoratedCalc = new MeteredCalculatorDecorator(simpleCalc);
decoratedCalc.add(10, 5);
decoratedCalc.add(2, 3);
decoratedCalc.subtract(10, 5);
assertEquals(15, decoratedCalc.add(10, 5), "Core functionality must still work.");
assertEquals(3, decoratedCalc.getCallCount("add"), "The 'add' method should have been called 3 times.");
assertEquals(1, decoratedCalc.getCallCount("subtract"), "The 'subtract' method should have been called 1 time.");
}
5. Using a JDK Dynamic Proxy
Alternatively, we can use a JDK dynamic proxy. A JDK dynamic proxy generates a proxy class and object at runtime, implementing one or more interfaces. Further, it redirects method calls on the proxy to a custom InvocationHandler.
Let’s create an invocation handler that intercepts all method calls on the proxy to add logging:
public class LoggingInvocationHandler implements InvocationHandler {
private final Object target;
public LoggingInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.debug("PROXY LOG: Intercepting method: {}", method.getName());
Object result = method.invoke(target, args);
log.debug("PROXY LOG: Method {} executed.", method.getName());
return result;
}
}
Afterward, let’s use a test method to verify that we can generate and use the dynamic proxy with a Calculator object:
@Test
void givenACalculator_whenUsingJdkDynamicProxy_thenJdkDynamicProxyCanBeUsed() {
Calculator simpleCalc = new SimpleCalculator();
LoggingInvocationHandler handler = new LoggingInvocationHandler(simpleCalc);
Calculator proxyCalc = (Calculator) Proxy.newProxyInstance(
Calculator.class.getClassLoader(),
new Class<?>[] { Calculator.class },
handler
);
assertEquals(30, proxyCalc.add(20, 10));
assertEquals(10, proxyCalc.subtract(20, 10));
}
6. Using Spring’s ProxyFactory
Or, we can use Spring’s ProxyFactory, a sophisticated utility that abstracts the proxy creation mechanism. Additionally, it automatically chooses between JDK Dynamic Proxy (for interfaces) and CGLIB (for concrete classes), enabling us to inject method interceptors (AOP Advice).
Spring’s MethodInterceptor is similar to InvocationHandler but uses AOP-standard interfaces instead.
To begin with, let’s add dependencies for Spring AOP (aspect-oriented programming) to the pom.xml:
<dependencies>
...
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.11</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.11</version>
</dependency>
</dependencies>
Then, let’s create the Spring equivalent of an InvocationHandler:
public class LoggingMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.debug("SPRING PROXY: Intercepting method: {}", invocation.getMethod().getName());
Object result = invocation.proceed();
log.debug("SPRING PROXY: Method {} completed.", invocation.getMethod().getName());
return result;
}
}
Afterward, let’s verify that we can use Spring’s ProxyFactory with a Calculator object:
@Test
void givenACalculator_whenUsingSpringProxyFactory_thenSpringProxyFactoryCanBeUsed() {
SimpleCalculator simpleCalc = new SimpleCalculator();
ProxyFactory factory = new ProxyFactory();
factory.setTarget(simpleCalc);
factory.addAdvice(new LoggingMethodInterceptor());
Calculator proxyCalc = (Calculator) factory.getProxy();
assertEquals(60, proxyCalc.add(50, 10));
assertEquals(40, proxyCalc.subtract(50, 10));
}
7. Choosing an Approach
Let’s review the use case for each of these approaches:
| Feature | Subclassing | Decorator Pattern | JDK Dynamic Proxy | Spring’s ProxyFactory |
|---|---|---|---|---|
| When to Use | When we want to change behavior for all new instances of a derived type. | When we need to dynamically add new behavior to individual objects, and these modifications must be applied at the point of instantiation/assembly. Excellent for cross-cutting concerns like logging, caching, or validation. | When we need to apply cross-cutting concerns (logging, security, transactions) to many objects without modifying the source code, and without writing specific decorator classes. | When working within the Spring ecosystem, or when we need a robust, unified way to apply cross-cutting concerns (AOP) that handles both interfaces (JDK proxy) and classes (CGLIB) automatically. |
| Limitations | We can’t modify the behavior of an already instantiated object. | It requires manual creation of a Decorator class for every set of new behaviors, which can lead to a large number of small, single-purpose classes. | JDK Dynamic Proxy can only proxy interfaces. It can’t proxy concrete classes directly. Performance can be slightly lower than direct method calls. | It requires the Spring AOP/Context dependency. It’s perhaps too heavyweight for a simple, standalone application. |
8. Conclusion
In this article, we explored four ways to override a method’s behavior in Java. We also used a comparison table to choose the best approach for our use case.
As always, the full code for the examples is available over on GitHub.
















