Generics in Java

Beknazar
5 min readDec 15, 2021

--

Generics let us abstract the type of the object. Generics were introduced in java 1.5.

Agenda for this article:

  • Generics Overview
  • Generic Methods
  • Bounds
  • Wildcards

Generics Overview

Let’s have an example

List list = new ArrayList();
list.add("apple");
list.add("kiwi");

In the above example, we used List without using generics.

When type is not specified in <>, the default type of the list is Object
It’s equivalent to List<Object>

Now let’s see with generics

List<String> list = new ArrayList<>();
list.add("apple");
list.add("kiwi");

new ArrayList<String>() and new ArrayList<>() are equivalent. Compiler can find out the type by context and the second one just shorter version. This future available from Java 1.7

<String> will define what data type our list should hold. So why do we need generics if it works without them just fine?

We want to write our code with fewer bugs. By using generics we can control what data type can be used by our list and if we use different data types we will get a compiler error.

Let’s see one more example without generics

  • We can see that it allowed StringBuilder to get into our list. Because its list of Object and StringBuilder is a child of the object so it’s totally fine for a compiler.
  • And during runtime when we get back items from the list trying to convert to String, we get our exception.

Now, let’s see an example with generics

  • We are getting compiler errors when we try to add StringBuilder to our list. The error during compilation is always better than during runtime.

Ok, so we used generics where it was already created for us. Now let’s create something by ourselves.

  • Bucket<E> we mention in the declaration of the class that we use generics and then inside our class we can use it as we have this type.
  • The actual type of <E> will be assigned by the client code.
  • In the main method take a look how our Bucket class can take String and then Integer.

Let’s clarify some definitions:

Bucket<E> when we use generics like this we refer to it as type parameter

Bucket<String> and when we actually pass the type for our generic we refer to it as type argument.

The most commonly used type parameter names are:
E — Element
K — Key
N — Number
T — Type
V — Value
S, U, V etc. — second, third, and fourth types

One more example

  • <E, S, T> that’s how we use multiple generics type parameters.
  • Interfaces can utilize generics as well.

Generic Methods

It’s a similar concept to generic classes and interfaces. We can have methods with their own type parameters that have a scope limited to the method where they are declared.

  • <E, S> booleanWe need to specify type parameters before method return type and then we can use them as method arguments
    isSame(E element, S secondElement)

Bounds

What if we want to use generics but we don’t want the client code to pass any object type. The bounds help us to restrict the types that can be passed as type arguments.

  • We use extends to specify outbounds. Every class, including the bound class itself, that extends/implements our bound class can be used as a type argument for generics.

Why not just have private Car car; in the Garage class instead of having private T car;. It’s actually the same reason here — fewer defects. It is better to have a compilation error than a runtime exception.

Garage g = new Garage();
g.putInside(new BMW());
Audi audi = (Audi)g.getOut(); // runtime exception
// ------Garage<Audi> g1 = new Garage<>();
g1.putInside(new BMW()); // does not compile here

Wildcards

We can use wildcards to lose the restrictions of generics. ? means every type.

Upper Bounded Wildcard

Let’s say we want to create a method that can accept List<Number>, List<Integer>, List<Double> we can use as the argument type of our method List<? extends Number>

  • Integer and Double both extend Number so we can pass a list of them safely in our method.
  • Upper bounded let us use an unknown type(?) or a subtype of it.

Lower Bounded Wildcard

It is opposed to upper bounded. Let’s say we have this structure

Audi -> Car -> Object
----------------------
public static void printList(List<? super Audi> list) {
// code here..
}
  • printList method now can accept List<Audi>, List<Car>, List<Object>

Unbounded Wildcard

Let’s say we want to create a method that is able to print out the list of any object.

public static void printList(List<Object> list) {
for (Object el : list) {
System.out.println(el);
}
}

Is it the correct implementation? No. For example, it will not work with List<String> , actually, it will work with List<Object> only.

Now, let’s see the correct implementation

public static void printList(List<?> list) {
for (Object el : list) {
System.out.println(el);
}
}
  • Now we can pass List with any type

Summary

Generics let us abstract the type of the object. By using generics we avoid runtime exceptions and catch issues with types during the compile time. There is a concept called Type Erasure that actually replaces all type parameters in generic types with their bounds or Object type.

Thank you for reading.

The resources used in this article:
1. Generics documentation from Oracle

--

--