Overview

Skill Level: Intermediate

Beginners can also make use of this, so long as they have been through Unit 20 of the Intro to Java Programming Learning Path

This recipe defines type erasure, how it relates to Java Generics, and describes a few of those cryptic-looking error and warning messages you get when you do not use Generics correctly (we have all been there, trust me).

Ingredients

Step-by-step

  1. Let's setup your development environment

    You will need the JDK and Eclipse IDE installed to complete this recipe. I’ll assume you have some basic Java knowledge. If not, please check out the Intro to Java Programming learning path here at IBM developerWorks.

    You will also need a working development environment. If you already have a Java development environment, skip to Step 2.

    Otherwise, check out the Introduction to Java Programming learning path, Unit 2 for step-by-step instructions. There are videos in this section as well that can assist you if you need more help.

    First, download and install the Java Development Kit (JDK), version 8, and install it on your machine. Please refer to the video below if you need help.

     Next, install the Eclipse IDE to your computer. Please refer to the video below if you need help.

    Once you have your development environment setup and ready to go, you can proceed to Step 2, where I’ll define Type Erasure.

  2. Let's define type erasure

    We all make mistakes when writing Java code, and when we do the Java compiler is there to provide warning and error messages. But sometimes the messages we get from the Java compiler are cryptic and confusing, especially when using Java Generics (unless you understand Type Erasure).

    In this recipe I’ll show you some of the most common warnings and errors you’ll see related to Java Generics, and how to avoid or fix them. First, we need to define a pivotal concept to making Generics work, and that’s Type Erasure.
    Type Erasure is a technique employed by the Java compiler to support the use of Generics. In Unit 20 of the Intro to Java Programming learning path, I showed you how to use Java Generics, where you saw how to create parameterized classes and methods. I didn’t really talk about Type Erasure because it’s a fairly complicated topic, and if you use Java Generics correctly, one you don’t really need to understand.

    If you write Java code long enough, you’ll see some or all of the messages I’m about to show you. And when you’re finished with this recipe, you should understand what those messages mean, and how to avoid them forever!
    When you use Generics to define a parameterized class, the Java compiler doesn’t actually create a new type (for all kinds of deep technical reasons I won’t go into here). Instead, the compiler takes the type you specify and erases it back to one of two types: the upper bound (if you specify one) or Object (if you do not). Consider this example:


    public class ObjectContainer<T> {

    private T contained;

    public ObjectContainer(T contained) {
    this.contained = contained;
    }

    public T getContained() {
    return contained;
    }

    }

    In this example, the parameterized type ObjectContainer is declared with no upper bound, so the compiler generates this code:

    public class ObjectContainer {

    private Object contained;

    public ObjectContainer(Object contained) {
    this.contained = contained;
    }

    public Object getContained() {
    return contained;
    }

    }

    The type of the parameter (T) is erased back to Object since there was no upper bound. And when the ObjectContainer is declared, the compiler inserts a cast so that the code works like this:

     ObjectContainer<Person> personContainer = new ObjectContainer<>(new Person("Steve", 49));
    Person contained = personContainer.getContained();
    System.out.println("ObjectContainer<Person> contains: " + contained.toString());

    The compiler, however, generates this code:

     ObjectContainer personContainer = new ObjectContainer(new Person("Steve", 49));
    Person contained = (Person)personContainer.getContained();
    System.out.println("ObjectContainer<Person> contains: " + contained.toString());

    Notice the cast to Person above. This is because under the covers the compiler has erased the declared type (Person) back to Object and must insert a cast in order for the code to work correctly.

    When a bounded type is used, a similar thing occurs, except that instead of Object as the upper bound, the specified upper bound is used.

    Consider this code:

    public class ObjectContainer<T extends Person> {

    private T contained;

    public ObjectContainer(T contained) {
    this.contained = contained;
    }

    public T getContained() {
    return contained;
    }

    }

    In this case, the compiler generates this code:

    public class ObjectContainer {

    private Person contained;

    public ObjectContainer(Person contained) {
    this.contained = contained;
    }

    public Person getContained() {
    return contained;
    }

    }

    The type of the parameter (T extends Person) is erased back to Person, which is the upper bound. When the ObjectContainer<Employee> is declared, the compiler inserts a cast so that the code works like this:

     ObjectContainer<Employee> personContainer =
    new ObjectContainer<>(new Employee("Steve", 49, "EMP001"));
    Employee contained = personContainer.getContained();
    System.out.println("ObjectContainer<Employee> contains: " + contained.toString());

    The compiler, however, generates this code:

     ObjectContainer<Employee> personContainer =
    new ObjectContainer<>(new Employee("Steve", 49, "EMP001"));
    Employee<String> contained = (Employee)personContainer.getContained();
    System.out.println("ObjectContainer<Employee> contains: " + contained.toString());

     

     

  3. So What?

    Generics are easy to work with. Until they aren’t. In my experience that happens when I try and do something that makes sense from an object oriented standpoint (usually covariance), but isn’t supported by Generics.

    In the next three sections, I’ll introduce you to two errors and one warning that, if you use Generics long enough, I guarantee you are going to see at some point. And when you do, you’ll know what to do to fix the issues.

    First, I’ll introduce the errors. When you get these errors (and you will), you should know what is going on so you can fix them.

    Then the warning that I’ve seen more than any other related to Generics. When you get this warning (and you will) you’ll know what to do.

  4. Error 1 - “Erasure of method xyz(…) is the same as another method in type Abc”

    Consider this code:

    public class App {

    public int process(List<Person> people) {
    for (Person person : people) {
    log.info("Processing person: " + person.toString());
    }
    return person.size();
    }

    public int process(List<Employee> employees) {
    for (Employee employee : employees) {
    log.info("Processing employee: " + employee.toString());
    }
    return employees.size();
    }

    }

    The code above looks fine at first glance (like process() is simply an overloaded method), but when you compile it, you get these messages:

    Erasure of method process(List<Person>) is the same as another method in type App
    Erasure of method process(List<Employee>) is the same as another method in type App

    What’s going on? These methods have different method signatures, so the process() method is overloaded, right? Wrong. Remember from Step 2, when you use Generics, the compiler erases the type specified in the <> with Object (in this case). The code generated by the compiler looks like this:

    public class App {

    public int process(List people) {
    for (Person person : people) {
    log.info("Processing person: " + person.toString());
    }
    return person.size();
    }

    public int process(List employees) {
    for (Employee employee : employees) {
    log.info("Processing employee: " + employee.toString());
    }
    return employees.size();
    }

    }

    Now it’s obvious what the problem is. Two methods with the same signature (process(List)) cannot exist in the same class.

    Knowing how the compiler is erasing the type here, we can make a small design change to fix the issue:

    public class App {

    public int processPeople(List<Person> people) {
    for (Person person : people) {
    log.info("Processing person: " + person.toString());
    }
    return person.size();
    }

    public int processEmployees(List<Employee> employees) {
    for (Employee employee : employees) {
    log.info("Processing employee: " + employee.toString());
    }
    return employees.size();
    }

    }

    Now the code compiles just fine, and the method names more accurately reflect what they actually do.

     

     

     

  5. Error 2 - “The method xyz(Foo) in the type Abc is not applicable for the arguments (Foo)”

    Usually you’ll see this error when, say, A is a superclass of B, and it seems logical to assume therefore that some generic type Foo<B> is therefore a subclass of Foo<A> (or at behaves like one, that is, provides covariant behavior). However, Java Generics are NOT covariant, which is a fancy way of saying that even though B is a subclass of A, but SomeGenericType<B> is NOT a subclass of SomeGenericType<A>, nor does it behave like one.

    Basically, this error is a close cousin of the error we already looked at for method names, but this one applies to method parameters. The fundamental issue is the same.

    Consider this code (note: Employee is a subclass of Person):

    public class App {

    public int processPeople(List<Person> people) {
    for (Person person : people) {
    log.info("Processing person: " + person.toString());
    }
    return person.size();
    }
    .
    .
    }

    .
    .
    List<Employee> employees;
    employees = new ArrayList<>();
    employees.add(employee1);
    employees.add(employee2);
    App app = new App();
    // ERROR ON NEXT LINE!
    app.processPeople(employees);
    .
    .

    The call to App.processPeople(List<Employee>) produces this error message:

    The method processPeople(List<Person>) in the type App is not applicable for the arguments
    (List<Employee>)

    At first, it seems logical since Employee is a subclass of Person that we can pass a List<Employee> to a method that is expecting a List<Person>, right?

    Wrong. Because of Type Erasure, the List<Person> and List<Employee> get erased back to List. The message is confusing (in my opinion) and should mention the erasure aspect of the error (like the first error we looked at does).

    Since the type gets erased back to Object, you might think the compiler would let this go through. The thing is though, the compiler knows that because of Type Erasure, List<Employee> is not a suitable substitute for List<Person>. Once the type is erased, the information about the actual parameter type is lost, and runtime problems could result from allowing this code to compile.

    So how to fix it? You could also use (or create) a method specifically to process Employees (from the earlier example).

    public class App {

    public int processPeople(List<Person> people) {
    for (Person person : people) {
    log.info("Processing person: " + person.toString());
    }
    return person.size();
    }

    public int processEmployees(List<Employee> employees) {
    for (Employee employee : employees) {
    log.info("Processing employee: " + employee.toString());
    }
    return employees.size();
    }
    }

    .
    .
    List<Employee> employees;
    employees = new ArrayList<>();
    employees.add(employee1);
    employees.add(employee2);
    App app = new App();
     // ERROR ON NEXT LINE!
    app.processEmployees(employees);
    .
    .

     But what if, say, there is no processEmployees() method, and the logic of “process person” is truly shared between Person and Employee? Then you could change the signature of processPeople() to:

     public int processPeople(List<? extends Person> people) . . .
    .
    .
    .
    List<Employee> employees;
    employees = new ArrayList<>();
    employees.add(employee1);
    employees.add(employee2);
    App app = new App();
     // THIS WORKS GREAT NOW!
    app.processPeople(employees);

    Now the compiler sees the upper bound of the type parameter as Person, and uses Person in its generated code, instead of Object, and it works fine.

  6. Warning - “Foo is a raw type. References to generic type Foo should be parameterized”

    A warning doesn’t prevent your program from running, but the fact that you are getting a warning says something is potentially wrong with your code. When you see a warning like this one, it would help you to know what is going on, so you know whether your code is fine, or is a ticking time bomb.

    Generics were designed to be backward-compatible with raw types (e.g., references to List<T> would work with List). However, any new code you write that makes use of Generics should NEVER use a raw type.

    Why not? Calling methods through a reference on an unparameterized generic type like this is dangerous. It can lead to problems like heap pollution, as I’ll show you.

    Consider this code:

    public class ObjectContainer<T> {

    private T contained;

    public ObjectContainer(T contained) {
    this.contained = contained;
    }

    public T getContained() {
    return contained;
    }

    public void setContained(T contained) {
    this.contained = contained;
    }

    @Override
    public String toString() {
    return contained.toString();
    }

    }
    .
    .
    .
    public class PersonContainer extends ObjectContainer<Person> {

    public PersonContainer(Person contained) {
    super(contained);
    }

    @Override
    public void setContained(Person contained) {
    super.setContained(contained);
    }
    }
    .
    .
    PersonContainer pc = new PersonContainer(new Person("Test", 23));

    ObjectContainer oc = pc;// WARNING occurs here
    System.out.println("PersonContainer (through ObjectContainer): " + oc.toString());

    The warning I’m referring to occurs on the line above where I’ve indicated. The exact warning in this case says:

    ObjectContainer is a raw type. References to generic type ObjectContainer<T> should be 
    parameterized

    Bad things can happen when you use a raw generic type. You may be wondering what sorts of bad things. Read on.

    Consider this code (which builds off the definition of ObjectContainer from the previous section):

    Because of Type Erasure the PersonContainer is no longer polymorphic through the setContained() method. Remember, a generic type is erased back to its upper bound, so ObjectContainer really looks like this:

    public class ObjectContainer {

    private Object contained;

    public ObjectContainer(Object contained) {
    this.contained = contained;
    }

    public Object getContained() {
    return contained;
    }

    public void setContained(Object contained) {
    this.contained = contained;
    }

    @Override
    public String toString() {
    return contained.toString();
    }

    }

    So far so good, but the problem manifests because I’ve provided a version of setContained() in PersonContainer that takes a Person object. Now the signatures of setContained() differ between PersonContainer and its superclass, ObjectContainer. At first glance, it might seem that setContained() is overridden when, in fact, it is not.

    To preserve polymorphism, the compiler generates an override of setContained() bridge method for PersonContainer, so that PersonContainer really looks like this:

    public class PersonContainer extends ObjectContainer<Person> {

    public PersonContainer(Person contained) {
    super(contained);
    }

    // Bridge method generated by the compiler – you never see this method
    // (unless there is a problem)
    public void setContained(Object contained) {
    setContained((Person)contained);
    }

    @Override
    public void setContained(Person contained) {
    super.setContained(contained);
    }

    }

    Now consider what happens if I run this code – let’s say I put it in a test method:

     @Test
    @DisplayName("Testing PersonContainer - will throw ClassCastException")
    public void testSetContainedPerson() {
    PersonContainer pc = new PersonContainer(new Person("Test", 23));

    ObjectContainer oc = pc;// WARNING occurs here
    System.out.println("PersonContainer (through ObjectContainer): " + oc.toString());
    // ClassCastException. Not good.
    assertThrows(ClassCastException.class, () -> oc.setContained("Howdy!"));
    }

    That is what we call Heap Pollution. And it’s not good.

    Type Erasure enables backward compatibility for generic types, but it can lead to all kinds of nasty problems if generics are not used properly.

    Now that you have a better understanding of Type Erasure, when you get error and warning messages related to generics, you are better armed to deal with them.

    The best rule of thumb when using generics is this: NEVER let generics-related warnings slide. The compiler is warning you that you are not using generics properly, and you would do well to heed those warnings.

     

  7. So What? (The Video)

    I have created a video where I walk through the code in the previous sections, pointing out the various errors we saw and some warnings that the compiler issues in an effort to tell you “Danger!” when using generics improperly.

    In the video I show you how to:

    • Clone the Github repo that contains the code for this Recipe.
    • Import the new Maven project based on the code in Github into Eclipse.
    • Demonstrate:
      • Errors 1 and 2, and
      • What happens when the Warning is ignored (bad things!)
      • A bonus warning, where other bad things happen if ignored!
  8. What's next?

    The web is full of resources related to Type Erasure and Java generics. I’ve included a few of my favorites in this section. Enjoy!

    Angelika Langer – Type Erasure

    Oracle Docs – Type Erasure

Join The Discussion