Guide to Understanding Generics in Java

Introduction

Java is a type-safe programming language. Type safety ensures a layer of validity and robustness in a programming language. It is a key part of Java's security to ensure that operations done on an object are only performed if the type of the object supports it.

Type safety dramatically reduces the number of programming errors that might occur during runtime, involving all kinds of errors linked to type mismatches. Instead, these types of errors are caught during compile-time which is much better than catching errors during runtime, allowing developers to have less unexpected and unplanned trips to the good old debugger.

Type safety is also interchangeably called strong typing.

Java Generics is a solution designed to reinforce the type safety that Java was designed to have. Generics allow types to be parameterized onto methods and classes and introduces a new layer of abstraction for formal parameters. This will be explained in detail later on.

There are many advantages of using generics in Java. Implementing generics into your code can greatly improve its overall quality by preventing unprecedented runtime errors involving data types and typecasting.

This guide will demonstrate the declaration, implementation, use-cases, and benefits of generics in Java.

Why Use Generics?

To provide context as to how generics reinforce strong typing and prevent runtime errors involving typecasting, let's take a look at a code snippet.

Let's say you want to store a bunch of String variables in a list. Coding this without using generics would look like this:

List stringList = new ArrayList();
stringList.add("Apple");

This code won't trigger any compile-time errors but most IDEs will warn you that the List that you've initialized is of a raw type and should be parameterized with a generic.

IDE-s warn you of problems that can occur if you don't parameterize a list with a type. One is being able to add elements of any data type to the list. Lists will, by default, accept any Object type, which includes every single one of its subtypes:

List stringList = new ArrayList();
stringList.add("Apple");
stringList.add(1);

Adding two or more different types within the same collection violates the rules of type safety. This code will successfully compile but this definitely will cause a multitude of problems.

For example, what happens if we try to loop through the list? Let's use an enhanced for loop:

for (String string : stringList) {
    System.out.println(string);
}

We'll be greeted with a:

