Generics refers to the use of type parameters, which provide a way to define methods, classes, and interfaces that can operate with different data types. The benefits of generics are that they provide compile-time type safety and they eliminate the need for most type conversions.
Generic classes
Generic classes allow class members to use type parameters. Such a class is defined by adding a type parameter section after the class name, which contains a type parameter enclosed between angle brackets. The naming convention for type parameters is that they should consist of a single uppercase letter. Typically, the letter T for type is used. The following example defines a generic container class that can hold a single element of the generic type:
When an object of this generic class is instantiated, the type parameter must be replaced with an actual data type, such as Integer:
Alternatively, as of Java 7, a generic class can be instantiated with an empty set of type parameters. This type of instantiation is possible as long as the compiler can infer (determine) the type parameters from the context:
When an instance of MyBox is created, each type parameter in the class definition is replaced with the passed-in type argument. The object therefore behaves as a regular object, with a single field of the Integer type:
Notice that no casting is required when the stored value is set or retrieved from the box field. Furthermore, if the generic field is mistakenly assigned to or set to an incompatible type, the compiler will point that out:
Generic methods
A method can be made generic by declaring it with a type parameter section before the method’s return type. The type parameter can be used like any other type inside of the method. You can also use it for the method’s return type, in the throws clause and for its parameter types. The next example shows a generic class method that accepts a generic array parameter, the content of which is printed out:
The preceding shown class isn’t generic. Methods can be declared as generic, independently of whether or not the enclosing class or interface is generic. The same is true for constructors.
Calling generic methods
A generic method is typically invoked just as a regular (non-generic) method, without specifying the type argument:
In most cases, the Java compiler can infer the type argument of a generic method call, so it doesn’t have to be included. But if that’s not the case, then the type argument will need to be explicitly specified:
Generic interfaces
Interfaces that are declared with type parameters become generic interfaces. Generic interfaces have the same two purposes as regular interfaces: they’re either created to expose members of a class that will be used by other classes, or to force a class to implement a specific functionality. When a generic interface is implemented, the type argument must be specified. The generic interface can be implemented by both generic and non-generic classes:
Generic type parameters
The passed-in type argument for a generic can either be a class type, interface type, or another type parameter, but it can’t be a primitive type. Generics can have more than one type parameter defined, by adding more of them between the angle brackets in a comma-separated list. Bear in mind that each parameter within the brackets must be unique:
If a generic has multiple type parameters defined, the same number of type arguments need to be specified when the generic is used:
Generic variable usages
Generics are only a compile-time construct in Java. After the compiler has checked that the types used with generic variables are correct, it will then erase all type-parameter and argument information from the generic code and insert the appropriate casts instead. That means generics don’t provide any performance benefits over non-generic code, because of removed runtime casts, as they do in, for example, C#. It also means generic types can’t be used for anything that requires runtime information—such as creating new instances of generic types or using the instanceof operator with type parameters. Operations that are allowed include declaring variables of the generic type, assigning null to generic variables, and calling Object methods:
The process of removing type information from generic code is known as type erasure. For example, MyBox<Integer> would be reduced to MyBox, which is called the raw type. This step is performed in order to maintain backward-compatibility with code written before generics became part of the language in Java 5.
Bounded type parameters
It’s possible to apply compile-time enforced restrictions on the kinds of type parameters that a generic may be used with. These restrictions, called bounds, are specified within the type parameter section using the extends keyword. Type parameters can be bounded by either superclass or interface. For example, the following class B may only be instantiated with a type argument that’s either of the type A or has that class as a superclass:
The next example specifies an interface as the bound. This will restrict the type parameter to only those types that implement the specified interface or are of the interface type itself:
Multiple bounds can be applied to a type parameter by specifying them in a list separated by ampersands:
The ampersand acts as the separator instead of a comma because comma is already used for separating type parameters:
Aside from restricting the use of a generic to only certain parameter types, another reason for applying bounds is to increase the number of permitted method calls supported by the bounded type. An unbounded type may only call the Object methods. However, by applying a superclass or interface bound, the accessible members of that type will also become available:
Generics and Object
Before generics were introduced in Java 5, the Object type was used to create container classes that could store any type of objects. Now that generics are available, this use of the Object type as a universal container should be avoided. That’s because the compiler helps ensure that generics are type safe at compile-time, which can’t be done when using the Object type.
The collection classes in the Java library, among them ArrayList, have all been replaced with generic versions. Even so, any generic class can still be used as if it weren’t generic, simply by leaving out the type argument section. The default Object type will then be used as the type argument. That’s why the non-generic version of ArrayList is still allowed. Consider the following use of a non-generic ArrayList:
This String-to-Integer conversion will fail at runtime by throwing a ClassCastException. Had a generic ArrayList been used instead, the mistaken conversion would have been detected upon compilation, or immediately in an IDE such as Netbeans. This compile-time debugging feature is a major advantage with using generics over other coding approaches:
With the generic alternative, only the specified type argument will be allowed into the ArrayList collection. Additionally, values obtained from the collection don’t have to be cast to the correct type because the compiler takes care of that.