Java: является ли полиморфизм практичным только для методов с идентичными сигнатурами?

#java #parameters #polymorphism #factory

#java #параметры #полиморфизм #фабрика

Вопрос:

Единственные примеры переопределения полиморфных методов, которые я когда-либо видел, включают методы, которые не принимают параметров или, по крайней мере, имеют идентичные списки параметров. Рассмотрим общий пример Animal / Dog / Cat:

 public abstract class Animal
{
    public abstract void makeSound();
}

public class Dog extends Animal
{
    public void makeSound()
    {
        System.out.println("woof");
    }
}

public class Cat extends Animal
{
    public void makeSound()
    {
        System.out.println("meow");
    }
}

public class ListenToAnimals
{
    public static void main(String[] args)
    {
        AnimalFactory factory = new AnimalFactory();
        Animal a = factory.getRandomAnimal(); // generate a dog or cat at random
        a.makeSound();
    }
}
 

В этом случае все работает просто отлично. Теперь давайте добавим еще один метод, который частично реализуется в абстрактном классе, получая при этом более конкретное поведение в подклассах:

 public abstract class Animal
{   
    public abstract void makeSound();

    public void speak(String name)
    {
        System.out.println("My name is "   name);
    }
}

public class Dog extends Animal
{
    public void makeSound()
    {
        System.out.println("woof");
    }

    public void speak(String name)
    {
        super.speak(name);
        System.out.println("I'm a dog");
    }
}

public class Cat extends Animal
{
    public void makeSound()
    {
        System.out.println("meow");
    }

    public void speak(String name, int lives)
    {
        super.speak(name);
        System.out.println("I'm a cat and I have "   lives   " lives");
    }
}

public class ListenToAnimals
{
    public static void main(String[] args)
    {
        AnimalFactory factory = new AnimalFactory();
        Animal a = factory.getRandomAnimal(); // generate a dog or cat at random
        a.makeSound();
        // a.speak(NOW WHAT?
    }
}
 

В этой последней (прокомментированной) строке основного метода я не знаю, что туда поместить, потому что я не знаю, какой у меня тип животного. Раньше мне не приходилось беспокоиться об этом, потому что makeSound() не принимал никаких аргументов. Но speak() делает, и аргументы зависят от типа Animal .

Я читал, что некоторые языки, такие как Objective-C, допускают списки переменных аргументов, поэтому подобная проблема никогда не должна возникать. Кто-нибудь знает о хорошем способе реализации такого рода вещей на Java?

Комментарии:

1. Я не хочу давать ответ, так как я бы с этим не согласился, но Java имеет varargs . Таким образом, метод может быть определен как speak(String... args) или speak(String[] args) . Оба этих варианта являются слабыми.

Ответ №1:

Вы путаете переопределение метода и перегрузку метода. В вашем примере Cat класс имеет два метода:

 public void speak(String name) // It gets this from its super class
public void speak(String name, int lives)
 

Перегрузка — это способ определения методов с похожими функциями, но разными параметрами. Не было бы никакой разницы, если бы вы назвали метод таким образом:

 public void speakWithLives(String name, int lives)
 

Чтобы избежать путаницы, рекомендация в java заключается в использовании @Override аннотации при попытке переопределить метод. Поэтому:

  // Compiles
@Override
public void speak(String name)

// Doesn't compile - no overriding occurs!
@Override
public void speak(String name, int lives)
 

РЕДАКТИРОВАТЬ: в других ответах упоминается об этом, но я повторяю это для акцента. Добавление нового метода сделало Cat класс более не способным быть представленным как an Animal во всех случаях, тем самым устраняя преимущество полиморфизма. Чтобы использовать новый метод, вам нужно будет понизить его до Cat типа:

 Animal mightBeACat = ...
if(mightBeACat instanceof Cat) {
  Cat definitelyACat = (Cat) mightBeACat;
  definitelyACat.speak("Whiskers", 9);
} else {
  // Definitely not a cat!
  mightBeACat.speak("Fred");
}
 

Инструмент проверки кода в моей IDE выдает предупреждение instanceof , поскольку ключевое слово указывает на возможный сбой полиморфной абстракции.

Комментарии:

