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 String
s:
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 Object
s. 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 Object
s:
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 Object
s. 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) {
// ...
}
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 String
s 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, whilepublic <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.
- 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.
- 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.