Выбор перегруженного метода на основе реального типа параметра

115

Я экспериментирую с этим кодом:

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)?

Сергей Миханов
источник

Ответы:

97

Я ожидаю, что при выборе метода будет учитываться реальный (а не заявленный) тип параметра. Я что-то упускаю?

Да. Ваше ожидание неверно. В Java отправка динамического метода происходит только для объекта, для которого вызывается метод, а не для типов параметров перегруженных методов.

Ссылаясь на спецификацию языка Java :

Когда вызывается метод (§15.12), количество фактических аргументов (и любых явных аргументов типа) и типы аргументов во время компиляции используются во время компиляции для определения сигнатуры метода, который будет вызван ( §15.12.2). Если метод, который должен быть вызван, является методом экземпляра, фактический вызываемый метод будет определен во время выполнения с использованием динамического поиска метода (§15.12.4).

Майкл Боргвардт
источник
4
Не могли бы вы объяснить приведенную вами спецификацию? Кажется, что эти два предложения противоречат друг другу. В приведенном выше примере используются методы экземпляра, но вызываемый метод явно не определяется во время выполнения.
Alex Worden
16
@Alex Worden: в данном случае тип времени компиляции параметров метода используется для определения сигнатуры вызываемого метода foo(Object). Во время выполнения класс объекта, для которого вызывается метод, определяет, какая реализация этого метода вызывается, с учетом того, что это может быть экземпляр подкласса объявленного типа, который переопределяет метод.
Майкл Боргвардт
86

Как упоминалось ранее, разрешение перегрузки выполняется во время компиляции.

У 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) менее конкретен. (Каждый двойной массив является объектом , но не каждый объект является двойным массивом .) Поэтому наиболее конкретным конструктором является Confusing (double []) , который объясняет вывод программы.

Такое поведение имеет смысл, если вы передаете значение типа double [] ; это нелогично, если вы передадите null . Ключ к пониманию этой загадки заключается в том, что тест, для которого метод или конструктор наиболее специфичен, не использует фактические параметры : параметры, появляющиеся в вызове. Они используются только для определения возможных перегрузок. Как только компилятор определяет, какие перегрузки применимы и доступны, он выбирает наиболее конкретную перегрузку, используя только формальные параметры: параметры, указанные в объявлении.

Чтобы вызвать конструктор Confusing (Object) с параметром null , напишите new Confusing ((Object) null) . Это гарантирует, что применим только Confusing (Object) . В более общем смысле, чтобы заставить компилятор выбрать конкретную перегрузку, приведите фактические параметры к объявленным типам формальных параметров.

денис.жданов
источник
4
Надеюсь, еще не поздно сказать - «одно из лучших объяснений по SOF». Спасибо :)
TheLostMind
5
Я считаю, что если бы мы также добавили конструктор private Confusing (int [] iArray), он бы не компилировался, не так ли? Потому что теперь есть два конструктора с одинаковой спецификой.
Risser
Если я использую динамические возвращаемые типы в качестве входных данных, он всегда использует менее конкретный ... сказал, что метод, который можно использовать для всех возможных возвращаемых значений ...
kaiser
16

Возможность отправки вызова метода на основе типов аргументов называется многократной отправкой . В Java это делается с помощью шаблона Visitor .

Однако, поскольку вы имеете дело с Integers и Strings, вы не можете легко включить этот шаблон (вы просто не можете изменять эти классы). Таким образом, switchвашим любимым оружием будет гигант во время выполнения объекта.

Антон Гоголев
источник
11

В 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 и реализовать какой-то шаблон стратегии для делегирования соответствующей обработки для каждого типа объекта.

Ишай
источник
4

У меня была аналогичная проблема с вызовом правильного конструктора класса под названием «Parameter», который мог принимать несколько основных типов 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 или шаблона посетителя не требуется! :)

Алекс Уорден
источник
2

Java смотрит на ссылочный тип при попытке определить, какой метод вызвать. Если вы хотите принудительно использовать свой код, вы выбираете «правильный» метод, вы можете объявить свои поля как экземпляры определенного типа:

Integeri = new Integer(12);
String s = "foobar";
Object o = new Object();

Вы также можете указать свои параметры как тип параметра:

callee.foo(i);
callee.foo((String)s);
callee.foo(((Integer)o);
АКФ
источник
1

Если есть точное совпадение между количеством и типами аргументов, указанных в вызове метода, и сигнатурой метода перегруженного метода, то будет вызван именно этот метод. Вы используете ссылки на объекты, поэтому java решает во время компиляции, что для параметра объекта существует метод, который принимает объект напрямую. Он вызвал этот метод 3 раза.

Ашиш Тукрал
источник