Generics
Generic Types Recap
In week 1, you have learned about Generic types. Here we provide you with a short recap on generics:
A generic class is defined with the following format: class name<T1, T2, ..., Tn> { ... }
.
Let us look at an example from the Java Documentation on Generics:
/**
* Generic version of the Box class.
* @param <T> the type of the value being boxed
*/
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
If you want to make a Box object with the new
keyword, you have to pass a non-primitive type. A type variable can be any non-primitive type you specify: any class type, any interface type, any array type, or even another type variable.
For instance, you can use the Integer
type like this: Box<Integer> numberBox = new Box<Integer>();
. Any type T
in the class code, will now be replaced by the Integer
type.
You can also write the statement as follows: Box<Integer> numberBox = new Box<>();
. Note that the second pair of < and > does not contain the type anymore. However, the compiler will fill in the first used type automatically. The empty <> is called a diamond operator and can be used because the type of the variable, which is Box<Integer>
implies that call to the constructor should have Integer
as the generic type as well. The diamond operator can only be used if the compiler is able to figure out what type should be filled in between the <>
.
A significant portion of the Java data structures use type parameters, which enables them to handle different types of variables. ArrayList, for instance, receives a single type parameter, while the HashMap (which we will cover later this week) receives two.
List<String> strings = new ArrayList<>();
Map<String, String> keyValuePairs = new HashMap<>();
By convention, type parameter names are single, uppercase letters. This stands in sharp contrast to the variable naming conventions that you already know about, and with good reason: Without this convention, it would be difficult to tell the difference between a type variable and an ordinary class or interface name.
The most commonly used type parameter names are:
- E: Element (used extensively by the Java Collections Framework that is discussed this week)
- K: Key
- N: Number
- T: Type
- V: Value
Sometimes, other names such as U
or S
are also used, or even names such as E1
and E2
.
Generic Type Constraints
Note: you only need to understand the remainder of this section passively - there will not be any exam questions that test if you understand it. However, you will come accross generic type constraints on the slides and in the documentation and therefore it is important you are aware they exist.
Suppose we have two types, Person
and Student
, where Student
is a subtype of Person
.
This means that the following code would work without any problems (assuming some sensible
arguments come in the place of the ...
):
Student s = new Student(...);
Person p = s;
However, the following code would not work:
// This is not allowed, although intuitively it would make sense.
List<Student> students = new ArrayList<>();
List<Person> persons = students;
Logically, you might expect that this would work, since the students
list containts objects
that can be converted to Person
types. However, when it comes to generic types, the Java
compiler desires in this example that the types match exactly, and here it argues that a
Student
is not the exact same type as a Person
. The reason the compiler is so strict
about this is that generic types are used in more situations than just Collection
types,
and it may not be logical to allow this type of conversion in all cases.
A clear example of this can be seen when we consider the same conversion for Comparator
objects:
// This is not allowed, and intuitively that makes sense.
Comparator<Student> stdComp = new StudentComparator();
Comparator<Person> personComp = stdComp;
The problem here is that a Comparator<Student>
may use information that is specific to a
Student
, for example their student number, that might not be available for Person
objects
that are not a student. If that is the case, we should indeed be careful that we can not use
a Comparator
for a Student
objects as a Comparator
for Person
objects.
To overcome these issues, we can specify generic type constraints or bounded type parameters that allow us to still perform type conversion in the cases where we would expect this is possible. These come in three flavours: wildcard types, extends type constraints and super type constraints.
Wildcard Types
Sometimes we do not care what type of object is stored in a particular list. Suppose we want to create
a method that accepts a list of anything, which will check if it is at least of a certain size.
In such a case, can use a wildcard type, indicated by a ?
in a place where you would normally
expect a type variable. The following method gives such an example.
public static boolean sizeAtLeast(List<?> list, int minSize) {
return minSize <= list.size();
}
The main thing to note is that the size
method of the Collection
interface will return an
int
regardless of what type of objects are stored in the List
. The nice thing about specifying
list
with a wildcard type is that we can pass any type of List
into this method.
Extends type constraints
In the example where we tried to assign a List<Student>
to a variable of type List<Person>
,
intuitively you would expect this to be possible. With a small adjustment, this is in fact possible.
If we use an extends type constraint, we can indicate that we accept a List
of any type that is
equal to or a sub-type of a given type. This would give the following example:
// This will compile!
List<Student> students = new ArrayList<>();
List<? extends Person> persons = students;
Here we indicate that the variable persons does not have to hold a List
of things that are
exactly a person, but a List
of anything that is either a Person
or a subtype of it.
We can still use the variable persons
like we would use a variable of type List<Person>
,
for example, the following would function as expected:
for (Person p : persons) {
System.out.println(p.getName());
}
Super type constraints
In the example where we tried to assign a Comparator<Student>
to a variable of type
Comparator<Person>
, we argued that it makes sense that this is not allowed, since the
Comparator<Student>
could make use of information that is only specific to a Student
.
However, if we would have a Comparator<Person>
, we would expect that it can still be
used to compare Student
objects, since Students
will contain all the information
of a basic Person
(and likely more), because it is a subtype.
In a case like this, we could do this type of conversion by stating that we want a
Comparator
that can compare Student
objects, or any super-type of it. Similar to the
? extends E
notation, we can use the super
keyword.
In code this would look as follows:
List<Student> students = new ArrayList<>();
// Add some students
Comparator<Person> personComp = new PersonComparator();
Comparator<? super Student> studentComp = personComp;
Collections.sort(students, studentComp);
In this example, it makes sense that if we have a way to order persons, we can also use that method to order students. By using the right type constraints, we can inform the compiler that we want this to be possible.
Main take-away: these generic type constraints appear in some methods of the Collections framework to make sure that we can do things we would expect are possible, while the compiler can still be strict in checking if things make sense. Usually, when you work with methods where these constraints appear, you can just use them without even noticing they are there.
Test your knowledge
In this quiz, you can test your knowledge on the subjects covered in this chapter.
What is the advantage of using generic types?
What are disadvantages of using generic types?
What is the usage difference between generic types and interfaces?
What does the diamond operator do and what does it look like in code?
When do you use ?
as a type parameter and what is it called?