Reputation: 6870
Trying to refactor some code and I stumbled on a problem that can be demonstrated by the following code:
public static abstract class Animal {
public abstract void attack(Animal other);
}
public static class Cat extends Animal {
@Override
public void attack(Animal other) {
catAttack(other); <-------- Problem here
}
private void catAttack(Cat other) {
// Maybe a meow showdown wins the fight, no need to get physical
}
private void catAttack(Dog other) {
// Dogs are dangerous, run!
}
}
public static class Dog extends Animal {
@Override
public void attack(Animal other) {
}
}
Why is it that the specific type of other
in catAttack(other)
cannot be found so that the most specific method can be called? Instead this check must be done:
if (other instanceof Cat)
catAttack((Cat) other);
if (other instanceof Dog)
catAttack((Dog) other);
Edit: The question was perhaps a bit unclear, here is a clarification I posted as comment to an answer:
the cat can attack a dog or another cat which both happen to be animals, I am curious as to why the burden of casting the other animal to either dog or cat is on me, why doesn't java try and see if it is either cat or dog because I have two methods that match either a cat or a dog and then throw an error if it was an elephant instead?
Upvotes: 2
Views: 988
Reputation: 2881
This is a case of double-dispatch; see also this SO question, and this code smell post.
We use double-dispatch when we need behavior that depends on the type of two objects, in this case, your two animals. It is most useful when you will only rarely get new classes.
There are different ways of implementing it, but here is one.
In the Animal class, we have abstract methods (Animal also has a name attribute):
public abstract void attack(Animal other);
public abstract void attackedBy(Cat other);
public abstract void attackedBy(Dog other);
public abstract void attackedBy(Rat other);
In the calling code we might have
Animal c1 = new Cat("C1");
Animal c2 = new Cat("C2");
Animal d1 = new Dog("D1");
Animal d2 = new Dog("D2");
Animal r1 = new Rat("R1");
Animal r2 = new Rat("R2");
c1.attack(c2);
c1.attack(d1);
c1.attack(r1);
d1.attack(c1);
d1.attack(d2);
d1.attack(r1);
r1.attack(c1);
r1.attack(d1);
r1.attack(r2);
In Cat, and each of the other classes we have code such as
public void attack(Animal other) {
System.out.println("Cat("+name+") attacks other");
other.attackedBy(this);
}
public void attackedBy(Cat other) {
System.out.println("Cat("+name+") attacked by Cat("+other.name+")");
}
public void attackedBy(Dog other) {
System.out.println("Cat("+name+") attacked by Dog("+other.name+")");
}
public void attackedBy(Rat other) {
System.out.println("Cat("+name+") attacked by Rat("+other.name+")");
}
A call such as
c1.attack(d1);
calls Cat.attack(Animal other)
, which transfers control to the other Animal object (the Dog):
public void attack(Animal other) {
System.out.println("Cat("+name+") attacks other");
other.attackedBy(this);
}
The call arrives at Dog.attackedBy(Cat other)
:
public void attackedBy(Cat other) {
System.out.println("Dog("+name+") attacked by Cat("+other.name+")");
}
So you see why it's called Double Dispatch. This code can be greatly improved, and you certainly should consider implications and alternatives.
Upvotes: 0
Reputation: 4092
the cat can attack a dog or another cat which both happen to be animals, I am curious as to why the burden of casting the other animal to either dog or cat is on me, why doesn't java try and see if it is either cat or dog because I have two methods that match either a cat or a dog and then throw an error if it was an elephant instead?
Unfortunately, what you are suggesting is not aligned with current java compiler implementation's method matching system.
The compiler has to perform what is referred as static type checking, which consists basically in checking statically (i.e. before the program is run) whether the code is type-safe or not, which would in the latter case result in an error at run-time.
As an example, consider:
float b = 4.6f;
int a = 5 + b;
Here the compiler knows that b is of type float, knows that operator + has an entry for (int + float = float), and knows that a is of type integer. Thus, it infers that the expression has type float and must be cast to integer to be assigned to a. This conversion is not permitted in java, so it outputs an error to prevent loss of precision.
With objects, it is basically the same, knowing that classes deeper in hierarchy can be "converted" to their shallower parents, but not the contrary.
Therefore, in your case:
public void attack(Animal other) {
catAttack(other); <-------- Problem here
}
private void catAttack(Cat other) {
// Maybe a meow showdown wins the fight, no need to get physical
}
private void catAttack(Dog other) {
// Dogs are dangerous, run!
}
The java compiler can not infer the real class of variable other
in method attack(Animal other)
without running the whole program.
It can only know that the "pointer" is of type Animal, so the real class can be anything that extends animal, and it has to type-check (and statically resolve method invocation) your call to catAttack(other)
knowing only that.
Since there is no catAttack(Animal)
method, java compiler cannot guarantee type safety for your code, and outputs an error.
What you can do instead, is:
public static abstract class Animal {
public abstract void attack(Animal other);
public abstract void beAttackedByCat(Animal cat);
public abstract void beAttackedByDog(Animal dog);
}
public static class Cat extends Animal {
@Override
public void attack(Animal other) {
other.beAttackedByCat(this);
}
public void beAttackedByCat(Animal cat){ // cat > cat }
public void beAttackedByDog(Animal dog){ // dog > cat }
}
public static class Dog extends Animal {
@Override
public void attack(Animal other) {
other.beAttackedByDog(this);
}
public void beAttackedByCat(Animal cat){ // cat > dog }
public void beAttackedByDog(Animal dog){ // dog > dog }
}
Which is probably miles away from the perfect solution, but just to give you the idea of what you can do and what you can not.
Upvotes: 1
Reputation: 709
I'm pretty sure in java you cannot make a class static unless it is an inner class; but that goes besides the point of your question.
You have to cast because you can't match the proper prototype of the method private void catAttack(Cat other)
or private void catAttack(Dog other)
with catAttach(Animal other)
.
To get around this, you program to the super-class or interface.
Typically this is done in this manner:
public interface Animal {
public void attack(Animal other);
}
public class Cat implements Animal {
public void attack(Animal other) {
/* TODO: implement attack method */
}
}
public class Dog implements Animal {
public void attack(Animal other) {
/* TODO: implement attack method */
}
}
If you think about it, this approach makes sense. A Cat
in this instance, has its own nature of attacking. You may do something like:
public class Cat implements Animal {
public void attack(Animal other) {
if (this.equals(other)) {
System.out.println("The Cat won't fight itself.");
} elsif (other instanceof Cat) {
System.out.println("Cats are friendly to one another, so the Cat forfeits the fight!");
} elsif (other instanceof Dog) {
System.out.println("Cats hate Dogs, so the Cat viciously attacks the Dog!");
} else {
System.out.println("The Cat seems to be unamused by the other animal, and walks away...");
}
}
And you can make it even more complex:
public class MountainLion extends Cat {
@Override
public void attack(Animal other) {
System.out.println("Mountain Lions do not like to be challenged and will strike down any predator with the fire inside their heart!");
}
}
But in your class that uses them, you just follow the interface pattern, like so:
public class AnimalKingdom {
public static void main(final String[] args) {
Animal cat = new Cat(); // could be read in from something like Spring using context.getBean("catObject", Animal.class);
Animal dog = new Dog(); // same as above...
Animal randomAnimalType = new MountainLion(); // same as above...
cat.attack(cat);
cat.attack(dog);
cat.attack(randomAnimalType);
dog.attack(cat);
dog.attack(dog);
dog.attack(randomAnimalType);
randomAnimalType.attack(cat);
randomAnimalType.attack(dog);
randomAnimalType.attack(randomAnimalType); // this doesn't use super, doesn't check if the animal is the same instance...
}
}
The point being, if I can now create the behavior of AnimalKingdom without worrying about what type of Animal it is, just that it is an Animal (so it follows the Animal interface). If I use some kind of framework (such as Spring), I can dynamically inject what runs based on some external configuration file to create many possible scenarios without having to copy and paste or re-write code.
Hope this was helpful.
Thanks
Upvotes: 0
Reputation: 4746
I think this is a good question. Some one might feel that cause Animal is a abstract class it can not be instantiated. Thus compiler should support catAttack(other)
but the problem is compiler knows only two methods
private void catAttack(Cat other) {
// Maybe a meow showdown wins the fight, no need to get physical
}
private void catAttack(Dog other) {
// Dogs are dangerous, run!
}
But there is a possibility to have other classes that extends Animal as well .Other than Cat and Dog .Lets say Rabbit. So in the compile time the other instance can be something extends Animal. It can be a Rabbit too . Since the compiler is not smart enough or helpful enough to find out each and every class that extends the animal and check the type of other instance which is passed to catAttack it shows a compile time error. If you had a method
private void catAttack(Animal other) {
}
then it will be compiled
Upvotes: 1
Reputation: 6695
Put simply.Because you do not have below method in your Cat Class.
private void catAttack(Animal other) {
}
Edit: Remember you can assign a superClass reference to a subclass directly.This is actually what you are doing.Suppose If you something like below
Animal animal=new Cat();
And then you are calling animal.Attack(animal)
the to call your CatAttack method you can do
if(animal instanceOf Cat)
{
catAttack((Cat)animal)
}
Because animal reference contains an object of cat,you can do the casting explicitly.
Upvotes: 0