I have a class from another library that is closed-source, but I want to be able to use an interface for it. The reason being that I don't want to do instanceof
checks or null
-checks everywhere, but I also don't want to extend the existing class.
For example, let's say I have this code:
public class Example {
// QuietFoo is from another library that I can't change
private static QuietFoo quietFoo;
// LoudFoo is my own code and is meant to replace QuietFoo
private static LoudFoo loudFoo;
public static void main(String[] args) {
handle(foo);
}
private static void handle(Object foo) {
if (foo instanceof QuietFoo)
((QuietFoo) foo).bar();
else if (foo instanceof LoudFoo)
((LoudFoo) foo).bar();
}
}
I can't change QuietFoo
:
public class QuietFoo {
public void bar() {
System.out.println("bar");
}
}
But I can change LoudFoo
:
public class LoudFoo {
public void bar() {
System.out.println("BAR!!");
}
}
The problem is, there may be many other implementations of bar
in many classes, and there may be more methods than just bar
, so not only would my handle
method get slow and ugly with lots of instanceof
statements, but I would have to write one of these handle methods for each method on QuietFoo
and LoudFoo
. Extending isn't a viable solution because it violates the whole is-a contract since LoudFoo
is not a QuietFoo
.
Basically, given Foo
:
public interface Foo {
void bar();
}
How can I make QuietFoo
implement Foo
without changing its source so I don't have to do casting and instanceof
calls everywhere in my code?
Yes, it is possible. This is the catch: java does not support multiple inheritance, i.e. class cannot extend more than one class. However class can implement multiple interfaces.
Yes, it is mandatory to implement all the methods in a class that implements an interface until and unless that class is declared as an abstract class. Implement every method defined by the interface.
Declare the missing methods abstract in your class. This forces you to declare your class abstract and, as a result, forces you to subclass the class (and implement the missing methods) before you can create any objects.
To declare a class that implements an interface, you include an implements clause in the class declaration. Your class can implement more than one interface, so the implements keyword is followed by a comma-separated list of the interfaces implemented by the class.
There are two approaches:
Proxy
The adapter approach will be simpler but less flexible, and the Proxy
approach will be more complex but more flexible. Even though the Proxy
approach is more complex, that complexity is all restricted to a couple of classes.
Adapter
The adapter pattern is simple. For your example, it would just be one class, like so:
public class QuietFooAdapter implements Foo {
private QuietFoo quietFoo;
public QuietFooAdapter(QuietFoo quietFoo) {
this.quietFoo = quietFoo;
}
public void bar() {
quietFoo.bar();
}
}
Then to use it:
Foo foo = new QuietFooAdapter(new QuietFoo());
foo.bar();
This is good, but if you have more than one class to make an adapter for, this can be tedious since you'll need a new adapter for each class you have to wrap.
Java's Proxy
Class
Proxy
is a native Java class that's part of the reflection libraries that will allow you to create a more generic, reflective solution. It involves 3 parts:
Foo
)InvocationHandler
Proxy.newProxyInstance
)We already have the interface, so we're fine there.
The InvocationHandler
is where we do our "auto-adapting" via reflection:
public class AdapterInvocationHandler implements InvocationHandler {
private Object target;
private Class<?> targetClass;
public AdapterInvocationHandler(Object target) {
this.target = target;
targetClass = target.getClass();
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Method targetMethod = targetClass.getMethod(method.getName(), method.getParameterTypes());
if (!method.getReturnType().isAssignableFrom(targetMethod.getReturnType()))
throw new UnsupportedOperationException("Target (" + target.getClass().getName() + ") does not support: " + method.toGenericString());
return targetMethod.invoke(target, args);
} catch (NoSuchMethodException ex) {
throw new UnsupportedOperationException("Target (" + target.getClass().getName() + ") does not support: " + method.toGenericString());
} catch (IllegalAccessException ex) {
throw new UnsupportedOperationException("Target (" + target.getClass().getName() + ") does not declare method to be public: " + method.toGenericString());
} catch (InvocationTargetException ex) {
// May throw a NullPointerException if there is no target exception
throw ex.getTargetException();
}
}
}
The important code here is in the try
block. This will handle the process of adapting any method calls that are called on the proxy to the inner target
object. If a method is called on the interface that isn't supported (non-public
, wrong return type, or just flat out doesn't exist), then we throw an UnsupportedOperationException
. If we catch an InvocationTargetException
, we rethrow the exception that caused it via InvocationTargetException.getTargetException
. This occurs when the method we called reflectively throws an exception. Java wraps it in a new exception and throws that new exception.
Next, we need something to create the adapters:
public class AdapterFactory {
public static <T> T createAdapter(Object target, Class<T> interfaceClass) {
if (!interfaceClass.isInterface())
throw new IllegalArgumentException("Must be an interface: " + interfaceClass.getName());
return (T) Proxy.newProxyInstance(null, new Class<?>[] { interfaceClass }, new AdapterInvocationHandler(target));
}
}
You could also nest the AdapterInvocationHandler
class in the AdapterFactory
class, if you like, so that everything is self-contained in AdapterFactory
.
Then to use it:
Foo foo = AdapterFactory.createAdapter(new QuietFoo(), Foo.class);
foo.bar();
This approach requires more code than implementing a single adapter, but will be generic enough that it can be used to create auto-adapters for any class and interface pair, not just the QuietFoo
and Foo
example. Granted, this method uses reflection (the Proxy
class uses reflection, as does our InvocationHandler
), which can be slower, but recent improvements in the JVM have made reflection much faster than it used to be.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With