Выбор перегруженного метода на основе реального типа параметра
Я экспериментирую с этим кодом:
interface Callee {
public void foo(Object o);
public void foo(String s);
public void foo(Integer i);
}
class CalleeImpl implements Callee
public void foo(Object o) {
logger.debug("foo(Object o)");
}
public void foo(String s) {
logger.debug("foo(\"" + s + "\")");
}
public void foo(Integer i) {
logger.debug("foo(" + i + ")");
}
}
Callee callee = new CalleeImpl();
Object i = new Integer(12);
Object s = "foobar";
Object o = new Object();
callee.foo(i);
callee.foo(s);
callee.foo(o);
Это печатает foo(Object o)
три раза. Я ожидаю, что выбор метода примет во внимание реальный (не объявленный) тип параметра. Я что-то пропустил? Есть ли способ изменить этот код, чтобы он печатал foo(12)
, foo("foobar")
а также foo(Object o)
?
7 ответов
Я ожидаю, что выбор метода примет во внимание реальный (не объявленный) тип параметра. Я что-то пропустил?
Да. Ваше ожидание неверно. В Java динамическая диспетчеризация метода происходит только для объекта, для которого вызывается метод, а не для типов параметров перегруженных методов.
Ссылаясь на спецификацию языка Java:
Когда метод вызывается (§15.12), число фактических аргументов (и любых явных аргументов типа) и типы аргументов во время компиляции используются во время компиляции, чтобы определить сигнатуру метода, который будет вызван (§15.12.2). Если метод, который должен быть вызван, является методом экземпляра, фактический метод, который должен быть вызван, будет определен во время выполнения, используя динамический поиск метода (§15.12.4).
Как упоминалось ранее, разрешение перегрузки выполняется во время компиляции.
Java Puzzlers имеет хороший пример для этого:
Головоломка 46: дело запутанного конструктора
Эта головоломка представляет вам двух сбивающих с толку конструкторов. Основной метод вызывает конструктор, но какой? Вывод программы зависит от ответа. Что печатает программа, или это даже законно?
public class Confusing {
private Confusing(Object o) {
System.out.println("Object");
}
private Confusing(double[] dArray) {
System.out.println("double array");
}
public static void main(String[] args) {
new Confusing(null);
}
}
Решение 46: Случай запутанного конструктора
... Процесс разрешения перегрузки Java работает в два этапа. На первом этапе выбираются все методы или конструкторы, которые доступны и применимы. На втором этапе выбираются наиболее конкретные методы или конструкторы, выбранные на первом этапе. Один метод или конструктор менее специфичен, чем другой, если он может принимать любые параметры, переданные другому [JLS 15.12.2.5].
В нашей программе оба конструктора доступны и применимы. КонструкторConfusing(Object) принимает любой параметр, переданный в Confusing (double []), поэтомуConfusing(Object) менее конкретен. (Каждый двойной массив является объектом, но не каждый объект является двойным массивом.) Поэтому наиболее конкретным конструктором является Confused (double []), который объясняет вывод программы.
Такое поведение имеет смысл, если вы передаете значение типа double []; Это нелогично, если вы передаете ноль. Ключом к пониманию этой загадки является то, что тест, для которого метод или конструктор является наиболее конкретным, не использует фактические параметры: параметры, появляющиеся в вызове. Они используются только для определения того, какие перегрузки применимы. Как только компилятор определяет, какие перегрузки применимы и доступны, он выбирает наиболее специфическую перегрузку, используя только формальные параметры: параметры, указанные в объявлении.
Чтобы вызвать конструктор Confusing(Object) с нулевым параметром, напишите новый Confusing ((Object) null). Это гарантирует, что применим только Confusing(Object). В более общем смысле, чтобы заставить компилятор выбирать конкретную перегрузку, приведите фактические параметры к объявленным типам формальных параметров.
Возможность отправки вызова в метод, основанный на типах аргументов, называется множественной диспетчеризацией. В Java это делается с помощью шаблона Visitor.
Тем не менее, так как вы имеете дело с Integer
с и String
s, вы не можете легко включить этот шаблон (вы просто не можете изменить эти классы). Таким образом, гигант switch
во время выполнения объекта будет вашим выбором оружия.
В Java вызываемый метод (например, в какой сигнатуре метода используется) определяется во время компиляции, поэтому он соответствует типу времени компиляции.
Типичным шаблоном для решения этой проблемы является проверка типа объекта в методе с помощью сигнатуры объекта и делегирование метода при помощи приведения.
public void foo(Object o) {
if (o instanceof String) foo((String) o);
if (o instanceof Integer) foo((Integer) o);
logger.debug("foo(Object o)");
}
Если у вас есть много типов, и это неуправляемо, то перегрузка методов, вероятно, не является правильным подходом, скорее, публичный метод должен просто взять Object и реализовать некоторый шаблон стратегии, чтобы делегировать соответствующую обработку для типа объекта.
У меня была похожая проблема с вызовом правильного конструктора класса с именем "Параметр", который мог принимать несколько базовых типов Java, таких как String, Integer, Boolean, Long и т. Д. Учитывая массив объектов, я хочу преобразовать их в массив из моих объектов Parameter путем вызова наиболее конкретного конструктора для каждого объекта во входном массиве. Я также хотел определить конструктор Parameter(Object o), который будет генерировать исключение IllegalArgumentException. Конечно, я обнаружил, что этот метод вызывается для каждого объекта в моем массиве.
Решение, которое я использовал, состояло в том, чтобы искать конструктор с помощью отражения...
public Parameter[] convertObjectsToParameters(Object[] objArray) {
Parameter[] paramArray = new Parameter[objArray.length];
int i = 0;
for (Object obj : objArray) {
try {
Constructor<Parameter> cons = Parameter.class.getConstructor(obj.getClass());
paramArray[i++] = cons.newInstance(obj);
} catch (Exception e) {
throw new IllegalArgumentException("This method can't handle objects of type: " + obj.getClass(), e);
}
}
return paramArray;
}
Никаких уродливых instanceof, операторов switch или шаблона посетителя не требуется!:)
Java смотрит на ссылочный тип, когда пытается определить, какой метод вызывать. Если вы хотите форсировать свой код, вы выбираете "правильный" метод, вы можете объявить свои поля как экземпляры определенного типа:
Integeri = new Integer(12);
String s = "foobar";
Object o = new Object();
Вы также можете использовать свои параметры как тип параметра:
callee.foo(i);
callee.foo((String)s);
callee.foo(((Integer)o);
Если существует точное соответствие между числом и типами аргументов, указанными в вызове метода, и сигнатурой метода перегруженного метода, то этот метод будет вызываться. Вы используете ссылки на Object, поэтому во время компиляции java решает, что для параметра Object есть метод, который принимает непосредственно Object. Так он назвал этот метод 3 раза.