Java Generics Best Practices

In this article, we will discuss what are Java generics best practices we should follow. I referred to 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 Java 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 article tells you how to maximize the benefits and minimize the complications.
If you are new to Java generics then read our recommended Java Generics Tutorial.
Let's discuss each above generics best practices with explanation and examples.

1. Don’t use raw types

A raw type is the name of a generic class or an interface without any type arguments.
For examples: List, Collection, Set etc
- Each generic type defines a set of parameterized types, example: List<String>
- A raw type is the generic type definition without type parameters, example: - 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.

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();
        
    }
}
Read more about Raw types on Oracle Java Tutorial.

Eliminate unchecked warnings

When you program with generics, you'll see a lot of compiler warnings:
  • unchecked cast warnings
  • unchecked method invocation 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.

SupressWarnings

If you can't eliminate a warning and you can prove that the code that provoked the warning is typesafe, then (and only then) suppress 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.
Let's look at few @SuppressWarnings() annotation examples.

SupressWarnings Examples

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
}
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.

Prefer Lists to Arrays

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 illegal to create an array of a generic 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
    }

Favor Generic Types

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.

Non-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 of the above program:
  • You have to cast objects that are popped off the stack
  • Those casts might fail at runtime

Generic Stack Implementation

Let's parameterize above non-generic stack implementation 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());
    }
}
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.
Let's learn how to create Generic Types in my separate article Java Generic Types with Example.

Favor generic methods

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 binary search 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 a 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;
 }
Let's learn how to create generic methods in my separate article How to Create Generic Methods with Examples?

Use Bounded Wildcards to Increase API Flexibility

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> - Upper bounded wildcards.
  • Collection of some supertype of E' can be written as Collection<? super E> - Lower bounded wildcards.
Examples of upper and lower 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;
 }
Read more about Java generics upper bounded wildcards on Java Generics Upper Bounded Wildcards Example
Read more about Java generics lower bounded wildcards on Java Generics Lower Bounded Wildcards Example

Combine Generics and varargs Judiciously

varargs and generics do not interact well because the varargs facility is a leaky abstraction built atop arrays, and arrays have a different type of 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.
Example:
// 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.

Consider typesafe heterogeneous containers

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.
Learn more about Generics on top tutorial Java Generics Tutorial
Please comment if you have any suggestions or feedback about this article would be appreciated.
Happy learning and keep coding !!!

Comments