Утиная типизация в Java
Если что-то ходит как утка, и крякает как утка, то будем относиться к этому как к утке. Так неформально описывается принцип (Duck Typing). Утиная типизация «развязывает нам руки», позволяя полиморфно работать с объектами, которые не связаны в иерархии наследования, но имеют необходимый набор методов. Здесь мы подходим к извечному спору о том, что лучше – динамическая или статическая типизация. Обойдём его стороной, сославшись на известную статью Мейера и Дрейтона под названием , и лучше посмотрим, как реализовать «утиное» поведение на Java.
Зачем мне эти утки?
Зачем может нам понадобиться утиная типизация в Java? Например, мы работаем с классом, к исходному коду которого мы не имеем доступа. Этот класс, как и некоторые наши собственные, имеет некий одинаковый набор методов, так что мы хотели бы работать со всеми этими классами через единый интерфейс. Если для наших классов мы можем залезть в исходный текст и указать, что они реализуют этот интерфейс, то с чужими классами такое не пройдёт. Конечно, мы можем написать класс-адаптер, или обвёртку, которая реализует необходимый интерфейс и вызывает нужные методы. Но мы можем выполнить всё это «на лету», воспользовавшись рефлексией, proxy-классами и прочими полезными штуками из пакета java.lang.reflect, эмулируя утиную типизацию.
Рассмотрим ситуацию на тривиальном примере. У нас есть интерфейс
public interface IQuackable {
void quack();
}
И какие-то классы:
public class Duck {
public void quack () {
System.out.println(“I am a Duck!”);
}
}
public class Frog {
public void quack () {
System.out.println(“I am a Frog!”);
}
}
Видно, в определениях Duck и Frog не указано, что они реализует IQuackable, однако необходимый метод у них имеется. Мы хотим получить такое поведение:
// ...
IQuackable[] quakers = new IQuackable[] {
Cast(IQuackable.class, new Duck()),
Cast(IQuackable.class, new Frog())
}; for(IQuackable q : quakers) {
q.quack();
}
// …
Стоит отметить, что наши искомые классы должны быть объявлены с модификатором доступа public, иначе во время преобразования может возникнуть исключение java.lang.IllegalAccessException. Это требование обусловлено тем, что реализация должна иметь доступ к искомому классу, чтобы вызывать его методы.
Необходимость в использовании интерфейса вытекает из статической типизации Java. Например, в Groovy, который напрямую поддерживает динамическую и утиную типизацию, мы могли бы поступить так и обойтись без интерфейсов вовсе:
class Duck {
quack() { println "I am a Duck" }
}
class Frog {
quack() { println "I am a Frog" }
}
quackers = [ new Duck(), new Frog() ]
for (q in quackers) {
q.quack()
}
Реализация и Proxy-уточки
Вернёмся к Java, и посмотрим, как же всё это реализовать.
Так, мы создадим три статических метода, которые будут преобразовывать объекты к соответствующим интерфейсам:
- <I> I Cast(Class<I> i, Object o) – приводит ссылку на Object к ссылке на интерфейс I. o должен иметь соответствующий этому интерфейсу набор методов.
- <C> C UnCast(Class<C> c, Object o) – обратное преобразование.
- boolean CanCast(Class<?> i, Object o) – проверяет, может ли объект o быть приведён к соответствующему интерфейсу.
Прежде, чем приступить к реализации этих методов следует уделить время рассмотрению динамических proxy-классов, которые впервые появились в стандартной библиотеке Java 1.3. Они полезны в случае, когда программист на этапе разработки ещё не знает, какие интерфейсы ему предстоит реализовать. Proxy-классы особо полезны для генерации классов-заглушек в технологиях вроде RMI. Но здесь мы воспользуемся ими для эмуляции утиной типизации.
Подробную информацию можно найти на странице .
Динамические proxy-классы реализуют интерфейсы во время выполнения. Каждый вызов методы этого класса через какой-то интерфейс приводит к вызову единственного метода объекта обработчика вызовов, в который передаётся информация о вызванном методе в виде объекта java.lang.reflect.Method и массив Object, содержащий аргументы. Внутри обработчика вызовов мы можем определить необходимое нам поведение. Однако мы не можем генерировать новый код во время выполнения, а всего лишь выполнять заранее подготовленный.
Мы создадим обработчик вызова DuckHandler, который будет вызывать необходимый метод на экземпляре искомого объекта. Сам искомый объект будем хранить внутри обработчика вызовов. Изображение всего этого в виде произвольной схемы будет выглядеть следующим образом:
Так, определение класса-обработчика:
class DuckHandler implements InvocationHandler {
public DuckHandler(Object t) {
target = t;
targetclass = target.getClass();
} @Override
public Object invoke(Object p, Method m, Object[] args)
throws Throwable {
Method me = targetclass.getMethod(m.getName(),
m.getParameterTypes());
return me.invoke(getTarget(),args);
} public Object getTarget() {
return target;
}
private Object target;
private Class<?> targetclass;
}
Всё «мясо» находится в методе invoke. Видно, в ему передаётся экземпляр proxy-класса, который мы игнорируем, метод и аргументы.
Далее реализация методов приведения. Сначала метод проверки того, может ли наш объект динамически реализовать интерфейс. Мы просто сканируем все методы интерфейса с помощью рефлексии и проверяем, имеет ли их класс искомого объекта o.
public static boolean CanCast(Class<?> i, Object o) {
Class<?> c = o.getClass();
for (Method method : i.getMethods()) {
try {
c.getMethod(method.getName(), method.getParameterTypes());
}
catch (NoSuchMethodException e) {
return false;
}
}
return true;
}
Далее, само «преобразование». Перед созданием proxy-класса мы проверяем с помощью метода, определённого выше, может ли класс нашего объекта реализовать необходимый интерфейс. Если может, используя статический метод Proxy.newProxyInstance мы создаём proxy-класс. Первый аргумент этого метода – загрузчик класса, второй – массив интерфейсов, которые необходимо реализовать (у нас один интерфейс). И наконец, третий аргумент – обработчик вызовов. Сюда мы передаём экземпляр нашего класса, в котором сохраняется искомый объект о (помните, на нём вызываются все методы).
@SuppressWarnings("unchecked") public static <I> I Cast(Class<I> c, Object o) { if (!CanCast(c, o))
throw new ClassCastException();
Object proxy = Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{c}, new DuckHandler(o)); return (I) proxy;
}
Ну и обратный предыдущему метод, который принимает тип искомого класса и объект proxy-класса, и возвращает исходный объект. Он хранится внутри обработчика вызовов, так что получить его не составляет особого труда.
@SuppressWarnings("unchecked") public static <C> C UnCast(Class<C> c, Object o) { DuckHandler h = (DuckHandler) Proxy.getInvocationHandler(o); return (C) h.getTarget();
}
Заключение
Утиная типизация представляется одной из тех вещей, без которых с успехом можно обойтись. Но в некоторых случаях она может прийтись весьма кстати, помогая сократить ручное кодирование. Так, в этой статье было рассмотрено, как с помощью рефлексии и динамических Proxy-классов Java добиться эмуляции такого «утиного» поведения. Была так же создана совсем крохотная инструментальная библиотека для демонстрации.
Исходный код вы можете загрузить .
Как раз во время написания статьи наткнулся на аналогичную. Вот ссылка: (англ).