Generics are way to propagate types from when a value is created to when the value is used. Historically, Java did not have a way to specify generics, by backward compatibility, you can still create a code without generics
Let suppose we have a code somewhere to extract credentials from a config file Note: this code generates two warnings, that we will discuss later.
record Pair(Object first, Object second) { }
List getCredentials() {
List list = new ArrayList();
list.add(new Pair("login", "admin"));
list.add(new Pair("password", "password"));
return list;
}
System.out.println(getCredentials());
And in another code, we want to use the method getCredential()
, we may write
a code like this
List list = getCredentials();
String value = (String) list.get(0);
This code will compile but fail at runtime, it throws a ClassCastException
because in getCredential()
, we are creating a list of pairs but in the code
above, we try to extract the first value from the list as a String
Here, we have lost the fact that the list returned by getCredential()
is a list of pairs.
Generics allows to propagate the type of values stored inside a class or
more generally, allows to declare relations between types
We have said above that the code generate warnings, it's because java.util.List is declared as a generics class in the JDK API. So the compiler let you use List as type if you interact with a code written before generics were introduced to Java (2004) but emits a warning saying you should not declare it that way.
There are two kinds of generics
- parameterized class
- parameterized method
A parameterized class is a class that declares type variables (variable that contains a type) and use them whenever we can use a type.
The type variables are declared after the name of the class.
record Pair<F, S>(F first, S second) { }
When used, as a user of the generics you have to specify the type of each type variable (here F is String and S is Integer) so pair.first() which is typed as F will return a String and pair.second() which is typed as S will return an int.
var pair = new Pair<String, Integer>("port", 8080);
String first = pair.first();
int second = pair.second();
If you don't understand why in between the '<' and '>', there is a Integer here and not an int Don't worry, it's explained in next chapter
So the idea of a generics class is to specify the type arguments <String, Integer>
when you use it and the compiler will propagate the types.
You may have notice that two different instances may have different type arguments
var pair1 = new Pair<String, Integer>("port", 8080);
var pair2 = new Pair<String, String>("captain amarica", "shield");
So the type variable (F and S) are not available inside a static method of Pair.
A static method is called on the class Pair.hello()
, not on an instance.
record Pair<F, S>(F first, S second) {
static void hello() {
// can not access F and S here !
}
}
Like a class, a method can be parameterized, by declaring the type variables
in between <
and >
before the return type
So instead of
Object chooseOne(Object o1, Object o2) {
var random = ThreadLocalRandom.current();
return random.nextBoolean()? o1: o2;
}
/*
String s = chooseOne("day", "night");
*/
We can write
<T> T chooseOne(T o1, T o2) {
var random = ThreadLocalRandom.current();
return random.nextBoolean()? o1: o2;
}
String s = chooseOne("day", "night");
System.out.println(s);
So, we can now rewrite getCredentials()
, to correctly type it
List<Pair<String, String>> getCredentials() {
List<Pair<String, String>> list = new ArrayList<Pair<String, String>>();
list.add(new Pair<String, String>("login", "admin"));
list.add(new Pair<String, String>("password", "password"));
return list;
}
System.out.println(getCredentials());
but it's quite verbose, so in Java, we have a mechanism called inference to let the compiler try to guess itself the type arguments instead of having to specify them by hand
The keyword var
ask the compiler to find the type of the left of =
from the type of the right of =
.
So instead of
List<Pair<String, String>> list = new ArrayList<Pair<String, String>>();
System.out.println(list);
using var
we get
var list = new ArrayList<Pair<String, String>>();
System.out.println(list);
You can ask the compiler to find the type using the left type and the arguments
using the diamond syntax <>
.
So instead of
Pair<String, String> pair = new Pair<String, String>("login", "admin");
using the diamond syntax <>
Pair<String, String> pair = new Pair<>("login", "admin");
System.out.println(pair);
The left type can be also found when you do a return
Pair<String, String> getOnePair() {
return new Pair<>("login", "admin");
}
System.out.println(getOnePair());
or using the type of the parameter of the method
var list = new ArrayList<Pair<String, String>>();
list.add(new Pair<>("login", "admin"));
You may wonder what if we are using var
and the diamond <>
at the same time
When the inference doesn't known, it using Object
var objectList = new ArrayList<>(); // this is a list of Object
Type arguments of a parameterized method are inferred by default and we have to use a special syntax if we want to specify the type arguments
That's why when we have
class Utils {
static <T> T chooseOne(T o1, T o2) {
var random = ThreadLocalRandom.current();
return random.nextBoolean()? o1: o2;
}
}
we can write
System.out.println(Utils.chooseOne("foo", "bar"));
and if we want to specify the type arguments, you have to
specify them in between <
and >
, after the .
and
before the name of the method
System.out.println(Utils.<String>chooseOne("foo", "bar"));
Types without the '<' '>', raw types in Java speak, are still supported to interact with old codes so you may by mistake forget the '<' '>' and have the declaration to compile. But it will be nasty when trying to use such type.
The for-loop below doesn't compile because StringList is an AbstractList so a List of Object and not a List
class StringList extends AbstractList { // should be AbstractList<String>
public int size() {
return 5;
}
public String get(int index) {
Objects.checkIndex(index, 5);
return "" + index;
}
}
for(String s: new StringList());
getCredentials()
can be simplified to
List<Pair<String, String>> getCredentials() {
var list = new ArrayList<Pair<String, String>>();
list.add(new Pair<>("login", "admin"));
list.add(new Pair<>("password", "password"));
return list;
}
var list = getCredentials();
/*
String value = (String) list.get(0);
*/
And the last line (commented) that was throwing a ClassCastException now does not compile thank to the use of generics.
By default a type variable like T
acts as Object, i.e.
you can call on T only the public methods of java.lang.Object.
you can defines a more precise bound reusing the keyword extends
Note: extends
in this context mean subtype not inherits from.
<T extends Comparable<T>> T min(T o1, T o2) {
return (o1.compareTo(o2) < 0)? o1: o2;
}
System.out.println(min("day", "night"));
Because the bound of T is an object, you can not declare a List ! But you can declare a List instead, see the next chapter for that !