Ploymorphism with Generics in Java 5 or later
Ploymorphism with Generics
Generic collections give us sane advantages of type safety that we have always had with arrays, but there are some crucial differences
Most of these have to do with polymorphism.
You've already seen that polymorphism applies to the "base" type of the collection:
List<Integer> myList = new ArrayList<Integer>();
In other words, we were able to assign an ArrayList to a List reference, because
List is a supertype of ArrayList.
But what about this?
class Parent { }
class Child extends Parent { }
List<Parent> myList = new ArrayList<Child>();
No, it will not work.
Rule : The type of the variable declaration must match the type you pass to the actual object type. If you declare List<Parent> parentList then whatever you assign to the foo reference MUST be of the generic type <Parent>. Not a subtype of <Parent>. Not a supertype of <Parent>. Just <Parent>.
Wrong:
List<Object> myList = new ArrayList<JButton>(); // NO!
List<Number> numbers = new ArrayList<Integer>(); // NO!
// remember that Integer is a subtype of Number
But these are fine:
List<JButton> myList = new ArrayList<JButton>(); // yes
List<Object> myList = new ArrayList<Object>(); // yes
List<Integer> myList = new ArrayList<Integer>(); // yes
---------------------------------------------------------------
See example:
import java.util.*;
abstract class Animal {
public abstract void checkup();
}
class Dog extends Animal {
public void checkup() { // implement Dog-specific code
System.out.println("Dog checkup");
}
}
class Cat extends Animal {
public void checkup() { // implement Cat-specific code
System.out.println("Cat checkup");
}
}
public class AnimalDoctor {
// method takes an array of any animal subtype
public void checkAnimals(Animal[] animals) {
for(Animal a : animals) {
a.checkup();
}
}
public static void main(String[] args) {
// test it
Dog[] dogs = {new Dog(), new Dog()};
Cat[] cats = {new Cat(), new Cat(), new Cat()};
AnimalDoctor doc = new AnimalDoctor();
doc.checkAnimals(dogs); // pass the Dog[]
doc.checkAnimals(cats); // pass the Cat[]
}
}
Her you see that we can send subtype reference to parent type. But this can't be done in generics, see the next code
public class AnimalDoctorGeneric {
// change the argument from Animal[] to ArrayList<Animal>
public void checkAnimals(ArrayList<Animal> animals) {
for(Animal a : animals) {
a.checkup();
}
}
public static void main(String[] args) {
// make ArrayLists instead of arrays for Dog, Cat, Bird
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog());
dogs.add(new Dog());
List<Cat> cats = new ArrayList<Cat>();
cats.add(new Cat());
cats.add(new Cat());
// this code is the same as the Array version
AnimalDoctorGeneric doc = new AnimalDoctorGeneric();
// this worked when we used arrays instead of ArrayLists
doc.checkAnimals(dogs); // send a List<Dog>
doc.checkAnimals(cats); // send a List<Cat>
}
}
So what will happen if you will compile this:
javac AnimalDoctorGeneric.java
AnimalDoctorGeneric.java:51: checkAnimals(java.util.
ArrayList<Animal>) in AnimalDoctorGeneric cannot be applied to
(java.util.List<Dog>)
doc.checkAnimals(dogs);
^
AnimalDoctorGeneric.java:52: checkAnimals(java.util.
ArrayList<Animal>) in AnimalDoctorGeneric cannot be applied to
(java.util.List<Cat>)
doc.checkAnimals(cats);
^
--------------------------------------------------------------
Her you can see that polymorphism with arrays, where the same scenario (Animal[] can refer to
Dog[], Cat[], or Bird[]) works as you would expect, but not with generics.
So we have two real issues:
1. Why doesn't this work?
2. How do you get around it?
See the code:
public void show() {
Dog[] dogs = {new Dog(), new Dog()};
addAnimal(dogs); // no problem, send the Dog[] to the method
}
public void addAnimal(Animal[] animals) {
animals[0] = new Dog(); // ok, any Animal subtype works
}
It's fine that you can add Dog object into animals because animals is type of Dog
Now suppose any developer has changed some code like...
public void show() {
Cat[] cats = {new Cat(), new Cat()};
addAnimal(cats); // no problem, send the Cat[] to the method
}
Now you are sending a cat object to addAnimal() method, which is still same:
public void addAnimal(Animal[] animals) {
animals[0] = new Dog(); // Ahhhh! We just put a Dog in a Cat array!
}
The compiler thinks it is perfectly fine to add a Dog to an Animal[] array, since a Dog can be assigned
to an Animal reference.
The problem is, if you passed in an array of an Animal subtype (Cat, Dog), the compiler does not know.
The compiler does not realize that out on the heap somewhere is an array of type Cat[], not Animal[],
and you're about to try to add a Dog to it. To the compiler, you have passed in an array of type Animal,
so it has no way to recognize the problem.
THIS is the scenario we're trying to prevent, regardless of whether it's an array or
an ArrayList. The difference is, the compiler lets you get away with it for arrays, but
not for generic collections.
Explanation: At runtime the JVM KNOWS the type of arrays, but does NOT know the type of a collection. All the generic type information is removed during compilation, so by the time it gets to the JVM, there is simply no way to recognize the disaster of putting a Cat into an ArrayList<Dog> and vice versa (and it becomes exactly like the problems you have when you use legacy, non-type safe code).
Solution:
There IS a mechanism to tell the compiler that you can take any generic subtype of the declared argument type because you won't be putting anything in the
collection. And that mechanism is the wildcard <?>. The method signature would change from
public void addAnimal(List<Animal> animals)
to
public void addAnimal(List<? extends Animal> animals)
By saying <? extends Animal>, we're saying, "I can be assigned a collection that is a subtype of List and typed for <Animal> or anything that extends Animal.
And oh yes, I SWEAR that I will not ADD anything into the collection." (There's a little more to the story, but we'll get there.)
So of course the addAnimal() method above won't actually compile even with the wildcard notation, because that method DOES add something.
public void addAnimal(List<? extends Animal> animals) {
animals.add(new Dog()); // NO! Can't add if we use <? extends Animal>
}
You'll get a very strange error that might look something like this:
javac AnimalDoctorGeneric.java
AnimalDoctorGeneric.java:38: cannot find symbol
symbol : method add(Dog)
location: interface java.util.List<capture of ? extends Animal>
animals.add(new Dog());
^
1 error
Note: The keyword extends in the context of a wildcard represents BOTH subclasses and interface implementations. There is no <? implements Serializable> syntax. If
you want to declare a method that takes anything that is of a type that implements Serializable, you'd still use extends like this:
void show(List<? extends Serializable> list) // odd, but correct to use "extends"
----------------------------------------------------------------------------------------
However, there is another scenario where you can use a wildcard AND still add to the collection, but in a safe way—the keyword super.
Imagine, for example, that you declared the method this way:
public void addAnimal(List<? super Dog> animals) {
animals.add(new Dog()); // adding is sometimes OK with super
}
public static void main(String[] args) {
List<Animal> animals = new ArrayList<Animal>();
animals.add(new Dog());
animals.add(new Dog());
AnimalDoctorGeneric doc = new AnimalDoctorGeneric();
doc.addAnimal(animals); // passing an Animal List
}
Now what you've said in this line
public void addAnimal(List<? super Dog> animals)
is essentially, "Hey compiler, please accept any List with a generic type that is of type Dog, or a supertype of Dog. Nothing lower in the inheritance tree can come in, but
anything higher than Dog is OK."
Comments
Post a Comment