Duck typing in java
- 7 minsOriginally posted on dev.to
Disclaimer: please don’t take this too serious. It’s just fun and games.
Duck typing is an idiom known mostly from dynamically typed languages. It states that you can treat unrelated Objects of type X as Objects of type Y as long as both have the same public interface.
If it looks like duck, moves like duck and makes sounds like a duck then it must be a duck!
Java’s static type system on the other hand, imposes strong rules on type compatibility. Just having the same methods doesn’t qualify two classes to be “type compatible”. Consider this example:
public class Duck {
public String makeNoise() {
return "Quak Quak";
}
}
public class Dog {
public String makeNoise() {
return "woof woof";
}
}
Both classes have the same interface, so can we assign instances to the respective other type?
final Duck duck = new Dog(); // fails
assertEquals("woof woof", duck.makeNoise());
Of course we can’t. Java’s type system needs explicit definitions of compatible classes using inheritance.
Let’s now make our example work using the easiest imaginable change to our code:
public class Dog extends Duck {
@Override
public String makeNoise() {
return "woof woof";
}
}
Besides everything you learned in biology classes our Dog
now is a duck and is thereby assignable to variables with type Duck
. This change should have made the test above turn green.
Ok, now our Dog is a Duck, but what about all the other classes in our code base that also have a makeNoise
method? Should they all extend the Duck
class? That seems impractical and confusing (class Train extends Duck
is even stranger than a Dog being a Duck).
Maybe we can build a more dynamic Duck by using reflection. During runtime the JVM has pretty extensive type information about every object. Besides accessing type information, reflection also allows us to dynamically retrieve and call methods on arbitrary objects.
public class DynamicDuck extends Duck {
private final Object notADuck;
public DynamicDuck(Object notADuck) {
this.notADuck = notADuck;
}
@Override
public String makeNoise() {
// try block is needed because the reflection stuff will
// throw all kind of horrible exceptions if you use it wrong
try {
// retrieve dynamic type information of the non-duck object
final Class<?> notADuckType = this.notADuck.getClass();
// find the method of the class that is not a duck by name
final Method delegate = notADuckType.getMethod("makeNoise");
// invoke the method on the object that is not a duck
return (String) delegate.invoke(this.notADuck);
} catch (Exception e) {
throw new IllegalStateException("DynamicDuck error");
}
}
}
Using our DynamicDuck
we can get a “duck-view” on every non-duck Object. Let’s adjust our initial test accordingly (and also remove the extends
clause in the Dog
class):
final Duck duck = new DynamicDuck(new Dog());
assertEquals("woof woof", duck.makeNoise());
That’s a little more dynamic but still doesn’t scale well. If the Duck
class had more public methods we’d have to delegate every method to the non-duck Object like we did above. And if we decided not to view everything as a Duck, but as a Goose, we need to additionally create a DynamicGoose
class. That’s all way too cumbersome.
Luckily, Java has another useful concept that comes in handy for our use case. Every class that is available during runtime will be loaded from its byte code representation using a ClassLoader
. As this is a pure runtime feature you can make up artificial classes during runtime that did not exist during compile time.
We can dynamically generate the byte code of a class that extends Duck
and which automatically delegates every called method to the equivalent method of an arbitrary non-duck Object. The following code uses the popular libraries cglib (for byte code generation) and Objenesis (for instantiating the dynamically generated classes.
import net.sf.cglib.core.DefaultNamingPolicy;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.InvocationHandler;
import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;
import java.lang.reflect.Method;
public class NotADuck {
/**
* The unique callback index to which each method in the proxy object is mapped.
*/
private static final int CALLBACK_INDEX = 0;
/**
* Maps all methods to index {@link #CALLBACK_INDEX}.
*/
private static final CallbackFilter ZERO_CALLBACK_FILTER = method -> CALLBACK_INDEX;
// with caching
private static final Objenesis OBJENESIS = new ObjenesisStd(true);
public static <T> T asDuck(Object notADuck, Class<T> type) {
final Enhancer enhancer = new Enhancer();
final InvocationHandler invocationHandler = new DelegateDuckToNonDuckMethod(notADuck);
enhancer.setSuperclass(type);
enhancer.setUseFactory(true);
enhancer.setNamingPolicy(new DefaultNamingPolicy());
enhancer.setCallbackFilter(ZERO_CALLBACK_FILTER);
enhancer.setCallbackType(invocationHandler.getClass());
final Class<T> proxyClass = enhancer.createClass();
final T duckInstance = OBJENESIS.getInstantiatorOf(proxyClass).newInstance();
final Factory factory = (Factory) duckInstance;
factory.setCallback(CALLBACK_INDEX, invocationHandler);
return duckInstance;
}
private static class DelegateDuckToNonDuckMethod implements InvocationHandler {
private final Object notADuck;
private DelegateDuckToNonDuckMethod(Object notADuck) {
this.notADuck = notADuck;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
final Method nonDuckMethod = notADuck.getClass().getMethod(method.getName(), method.getParameterTypes());
return nonDuckMethod.invoke(notADuck, args);
}
}
}
Using this class, we can create a dynamic, duck-view of a Dog instance:
final Duck duck = NotADuck.asDuck(new Dog(), Duck.class);
assertEquals("woof woof", duck.makeNoise());
The code is no longer restricted to only create Duck
objects:
List list = NotADuck.asDuck("I'm a String", List.class);
// both List and String have an isEmpty method
assertFalse(list.isEmpty());
This was just a little experiment. There are likely bugs, uncovered edge cases, performance issues and general lack of practical use cases in this implementation. Nevertheless it’s fun to see how far you can go with Java’s dynamic type information and materialization of arbitrary byte code.
Don’t do this at home!