Main.java:9: error: incompatible types: Object cannot be converted to String
        for (String string : stringList) {

In fact, this isn't because we've put a String and Integer together. If we changed the example around and added two Strings:

List stringList = new ArrayList();
stringList.add("Apple");
stringList.add("Orange");
        
for (String string : stringList) {
    System.out.println(string);
}

We'd still be greeted with:

Main.java:9: error: incompatible types: Object cannot be converted to String
        for (String string : stringList) {

This is because without any parametrization, the List only deals with Objects. You can technically circumvent this by using an Object in the enhanced for-loop:

List stringList = new ArrayList();
stringList.add("Apple");
stringList.add(1);
        
for (Object object : stringList) {
    System.out.println(object);
}

Which would print out:

Apple
1

However, this is very much against intuition and isn't a real fix. This is just avoiding the underlying design problem in an unsustainable way.

Another problem is the need to typecast whenever you access and assign elements within a list without generics. To assign new reference variables to the elements of the list, we must typecast them, since the get() method returns Objects:

String str = (String) stringList.get(0);
Integer num = (Integer) stringList.get(1);

In this case, how will you be able to determine the type of each element during runtime, so you know which type to cast it to? There aren't many options and the ones at your disposal complicate things way out of proportion, like using try/catch blocks to try and cast elements into some predefined types.

Also, if you fail to cast the list element during assignment, it will display an error like this:

Type mismatch: cannot convert from Object to Integer

In OOP, explicit casting should be avoided as much as possible because it isn't a reliable solution for OOP-related problems.

Lastly, because the List class is a subtype of Collection, it should have access to iterators using the Iterator object, the iterator() method, and for-each loops. If a collection is declared without generics, then you definitely won't be able to use any of these iterators, in a reasonable manner.

This is why Java Generics came to be, and why they're an integral part of the Java ecosystem. Let's take a look at how to declare generic classes, and rewrite this example to utilize generics and avoid the issues we've just seen.

Generic Classes and Objects

Let's declare a class with a generic type. To specify a parameter type on a class or an object, we use the angle bracket symbols <> beside its name and assign a type for it inside the brackets. The syntax of declaring a generic class looks like this:

public class Thing<T> { 
    private T val;
     
    public Thing(T val) { this.val = val;}
    public T getVal() { return this.val; }
  
    public <T> void printVal(T val) {
      System.out.println("Generic Type" + val.getClass().getName());
    }
}

Note: Generic types can NOT be assigned primitive data types such as int, char, long, double, or float. If you want to assign these data types, then use their wrapper classes instead.

The letter T inside the angle brackets is called a type parameter. By convention, type parameters are single lettered (A-Z) and uppercase. Some other common type parameter names used are K (Key), V (Value), E (Element), and N (Number).

Although you can, in theory, assign any variable name to a type parameter that follows Java's variable conventions, it is with good reason to follow the typical type parameter convention to differentiate a normal variable from a type parameter.

The val is of a generic type. It can be a String, an Integer, or another object. Given the generic class Thing declared above, let's instantiate the class as a few different objects, of different types:

public void callThing() {
    // Three implementations of the generic class Thing with 3 different data types
    Thing<Integer> thing1 = new Thing<>(1); 
    Thing<String> thing2 = new Thing<>("String thing"); 
    Thing<Double> thing3 = new Thing<>(3.5);
  
    System.out.println(thing1.getVal() + " " + thing2.getVal() + " " + thing3.getVal());
}

Notice how we're not specifying the parameter type before the constructor calls. Java infers the type of the object during initialization so you won't need to retype it during the initialization. In this case, the type is already inferred from the variable declaration. This behavior is called type inference. If we inherited this class, in a class such as SubThing, we also wouldn't need to explicitly set the type when instantiating it as a Thing, since it'd infer the type from its parent class.

You can specify it in both places, but it's just redundant:

Thing<Integer> thing1 = new Thing<Integer>(1); 
Thing<String> thing2 = new Thing<String>("String thing"); 
Thing<Double> thing3 = new Thing<Double>(3.5);

If we run the code, it'll result in:

1 String thing 3.5

Using generics allows type-safe abstraction without having to use type casting which is much riskier in the long run.

In a similar vein, the List constructor accepts a generic type:

public interface List<E> extends Collection<E> {
// ...
}

In our previous examples, we haven't specified a type, resulting in the List being a List of Objects. Now, let's rewrite the example from before:

List<String> stringList = new ArrayList<>();
stringList.add("Apple");
stringList.add("Orange");
        
for (String string : stringList) {
    System.out.println(string);
}

This results in:

Apple
Orange

Works like a charm! Again, we don't need to specify the type in the ArrayList() call, since it infers the type from the List<String> definition. The only case in which you'll have to specify the type after the constructor call is if you're taking advantage of the local variable type inference feature of Java 10+:

var stringList = new ArrayList<String>();
stringList.add("Apple");
stringList.add("Orange");

This time around, since we're using the var keyword, which isn't type-safe itself, the ArrayList<>() call can't infer the type, and it'll simply default to an Object type if we don't specify it ourselves.

Generic Methods

Java supports method declarations with generic parameters and return types. Generic methods are declared exactly like normal methods but have the angle brackets notation before the return type.

Let's declare a simple generic method that accepts 3 parameters, appends them in a list, and return it:

public static <E> List<E> zipTogether(E element1, E element2, E element3) {
    List<E> list = new ArrayList<>();
    list.addAll(Arrays.asList(element1, element2, element3));
    return list;
}

Now, we can run this as:

System.out.println(zipTogether(1, 2, 3));

Which results in:

[1, 2, 3]

But also, we can throw in other types:

System.out.println(zipTogether("Zeus", "Athens", "Hades"));

Which results in:

[Zeus, Athens, Hades]

Multiple types of parameters are also supported for objects and methods. If a method uses more than one type parameter, you can provide a list of all of them inside the diamond operator and separate each parameter using commas:

// Methods with void return types are also compatible with generic methods
public static <T, K, V> void printValues(T val1, K val2, V val3) {
    System.out.println(val1 + " " + val2 + " " + val3);
}

Here, you can get creative with what you pass in. Following the conventions, we'll pass in a type, key and value:

printValues(new Thing("Employee"), 125, "David");

Which results in:

Thing{val=Employee} 125 David

Though, keep in mind that generic type parameters, that can be inferred, don't need to be declared in the generic declaration before the return type. To demonstrate, let's create another method that accepts 2 variables - a generic Map and a List that can exclusively containString values:

public <K, V> void sampleMethod(Map<K, V> map, List<String> lst) {
    // ...
}
Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

Here, the K and V generic types are mapped to the Map<K, V> since they're inferred types. On the other hand, since the List<String> can only accept strings, there's no need to add the generic type to the <K, V> list.

We've now covered generic classes, objects, and methods with one or more type parameters. What if we want to limit the extent of abstraction that a type parameter has? This limitation can be implemented using parameter binding.

Bounded Type Parameters

Parameter Binding allows the type parameter to be limited to an object and its subclasses. This allows you to enforce certain classes and their subtypes, while still having the flexibility and abstraction of using generic type parameters.

To specify that a type parameter is bounded, we simply use the extends keyword on the type parameter - <N extends Number>. This makes sure that the type parameter N we supply to a class or method is of type Number.

Let's declare a class, called InvoiceDetail, which accepts a type parameter, and make sure that that type parameter is of type Number. This way, the generic types we can use while instantiating the class are limited to numbers and floating-point decimals, as Number is the superclass of all classes involving integers, including the wrapper classes and primitive data types:

class InvoiceDetail<N extends Number> {
    private String invoiceName;
    private N amount;
    private N discount;
  
    // Getters, setters, constructors...
}

Here, extends can mean two things - extends, in the case of classes, and implements in the case of interfaces. Since Number is an abstract class, it's used in the context of extending that class.

By extending the type parameter N as a Number subclass, the instantiation of amount and discount are now limited to Number and its subtypes. Trying to set them to any other type will trigger a compile-time error.

Let's try to erroneously assign String values, instead of a Number type:

InvoiceDetail<String> invoice = new InvoiceDetail<>("Invoice Name", "50.99", ".10");

Since String isn't a subtype of Number, the compiler catches that and triggers an error:

Bound mismatch: The type String is not a valid substitute for the bounded parameter <N extends Number> of the type InvoiceDetail<N>

This is a great example of how using generics enforces type-safety.

Additionally, a single type parameter can extend multiple classes and interfaces by using the & operator for the subsequently extended classes:

public class SampleClass<E extends T1 & T2 & T3> {
    // ...
}

It's also worth noting that another great usage of bounded type parameters is in method declarations. For example, if you want to enforce that the types passed into a method conform to some interfaces, you can make sure that the type parameters extend a certain interface.

A classic example of this is enforcing that two types are Comparable, if you're comparing them in a method such as:

public static <T extends Comparable<T>> int compare(T t1, T t2) {
    return t1.compareTo(t2);
}

Here, using generics, we enforce that t1 and t2 are both Comparable, and that they can genuinely be compared with the compareTo() method. Knowing that Strings are comparable, and override the compareTo() method, we can comfortably use them here:

System.out.println(compare("John", "Doe"));

The code results in:

6

However, if we tried using a non-Comparable type, such as Thing, which doesn't implement the Comparable interface:

System.out.println(compare(new Thing<String>("John"), new Thing<String>("Doe")));

Other than the IDE marking this line as erroneous, if we try running this code, it'll result in:

java: method compare in class Main cannot be applied to given types;
  required: T,T
  found:    Thing<java.lang.String>,Thing<java.lang.String>
  reason: inference variable T has incompatible bounds
    lower bounds: java.lang.Comparable<T>
    lower bounds: Thing<java.lang.String>

In this case, since Comparable is an interface, the extends keyword actually enforces that the interface is implemented by T, not extended.

Wildcards in Generics

Wildcards are used to symbolize any class type, and are denoted by ?. In general, you'll want to use wildcards when you have potential incompatibilities between different instantiations of a generic type. There are three types of wildcards: upper-bounded, lower-bounded and unbounded.

Choosing which approach you'll use is usually determined by the IN-OUT principle. The IN-OUT principle defines In-variables and Out-variables, which, in simpler terms, represent if a variable is used to provide data, or to serve in its output.

For example, a sendEmail(String body, String recipient) method has an In-variable body and Out-variable recipient. The body variable provides data on the body of the email you'd like to send, while the recipient variable provides the email address you'd like to send it to.

There are also mixed variables, which are used to both provide data, and then reference the result itself, in which case, you'll want to avoid using wildcards.

Generally speaking, you'll want to define In-variables with upper bounded wildcards, using the extends keyword and Out-variables with lower bounded wildcards, using the super keyword.

For In-variables that can be accessed through the method of an object, you should prefer unbounded wildcards.

Upper-Bounded Wildcards

Upper-bound wildcards are used to provide a generic type that limits a variable to a class or an interface and all its subtypes. The name, upper-bounded refers to the fact that you bound the variable to an upper type - and all of its subtypes.

In a sense, upper-bounded variables are more relaxed than lower-bounded variables, since they allow for more types. They're declared using the wildcard operator ? followed by the keyword extends and the supertype class or interface (the upper bound of their type):

<? extends SomeObject>

Here, extends, again, means extends classes and implements interfaces.

To recap, upper-bounded wildcards are typically used for objects that provide input to be consumed in-variables.

Note: There's a distinct difference between Class<Generic> and Class<? extends Generic>. The former allows only the Generic type to be used. In the latter, all subtypes of Generic are also valid.

Let's make an upper-type (Employee) and its subclass (Developer):

public abstract class Employee {
    private int id;
    private String name;
    // Constructor, getters, setters
}

And:

public class Developer extends Employee {
    private List<String> skillStack;

    // Constructor, getters and setters

    @Override
    public String toString() {
        return "Developer {" +
                "\nskillStack=" + skillStack +
                "\nname=" + super.getName() +
                "\nid=" + super.getId() +
                "\n}";
    }
}

Now, let's make a simple printInfo() method, that accepts an upper-bounded list of Employee objects:

public static void printInfo(List<? extends Employee> employeeList) {
    for (Employee e : employeeList) {
        System.out.println(e.toString());
    }
}

The List of employees we supply is upper-bounded to Employee, which means we can chuck in any Employee instance, as well as its subclasses, such as Developer:

List<Developer> devList = new ArrayList<>();

devList.add(new Developer(15, "David", new ArrayList<String>(List.of("Java", "Spring"))));
devList.add(new Developer(25, "Rayven", new ArrayList<String>(List.of("Java", "Spring"))));

printInfo(devList);

This results in:

Developer{
skillStack=[Java, Spring]
name=David
id=15
}
Developer{
skillStack=[Java, Spring]
name=Rayven
id=25
}

Lower-Bounded Wildcards

Lower-bounded wildcards are the opposite of upper-bounded. This allows a generic type to be restricted to a class or interface and all its supertypes. Here, the class or interface is the lower bound:

Declaring lower-bounded wildcards follows the same pattern as upper-bounded wildcards - a wildcard (?) followed by super and the supertype:

<? super SomeObject>

Based on the IN-OUT principle, lower-bounded wildcards are used for objects that are involved in the output of data. These objects are called out variables.

Let's revisit the email functionality from before and make a hierarchy of classes:

public class Email {
    private String email;
    // Constructor, getters, setters, toString()
}

Now, let's make a subclass for Email:

public class ValidEmail extends Email {
    // Constructor, getters, setters
}

We'll also want to have some utility class, such as MailSender to "send" emails and notify us of the results:

public class MailSender {
    public String sendMail(String body, Object recipient) {
        return "Email sent to: " + recipient.toString();
    }
}

Finally, let's write a method that accepts a body and recipients list and sends them the body, notifying us of the result:

public static String sendMail(String body, List<? super ValidEmail> recipients) {
    MailSender mailSender = new MailSender();
    StringBuilder sb = new StringBuilder();
    for (Object o : recipients) {
        String result = mailSender.sendMail(body, o);
        sb.append(result+"\n");
    }
    return sb.toString();
}

Here, we've used a lower-bounded generic type of ValidEmail, which extends Email. So, we're free to create Email instances, and chuck them into this method:

List<Email> recipients = new ArrayList<>(List.of(
        new Email("[email protected]"), 
        new Email("[email protected]")));
        
String result = sendMail("Hello World!", recipients);
System.out.println(result);

This results in:

Email sent to: Email{email='[email protected]'}
Email sent to: Email{email='[email protected]'}

Unbounded Wildcards

Unbounded wildcards are wildcards without any form of binding. Simply put, they are wildcards that extend every single class starting from the base Object class.

Unbounded wildcards are used when the Object class is the one being accessed or manipulated or if the method it's being used on does not access or manipulate using a type parameter. Otherwise, using unbounded wildcards will compromise the type safety of the method.

To declare an unbounded wildcard, simply use the question mark operator encapsulated within angle brackets <?>.

For example, we can have a List of any element:

public void print(List<?> elements) {
    for(Object element : elements) {
        System.out.println(element);
    }
}

System.out.println() accepts any object, so we're good to go here. If the method were to copy an existing list into a new list, then upper-bounded wildcards are more favorable.

Difference Between Bounded Wildcards and Bounded Type Parameters?

You may have noticed the sections for bounded wildcards and bounded type parameters are separated but more or less have the same definition, and on the surface level look like they're interchangeable:

<E extends Number>
<? extends Number>

So, what's the difference between these two approaches? There are several differences, in fact:

  • Bounded type parameters accept multiple extends using the & keyword while bounded wildcards only accept one single type to extend.
  • Bounded type parameters are only limited to upper-bounds. This means that you cannot use the super keyword on bounded type parameters.
  • Bounded wildcards can only be used during instantiation. They can not be used for declaration (e.g. class declarations and constructor calls. A few examples of invalid use of wildcards are:
    • class Example<? extends Object> {...}
    • GenericObj<?> = new GenericObj<?>()
    • GenericObj<? extends Object> = new GenericObj<? extends Object>()
  • Bounded wildcards should not be used as return types. This will not trigger any errors or exceptions but it forces unnecessary handling and typecasting which is completely against the type safety that generics achieves.
  • The operator ? can not be used as an actual parameter and can only be used as a generic parameter. For example:
    • public <?> void printDisplay(? var) {} will fail during compilation, while
    • public <E> void printDisplay(E var) compiles and runs successfully.

Benefits of Using Generics

Throughout the guide, we've covered the primary benefit of generics - to provide an additional layer of type safety for your program. Apart from that, generics offer many other benefits over code that doesn't use them.

  1. Runtime errors involving types and casting are caught during compile time. The reason why typecasting should be avoided is that the compiler does not recognize casting exceptions during compile time. When used correctly, generics completely avoids the use of typecasting and subsequently avoids all the runtime exceptions that it might trigger.
  2. Classes and methods are more reusable. With generics, classes and methods can be reused by different types without having to override methods or create a separate class.

Conclusion

Applying generics to your code will significantly improve code reusability, readability, and more importantly, type safety. In this guide, we've gone into what generics are, how you can apply them, the differences between approaches and when to choose which.

Last Updated: February 28th, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms