Java: Generic Types

Before Java 5, it was not possible to tell to a collection (like a List, a Map or a Set) only to accept objects of a specific type. At that moment, collections could hold anything that was not a primitive type (which are boxed when added into collections). For that reason, applications before Java 5 were “less type safe” and more prone do errors when manipulating collections. Here we have an example (examples on this post were built using a JDK 6):

        List list = new ArrayList();

        // We can add whatever we want...
        list.add("string");
        list.add(1);
        list.add(4.0);
        list.add(new Object());

This approach can be a little bit dangerous, particularly when working with external APIs. Suppose that in your code you call a method of an external class which manipulates lists of Strings and gets a List as parameter. For any reason, you put an Integer inside your List and you pass it to the method. Inside the method, a cast is required to get each element of the list and assign them to a variable, like String value = (String) list.get(0). Since you can have no access to the foreign code, you can inadvertently get a RuntimeException when the method will try to cast the Integer element into a String O_o

Java 5 introduced Generics. With generics now, we can guarantee which object type a collection will hold. If we mark a collection with a certain type and we try to add to it an object of a different type, we get a compiler error. The type validation is taken in charge by the compiler. Generics are not present inside the bytecode, so they are not used on runtime.

Activating generics is simple! Just add a type inside angle brackets <> immediately following the collection. This needs to be done in the variable declaration and in the constructor call. Finally, generics can also be used as type parameters and return types for methods.

import java.util.ArrayList;
import java.util.List;

public class GenericTypes {
    public static void main(String... args) {
        GenericTypes gt = new GenericTypes();

        // We define a list of Strings
        List<String> people = gt.getArrayList();

        people.add("Alice");
        people.add("Bob");

        gt.printList(people);
    }

    void printList(List<String> list) {
        // Now casts are not necessary
        for(String name : list) {
            System.out.println(name);
        }
    }

    List<String> getArrayList() {
        return new ArrayList<String>();
    }
}

Legacy code

As said before, generic types are not present on runtime. The verification for type compliance is made by the compiler. Once the code is compiled, collections post and pre-Java 5 are the same thing. The reason is simple: Sun did not want to broke all code produced before Java 5, so the type verification does not exist in runtime. By doing this, legacy and post-Java 5 code can cohabit.

import java.util.ArrayList;
import java.util.List;

public class LegacyExample {
    public static void main(String... args) {
        List<String> list = new ArrayList<String>();
        list.add("1");
        list.add("2");

        LegacyExample le = new LegacyExample();
        le.addObject(list);
    }

    void addObject(/*Parameters here are non-type safe*/ List list) {
        list.add(1);
    }
}

The code above compiles and runs without errors. Even if the object added to list inside the addObject method is different from the type hold by the list, it works. This happens because type-safe protection does NOT exist at runtime. Nevertheless, the compiler will notice that something weird can happen and advert you with warnings:

$ javac LegacyExample.java
Note: LegacyExample.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

As show in the message, it is possible to identify which method is causing problems. For that, just recompile with -Xlint:unchecked option.

$ javac -Xlint:unchecked LegacyExample.java
LegacyExample.java:18: warning: [unchecked] unchecked call to add(E) as a member of the raw type java.util.List
        list.add(1);
                ^
1 warning

So, at this point you may have a question. If it is not possible to have type-safe protection at runtime like arrays, what are the gains in using generics? Mainly, there are two main advantages:

  • Type-safe verification at compile time
  • No need to cast objects when you get something out

Generics and Polymorphism

It is important to remark that there is a couple of differences between arrays and generic collections. Let’s see some examples:

import java.util.ArrayList;
import java.util.List;

class Professional{}
class Engineer extends Professional {}

public class ArraysXCollections {
    public static void main(String... args) {

        Professional[] professionals = new Engineer[5]; // This works fine
        List<Professional> professionalList =
                new ArrayList<? extends Professional>(); // But this does not compile

    }
}

Generic types do not support natural polymorphism. The reason is simple. Imagine that we have a method receiving List<Professional> list as parameter. So, we could perfectly pass an ArrayList<Engineer> list to that method. Ok. The problem is, by doing this, we allow the code inside the method to add to the list any object that passes an IS-A test with Professional that is not an Engineer. In the end, there is no guarantee that the collection of engineers given to the method in the past still holds only engineers and this can be dangerous :S

However, it is possible to add some Engineers into a collection of Professionals without problems.

public class CollectionsExample {
    public static void main(String... args) {

        List<Professional> professionalList =
                new ArrayList<Professional>();  // No problem here

        professionalList.add(new Engineer());   // This is allowed

        addElements(professionalList);          // Everything is fine
    }

    static void addElements(List<Professional> list) {
        list.add(new Engineer());
    }
}

To solve the problem of manipulating sub and super types of generic collections Java brings a mechanism called “wildcards” <?>. By using the type List<? extends Professional> as a method parameter, we can pass any list to the method since its generic type passes an IS-A test for Professional. The only restriction is, when using something like List<? extends Professional> (or even simply List<?>), it is not possible to insert any object into the list. Otherwise, a compiler error will occur.

import java.util.ArrayList;
import java.util.List;

public class Wildcard {

    public static void main(String... args) {
        List<Engineer> engineers = new ArrayList<Engineer>();
        addElement(engineers);
    }

    static void addElement(List<? extends Professional> list) {
        list.add(new Engineer());   // This line does not compile
    }
}

In the example above, if we delete the instruction list.add(new Engineer()) the code compiles and runs fine.

On the other hand, it is possible to deal with the insert restriction imposed by the keyword extends used with the wildcard ?. By using the keyword super, it is possible to add elements into a collection received as parameter.

public class Wildcard2 {
    public static void main(String... args) {
        List<Professional> list = new ArrayList<Professional>();
        addElements(list);
    }

    static void addElements(List<? super Engineer> list) {
        list.add(new Engineer());   // this line compiles fine
    }
}

When the keyword super is used, Engineers and any supertype of Engineer – like Professional and obviously Object – will be accepted. Maybe this will not shock you, since upcasts are natural in Java (like putting a String into an Object).

Summarizing, List<?> and List<? extends Object> are the same thing. Nevertheless, neither of them are the same as List<Object>, because the later one will leave you add elements inside. Wildcards are employed only for reference declarations like arguments, return types and variables. They cannot be used as generic type to create new typed collections.

Parameterized Types

It is possible to use generic types to create “template classes” which can manipulate safely objects of any type. In this case, generic types are used as instance variables giving a class the chance to manipulate objects generically.

Following an example.

class Test<T> {
    T t;

    public Test(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
}

public class GenericTypeExample {
    public static void main(String... args) {
        Test<String> test = new Test<String>(<Generics>);
        System.out.print(test.getT());  // will print Generics
    }
}

In the example above, T is a placeholder used to designate a parameter type for a class. We could have used anything else, but T is a convention for class parameter types. The placeholder E, for example, designate “Element” and is used when the template is a collection (see List for example). Wildcard notation can be used in many ways to combine types and create templates.

class Converter<T, Y> {}
class Calculator<T extends Number> {}

public class GenericTypeExample2 {
    Converter<Integer, Long> converter = new Converter<Integer, Long>();
    Calculator<Integer> calculator = new Calculator<Integer>();
}

However, it is also possible to use parameterized types to ensure type safety only in methods. This is used when only the method needs to be aware of the generic type, not the whole class.

Let’s suppose we need a generic method to create a parameterized Map for cache purposes.

import java.util.HashMap;
import java.util.Map;

class Cache {
    static <K, V>  Map<K,V> getCache() {
        return new HashMap<K, V>();
    }
}

public class CacheTest {
    public static void main(String... args) {
        Map<Integer, String> cache = Cache.<Integer,String>getCache();
        cache.put(1, "test1");
        cache.put(2, "test2");
    }
}

In this example, K is replaced by Integer and V by String. In generic methods, it is necessary to declare the parameterized types before using them: static <K, V> Map<K,V> getCache(). We must do this to define what K and V are before using them as parameters or return type. If the type is defined in the class, it is not necessary to re-declare in the method. The same thing could be done using boundaries: static <K, V extends Object> Map<K,V> getCache().

Advertisements

About Diego Lemos

Trainer, coach and polyglot programmer. Agile and software craftsmanship enthusiast.
This entry was posted in Java and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s