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 likeObject
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
abstractclass 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());
No comments:
Post a Comment