Выбор правильного метода для вызова - PullRequest
1 голос
/ 28 апреля 2011

Возьмите следующий код:

public class Parent
{
    public String doIt(Object o) {
        return "parent";
    }
}

public class Child extends Parent
{
    public String doIt(Object s) {
        return super.doIt(s) + ": " + "child";
    }
}

public class Poly
{
    public String makeItHappen() {
        Parent p = new Child();
        return p.doIt("test");
    }
}

Из-за того, что Child.doIt () переопределяет Parent.doIt (), вызов Poly.makeItHappen () приводит к тому, что это выводится на консоль:

parent: child

Однако, если я сделаю изменение в Child.doIt (), чтобы оно выглядело так:

public String doIt(String s) {
    return super.doIt(s) + ": " + "child";
}

Теперь Child.doIt () не переопределяет Parent.doIt (). Когда вы вызываете Poly.makeItHappen (), вы получаете такой результат:

parent

Я немного озадачен этим. Тип p во время компиляции - Parent, поэтому я определенно понимаю, что Parent.doIt () является потенциально применимым методом, но, учитывая, что тип p во время выполнения - Child, я не уверен, почему Child.doIt ( ) не является. Предполагая, что они оба определены как потенциально применимые методы, я ожидаю, что Child.doIt (String) будет вызываться через Parent.doIt (Object), поскольку он более конкретен.

Я попытался обратиться к JLS и нашел этот бит:

15.12.1 Время компиляции Шаг 1: Определить класс или интерфейс для поиска

...

Во всех других случаях квалифицированное имя имеет форму FieldName. Идентификатор; тогда именем метода является Идентификатор, а класс или интерфейс для поиска - это объявленный тип T поля, названного FieldName, если T является классом или типом интерфейса, или верхняя граница T, если T является переменной типа ,

Для меня это говорит о том, что во время компиляции будет использоваться тип p. Это имеет некоторый смысл, когда я вижу, что Parent.doIt (Object) вызывается, а Child.doIt (String) - нет. Но это не имеет смысла с точки зрения полиморфного поведения, отмеченного, когда Child.doIt () корректно переопределяет Parent.doIt () - в этом случае оба метода Parent и Child анализируются, чтобы найти потенциально применимые методы, так почему бы и нет во втором же случае?

Я знаю, что я что-то здесь упускаю, но я просто не могу понять, почему я вижу поведение, которым я являюсь. Если бы кто-нибудь мог пролить свет на это, я был бы признателен.

РЕДАКТИРОВАТЬ: НАЙТИ ОТВЕТ:

Благодаря ответу Джека я смог найти ответ в JLS. Раздел JLS, на который я ссылался ранее, был фактически сосредоточен на поиске правильного метода во время компиляции и не охватывал процесс вызова правильного метода во время выполнения. Эта часть процесса может быть найдена в разделе JLS под названием 15.12.4. Оценка вызова метода во время выполнения .

.

Там я нашел этот бит текста:

В противном случае режим вызова является интерфейсным, виртуальным или супер, и может произойти переопределение. Динамический метод поиска используется. Процесс динамического поиска начинается с класса S, определенного следующим образом:

Мой режим вызова виртуальный, поэтому применимо приведенное выше утверждение ...

Если режим вызова является интерфейсным или виртуальным, то изначально S является фактическим классом времени выполнения R целевого объекта.

...

Динамический поиск метода использует следующую процедуру для поиска класса S, а затем, при необходимости, суперклассы класса S для метода m.

Хорошо, мне это показалось очень странным. В соответствии с этим JVM начнет искать подходящий метод для вызова в Child, что заставит меня поверить, что Child.doIt (String) будет вызван. Но, читая на ...

Пусть X будет типом времени компиляции целевой ссылки вызова метода.

...

Если класс S содержит объявление для неабстрактного метода с именем m с тем же дескриптором (тем же числом параметров, теми же типами параметров и тем же типом возврата), который требуется для вызова метода, определенного во время компиляции (§ 15.12.3), затем:

Класс «S», который был бы Child, действительно содержит метод с тем же дескриптором, что и вызов метода, определенный во время компиляции (в конце концов, String - это «Объект», поэтому дескрипторы идентичны).Кажется, все еще похоже на то, что Child.doIt (String) должен вызываться, но читается на ...

Если режим вызова виртуальный, и объявление в S переопределяет (§8.4.8.1) Xm , тогда метод, объявленный в S, является вызываемым методом, и процедура завершается.

...

В противном случае, если S имеет суперкласс, эта же процедура поиска выполняется рекурсивно с использованием прямого суперкласса S вместо S;метод, который должен быть вызван, является результатом рекурсивного вызова этой процедуры поиска.

