Monday, August 24, 2015

5 Terms That You Must Know About Java Generics

Learn the 5 terms below could help you to master Java Generic programming.

1. Generic Type

Class or interface that is designed for generic usage and type-safe by using Java Generic in their definition. You should understand that generic type is different than the normal class that does not use Java Generic but is implemented for generic usage.

Normal class which is implemented for generic usage - not type-safe

public class Box {

    private Object item;

    public Object getItem() {
        return item;
    }

    public void setItem(Object item) {
        this.item = item;
    }
}

The Box class above can be used to store and retrieve any object (without a specific type). However, Java compiler can't help to ensure it is type-safe. It is the calling code's responsibility to make sure the type casting to the returned object is correct. The worst thing is we could only know the casting is incorrect during runtime, and this is not efficient and something that we really want to avoid from. The following code snippet can be compiled successfully but failed during runtime.

Box box = new Box();
box.setItem(1); // set integer object into box
String item = (String) box.getItem(); // ClassCastException error during Runtime!!

Generic type for generic usage - type-safe

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Box<T> {

    private T item;

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

The Box class above is a generic type. You could recognize it by the declaration of a type parameter (refer to item 3), <T> at line #1. This type parameter is effective within the class scope, hence the type of instance variable (item), the return type of getter method (getItem()) and method parameter type of setter method (setItem()) are all referring to the declared type parameter.

While the Box generic type serves the general usage purpose but more importantly, it is type-safe. Using the generic type correctly, Java compiler could help to detect any non-type-safe operation during compilation time, moreover, Java compiler takes care of the type casting. This greatly prevents ClassCastException being thrown during runtime. In the following code snippet, you get the compilation errors in any attempt to store object that besides Integer type.

Box<Integer> box = new Box<>();
box.setItem(1); // set integer object into box. OK
Integer item = box.getItem(); // get integer without mannual casting. OK

box.setItem("string"); // Compilation error!!
String item = box.getItem(); // Compilation error!!

2. Generic Method

Static or non-static method or even constructor that is designed for generic usage and type-safe by using Java Generic in their definition. Same as the generic type, a generic method has type parameter declaration <T> located before its method return type. This type parameter is only effective within the method scope.

public class Box {
    
    private Object item;
    
    // Generic method - Constructor.
    public <T> Box(T t) {
        this.item = t;
    }

    // Generic method - Non-static method.
    public <T> boolean compare(T t) {
        return t.equals(item);
    }

    // Generic method - Static method.
    public static <T> boolean compare(T t1, T t2) {
        return t1.equals(t2);
    }
    
    public static void main(String[] args) {
        Box box = new Box("item");
        System.out.println(box.compare("item")); 
        System.out.println(Box.compare("item1", "item2"));
    }
}

Type parameter in generic type and generic method are different

It may lead to confusion when a generic type also has the generic method in it, especially when their type parameters are named with the same text.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Box<T> { // Generic type
    
    private T item; // Type parameter for class
    
    // Non-generic method.
    public void put(T input) {
        item = input; // OK
    }

    // Generic method.
    public <T> void genericPut(T input) { // Type parameter for method
        item = input; // Compilation error! T#1 != T#2
    }
}

The generic type Box above has 2 setter methods. The highlight here is at line #12, the assignment of input to item in genericPut() method. Although input and item have the same named type parameter T, but this assignment causing the compilation error. Why? Box (generic type) and genericPut() (generic method) declared their type parameter respectively. Although they have the same name, but they actually working in different context. Java compiler treats them as 2 incompatible types hence the assignment is not valid.

Generic method is generic but method in generic type is not

Refer back to the same Box generic type above. Although put() method uses type parameter, but it is not generic. It works specifically for the type argument (refer to item 4) given to the Box generic type.

Box<String> box = new Box<>();
box.put("item"); // OK
box.put(1); // Compilation error! Incompatible type.

Once the String is defined as type argument for Box instance, the put() method can only accept String input. Other than that Java compiler will generate the compilation error.

Assume that the generic method genericPut() is valid (although it did not make sense). It can accept any type of object even though the Box instance is declared with String type argument. This behavior is what generic method have promised to have.

Box<String> box = new Box<>();
box.genericPut("string"); // OK
box.genericPut(1); // OK
box.genericPut(new Date()); // OK

3. Type Parameter

Also known as Type Variable or Formal Type Parameter. It is the intrinsic object type that used in the generic type and generic method definition. It is the placeholder which meant to be substituted by the type argument (actual object type). You should have seen how to declare and use type parameter in generic type (item 1) and generic method (item 2) above. By convention, type parameter names are single, uppercase letters which distinguish itself from variable names, in order to increase readability. Type parameter could appear in many forms as described below.

Bounded type parameter

We can specify the boundary for a type parameter to restrict the type that can be used as a type argument on it. Furthermore, boundary type provides the type information to Java Compiler and hence allow us to invoke the methods of the boundary class.

public class NumericBox<T extends Number> { // Bounded to Number class

    private T item;

    public void setItem(T item) { // accept any object that is a Number
        this.item = item;
    }
    
    public T getItem() {
        return item;
    }
    
    public double getItemAsDouble() {
        return item.doubleValue(); // call Number.doubleValue()
    }
    
    public int getItemAsInt() {
        return item.intValue(); // call Number.intValue()
    }
    
    public static void main(String[] args) {
        NumericBox<Integer> intBox = new NumericBox<>();
        intBox.setItem(10);
        System.out.println(intBox.getItemAsDouble()); // output: 10.0
        System.out.println(intBox.getItemAsInt()); // output: 10
        
        NumericBox<Double> douBox = new NumericBox<>();
        douBox.setItem(10.098);
        System.out.println(douBox.getItemAsDouble()); // output: 10.098
        System.out.println(douBox.getItemAsInt()); // 10
    }
}

Recursive type bound

In the case where boundary class is a generic type as well, then the type parameter could be used as a type argument for this boundary class.

public class Box<T extends Comparable<T>> {
    
    private T item;
    
    Box(T item) {
        this.item = item;
    }
    
    boolean check(T t) {
        return t.compareTo(item) == 0;
    }
}

The Box class above may look complicated but it is interesting. While the type parameter has set the boundary to any type that extends to Comparable, it also make sure that only the same type can be compared to itself. For example, given the Item class below,

class Item implements Comparable<Item> {

    private String name;
    
    Item(String name) {
        this.name = name;
    }
    
    @Override
    public int compareTo(Item otherItem) {
        return name.compareTo(otherItem.name);
    }
}

This Item class has the capability of comparing itself to other Item object. Therefore, it meets the boundary criteria defined by Box recursive type bound. The following code example is valid can be executed successfully.

Item ball = new Item("ball");
Item bell = new Item("bell");
Box<Item> box = new Box<>(ball);
System.out.println(box.check(bell)); // output: false
System.out.println(box.check(new Item("ball"))); // output: true

Multiple bounds

Type parameter supports multiple bounds. The more boundary you specify, the more specific of the type argument is allowed. However, there are a couple of rules to follow in specifying multiple bounds.
  • If the boundary is a Class, then it must be specified first for the type parameter.
  • Type parameter only takes one Class boundary.
  • Multiple Interfaces are allowed.
Given the Box generic type and all the classes and interfaces below:

// Generic type
public class Box<T extends Clazz & InterfaceA & InterfaceB> {   
}

class Clazz {
}
interface InterfaceA {
}
interface InterfaceB {
}

class Full extends Clazz implements InterfaceA, InterfaceB {
}
class ClazzOnly extends Clazz{
}
class InterfacesOnly implements InterfaceA, InterfaceB {
}

Only the Box declaration with Full type argument is valid. The other two are invalid.

new Box<Full>(); // OK

// Type argument ClazzOnly is not within bounds of type-variable T
new Box<ClazzOnly>(); // Compilation error!

// Type argument InterfacesOnly is not within bounds of type-variable T
new Box<InterfacesOnly>(); // Compilation error!

Multiple type parameters

By the way, we could have more than 1 type parameter in a generic definition.

// Generic type with multiple type parameters.
public class Pair<K, V> {
    ...
}

// Generic method with multiple type parameters.
public static <K, V, T> void test(K key, V value, T type) {
    ...
}

4. Type Argument

Also known as Actual Type Parameter. It is the actual object type that substitutes the type parameter declared and used in the generic type or generic method definition. Once the actual type for the generic definition is well-known, Java compiler is able to perform type-safe checking and auto casting based on the given type argument during compilation time.

A generic type variable that is declared with type argument is called Parameterized Type. It is type-safe guaranteed. A generic type variable that is declared without type argument is known as Raw Type. Raw type is not type-safe and Java compiler will generate warning during compilation.

public class Box<T> {
    
    private T item;
    
    void setItem(T item) {
        this.item = item;
    }
    
    public static void main(String[] args) {
        
        // Parameterized type.
        Box<String> box1 = new Box<>();
        box1.setItem("");
        box1.setItem(1); // Compilation error! Only String is allowed.
        
        // Raw type. Compiled with unchecked warning. Do not do this!
        Box box2 = new Box();
        box2.setItem("");
        box2.setItem(1); // Compiled success but not type-safe!
    }
}

The raw type is still allowed because Java need to backward support legacy code which written before Java Generic was introduced. We should never use raw type in our new implementation.

A type parameter of a generic type/method could be a type argument for another generic type/method. It sounds confusing but in fact, it is very common.

public class Box<T> { // 'T' is type parameter for Box class.

    // 'T' becomes type argument for List.
    private List<T> items = new ArrayList<>();
    
    public void setItem(T item) {
        this.items.add(item);
    }
    
    public T getItem(int index) {
        return this.items.get(index);
    }
    
    public static void main(String[] args) {
        Box<String> box = new Box<>();
        box.setItem("item");
        String item = box.getItem(0);
    }
}

Another example for the generic method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Example {
    
    // 'T' is type parameter for test() method.
    // 'T' becomes type argument for List method parameter.
    public static <T> T test(List<T> list) {
        return list.get(0);
    }
    
    public static void main(String[] args) {
        List<String> listOfString = new ArrayList<>();
        listOfString.add("item");
        Example.<String>test(listOfString); // OK
        
        // Compilation error! incompatible types.
        Example.<String>test(new ArrayList<Integer>());
    }
}

Do not be scared by the generic method invocation at line #12 and #15. In the code example above, I explicitly specify the 'String' type argument for both test() methods. Java compiler hence enforces only the list of String is allowed for method argument. In fact, we could get rid of this explicit specification of type argument for the generic method. If we do so, then Java compiler is able to infer the type argument based on the method argument object. Therefore, the generic methods invocations below are valid.

1
2
Example.test(listOfString); // OK
Example.test(new ArrayList<Integer>()); // OK

5. Wildcard

A type argument with unknown type. It appears as ? in coding. It is used when type is not a concern and can be remained unknown. Wildcard type argument could be used as an instance field, local variable or method parameter. However, unlike normal type argument, wildcard cannot be used as type argument for generic type object creation, generic method invocation or a supertype.

public class Box<T> {

    public static <T> void test(T t) {
    }
    
    public static void main(String[] args) {
        
        // Wildcard as type argument for generic object creation. 
        new Box<?>(); // Compilation error! Unexpected type.
        
        // Wildcard as type argument for generic method invocation.
        Box.<?>test(null); // Compilation error! Invalid script.
    }
}

// Wildcard as type argument for generic supertype
class Case extends Box<?> { // Compilation error! Unexpected type.
}

If wildcard ? in the code example above is changed to a type argument, let's say String. Then the codes become all valid and compile successfully.

Wildcard type is not equivalent to raw type

Although wildcard sounds like a raw type, but in fact, Java compiler will make sure wildcard type is type-safe. Consider the following static method countNonNullItems(). Its purpose is to count the items which is not null from a list. In this case, the type information is not necessary at all.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    public static int countNonNullItems(List<?> list) {
        // list.add(new Object()); // Compilation error.
        int count = 0;
        for (int i = 0; i < list.size(); i++) {
            Object obj = list.get(i);
            if (obj != null) {
                count++;
            }
        }
        return count;
    }

    public static void main(String[] args) {
        // countNonNullItems is able to take a list of any type.
        countNonNullItems(new ArrayList<String>());
        countNonNullItems(new ArrayList<Integer>());
        countNonNullItems(new ArrayList<Date>());
    }

There are 2 important points from the code example above. First, the List parameter of countNonNullItems() is type-safe. Other than getting Object from the list (line #5), we could never add any object into the list. Uncomment the line #2 will cause compilation error because Java compiler wants to prevent the list from holding a different type of object, which definitely break the type-safe of the list. This is the crucial difference compare to raw type List parameter.

The second point is, we gain the flexibility of using wildcard type as our method parameter. For example, List<String> parameter is restricted to only accept list of String, but List<?> could accept a list of any type (refer to line #15-17). This is something that programmer need to consider when designing the API.

Lower Bounded Wildcard

What? You still want to add object into a wildcard type list? Yes, you can by using lower bounded wildcard. Lower bounded wildcard tell the caller that "I have set the lower boundary to a certain Type, so I can only take the type that is the type or supertype of the lower boundary type." This also provides type information to Java compiler that, the wildcard type list allows storing any object that IS A type/subtype of lower boundary type.

    // Lower bounded type argument. List<? super Number> tell the caller
    // that only list of Number or supertype of Number is allowed.
    public static void makePUTFlexible(List<? super Number> parameter) {
        // Because the type given is at least a Number type hence,
        // the list could take any object that IS A Number.
        parameter.add(new Integer(1));
        parameter.add(new Double(1));
        parameter.add(new BigDecimal(1));
        
        // String does not extends Number.
        //parameter.add(""); // Compilation error. 
    }
    
    public static void main(String[] args) {
        List<Object> listOfObj = new ArrayList<>();
        listOfObj.add(new Object());
        makePUTFlexible(listOfObj); // OK
        
        List<Number> listOfNum = new ArrayList<>();
        listOfNum.add(new Integer(1));
        listOfNum.add(new Double(0.999));
        makePUTFlexible(listOfNum); // OK
        
        // Integer IS A Number but at the lower level in the class hierarchy.
        //makePUTFlexible(new ArrayList<Integer>()); // Compilation error. 
    }

Well, it is very tempting to put the following line in the makePUTFlexible() method.

Number item = (Number) parameter.get(0);

One might think that this casting is safe because the list parameter can take any object that IS A Number. Think twice, caller could pass list of Number or supertype of Number as per coded in the main() method. We will get ClassCastException for the code line above if we pass list of Object to the makePUTFlexible() method. In short, lower bounded wildcard gives the flexibility of putting object to a generic type.

Upper Bounded Wildcard

What? You want Number casting code line above working? Yes, you can by using upper bounded wildcard. Upper bounded wildcard tell the caller that "I have set the upper boundary to a certain Type, so I can only take the type that is the type or subtype of the low boundary type." This again provides type information to Java compiler that, the wildcard type list could only hold object that IS A type/subtype of upper boundary type.

    // Upper bounded type argument. List<? extends Number> tell the caller 
    // that only list of Number or subtype of Number is allowed.
    public static void makeGETFlexible(List<? extends Number> parameter) {
        Number num = parameter.get(0); // OK
        
        // Compilation error: Because the given type could be any subtype of Number,
        // hence adding operations as below are prohibited.
        //parameter.add(new Integer(1));
        //parameter.add(new Double(1));
        //parameter.add(new Object());
        
        // But null is ok because null is unknown type.
        parameter.add(null);
    }
    
    public static void main(String[] args) {
        makeGETFlexible(new ArrayList<Number>());
        makeGETFlexible(new ArrayList<Integer>());
        
        // Number extends Object and at the higher level in the class hierarchy.
        //makeGETFlexible(new ArrayList<Object>()); // Compilation error. 
    }

Using upper bounded wildcard, on the other hand, prohibit us to adding object into the list. This is because the type of list parameter could be any subtype of Number, which could break the type-safe of the list. However, adding null is allowed because it has no type. In short, upper bounded wildcard gives the flexibility is retrieving object from a generic type.


References:
Book: Effective Java 2nd Edition  - Joshua Bloch
Book: Java Generics and Collections - Maurice Naftalin & Philip Wadler
https://docs.oracle.com/javase/tutorial/java/generics/index.html



No comments: