With Java (and other programming language) it is possible to design and implement generic data structure and algorithms that works on multiple types. In this codelabs, we will see how to implements generic classes and methods, and not only use it, as you did until now (remember, Map for example).
"A generic type is a type with formal type parameters. A parameterized type is an instantiation of a generic type with actual type arguments." (Langer, 2015)
Generic can be find in Java since version 5.0. We can find examples of Generic types in many classes such as the Collections framework. For example, the class ArrayList is using the Generic type. It declare a generic type parameter "<E>". This generic type parameter will be used to determine which type of variable could be stored in the data structure. The "E" is not a reserved word but a type variable, and it is only by convention that we are using short and Uppercase name for the type parameters.
public class ArrayList<E> extends AbstractList<E>
To see the class implementation
Generic types are instantiated to form a parameterised types. The developper will provide the actual type arguments that replace the formal type parameters.
The actual type arguments will be provided between the angle brackets "<>". For example, in the following example, the ArrayList<String> is a parameterised type of ArrayList, and its actual type argument is String.
When the generic class is instantiate, the type that have been supplied (in this example String) will replace all the occurrences of the type variable E.
ArrayList<String> arrays = new ArrayList<>();
Using generic rather than non-generic classes or method will lead to a certain number of advantages such as:
The convention for the type parameters:
To declare a generic class, the developper needs to declare the number of parameter that this class could hold in the declaration of the class. In the following example, the class is declaring two parameters. The definition of the generic type is made after the name of the class and will be delimited by angle brackets. The list of identifiers is separated by comma.
The instance variables related to the list identifiers.
public class Pair<T,S>{
private T first;
private S second;
public Pair(T first, S second){
this.first = first;
this.second = second;
}
public T getFirst(){
return first;
}
public S getSecond(){
return second;
}
}
To demonstrate this generic class of a Pair of type X and Y, we will implement two classes, a class Tea and a class Cheese. We will create a small application that will pair a Tea with a Cheese.
public class Tea {
private String name;
public Tea (String name) {
this.name = name;
}
public String getName () {
return name;
}
}
And the class Cheese
public class Cheese {
private String name;
public Cheese (String name) {
this.name = name;
}
public String getName () {
return name;
}
}
Now the main class where we will use the class Pair to pair different Cheese and Tea together.
public class Main {
public static void main(String[] args) {
Tea english = new Tea("English Breakfast");
Tea darjeeling = new Tea("Darjeeling");
Cheese cheddar = new Cheese("Cheddar");
Cheese gruyere = new Cheese("Gruyere");
Pair<Tea,Cheese> teaCheeePair1 = new Pair<>(english,cheddar);
Pair<Tea,Cheese> teaCheeePair2 = new Pair<>(darjeeling,cheddar);
Pair<Tea,Cheese> teaCheeePair3 = new Pair<>(english,gruyere);
System.out.println(String.format("The first element %s and the second element %s",
teaCheeePair1.getFirst().getName(),teaCheeePair1.getSecond().getName()));
System.out.println(String.format("The first element %s and the second element %s",
teaCheeePair2.getFirst().getName(),teaCheeePair2.getSecond().getName()));
System.out.println(String.format("The first element %s and the second element %s",
teaCheeePair3.getFirst().getName(),teaCheeePair3.getSecond().getName()));
}
}
Now we will use this Pair class to pair Cheese with a specific Cartoon Mouse. Of course, we will need a class Mouse.
Mouse jerry = new Mouse("Jerry");
Mouse mickey = new Mouse("Mickey");
Pair<Cheese,Mouse> cheeseMousePair1 = new Pair<>(gruyere,jerry);
Pair<Cheese,Mouse> cheeseMousePair2 = new Pair<>(cheddar,mickey);
System.out.println(String.format("The first element %s and the second element %s",
cheeseMousePair1.getFirst().getName(),cheeseMousePair1.getSecond().getName()));
System.out.println(String.format("The first element %s and the second element %s",
cheeseMousePair2.getFirst().getName(),cheeseMousePair2.getSecond().getName()));
As we can see, the class Pair has two parameters, X and Y, these generic types are replaced by type arguments when we are declaring the class Pair. In the first example, we replace the generic types with the type Tea and Cheese, and for the second example, Cheese and Mouse.
Of course, if now we are trying to have:
Pair<Cheese,Mouse> cheeseMousePair3 = new Pair<>(english,jerry);
This will throws a compile time error, as we are declaring a Pair of Cheese, Mouse and we are trying to add variable of type Tea and Mouse in this Pair variable.
We can also define a method that will use type parameter; this is called a generic method. This type of method could belong to a generic class or not. The declaration will follow this shape. The type parameter section is delimited by angle brackets and appear between the modifiers and the method return type.
public <T> T getT()
If the method have parameter(s), the way to provide it is between the parenthesis (as any parameters for a method). For example, we will implement a method that will return the same type than the variable passed in the parameter. This method doesn't really do anything interesting other than returning the same value that the one passed as an argument. This method is only demonstrating how to implement a generic method.
public class GenericExampleMethod {
public <T> T getTheValue(T t){
return t;
}
}
To test this method.
GenericExampleMethod g = new GenericExampleMethod();
String a = "Hello";
String theValue = g.getTheValue(a);
System.out.println(theValue);
Tea theValue1 = g.getTheValue(english);
System.out.println(theValue1);
We can see that we can use this method to return whatever type of variable we will pass as an argument.
In this another example, we will define a method that will always return the middle of a List passed in parameters. If the size of the List is not bigger than 4 it will throw an exception saying that the list should be longer.
public <T> T getAlwaysTheMiddleOftheListFromAlistOf4(List<T> a) throws Exception {
if(a.size()<4) {
throw new Exception("this list is not long enough");
}else{
return a.get(a.size() / 2);
}
}
To test this new method we will test it on a list of Pair:
List<Pair> pairs = new ArrayList<>(List.of(cheeseMousePair1,cheeseMousePair2,teaCheeePair3,teaCheeePair3,teaCheeePair2));
Pair alwaysTheMiddleOftheListFromAlistOf4 = g.getAlwaysTheMiddleOftheListFromAlistOf4(pairs);
System.out.println(alwaysTheMiddleOftheListFromAlistOf4.getFirst();
Until now, the example are declaring unbounded type of generic. However, we can declare bounded type.
"A type parameter with one or more bounds. The bounds restrict the set of types that can be used as type arguments and give access to the methods defined by the bounds." (Langer, 2015)
The disadvantage of the unbounded type is that the compiler knows that it has to reserve a place holder of type T, but it doesn't know anything about this type T. It is alright for some implementation, but for others, we need to bound the type.
For example, if we want to use a generic method to compare objects in a List and return the smallest one, we need to be sure that the class we want to compare will have implemented the interface Comparable and by consequence the methode compareTo. We will bound the parameter type to the classes that implement Comparable. To be able to bound a type, we will use the keywords extends followed by the class or interface we want to bound the parameter with.
public <T extends Comparable<T>> T getSmaller(List<T> a) throws Exception {
T min = a.get(0) ;
for (T t : a) {
if (t.compareTo(min) < 0) {
min = t ;
}
}
return min ;
}
In the following example, only classes that implement the interface Comparable will be accepted as a parameter of this method. Let's try this with the following example. We know that String and Integer are implementing the interface Comparable by default.
String l = "Anne";
String t = "Anna";
String z = "Annie";
List<String> s = new ArrayList<>(List.of(l,t,z));
String smaller1 = g.getSmaller(s);
System.out.println(smaller1);
Integer b = 4;
Integer d = 10;
Integer db = 2;
List<Integer> integerList = new ArrayList<>(List.of(b,d,db));
Integer smaller = g.getSmaller(integerList);
System.out.println(smaller);
However, if now we try to compare Pair of object, we will see that we will have a compile time error.
Pair p = g.getSmaller(pairs);
The extends here is used in a generic way and can be applied for a class that extends a class or implements an interface. The bounded type could be multiple, however, with some constraints. It cannot have more than one class, and this class need to be declared first. It can have several interface, which will be declared after the class.
Inheritance of type parameters doesn't lead to inheritance of generic classes. For example, if we had a class Cat that inherits from a class Animal, if we have an List<Cat> and a List<Animal> the list of Cat has no relationships with the list of Animal even if both class (Animal and Cat) are related.
Imagining we have an application that will have a list of instances of Cat and a list of instances Animal.
List<Cat> cats = new ArrayList<>(List.of(new Cat("robert"), new Cat ("circe")));
List<Animal> Animals = new ArrayList<>(List.of(new Animal("animal1"),new Animal("animal2")));
Now, we would like a method that will read these lists. Cat inherits from Animal, so intuitively a lot of Developper will think that they could implement a method such as:
public void printElement(List<Animal> t){
for(Animal a:t){
System.out.println(a.getName());
}
}
You can test this method in your class test.
g.printElement(animals);
g.printElement(cats);
You will observe that it will not work, as a list of Cat is not a list of Animal.
Java does provide a trick for this, the wildcards. Java has 3 different type of wildcard:
We can use wildcards to resolve the problem from above. Instead of having two methods that will print the different list, we could use one of the wildcards. Every classes that extends Animal will be accepted.
public <T> void iterateList2(List<? extends Animal> a){
Iterator<? extends Animal> it = a.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}
You can test this code in your main:
g.printElementAll(cats);
g.printElementAll(animals);
Wildcards are useful in situations where no or only partial knowledge about the type argument of a parameterized type is required. In the previous example, a partial knowledge is required as the Iterator is taking as a parameter also a variable that extends the Animal to iterate through the List.
In generic code, the question mark (?), called the wildcard, represents an unknown type. The wildcard can be used in a variety of situations: as the type of a parameter, field, or local variable; sometimes as a return type (though it is better programming practice to be more specific). The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a supertype.
Langer, A., 2015. [online] Available at: http://www.angelikalanger.com/GenericsFAQ/ [Accessed 22 February 2021].
You can find the code for this example in https://git.cardiff.ac.uk/ASE_GROUP_2020/code_for_codelabs.git