Generics is a form of polymorphism that allow classes to be treated as variables. It creates more robust code, as errors become compile-time instead of runtime, which are easier to debug.
Generics:
- promote strong compiler type-checking instead of weak programmer defined Boolean checks
- eliminate (or at least, reduce) typecasting, especially upcasting and downcasting.
Uses (in Java)
Sorting
A good use case for generics is polymorphic sorting.
Imagine we need to implement a compareTo()
method from a Comparable
interface. This would be the case when we have a sorting function and need to sort objects of a given class, for example, the Circle
class below:
The ‘normal’ way is to check if the other object is a Circle, and then also check if it has the same radius.
interface Comparable {
//This uses Object as the type, because every class inherits from the Object class
public int compareTo(Object other);
}
---
class Circle implements Comparable {
private double radius;
private double[2] center
@Override
public int compareTo(Object other) {
//First need to make sure other object is a Circle
if (other instanceof Circle) {
Circle c = (Circle) other; //Downcasting Object to Circle;
if (other.radius > this.radius) {
return 1;
} else if (other.radius == this.radius) {
return 0;
} else {
return -1;
}
} else {
// If we used 0,1,-1, it would be thought that the object is smaller
//Actually, they shouldn't even be compared, so we return -2
return -2;
}
}
}
There is a few problems with this method:
- We define a
-2
to be an ‘error code’, when the otherObject
is not of classCircle
. However, this is both unscalable and confusing. If we have many checks we need to do, we need more numbers, and then we also need to account for each number - This ‘error code’ must be caught in some way, meaning if we don’t account for it properly, we’ll be doing illogical comparisons with an Object and a Circle.
We can instead use Generics:
interface Comparable<T> {
//This uses Object as the type, because every class inherits from the Object class
public int compareTo(T other);
}
class Circle implements Comparable<CircleT> {
private double radius = 0.0;
private double[2] center
@Override
public int compareTo(CircleT other) {
if (other.radius > this.radius) {
return 1;
} else if (other.radius == this.radius) {
return 0;
} else {
return -1;
}
}
}
What has happened above is that when Circle
implements Comparable<T>
, it renames the <T>
Type Parameter with <CircleT>
. This does not change anything. This is just to make things simpler for the developer, who understands that when a Circle object is created, it needs to include <Circle>
. We could just as easily set it to T.
Now we no longer need to do any casting, nor do we need to add an additional error check! However, when we run this code by creating a few Circles, we need to now specify the type parameter.
Circle<Circle> c1 = new Circle<Circle>();
//See [Type Parameter](Type%20Parameter.md) for type inference, which is what happens here.
Circle<Circle> c2 = new Circle<>();
Object o1 = new Object();
c1.compareTo(c2); //Returns 0 (radii are same)
c1.compareTo(o1); //Compiletime error
For Each Looping
https://docs.oracle.com/javase/1.5.0/docs/guide/language/foreach.html #TODO