Wednesday, October 5, 2011

Java Generics Part - 4


Bounding the Parameterized Types

Generics won't be complete if this section is not covered. It is all about bounding parametric types. Till now, we have seen parametric types operate on a single java type like Object or String. Now, let us see how the parametric types can be restricted by applying some constraints over them. For this to be illustrated, let us take a sample application called Animal Actions.
Animal Actions class performs various operations on a given Animal like: making them eat, sleep and run. The first constraint that we see here is that only an Animal object can be passed to the Animal Actions class. Let us represent the Animal class as follows.
Animal.java
package generics.bounds;
 
public abstract class Animal 
{
    // Some common functionalities here.
}
Note that the Animal class is declared as abstract, meaning that some other concrete class is going to extend this Animal class. Another restriction that we see in Animal Actions class is that they will make the Animals to sleep, eat and run. Since these are behaviors and we may give different representations for the same, let them be modeled as interfaces. Following code shows the interface design for the behaviors,
package generics.bounds;
 
interface Sleepable 
{
    public void sleep();
}
 
interface Runnable 
{
    public void run();
}
 
interface Eatable 
{
    public void eat();
}
Let us give implementation of the above behaviors for some animal, say Dog. The following code snippet is for the implementation of Dog which conforms to eating, sleeping and running behavior.
Dog.java
package generics.bounds;
 
public class Dog extends Animal implements Sleepable, Runnable, Eatable 
{
    public void sleep()
    {
        System.out.println("Dog is sleeping");
    }
 
    public void run()
    {
        System.out.println("Dog is running");
    }
 
    public void eat()
    {
        System.out.println("Dog is eating");
    }
}
Now, let us design the Animal Actions class. The restriction we have on Animal Actions class is that, we should operation on any type of object that is an Animal which can eat, sleep and run. Look into the following Animal Actions class,
AnimalActions.java
package generics.bounds;
 
public class AnimalActions<A extends Animal & Sleepable & Runnable & Eatable> 
{
    private A animal;
 
    public AnimalActions(A animal)
    {
        this.animal = animal;
    }
        
    public A getAnimal()
    {
        return animal;
    }
 
    public void setAnimal(A animal)
    {
        this.animal = animal;
    }
        
    public void doActions()
    {
        animal.sleep();
        animal.run();
        animal.eat();
    }   
}
The declaration of the parameterized class looks like the following,
public class AnimalActions<A extends Animal & Sleepable & Runnable & Eatable
 
Let us break down the pieces in the above declaration. The first trivial stuff that has to be noted is the declaration of the typed parameter A (which is for Animal). The next set of expressions are imposing restrictions on the typed parameter A. The phrase 'A extends Animal' tells that whatever type we pass for the substitution parameter must extend/implement the Animal class/interface. The type that comes after extends can either be an interface or a class. It is illegal to mention something like the following,
public class AnimalActions<A implements SomeInterface>
Only extends keyword is used for both class as well the interface and not the implements keyword. If we want the parametric type to confirm by more than one classes or interfaces, then every types should be separated by the symbol &. For example, in our case, we want some Animal to eat, sleep and run by implementing the Eatable, Sleepable and Runnable interface. So, we have declared something like AnimalActions<A extends Animal & Sleepable & Runnable & Eatable. To sum up things, the generic class declaration can be interpreted like this; it can be passed with any type that implements/extends Animal, Sleepable, Runnable and Eatable types.
Following code snippet makes use of the above Animal Actions class. In the below code, a new instance of Dog object is created and passed on to the constructor of the Animal Actions class. This is perfectly valid as the Dog class extends the Animal class and it also implements Sleepable, Eatable and Runnable interfaces. It then makes a call to AnimalActions.doActions(), thereby the execution gets directed towards Dog.sleep(), Dog.eat() and Dot.run().
AnimalActionsTest.java
package generics.bounds;
 
public class AnimalActionsTest 
{
    public static void main(String[] args)
    {
        AnimalActions<Dog> animal = new AnimalActions<Dog>(new Dog());
        animal.doActions();
    }
}

More on parametric bounds and wild-cards

The restriction on parametric types that is applied on class definition is also applicable to method definition. Let us move towards assignment now. Consider the following statement,
List<Animal> animals = new ArrayList<Animal>();
The above is a declaration of list that essentially tells to the compiler that it can hold Animal objects. So the following statements is perfectly valid.
animals.add(new Animal()); // Fine.
Assuming that Animal class is not abstract. Not only it is possible to add Animal objects but also any types that extends the Animal class. By having this rule in hand, it is perfectly possible to have the following statements,
animals.add(new Dog()); // This will work too.
animals.add(new Cat()); // This also.
Even though, it is possible to add any type that extends the Animal class, it is not possible to have the following statement.
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog()); dogs.add(new Dog());
               
animals = dogs; // This wont compile.
The compiler will warn you telling that it is not possible to convert List<Dog> to List<Animal>. Even a type-casting on the above statement doesn't work.
animals = (List<Animal>)dogs; // This won't work.
Since type-casting from one type to another type is a run-time operation and during run-time there is no such existence of the types List<Animal> or List<Dog> because of erasures. All the typed-parameters won't be available in the class-file and the above code doesn't work.

Upper Bound

The solution to this situation is the usage of wild-cards along with parametric bounding. Have a look over the following declaration.
List<? extends Animal> animals = new ArrayList<Animal>();
It tells that a list is being declared with type being anything (?) that is extending the Animal class. Though it looks very similar to the above declaration it has some differences. The first thing is that it is now possible for the animals reference to point to a list that is holding any sub-type of Animal objects.
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog()); dogs.add(new Dog());
        
animals = dogs;
One important difference is that, it is not possible to add elements to the animals list, though the only exception is adding null elements. This is called the Upper Bound for the animals list.

Lower Bound

Similar to Upper Bounds, we have Lower Bounds with the following syntax,
List<? super Dog> dogs = new ArrayList<Dog>();
This declaration tells that along with Dog type, all other sub-types of Dog is also allowed. Not any of the super-type of Dog can be added including java.lang.Object. So, if we have classes like GoodDog and BadDog, both extending the Dog class, then the following statements are legal.
dogs.add(new Dog());
dogs.add(new GoodDog());
dogs.add(new BadDog());

Conclusion

One of the major language and syntax change in Java 5.0 is Java Generics. Through Generics, it is now possible to have a programming model that operates on some parametric type and the same model can be re-used with different types. It is important to note that there will be only one class file for the whole Generic type, unlike other programming languages (in the name of templates in C++). Generics makes use of the concept of Erasure which erases all the Generic type information during the compilation process.

No comments: