Extend Java's type system with Generic Classes


Reading time: 25 minutes | Coding time: 10 minutes

Generics are a Java paradigm which extends the language's type system, allowing the development of less redundant code with an unnecessary amount of casting. This mechanic's useful because it can find type errors at compile-time, bringing a safety feeling for the programmer with will have the certainty that he will not come across a ClassCastException at run time.

The code below shows examples of using generics in a List of Objects, using brackets to define the List type, and your alternative without the generics, using casting.

    /* WITH GENERICS*/
    List<Apple> l = ...;
    Apple apple = l.get(0);
    
    /*WITHOUT GENERICS*/
    List l = ...;
    Apple apple = (Apple) l.get(0)

Can be observed in the case using casting to another object two mainly problems:

  • the developer will have to make a cast to the object type every time that he gets an element from the list.
  • if the object from the list and the cast are not compatible, it will generate an error at run time.

Otherwise, the use of generics doesn't present these problems, where the compiler itself can point an error if the types are not the same.

Creating a Generic Class

Generics let classes, methods, and interfaces to take other classes and interfaces as a type parameter. In the code beneath can be observed how to write a Java class using a generic type T.

    public class Param<T> {
        private T value;
        public T getValue() {
            return value;
        }
        public void setValue(T value) {
            this.value = value;
        }
    }

An object of this class can be instantiated providing a type argument in place of T, like Integer.

Param<Integer> integerParam = new Param<Integer>();

/*Version >= Java SE 7*/
Param<Integer> integerParam = new Param<>();

Wildcards

Despite the benefits of using generics and don't have to worry with reimplementation for different types, sometimes you might want to restrict the use of some generic class or method to a specific inherent relation. A simple way of doing that is with the application of wildcards, increasing the flexibility and control above the use of generics.

There are three types of Wildcards in Java's Generics:

  • Unknown Wildcard
  • Extends Wildcard
  • Super Wildcard

Unknown Wildcard

The Unknown Wildcard is the most Generic way to write a class or method. It's distinguished by has only a "?" character between the brackets, where "?" can be any possible type.

    public void processElements(List<?> elements){
        for(Object o : elements){
            System.out.println(o);
        }
    }
 
    List<A> listA = new ArrayList<A>();
    processElements(listA);

Extends Wildcard

"? extends T" represents an upper-bounded wildcard, the unknown type must be a subtype of T or the type T itself.
Extends is commonly applied among the necessity of "read" access.

    public void processElements(List<? extends Fruit> elements){
           for(Fruit a : elements){
              System.out.println(a.getValue());
           }
        }
 
    /* Now it's possible to use this method only passing a List of Fruits */
    List<Apple> listApple = new ArrayList<Apple>();
    processElements(listApple);
 
    List<Orange> listOrange = new ArrayList<Orange>();
    processElements(listOrange);
 
    List<Strawberry> listStrawberry = new ArrayList<Strawberry>();
    processElements(listStrawberry);

Super Wildcard

"? super T" represents a lower-bounded wildcard, the unknown type must be a supertype, or type T itself.
Super is usually employed when a "write" access seems necessary.

    public class Berry {}
    public class Strawberry extends Berry {}
    public class Blackberry extends Berry {}

    public void appendStrawberries(Collection<? super Strawberry> list, int n) {
        for (int i = 1; i <= n; i++) {
            list.add(new Strawberry());
    }
  
    List<Berry> berryList = new ArrayList<Berry>();
    berryList.add(new Strawberry());
    berryList.add(new Blackberry());
  
    /* If we need to add 3 Strawberries to the Berry List we can write*/
    appendStrawberries(berryList, 3); // Allowed because of the super wildcard

Applications

Generics in Java allows the programmer with better flexibility and control to code. Generic types remove redundant lines of code and add a cleaner syntax when compared with casting. Type errors move from the run-time to the compiled-time, bringing safety to development and a faster way to detect issues.

For the last sample will show all the power of generics with the wildcards. Consider the class relations below.

    class Shoe {}
    class IPhone {}
    interface Fruit {}
    class Apple implements Fruit {}
    class Banana implements Fruit {}
    class GrannySmith extends Apple {}
    
    public class FruitHelper {
        public void eatAll(Collection<? extends Fruit> fruits) {}
        public void addApple(Collection<? super Apple> apples) {}
    }

Now, the compiler will identify some wrong uses between the classes and the generic methods. Next, are some situations and how the compiler will behave for each one of them, respecting these relations. The generic wildcards give even more control and dynamism for the language, indicate how a method or class can behave through the subtypes and supertypes of an object.

    public class GenericsTest {
    public static void main(String[] args){
        FruitHelper fruitHelper = new FruitHelper() ;
        List<Fruit> fruits = new ArrayList<Fruit>();
        fruits.add(new Apple()); // Allowed, as Apple is a Fruit
        fruits.add(new Banana()); // Allowed, as Banana is a Fruit
        fruitHelper.addApple(fruits); // Allowed, as "Fruit super Apple"
        fruitHelper.eatAll(fruits); // Allowed
        Collection<Banana> bananas = new ArrayList<>();
        bananas.add(new Banana()); // Allowed
        // fruitHelper.addApple(bananas); // Compile error: may only contain Bananas!
        fruitHelper.eatAll(bananas); // Allowed, as all Bananas are Fruits
        Collection<Apple> apples = new ArrayList<>();
        fruitHelper.addApple(apples); // Allowed
        apples.add(new GrannySmith()); // Allowed, as this is an Apple
        fruitHelper.eatAll(apples); // Allowed, as all Apples are Fruits.
        Collection<GrannySmith> grannySmithApples = new ArrayList<>();
        // fruitHelper.addApple(grannySmithApples); //Compile error: Not allowed.
        // GrannySmith is not a supertype of Apple
        apples.add(new GrannySmith()); //Still allowed, GrannySmith is an Apple
        fruitHelper.eatAll(grannySmithApples);//Still allowed, GrannySmith is a Fruit
        Collection<Object> objects = new ArrayList<>();
        fruitHelper.addApple(objects); // Allowed, as Object super Apple
        objects.add(new Shoe()); // Not a fruit
        objects.add(new IPhone()); // Not a fruit
        //fruitHelper.eatAll(objects); // Compile error: may contain a Shoe, too!
    }

With this mechanism, the Generic shows more dinamysm

References/ Further reading

Generic's Java Documentation page