Generics are used to parameterize types. A piece of code can be written generically enough so that we can fit a variety of types in it. Being a strongly type-safe language, Java requires to specify types in all aspects of its code, methods, fields, interfaces, etc.
However, generics is one of the tricky ways to make it possible to use type parameterization so that one particular portion or entirely of a class or their subclass can support a variety of types if an entity such as class, interface, or method that operates on a parameterized type is said to be a generic entity.
Benefits of Generics
Type safety
Using generics helps us define a type to consume in and produce from, where it is used. This means we can be sure of adding a specific type of data in a Collection type for example (List
Type erasure
Type erasure means, all the additional information included while using Java Generics, is removed from bytecode while generated. In bytecode, it will be old java syntax prior to Java 5.
Alternatively, we can say, we will be enforced to use a type in compile-time, but the types are discarded at runtime.
Where we can use Generics
- Using Generics with wildcards
- Unbounded wildcards
- Bounded wildcards
- Upper bounded wildcards
- Lower bounded wildcards
- Arrays with Generic types
- Using Generic types as parameters of a class or an interface.
- Using Generic types with method or constructor definition.
Components of Generics
- <> – Diamond operator – we put a generic type inside it.
- T – known as a Generic type. We can actually use any character or word instead of T here. usually used as
- ? – known as a wildcard. We use it like
- extends – Used to set Upper Bound of Generic type. This means we can not use any parent class of this upper-bounded class.
- super – Used to set Lower Bound of Generic type. We cannot use any child class of this lower bounded class.
Let’s start our today’s discussion to explain using Generics with wildcards.
Using Generics with wildcards
We have talked about the wildcard above. Wildcard represents an unknown type. The followings are examples of wildcard parameterized generics.
Collection List Comparator
Wildcard can be used in the following situations:
- To define the type for a generic containing parameter
void addAll(List objects) {}
- To define the type for a generic containing field or local variable
Set numList = Set.of(3, 100_000_000_000_000L, 2.5F, 2.7);
- To define an unknown type for a generic containing return type. But it is better to make the type-specific.
ResponseEntity getAll() { // // code goes here // }
Unbounded wildcard parameterized type
A generic type that does not contain any boundary of the upper or lower limit only contains wildcard as type.
ArrayList list = new ArrayList(); //or ArrayList list = new ArrayList (); //or ArrayList list = new ArrayList ();
Bounded wildcard parameterized type
Bounded wildcards have 2 types – upper-bounded and lower bounded. These bounds put some restrictions on the range of classes that can be used as generic types. We usually achieve the upper bound limit by extends and the lower bound limit by super keyword.
Upper bounded wildcard
Let’s assume, we have a method, that takes a parameter of List. The list items can have different instances of a class, that has multiple child classes. Now, we don’t know, at which point of time which child class instance list will be passed through the method parameter. In such a case, we can simply set the upper-bounded wildcard.
Listints = Arrays.asList(1,2,3,4,5); System.out.println(sum(ints)); // and the sum method private static Number sum (List numbers){ double s = 0.0; for (Number n : numbers) s += n.doubleValue(); return s; }
Lower bounded wildcard
When we want to set a lower limit of a class hierarchy, and we don’t want to add any instance of any class below to that hierarchy, we use super keyword to make this lower bounded wildcard. This applies for a method parameter.
To demonstrate, let us create 3 classes – Fruit, Apple and AsianApple
class Fruit { @Override public String toString() { return "Any fruit!!"; } } class Apple extends Fruit { @Override public String toString() { return "This is an Apple !!"; } } class AsianApple extends Apple { @Override public String toString() { return "This is an AsianApple !!"; } }
We will also define a method that receives a List as a parameter.
public static void printApples(List apples) { System.out.println(apples); }
Now observe the following example. When we pass a list of AsianApple in the method printApples(…), it gives us compile-time error. But when we try to pass a list of Fruit , which is the super of Apple, it accepts it without any error.
The interesting thing to observe, we can add AsianApple to the 2nd list, and it is acceptable. But it is impossible to pass a list wholly of an unsupported type.
Listbasket1 = new ArrayList<>(); basket1.add(new AsianApple()); printApples(basket1); // Compilation error List basket2 = new ArrayList<>(); basket2.add(new AsianApple()); basket2.add(new Apple()); basket2.add(new Fruit()); printApples(basket2);
Now, let’s see some limitations and exceptional cases of extends and super.
Limitations and exceptional cases
- We can create a list like List and instantiate the list. But when we try to add elements in it, it will give us compilation error.
Set appleSet = new HashSet<>(); appleSet.add(new Apple()); // Compilation error appleSet.add(new AsianApple()); // Compilation error
The reason is that we actually don’t need to mention extends If we define a list of Apple, it simply can accept any child of Apple type.
- However, we can define something like the following when we define it in one go.
List basket = List.of(new Apple(), new AsianApple()); // Because we are populating all objects first, // then assigning the list in the left side object. // So the above code satisfies the below valid condition List basket = new ArrayList