简体   繁体   中英

Java: Is polymorphism only practical for methods with identical signatures?

The only examples of polymorphic method overriding I ever see involve methods that take no parameters, or at least have identical parameter lists. Consider the common Animal/Dog/Cat example:

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();
    }
}

In this case, everything works out just fine. Now let's add another method that gets partially implemented in the abstract class while getting the more specific behavior in the subclasses:

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?
    }
}

In that last (commented) line of the main method, I don't know what to put there because I don't know what type of Animal I have. I didn't have to worry about this before because makeSound() didn't take any arguments. But speak() does, and the arguments depend on the type of Animal.

I've read that some languages, such as Objective-C, allow for variable argument lists, so an issue like this should never arise. Is anyone aware of a good way to implement this kind of thing in Java?

You are confusing method overriding and method overloading . In your example the Cat class has two methods:

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

Overloading is a way to define methods with similar functions but different parameters. There would be no difference if you had named the method thusly:

public void speakWithLives(String name, int lives)

To avoid confusion the recommendation in java is to use the @Override annotation when you are attempting to override a method. Therefore:

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

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

EDIT: Other answers mention this but I am repeating it for emphasis. Adding the new method made the Cat class no longer able to be represented as an Animal in all cases, thus removing the advantage of polymorphism. To make use of the new method you would need to downcast it to the Cat type:

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

The code inspection tool in my IDE puts a warning on the instanceof as the keyword indicates possible polymorphic abstraction failure.

Your example Cat isn't polymorphic anymore, since you have to know it's a Cat to pass that parameter. Even if Java allowed it, how would you use it?

As far as I know java doesn't allow you to do that. speak(name, lives) is now just the Cat's function. Some languages do allow this type of flexibility. To force java to allow this, you can make the paramater an array of objects or some other collection.

However, consider that when you call speak, you now must know which parameters to pass in regardless, so the point is somewhat moot.

When you call a polymorphic method as:

a.speak("Gerorge");

You don't need to know what type of Animal has instantiated because this is the objective of polymorphism. Also since you have user the sentence:

super.speak(name);

Both Cat an Dog will have the behavior of Animal plus the own behavior.

You can do

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

However, I would advise making lives an instance variable of Cat and have your factory pass a default value (or have the constructor have a default parameter for it).

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);
    }
}

If you want to make a call like that, you could use reflection to get the class:

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

Of course this might not be the best design, and reflection should be used with care.

Java also has variable argument lists, but I'd argue that's not the "best" way to do it, at least not in all circumstances.

When subclasses have behavior that isn't defined by the interface, you don't have many options in Java that aren't verbose or a bit wonky.

You could have a speak () that takes a marker interface and delegate arg construction to a factory. You could pass a parameter map. You could use varargs.

Ultimately, you need to know what to pass to the method, no matter what language.

I agree with the comments about how you've really broken polymorphism if you must know the type of object before you can call the speak method. If you absolutely MUST have access to both speak methods, here is one way you could implement it.

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");
    }
}

Alternately you can put the UnsupportedOperationExceptions into the child classes (or you might want to used a checked exception). I'm not actually advocating either of these , but I think this is the closest way to implement what you requested, and I have actually seen systems that used something like this.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM