Подтвердить что ты не робот

В чем причина отказа от переопределения супертипов на Java?

Следующий код считается недействительным компилятором:

class Foo {
    void foo(String foo) { ... }
}

class Bar extends Foo {
    @Override
    void foo(Object foo) { ... }
}

Я думаю, что это описано в JLS 8.4.8.1: "Подпись m1 является подзаголовкой (§8.4.2) подписи m2." и в 8.4.2: "границы соответствующих переменных типа одинаковы".

Мой вопрос: почему параметр в подтипе (Bar) не может быть супертипом параметра в супертипе (Foo). В примере Object является супертипом String. Насколько я вижу, это позволяет не нарушать Принцип замены Лискова.

Есть ли сценарий, позволяющий этому сломать код или это ограничение текущего JLS?

4b9b3361

Ответ 1

(Переписывать, под другим углом... мой исходный ответ содержал ошибку.:()

почему параметр в подтипе (Bar) не может быть супертипом параметра в супертипе (Foo).

Я считаю, что технически это возможно, и он не нарушил бы предковые контракты после подстановки типа (Принцип замещения Лискова).

  • Типы передаются по значению (включая ссылочные типы).
  • Вызывающий абонент никогда не может быть вынужден обращаться с разными типами параметров, чем с тем, что он передает.
  • Тело метода может поменять тип параметра, но не может вернуть его обратно вызывающему абоненту (нет таких параметров, как "типы выходных параметров" ).
  • Если ваше предложение было разрешено и был сделан вызов подписи метода предка, децентрализованный класс может переопределить метод с более широкими типами, но все же невозможно вернуть более широкий тип, чем тот, который задает вызывающий.
  • Переопределение никогда не может сломать клиент, который использует контракт с узким предком.

Из моего анализа ниже, я предположим/угадываю обоснование того, что вы не разрешаете свой сценарий:

  • Связанный с производительностью: использование более широких типов в переопределении повлияет на производительность во время выполнения, основная проблема
  • Функциональный: он лишь добавляет незначительную функциональность. Как бы то ни было, вы можете добавить свой "более широкий метод" как перегруженный метод без переопределения. Затем вы можете дополнительно переопределить исходный метод с точным соответствием подписи. Конечный результат: вы достигаете чего-то совершенно аналогичного, функционально.

Требования к компилятору для переопределения метода - JLS 7

Компилятор должен действовать в соответствии с вашим опытом. 8.4 Объявления метода:

Метод в подклассе может переопределить метод в классе предков iff:

  • имена методов идентичны (8.4.2 и 8.4.8.1)
  • параметры метода имеют одинаковые типы после стирания параметров типового типа (8.4.2 и 8.4.8.1)
  • тип возвращаемого типа заменяется на тип возвращаемого типа в классе предков, т.е. тот же тип или более узкий (8.4.8.3)

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


Компилятор v Обработка времени выполнения для сопоставления и вызова метода

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

  • Определить класс или интерфейс для поиска (определение времени компиляции)

    • Получить базовый тип T, на который вызывается метод.
    • Это ссылочный тип, объявленный компилятору, а не тип среды выполнения (где подтип может быть заменен).
  • Определить подпись метода (определение времени компиляции)

    • Искать базовый тип времени компиляции T, для применимых методов, соответствующих типам имен, параметров и возвращаемых данных, соответствующих запросу.
      • разрешить общие параметры для T, явно переданные или неявно выведенные из типов аргументов метода вызова
      • этап 1: методы, применяемые через согласованные типы/подтипы ( "подтипирование" )
      • этап 2: методы, применяемые при автоматическом боксировании/распаковке плюс подтипирование
      • Этап 3: методы, применяемые при автоматическом боксировании/распаковке плюс подтипирование плюс параметры переменной arity
      • определяют наиболее специфичную подпись подписи метода (то есть подпись метода, которая может быть успешно передана ко всем другим сигнатурам метода сопоставления); if none: ошибка компилятора/неоднозначности
  • Проверьте: подходит ли выбранный метод? (Определение времени компиляции)

  • Оценка вызова метода (определение времени выполнения)

    • Определение целевого ссылочного типа среды выполнения
    • Оценка аргументов
    • Проверить доступность метода
    • Метод locate - точное соответствие подписи под подписями подкомпилированной подписи
    • Invoke

Производительность

Пробой, с точки зрения текста в JLS:

Шаг 1: 5% Шаг 2: 60% Шаг 3: 5% Шаг 4: 30%

Не только шаг 2, объемный в тексте, он на удивление сложный. Он имеет сложные условия и множество дорогих условий испытаний/поиска. Это выгодно максимизировать выполнение компилятором более сложной и медленной обработки здесь. Если бы это было сделано во время выполнения, было бы перетащить на производительность, потому что это произойдет для каждого вызова метода.

Этап 4 все еще имеет значительную обработку, но он максимально упрощен. Чтение через 15.12.4, оно не содержит шагов обработки, которые можно было перемещать для компиляции, без принуждения типа выполнения чтобы точно соответствовать типу времени компиляции. Не только это, но и простое точное совпадение на сигнатуре метода, а не сложное совпадение типа предка

Ответ 2

Предположим, вы могли это сделать. Теперь ваш суперкласс выглядит следующим образом:

class Foo {
    void foo(String foo) { ... }
    void foo(Number foo) { ... }
}

и ваш подкласс:

class Bar extends Foo {
    @Override
    void foo(Object foo) { ... }
}

Язык, возможно, мог бы позволить такую ​​вещь (и просто отправить как Foo.foo(String), так и Foo.foo(Number) в Bar.foo(Object)), но, по-видимому, решение для Java здесь заключается в том, что один метод только может переопределить один и тот же метод.

[изменить]

Как сказал в своем ответе dasblinkenlight, у него может быть foo (Object) без @Override, но это просто перегружает функции foo и не отменяет их. При вызове java выбирает наиболее специфический метод, поэтому foo ( "Hello World" ) всегда будет отправлен в метод foo (String).

Ответ 3

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

Когда ваш подкласс объявляет метод с тем же именем и параметром, что и в его суперклассе, Java должен решить две возможности:

  • Вы хотите, чтобы метод в подклассе переопределял метод в суперклассе или
  • Вы хотите, чтобы метод в подклассе перегружал метод в суперклассе.

Если Java разрешил foo(Object) переопределять foo(String), язык должен был бы ввести альтернативный синтаксис для указания намерения перегрузить метод. Например, они могли бы сделать это способом, подобным С# new и override в объявлениях методов. По какой-то причине разработчики решили отказаться от этого нового синтаксиса, оставив язык с правилами, указанными в JLS 8.4.8.1.

Обратите внимание, что текущий дизайн позволяет реализовать функциональность переопределения путем перенаправления вызова из перегруженной функции. В вашем случае это будет означать вызов Bar.foo(Object) из Foo.foo(String) следующим образом:

class Foo {
    public void foo(String foo) { ... }
}

class Bar extends Foo {
    @Override
    public void foo(String foo) { this.foo((Object)foo); }
    public void foo(Object foo) { ... }
}

Вот демон на ideone.

Ответ 4

Ответ простой, на Java, для переопределения метода, вы должны иметь точную подпись супер-типа. Однако, если вы удалите аннотацию @Override, ваш метод будет перегружен, и ваш код не сломается. Это реализация Java, которая гарантирует, что вы подразумеваете, что реализация метода должна переопределять реализацию супер-типа.

Переопределение метода работает следующим образом.

class Foo{ //Super Class

  void foo(String string){

    // Your implementation here
  }
}

class Bar extends Foo{

  @Override
  void foo(String string){
    super(); //This method is implied when not explicitly stated in the method but the @Override annotation is present.
    // Your implementation here
  }

  // An overloaded method
  void foo(Object object){
    // Your implementation here
  }
}

Вышеуказанные методы являются правильными, и их реализация может различаться.

Надеюсь, это поможет вам.

Ответ 5

Чтобы ответить на вопрос в заголовке сообщения What is the reasoning behind not allowing supertypes on Java method overrides?:

Дизайнеры Java хотели простого объектно-ориентированного языка, и они специально отклонили функции С++, где, по их мнению, сложность/ловушки этой функции не стоили пользы. То, что вы описали, возможно, попало в эту категорию, где дизайнеры решили разработать/уточнить эту функцию.

Ответ 6

Проблема с вашим кодом заключается в том, что вы сообщаете компилятору, что метод foo в классе Bar переопределяется из родительского класса Bar, то есть Foo. Поскольку overridden methods must have same signature, но в вашем случае, в соответствии с синтаксисом, это перегруженный метод, так как вы изменили параметр в методе foo класса Bar.

Ответ 7

Если вы можете переопределить суперклассы, почему бы и не с подклассами?

Рассмотрим следующее:

class Foo {
  void foo(String foo) { ... }
}

class Bar extends Foo {
  @Override
  void foo(Object foo) { ... }
}
class Another extends Bar {
  @Override
  void foo(Number foo) { ... }
}

Теперь вы успешно преодолели метод, исходным параметром которого был String, чтобы принять a Number. Не рекомендуется, по меньшей мере,...

Вместо этого предполагаемые результаты могут быть реплицированы с использованием перегрузки и следующего более явного кода:

class Foo {
    void foo(String foo) { ... }
}

class Bar extends Foo {
    @Override
    private void foo(String foo) { ... }
    void foo(Object foo) { ... }
}
class Another extends Bar {
    @Override
    private void foo(Object foo) { ... }
    void foo(Number foo) { ... }
}