Переопределение методов путем передачи в качестве аргумента объекта подкласса, где ожидается супертип

12

Я просто изучаю Java, а не практикующий программист.

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

Мой вопрос : почему аргументы, передаваемые переопределяющему методу, не могут быть типом подкласса ожидаемого супертипа?

В перегруженном методе любой метод, который я вызываю для объекта, гарантированно будет определен для объекта.


Примечания по предлагаемым дубликатам:

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

Второе предложение объясняет , как сделать то , что я прошу, но не почему это должно быть сделано таким образом. Мой вопрос сосредоточен на том, почему.

user1720897
источник
1
спросил и ответил в предыдущем вопросе : «Это обычно считается нарушением принципа подстановки Лискова (LSP), потому что добавленное ограничение делает то, что операции базового класса не всегда подходят для производного класса ...»
gnat
@gnat вопрос не в том, нарушает ли требование более конкретных подтипов для аргументов какой-либо компьютерный принцип, вопрос в том, возможно ли это, на что ответил edalorzo.
user949300

Ответы:

18

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

Ковариантные возвращаемые типы работают, потому что метод должен возвращать объект определенного типа, а переопределяющие методы могут фактически возвращать его подкласс. Основываясь на правилах подтипирования языка, такого как Java, если Sэто подтип T, то где бы Tмы ни появлялись, мы можем передать S.

Таким образом, безопасно возвращать Sпри переопределении метода, который ожидал a T.

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

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

interface Hunter {
   public void hunt(Animal animal);
}

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

Но давайте предположим, что мы можем переопределить этот метод, как вы предложили:

class MammutHunter implements Hunter {
  @Override
  public void hunt(Mammut animal) {
  }
}

Вот забавная часть, теперь вы можете сделать это:

AnimalHunter hunter = new MammutHunter();
hunter.hunt(new Bear()); //Uh oh

Согласно общедоступному интерфейсу AnimalHunterвы должны быть в состоянии охотиться на любое животное, но согласно вашей реализации MammutHunterвы принимаете только Mammutобъекты. Поэтому переопределенный метод не удовлетворяет общедоступному интерфейсу. Мы только что нарушили надежность системы типов здесь.

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

interface AnimalHunter<T extends Animal> {
   void hunt(T animal);
}

Тогда вы можете определить свой MammutHunter

class MammutHunter implements AnimalHunter<Mammut> {
   void hunt(Mammut m){
   }
}

А используя общую ковариацию и контравариантность, вы можете при необходимости ослабить правила в свою пользу. Например, мы могли бы убедиться, что охотник на млекопитающих может охотиться только на кошек в заданном контексте:

AnimalHunter<? super Feline> hunter = new MammalHunter();
hunter.hunt(new Lion());
hunter.hunt(new Puma());

Предположим, MammalHunterорудия AnimalHunter<Mammal>.

В этом случае это не будет принято:

hunter.hunt(new Mammut()):

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

edalorzo
источник
3

Проблема не в том, что вы делаете в переопределенном методе с объектом, заданным в качестве аргумента. Проблема в том, какой тип аргументов разрешено передавать коду с помощью вашего метода.

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

Переопределение метода с более конкретным типом аргумента называется перегрузкой . Он определяет метод с тем же именем, но с другим или более конкретным типом аргумента. Перегруженный метод доступен помимо оригинального метода. Какой метод вызывается, зависит от типа, известного во время компиляции. Чтобы перегрузить метод, вы должны удалить аннотацию @Override.

Флориан Ф
источник