Learn Java Generics to make code more stable by detecting bugs at compile time
Developers prefer compile-time errors to run-time errors.
Generics are a type of generic programming; these features were added to the Java programming language in version J2SE 5.0. Generics provide an abstraction over types.
They were intended to enhance Java’s type system by allowing a type or method to operate on objects of various types while providing compile-time type safety. Now, the compiler can check the type correctness of the program during compilation.
Bugs are a fact of life in software engineering, but some are easier to detect than others. There are two basic types of bugs and errors:
- Compile-time errors — They are easy to detect since the application will not compile. By looking at the compiler’s error messages, you can determine what the problem is. Compile-time errors can be corrected easily.
- Run time errors — Run-time errors pose a greater threat. The problem doesn’t always manifest immediately, and it may appear at a point in the program that has nothing to do with the actual cause.
Why use Generics?
- Improved type checks at compile time — Generics increase the stability of your code and make most bugs and errors detectable at compile time instead of at run-time.
- Typecasting can be eliminated.
- Generics allow types (classes and interfaces) to be used as parameters when defining classes, interfaces, and methods.
- Using generic algorithms and methods, we can reuse the same code with different inputs.
If we don’t use generics, we have to overload methods to make them work when the data type changes even though the algorithm remains the same.
We must typecast explicitly if we don’t use generics otherwise a run-time error will occur. The JVM must test typecasting at run-time. Let’s see this with an example.
Exception in thread "main" java.lang.ClassCastException: class java.lang.Double cannot be cast to class java.lang.Integer (java.lang.Double and java.lang.Integer are in module java.base of loader 'bootstrap')
When used with Generics, we get the compile-time error instead of the run-time error.
Type mismatch: cannot convert from Double to Integer
Multiple Generic Types
Multiple generic types can also be used, such as those in HashMap.
Type in diamond operator <T> or <K, V> denotes that method is generic.
Bounded Generic Types
In parameterized types, we sometimes want to restrict the types that can be used as type arguments. That’s why we have bounded type parameters. You can invoke methods defined in the bounds of bounded types.
Let’s construct a method that can add 2 numbers be it an integer, float, or double with <T extends Number>.
When a compiler performs type inference, it looks at each method invocation and its corresponding declaration. It is necessary to determine the type arguments, such as generic type T, that make the invocation applicable.
Subtyping is a fundamental principle of object-oriented programming. A type is a subtype of another if it extends or implements it. Whenever one type is a subtype of another, the second type is a supertype of the first.
Integer is a subtype of Number
ArrayList<E> is a subtype of List<E>
List<E> is a subtype of Collection<E>
Since Integer is a subtype of Number but List<Integer> is not a subtype of List<Number> despite the fact an Integer is a Number, this is the reason why we need to consider wildcards.
A List<Integer> is a subtype of List<?>
Upper Bounded Wildcards
We may want to use wildcards with only subtypes so child classes, for example:
List<? extends T>
This can accept a list of any subclass of T. So we can read items from List<? extends T> but cannot insert items into List<? extends T> because we cannot guarantee what the list is really containing as any subclass can be inserted. The only thing we can do for sure is to read the items.
Lower Bounded Wildcards
We may want to use wildcards with supertypes so parent class. This is useful when we want to insert items into a generic data structure or collection.
List<? super T>
This can accept a list of any superclass of T. We cannot read items from List<? super T> but just objects because we cannot guarantee what the list is really containing but we can insert items into a List<? super T>.