Is List a subclass of List? Why are Java generics not implicitly polymorphic?
Java generics provide a way to parameterize types on classes, methods, and interfaces. They allow us to create classes and methods that can work with different types without sacrificing type safety. However, there is one aspect of Java generics that can be a bit confusing for newcomers: inheritance and polymorphism.
Understanding the problem
Let's start by understanding the problem with a simplified example. Consider the following hierarchy:
Animal (Parent)
|
|__ Dog
|
|__ Cat
Now, suppose we have a method doSomething(List<Animal> animals)
. Based on the rules of inheritance and polymorphism, you might assume that a List<Dog>
is a List<Animal>
and a List<Cat>
is also a List<Animal>
. Therefore, you might expect that you can pass a List<Dog>
or a List<Cat>
to the doSomething
method.
However, in Java, this is not the case. If you try to pass a List<Dog>
to the doSomething
method, you will get a compilation error. Java generics are not implicitly polymorphic.
The reason behind Java generics not being implicitly polymorphic
To understand why Java generics are not implicitly polymorphic, we need to dive into the concept of type safety. Java generics provide type safety by checking the types at compile-time, which prevents type-related errors at runtime.
If Java allowed implicit polymorphism with generics, it could lead to potential type-related issues. Let's consider an example:
// Suppose List<Dog> was a subtype of List<Animal>
List<Dog> dogs = new ArrayList<Dog>();
List<Animal> animals = dogs; // Allowed if List<Dog> is a subtype of List<Animal>
// Now, let's add a Cat to the list
animals.add(new Cat());
// At this point, we have a Cat in the dogs list
Dog dog = dogs.get(0); // ClassCastException at runtime
In this example, if List<Dog>
was a subtype of List<Animal>
, we would have added a Cat object to the dogs list, which violates the type safety. When we try to retrieve a Dog from the dogs list, we end up with a ClassCastException at runtime.
To prevent such type-related issues, Java introduced bounded wildcards with the ? extends Type
syntax. By using List<? extends Animal>
, we are allowing the method to accept a list of any subclass of Animal. This way, the type safety is maintained, and we can avoid adding incompatible objects to the list.
Example of bounded wildcards
Let's take a look at an example that demonstrates the usage of bounded wildcards:
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
}
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
}
public class AnimalService {
public static void printAnimalNames(List<? extends Animal> animals) {
for (Animal animal : animals) {
System.out.println(animal.getName());
}
}
}
public class Main {
public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog("Buddy"));
dogs.add(new Dog("Max"));
List<Cat> cats = new ArrayList<>();
cats.add(new Cat("Kitty"));
cats.add(new Cat("Luna"));
AnimalService.printAnimalNames(dogs);
AnimalService.printAnimalNames(cats);
}
}
In this example, the AnimalService.printAnimalNames
method accepts a list of any subclass of Animal. The method iterates over the list and prints the names of the animals.
When we call the method with a List<Dog>
and a List<Cat>
, the code compiles successfully because ? extends Animal
ensures that we can only pass lists of Animal and its subclasses.
If we try to add an object to the list inside the printAnimalNames
method, we will get a compilation error, as it would violate the type safety:
animals.add(new Dog("Charlie")); // Compilation error
By using bounded wildcards, we can ensure that the methods are safe to use with different subtypes of the declared type and maintain type safety throughout the code.
In conclusion
Java generics are not implicitly polymorphic to maintain type safety at compile-time. By using bounded wildcards, we can achieve flexibility while ensuring type safety and preventing potential type-related issues.
It is important to understand the reasoning behind Java generics' behavior to write code that is both efficient and avoids runtime errors. By explicitly specifying the type bounds, we can create more flexible and reusable code that can work with different subtypes of the declared type.