Suppose we want to create a pair data structure which can store elements of two specific types. In older versions of Java, there were only two options:
public class StringAndImagePair {
private final String a;
private final Image b;
//...
public String a(){
return a;
}
public Image b(){
return b;
}
}
This works, but it goes against the general goals we have of writing reusable code, because we have to create a new definition with almost identical code for every pair of types we want to support.
public class Pair(){
private final Object first;
private final Object second;
//...
public Object first(){
return first;
}
public Object second(){
return second;
}
}
This class can be reused for any two types we want, but this code is very dangerous, because it doesn’t effectively capture the types used in the pair.
public void iNeedAPairOfStrings(Pair pairOfStrings){
// Fails at runtime with ClassCastException!
final String first = (String) pairOfStrings.first();
final String second = (String) pairOfStrings.second();
//...
}
// oops!
iNeedAPairOfStrings(new Pair(someArray, someImage));
We must check types to make sure the Pair is used
safely.
public void iNeedAPairOfStrings(Pair pairOfStrings){
if(pairOfStrings.first() instanceof String &&
pairOFStrings.second() instanceof String){
//...
}
}
To resolve this issue, “Generics” were added to Java, which allows for types to be defined with type parameters which specify the concrete types referenced in generic data structure definitions.
public class Pair<T,U>{
//...
}
We define a generic Pair class which accepts
two type parameters. We can then instantiate different
types using this single definition.
Pair<String,Image> stringAndImage = new Pair<>(someString, someImage);
Pair<String,String> pairOfStrings = new Pair<>("hello","world");
public void iNeedAPairOfStrings(Pair<String,String> pair){
//...
}
Now…
// works fine!
iNeedAPairOfStrings(pairOfStrings);
// won't compile!
iNeedAPairOfStrings(stringAndImage);
public class Pair<T,U>{
private final T t;
private final U u;
public Pair(T t, U u){
this.t = t;
this.u = u;
}
public T first(){
return t;
}
public U second(){
return u;
}
}
public class Pair<T,U>{
//...
public <V> Pair<V,U> mapFirst(Function<T,V> f){
return new Pair<>(f.apply(t),u);
}
public <V> Pair<T,V> mapSecond(Function<U,V> f){
return new Pair<>(t,f.apply(u));
}
}
public class Pair<T,U>{
//...
public <V,W> Pair<V,W> map(Function<T,V> f, Function<U,W> g){
final V first = f.apply(t);
final W second = g.apply(u);
return new Pair<>(first,second);
}
}
What if we want to write a method that accepts a list of any kind of number?
You might think List<Number> would work for a List<Integer>, but it doesn’t!
List<Integer> is not a subtype of List<Number>.
To handle this, Java uses wildcards (?).
? extends Type: An upper-bounded wildcard.? super Type: A lower-bounded wildcard.Using a wildcard, we can now write a flexible sum method.
public double sum(List<? extends Number> list) {
double sum = 0.0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum;
}
A key thing to understand about generics in Java is type erasure.
After the compiler checks your code for type-safety,
it erases the generic type information. In the compiled
bytecode, your Pair<String, Image> is just a Pair,
and the fields are just Objects.
This was done for backward compatibility with pre-generics Java.
Because of type erasure, you can’t do certain things. For example, you can’t check the specific generic type at runtime:
Pair<String, String> pair = new Pair<>("a", "b");
// This will produce a compile-time warning!
if (pair instanceof Pair<String, String>) {
// ...
}
// You can only check the raw type
if (pair instanceof Pair) { // This is allowed
// ...
}
You also can’t create new instances of a generic type parameter, like new T().