-
Notifications
You must be signed in to change notification settings - Fork 17
Java Generics Best Practices
In this article, we will discuss what are Java generics best practices we should follow, I referred these best practices from Effective Java book. Thanks to Joshua Bloch (Author of Effective Java) providing such great best practices and tips to enhance our knowledge.
As we know that from Java 5, generics have been a part of the language. Before generics, you had to cast every object you read from a collection. If someone accidentally inserted an object of the wrong type, casts could fail at runtime. With generics, you tell the compiler what types of objects are permitted in each collection. The compiler inserts cast for you automatically and tell you at compile time if you try to insert an object of the wrong type. This results in programs that are both safer and clearer, but these benefits, which are not limited to collections, come at a price. This chapter tells you how to maximize the benefits and minimize the complications.
If you are new to Generics then read our recommended Java Generics Guide.
- Don’t use raw types
- Eliminate unchecked warnings
- Prefer lists to arrays
- Favor generic types
- Favor generic methods
- Use bounded wildcards to increase API flexibility
- Combine generics and varargs judiciously
- Consider typesafe heterogeneous containers
Let's discuss each above generics best practices with explanation and examples.
A raw type is the name of a generic class or interface without any type arguments. Generic classes and interfaces are the ones who have one or more type parameter as generic, i.e. List
Each generic type defines a set of parameterized types List<String>
Raw type is the generic type definition without type parameters - List
Using raw types can lead to exceptions at runtime, so don’t use them. They are provided only for compatibility and interoperability with legacy code that predates the introduction of generics. As a quick review, Set is a parameterized type representing a set that can contain objects of any type, Set<?> is a wildcard type representing a set that can contain only objects of some unknown type, and Set is a raw type, which opts out of the generic type system. The first two are safe, and the last is not.
This program demonstrates why not to use raw types and what is a problem by using raw type. Please refer to the comments are self-descriptive.
public class RawGenericsExample {
private class Coin {
public Coin(){
}
}
private class Stamp {
public Stamp() {
}
}
// Example Raw Collection Type - Don't do this!
public final Collection stamps;
// Example of Improved Type Declaration
private final Collection<Stamp> myStamps;
public RawGenericsExample() {
stamps = new ArrayList();
myStamps = new ArrayList();
}
public void insertErrorDuringRuntime() {
// If you accidentally put a coin into this stamp collection, the insert will comile and run without error.
stamps.add(new Coin());
// However, the error will occur when retrieving the coin from the stamp collection
for(Iterator i = stamps.iterator(); i.hasNext();) {
Stamp s = (Stamp) i.next();
}
}
public void insertErrorDuringCompileTime() {
// Below error occurs during run time
// myStamps.add(new Coin());
// No need to cast in loop
for(Iterator<Stamp> i = myStamps.iterator(); i.hasNext();) {
Stamp s = i.next();
}
// No need to cast in loop
for(Stamp s : myStamps) {
}
}
// Valid instanceof operator with Raw Type Set
public void validInstanceOf() {
Set<String> s1 = new HashSet<String>();
if(s1 instanceof Set) {
}
// Legitimate use of raw type - instanceof operator
if(s1 instanceof Set) { // Raw type
Set<?> m = (Set<?>) s1; // Wildcard type
}
}
// Invalid instanceof operator with Parameterized Type Set<String>
public void invalidInstanceOf() {
Set<String> s1 = new HashSet<String>();
// The following is invalid
// if(s1 instanceof Set<String>) {
//
// }
}
public static void main(String[] args) {
RawGenericsExample exampleGenerics = new RawGenericsExample();
String s = "hey";
exampleGenerics.insertErrorDuringRuntime();
exampleGenerics.insertErrorDuringCompileTime();
}
}
You lose type safety if you use a raw type such as List, but not if you use a parameterized type such as _List _. When you see a ClassCastException, you have to search through the code looking for a method that caused the error.
You must use raw types in class literals. Example:
List.class, String[].class, and int.class // are all legal
List<String>.class and List<?>.class // are not legal
Let's understand the different terms we use in Generics with code examples. Parameter Type
public List<String> e1;
Actual type parameter
public String e2;
Generic Type
public List<E> e3;
Formal type parameter
public E e4;
Unbounded wildcard type
public List<?> e5;
Raw type
public List e6;
public class QuickGenericsReference<E> {
// Parameter Type
public List<String> e1;
// Actual type parameter
public String e2;
// Generic Type
public List<E> e3;
// Formal type parameter
public E e4;
// Unbounded wildcard type
// Generic types with wildcards only make sense for variables and method parameters because this allows
// greater freedom in what can be assigned/passed into them.
public List<?> e5;
// Raw type
public List e6;
}
When you program with generics, you'll see a lot of compiler warnings:
- unchecked cast warnings
- unchecked method invcation warnings
- unchecked generic array creation warnings
- unchecked conversion warnings
Eliminate every unchecked warning that you can. If you eliminate all warnings, you are assured that the code is TYPESAFE. Means you won't get ClassCastException at runtime.
If you can't eliminate a warning and you can prove that the code that provoked the warning is typesafe, then (and only then) supress the warning with an @SupressWarnings("unchecked") annotation. Highly recommended to use SupressWarnings annotation on smallest scope possible.
Every time you use a @SuppressWarnings("unchecked") annotation, add a comment saying why it is safe to do so.This will help others understand the code, and more importantly, it will decrease the odds that someone will modify the code so as to make the computation unsafe.
Suppressing warnings on using unchecked generic types operations:
@SuppressWarnings("unchecked")
void uncheckedGenerics() {
List words = new ArrayList();
words.add("hello"); // this causes unchecked warning
}
If the above code is compiled without @SuppressWarnings("unchecked") annotation, the compiler will complain like this: XYZ.java uses unchecked or unsafe operations.
Suppressing warnings on using deprecated APIs:
@SuppressWarnings("deprecation")
public void showDialog() {
JDialog dialog = new JDialog();
dialog.show(); // this is a deprecated method
}
Without the @SuppressWarnings("deprecation") annotation, the compiler will issue this warning:
XYZ.java uses or overrides a deprecated API.
Suppress all unchecked and deprecation warnings for all code inside the Foo class below:
@SuppressWarnings({"unchecked", "deprecation"})
class Foo {
// code that may issue unchecked and deprecation warnings
}
Suppressing warnings on local variable declaration:
void foo(List inputList) {
@SuppressWarnings("unchecked")
List<String> list = (List<String>) inputList; // unsafe cast
}
In summary, unchecked warnings are important. Don’t ignore them. Every unchecked warning represents the potential for a ClassCastException at runtime. Do your best to eliminate these warnings. If you can’t eliminate an unchecked warning and you can prove that the code that provoked it is typesafe, suppress the warning with a @SuppressWarnings("unchecked") annotation in the narrowest possible scope. Record the rationale for your decision to suppress the warning in a comment.
Arrays are covariant, meaning that if Sub is a subtype of Super, then the array type Sub[] is a subtype of Super[].
Generics are invariant, meaning List is neither a subtype nor a supertype of List.
Arrays are reified, meaning arrays know and enforce element types at runtime.
Generics are implemented by erasure, meaning they enforce type constraints only at compile time and discard (or erase) their element type information at runtime.
It is illegal to create an array of a genertic type, parameterized type, or a type parameter. Example:
List<E>[] // illegal
new List<String>[] // illegal
new E[] // illegal
This example demonstrates why generic array creation is illegal. Please refer the comments are self-descriptive.
public void illegalGenericArray() {
List<String>[] stringLists = new List<String>[1]; // illegal
List<Integer> intList = Arrays.asList(42); // legal
Object[] objects = stringLists; // legal
objects[0] = intList; // legal
String s = stringLists[0].get(0); // ClassCastException at runtime
}
In summary, arrays and generics have very different type rules. Arrays are covariant and reified; generics are invariant and erased. As a consequence, arrays provide runtime type safety but not compile-time type safety, and vice versa for generics. As a rule, arrays and generics don’t mix well. If you find yourself mixing them and getting compile-time errors or warnings, your first impulse should be to replace the arrays with lists.
It is generally not too difficult to parameterize your declarations and make use of the generic types and methods provided by the JDK. Writing your own generic types is a bit more difficult, let's understand how to create with and without generic stack implementation.
Consider the following simple stack implementation.
public class ExampleNonGeneric {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public ExampleNonGeneric() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
size = 0;
}
public void push(Object o) {
ensureCapacity();
elements[size] = o;
size++;
}
public Object pop() {
if(size > 0) {
size--;
Object o = elements[size];
elements[size] = null;
return o;
}
return null;
}
public boolean isEmpty() {
if(size == 0)
return true;
else
return false;
}
/**
* Checks current capacity.
* If num of elements used is equal to the length of the array, make a new one with greater capacity.
*/
private void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
public String print() {
String s = "";
for(Object o : elements) {
s = s + (Integer) o + ", ";
}
return s;
}
public static void main(String[] args) {
ExampleNonGeneric e = new ExampleNonGeneric();
e.push(23);
e.push(55);
System.out.println(e.print());
e.pop();
System.out.println(e.print());
e.pop();
System.out.println(e.print());
}
}
Disadvantages from above progam:
- You have to cast objects that are popped off the stack
- Those casts might fail at runtime
Let's parameterize above non-generic stack implementatin with generic. Replace all the uses of the type Object with the appropriate type parameter.
public class ExampleGeneric<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// The elements array will contain only E instances from push(E)
// This is sufficient to ensure type safety but the runtime type of the array won't be E[]; it will always be Object[]
@SuppressWarnings("unchecked")
public ExampleGeneric() {
// Can't create array of generic type E
// elements = new E[];
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
size = 0;
}
public void push(E o) {
ensureCapacity();
elements[size] = o;
size++;
}
public E pop() {
if(size > 0) {
size--;
E o = elements[size];
elements[size] = null;
return o;
}
return null;
}
public boolean isEmpty() {
if(size == 0)
return true;
else
return false;
}
/**
* Checks current capacity.
* If num of elements used is equal to the length of the array, make a new one with greater capacity.
*/
private void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
public String print() {
String s = "";
for(E e : elements) {
s = s + (Integer) e + ", ";
}
return s;
}
public static void main(String[] args) {
ExampleGeneric<Integer> e = new ExampleGeneric<Integer>();
e.push(23);
e.push(55);
System.out.println(e.print());
e.pop();
System.out.println(e.print());
e.pop();
System.out.println(e.print());
}
}
In summary, generic types are safer and easier to use than types that require casts in client code. When you design new types, make sure that they can be used without such casts. This will often mean making the types generic. If you have any existing types that should be generic but aren’t, generify them. This will make life easier for new users of these types without breaking existing clients.
In previous best practices we saw how to create generic types so now we will see how to create generic methods and it's usage. Static utility methods that operate on parameterized types are usually generic. All of the “algorithm” methods in Collections (such as binarySearch and sort) are generic.
Writing generic methods is similar to writing generic types. Consider this deficient method, which returns the union of two sets:
public static Set incorrectUnion(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
Above method is deficient method, to fix these warnings and make the method typesafe, modify its declaration to declare a type parameter representing the element type for the three sets (the two arguments and the return value) and use this type parameter throughout the method.
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
Read more about generic methods on In summary, generic methods, like generic types, are safer and easier to use than methods requiring their clients to put explicit casts on input parameters and return values. Like types, you should make sure that your methods can be used without casts, which often means making them generic. And like types, you should generify existing methods whose use requires casts. This makes life easier for new users without breaking existing clients.
Parameterized types are invariant. List is neither subtype nor a supertype of List. There's a special kind of parameterized type called a BOUNDED WILDCARD TYPE.
- Iterable of some subtype of E' can be written as Iterable<? extends E>
- Collection of some supertype of E' can be written as Collection<? super E>
Bounded wildcards examples:
// pushAll with wildcard type - good!
// this is typesafe!
public void pushAll2(Iterable<? extends E> src) {
for(E e : src){
push(e);
}
}
public void popAll2(Collection<? super E> destination) {
while(!isEmpty()){
destination.add(pop());
}
}
public static <E> Set<E> union2(Set<? extends E> s1, Set<? extends E> s2) {
Set<E> result = new HashSet<E>();
result.addAll(s1);
result.addAll(s2);
return result;
}
Using wildcard types in your APIs, while tricky, makes the APIs far more flexible. If you write a library that will be widely used, the proper use of wildcard types should be considered mandatory. Remember the basic rule: producer-extends, consumer-super (PECS). Also remember that all comparables and comparators are consumers.
varargs and generics do not interact well because the varargs facility is a leaky abstraction built atop arrays, and arrays have different type rules from generics. Though generic varargs parameters are not typesafe, they are legal. If you choose to write a method with a generic (or parameterized) varargs parameter, first ensure that the method is typesafe, and then annotate it with @SafeVarargs so it is not unpleasant to use.
// Safe method with a generic varargs parameter
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
The rule for deciding when to use the SafeVarargs annotation is simple: Use @SafeVarargs on every method with a varargs parameter of a generic or parameterized type, so its users won’t be burdened by needless and confusing compiler warnings. This implies that you should never write unsafe varargs methods like dangerous or toArray.
The normal use of generics, exemplified by the collections APIs, restricts you to a fixed number of type parameters per container. You can get around this restriction by placing the type parameter on the key rather than the container. You can use Class objects as keys for such typesafe heterogeneous containers. A Class object used in this fashion is called a type token. You can also use a custom key type. For example, you could have a DatabaseRow type representing a database row (the container), and a generic type Column as its key.