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.