1. 1 Это единственный ответ, который затронул тот факт, что метод speak в Cat классе во втором примере перегружен , а не переопределен .

Ответ №2:

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

Комментарии:

1. 1 Простое, точное и, самое главное, точное объяснение.

Ответ №3:

Насколько я знаю, java не позволяет вам этого делать. speak(name, lives) теперь является просто функцией Cat . Некоторые языки допускают такую гибкость. Чтобы заставить java разрешить это, вы можете сделать параметром массив объектов или какую-либо другую коллекцию.

Однако учтите, что при вызове speak теперь вы должны знать, какие параметры передавать независимо, поэтому вопрос несколько спорный.

Ответ №4:

Когда вы вызываете полиморфный метод как:

 a.speak("Gerorge");
 

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

 super.speak(name);
 

Как кошка, так и собака будут иметь поведение Animal плюс собственное поведение.

Ответ №5:

Вы можете сделать

 public void speak(Map ... mappedData)
    {
        System.out.println("My name is "   mappedData.get("name")  " and I have " mappedData.get("lives");
    }
 

Тем не менее, я бы посоветовал сделать lives переменной экземпляра Cat и передать вашей фабрике значение по умолчанию (или иметь для конструктора параметр по умолчанию).

Ответ №6:

 In this case best way is to use a DTO,

public class SpeakDTO
{
     //use getters and setters when you actually implement this
     String name;
     int lives;
}

public class Dog extends Animal
{
    public void speak(SpeakDTO dto)
    {
        super.speak(dto.name);
        System.out.println("I'm a dog");
    }
}

public class Cat extends Animal
{
    public void speak(SpeakDTO dto)
    {
        super.speak(dto.name);
        System.out.println("I'm a cat and I have "   dto.lives   " lives");
    }
}

public class ListenToAnimals
{
    public static void main(String[] args)
    {
        AnimalFactory factory = new AnimalFactory();
        Animal a = factory.getRandomAnimal(); // generate a dog or cat at random
        a.makeSound();

        SpeakDTO dto = new SpeakDTO();
        dto.name = "big cat";
        dto.lives = 7;

        a.speak(dto);
    }
}
 

Комментарии:

1. 1 для смещения несправедливого, необъяснимого -1. Это (хотя и не очень элегантное) допустимое решение.

Ответ №7:

Если вы хотите выполнить подобный вызов, вы можете использовать отражение для получения класса:

 if (a.getclass() == Cat.class) {
// speak like a cat
} else if (a.getclass() == Dog.class) {
.
.
.
 

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

Комментарии:

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

2. @Downvoter: Есть предложения? Я уже говорил, что это может быть не очень хорошим решением, и я просто хотел указать на этот метод.

3. Если посмотреть на это еще раз, instanceof это может быть лучшим способом сделать это (поскольку он также будет перехватывать подклассы Cat ).

4. @BrendanLong: Определенно! Я снова преодолел доску: (Может быть, потому, что я читаю Java Reflection в действии? Плохое оправдание, плохое оправдание 🙂

Ответ №8:

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

Когда поведение подклассов не определено интерфейсом, у вас не так много опций в Java, которые не являются подробными или немного шаткими.

У вас может быть speak (), который принимает интерфейс маркера и делегирует построение аргументов на фабрику. Вы могли бы передать сопоставление параметров. Вы могли бы использовать varargs.

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

Ответ №9:

Я согласен с комментариями о том, как вы действительно нарушили полиморфизм, если вы должны знать тип объекта, прежде чем сможете вызвать метод speak . Если вам абсолютно НЕОБХОДИМ доступ к обоим методам speak, вот один из способов его реализации.

 public class Animal {      
    public void speak(String name) {
        throw new UnsupportedOperationException("Speak without lives not implemented");
    }
    public void speak(String name, int lives) {
        throw new UnsupportedOperationException("Speak with lives not implemented");
    }
}

public class Dog extends Animal {
    public void speak(String name) {
        System.out.println("My name is "   name);
        System.out.println("I'm a dog");
    }
}

public class Cat extends Animal {
    public void speak(String name, int lives) {
        System.out.println("My name is "   name);
        System.out.println("I'm a cat and I have "   lives   " lives");
    }
}
 

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