Отказ от ответственности: пожалуйста, не воспринимайте это слишком серьезно. Это просто забава и игры.
Утиный ввод – это идиома, известная в основном из динамически типизированных языков. В нем говорится, что вы можете рассматривать несвязанные объекты типа 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 staticT 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”