Как переопределить метод с областью видимости по умолчанию (пакет)?

Моя проблема в том, что я не могу понять, как работает разрешение метода в следующем случае: Предположим, у нас есть два пакета A и B. Существует два класса, A помещается внутри A, B внутри B.

А:

package com.eka.IO.a;
import com.eka.IO.b.B;

public class A {

    void foo() {
        System.out.println("parent");
    }

    public static void main(String... args) {
        B obj = new B();
        obj.foo();
    }

}

В:

package com.eka.IO.b;
import com.eka.IO.a.A;

public class B extends A {

    public void foo() {
        System.out.println("child");
    }

}

Приведенный выше код печатает "child", что отлично. Но если я изменил метод main следующим образом:

public static void main(String... args) {
    A obj = new B();
    obj.foo();
}

код печатает "родительский", и я не понимаю, почему. (obj имеет тип времени выполнения B, B имеет открытый метод foo)

Затем я изменяю видимость foo на публикацию,

public class A {

    public void foo() {

и код снова печатает "дочерний".

Насколько мне известно, методы экземпляра разрешены во время выполнения, используя следующий принцип:

  • JVM проверяет класс выполнения объекта.
  • JVM ищет метод класса runtime
  • Если метод найден, JVM вызывает его, в противном случае переходит к родительскому классу выполнения.

В моем примере в любом из трех случаев класс выполнения для obj всегда B. B метод foo всегда открыт. Почему во втором случае JVM вызывает метод A?

Up: Хорошие ответы, но все же некоторые вещи неясны для меня. a) Это компилятор, который проверяет, переопределяет ли метод другой метод. (Надеюсь, я прав). б) в случае A obj = new B(); компилятор генерирует следующий код:

INVOKEVIRTUAL com/eka/IO/a/A.foo ()V

b1), если A foo объявлен без модификатора (видимость пакета), тогда JVM вызывает метод A. b2), если A foo объявляется общедоступным, тогда JVM вызывает метод B.

Неясно, почему во втором случае INVOKEVIRTUAL фактически называет B.foo. Как он знает, что B отменяет метод?

Ответы

Ответ 1

Процесс немного отличается от описанного. Во-первых, Java будет создавать только те методы, которые существуют в объявленном классе и видны в текущей доступной области. Это уже сделано во время компиляции.

Во время выполнения

  • JVM проверяет класс выполнения объекта.
  • JVM проверяет, переопределил ли класс среды выполнения объект метода объявленного класса.
  • Если да, то метод вызвал. В противном случае вызывается объявленный метод класса.

Теперь сложная часть: "была ли она переопределена"?

Класс не может переопределить метод, который не отображается. Он может объявлять метод с тем же именем и с теми же аргументами, но этот метод не считается переопределяющим исходный метод. Это просто новый метод, как и любой другой метод, определенный в B, но не в A.

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

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

Почему компилятор не позволяет вам использовать имена методов, уже находящихся в родительском классе, тогда?

Хорошо, если вы получите пакет, и единственная информация, которую вы имеете о нем, - это то, что в контрактах классов, как написано в его Javadoc, вы даже не узнаете о существовании этого метода. Внезапно вы пишете метод, который, насколько вам известно, уникален, и вы получаете ошибку компиляции.

Нет причин для этого. То, что не видно вам, не должно мешать вам свободно называть ваши собственные методы. Таким образом, это разрешено.

Но если вы хотите, чтобы компилятор не позволял вам делать подобные ошибки, используйте аннотацию @Override всякий раз, когда вы пишете метод Предполагаемый, чтобы переопределить метод родительского класса. Таким образом, компилятор предупредит вас, если вы пытаетесь переопределить метод, который не является частью договора класса.

Ответ 2

Вы испытываете метод Shadowing. Из Спецификация языка Java. Глава 6. Имена. 6.4. Тени и затуманивания. 6.4.1. Shadowing (мой листок):

Некоторые объявления могут быть затенены в части их области с помощью другого объявления с тем же именем, и в этом случае простое имя не может использоваться для ссылки на объявленный объект

(...)

Объявление d считается видимым в точке p в программе, если область d включает p, а d не затеняется любым другим объявлением в p.

(...)

Объявление d метода с именем n затеняет объявления любых других методов с именем n, которые находятся в охватывающей области в точке, где d встречается во всей области d.

Пусть проверяет, отменяет ли B#foo A#foo. Из 8.4.8.1. Переопределение (по методам экземпляров):

Метод экземпляра mC, объявленный или унаследованный классом C, переопределяет из C другой метод mA, объявленный в классе A, если все верно:

  • A - суперкласс C.
  • C не наследует mA.
  • Подпись mC является поднаклейкой (§8.4.2) сигнатуры mA.
  • Верно одно из следующих утверждений:
    • mA является общедоступным. (не ваш случай)
    • mA защищен. (не ваш случай)
    • mA объявляется с доступом к пакету в том же пакете, что и C (не ваш случай, так как классы находятся в разных пакетах), и либо C объявляет mC, либо mA является членом прямого суперкласса C.
    • mA объявляется с доступом к пакету, а mC переопределяет mA из некоторого суперкласса C (не ваш случай, потому что должен быть другой класс между C и A, который позволяет вам отменить mA).
    • mA объявляется с доступом к пакету, а mC переопределяет метод m 'из C (m', отличный от mC и mA), так что m 'переопределяет mA из некоторого суперкласса C (не ваш случай потому что должен существовать другой класс между C и A, который позволяет вам отменить mA).

Итак, B#foo не переопределяет A#foo любым средним значением. Имея это в виду, когда вы вызываете obj.foo(), тогда foo будет получена на основе класса obj, назначенного во время компиляции. Объяснение этой части объясняется в 15.12. Выражения вызова метода

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

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