Отказ от ответственности: пожалуйста, не воспринимайте это слишком серьезно. Это просто забава и игры.
Утиный ввод – это идиома, известная в основном из динамически типизированных языков. В нем говорится, что вы можете рассматривать несвязанные объекты типа X как объекты типа Y, если оба имеют один и тот же открытый интерфейс.
Если он выглядит как утка, двигается как утка и издает звуки как утка, то это должна быть утка!
С другой стороны, система статических типов Java налагает строгие правила на совместимость типов. Простое использование одних и тех же методов не позволяет двум классам быть “совместимыми с типами” . Рассмотрим этот пример:
public class Duck {
public String makeNoise() {
return "Quak Quak";
}
}
public class Dog {
public String makeNoise() {
return "woof woof";
}
}
Оба класса имеют одинаковый интерфейс, поэтому можем ли мы назначить экземпляры соответствующему другому типу?
final Duck duck = new Dog(); // fails
assertEquals("woof woof", duck.makeNoise());
Конечно, мы не можем. Система типов Java нуждается в явных определениях совместимых классов с использованием наследования.
Давайте теперь заставим наш пример работать, используя самые простые мыслимые изменения в нашем коде:
public class Dog extends Duck {
@Override
public String makeNoise() {
return "woof woof";
}
}
Помимо всего, что вы узнали на уроках биологии, наша Собака теперь является уткой и, следовательно, может присваиваться переменным с типом Утка . Это изменение должно было заставить приведенный выше тест стать зеленым.
Хорошо, теперь ваша собака – Утка, но как насчет всех других классов в нашей кодовой базе, которые также имеют makeNoise метод? Должны ли они все расширять класс Duck ? Это кажется непрактичным и сбивающим с толку ( поезд класса расширяет утку еще более странно, чем Собака, являющаяся Уткой).
Возможно, мы сможем создать более динамичную утку, используя отражение . Во время выполнения JVM содержит довольно обширную информацию о типе каждого объекта. Помимо доступа к информации о типе, отражение также позволяет нам динамически извлекать и вызывать методы для произвольных объектов.
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");
}
}
}
Используя нашу Динамическую утку мы можем получить “утиный вид” для каждого объекта, не являющегося утиным. Давайте соответствующим образом скорректируем наш первоначальный тест (а также удалим предложение extends в классе Dog ):
final Duck duck = new DynamicDuck(new Dog());
assertEquals("woof woof", duck.makeNoise());
Это немного более динамично, но все равно плохо масштабируется. Если бы в классе Duck было больше открытых методов, нам пришлось бы делегировать каждый метод объекту, не являющемуся утиным, как мы сделали выше. И если мы решили рассматривать все не как Утку, а как Гуся, нам нужно дополнительно создать класс Динамический гусь . Все это слишком громоздко.
К счастью, в Java есть еще одна полезная концепция, которая пригодится в нашем случае использования. Каждый класс, доступный во время выполнения, будет загружен из своего представления байтового кода с использованием Загрузчика классов . Поскольку это чисто функция времени выполнения, вы можете создавать искусственные классы во время выполнения, которые не существовали во время компиляции.
Мы можем динамически генерировать байт-код класса, который расширяет Duck и который автоматически делегирует каждый вызываемый метод эквивалентному методу произвольного объекта, не являющегося утиным. В следующем коде используются популярные библиотеки cglib (для генерации байтового кода) и Objenesis (для создания экземпляров динамически генерируемых классов.
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 asDuck(Object notADuck, Class 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 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);
}
}
}
Используя этот класс, мы можем создать динамическое представление экземпляра собаки в виде утки:
final Duck duck = NotADuck.asDuck(new Dog(), Duck.class);
assertEquals("woof woof", duck.makeNoise());
Код больше не ограничивается только созданием Утиных объектов:
List list = NotADuck.asDuck("I'm a String", List.class);
// both List and String have an isEmpty method
assertFalse(list.isEmpty());
Это был всего лишь небольшой эксперимент. В этой реализации вероятны ошибки, выявленные крайние случаи, проблемы с производительностью и общее отсутствие практических вариантов использования. Тем не менее, интересно посмотреть, как далеко вы можете зайти с информацией о динамических типах Java и материализацией произвольного байтового кода.
Не делай этого дома!
Оригинал: “https://dev.to/skuzzle/duck-typing-in-java-6gh”