Жирный шрифт является действительно важной частью этого.Как я уже говорил, когда я изменил метод Child.doIt (), он больше не переопределял метод doIt () из Parent.Таким образом, даже несмотря на то, что JVM оценивает метод Child.doIt () в качестве потенциального кандидата для вызова, его не удается вызвать, поскольку он не переопределяет метод, определенный в X, который является Parent.Я действительно зависал, потому что думал, что JVM даже не проверяет Child.doIt как потенциально применимый метод, и это не кажется правильным.Теперь я считаю, что JVM проверяет этот метод как потенциально применимый метод, но затем игнорирует его, потому что он не переопределяет родительский метод должным образом.Это ситуация, в которой я думал, что метод из подкласса будет вызван не потому, что он переопределяет метод родительского класса, а потому, что он наиболее специфичен.Однако в этом случае это не так.

Следующая строка в JLS просто объясняет, что эта процедура выполняется рекурсивно над суперклассами, приводя к вызову Parent.doIt (Object).

Интуитивно, это имело для меня полный смысл, но я просто не мог понять, как на самом деле JVM выполняет этот процесс.Конечно, поиск правильной части JLS очень помог бы.

Ответы [ 5 ]

3 голосов
/ 28 апреля 2011

Динамическое связывание метода применяется во время выполнения между различными реализациями с одной и той же сигнатурой, но набор всех возможных методов определяется статически, когда компиляторы смотрят на сигнатуру.Поскольку вы объявляете p как класс Parent, во время выполнения он будет искать метод, который присутствует в классе Parent и, если присутствует более конкретная реализация (из-за подкласса, как в вашем примере)вместо этого будет выбран родительский.

Поскольку ваш метод ничего не переопределяет, во время компиляции он выберет другую сигнатуру (ту, которая имеет Object) и проигнорирует ее.Это не отображается как возможность сопоставления во время выполнения из-за типа атрибута p.

0 голосов
/ 28 апреля 2011

Хм, попытка ответить на вопрос «почему» среда выполнения не играет в прятки с полным определением класса, ища «лучшие» совпадения с методом, чем тот, который запрашивал компилятор, приведу пример.Очевидно, что этот API, в первую очередь, ужасен, но вы можете представить, как ситуация может случиться случайно с очень длинными сигнатурами методов.

/**
* Vendor API you program to
*/
public class IPv4Manager {
  public void terminateSocket(Object obj) {
    //terminate IPv4 socket
  }
}

/**
* Vendor class that's injected at runtime that you have no knowledge of 
* and do not compile against.
*/
public class IPv4And6Manager extends IPv4Manager {

  public void terminateSocket(Byte[] packet) {
    //terminate IPv6 socket
  }

}

/**
*Your user code
*/
public void terminateIPv4Socket(Byte[] packet) {
  IPv4Manager manager = managerFactory.getV4Manager(); //returns you an instance of 4And6Manager
  manager.terminateSocket(packet);
}

Вы действительно хотите, чтобы среда выполнения попыталась перехитрить вас и вызвать "лучший метод, чем тот, который просил компилятор?

0 голосов
/ 28 апреля 2011

Подпись вызываемого метода определяется во время компиляции, поэтому p.doIt ("test") будет вызывать метод doIt (Object o) соответствующего класса во время выполнения.doIt (String s) даже не просматривается.Представьте, что Child.java не существовал в то время, когда был написан Poly makeItHappen - вам также пришлось бы абстрагировать конструктор Child в метод фабрики в другом классе, чтобы заставить его компилироваться.Вы можете повторно реализовать фабричный метод и Child.java, даже не перекомпилировав Poly.java.Это позволяет программировать на интерфейс, предоставляемый базовым классом, и относительно эффективный вызов функций.Ваш Poly makeItHappen должен только знать, что существует doIt (Объект o).

Если вы думаете о возможной реализации наследования как через таблицу виртуальных функций, то doIt (String s) и doIt (Object o)есть разные записи в таблице.Родитель имеет запись только для doIt (Object o).У дочернего элемента есть и запись для doIt (Object o), которая имеет то же тело, что и Parent, и запись для doIt (String s), которая является его собственной.Во время компиляции вызывается метод, который находится в слоте doIt (Object o).

0 голосов
/ 28 апреля 2011

Ваш ответ на самом деле довольно прост: ваше изменение метода прервало ваше использование наследования.

У Parent есть только один метод doIt, и он принимает параметр Object.Когда вы вызываете doIt («test») для родителя, он проверяет, не переопределен ли он в дочернем элементе.Поскольку doIt (Object s) не переопределяется в дочернем элементе, используется родительский метод.Даже если вы передадите String, родительский метод все равно будет вызываться, поскольку doIt (Object s) отличается от doIt (String s).

Проще говоря, вы не переопределяете метод при измененииподпись, вы ее перегружаете.

0 голосов
/ 28 апреля 2011

Вы увеличили значение Child до Parent

Parent p = new Child();

Когда вы вызываете p.doIt("test");, единственный способ получить поведение Child - через переопределенный метод.

Поскольку вы изменили метод в Child на doIt(String s), он больше не переопределяет ничего в Parent, и поэтому doIt(Object o) вызывается в Parent.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...