Jump to content

Generics in Java

From Alfawiki
Java Generics
IndustryProgramming language feature
Founded2004; 22 years ago (2004)
Key people
Gilad Bracha, Martin Odersky, David Stoutamire, Philip Wadler
ProductsGeneric programming capability in Java

Generics represent a capability within generic programming that was incorporated into the Java programming language beginning with J2SE 5.0 in 2004. They enhance Java's type system by allowing types or methods to function with multiple object types while maintaining type safety during compilation. Because this safety is enforced at compile time, parametrically polymorphic functions are not implemented in the Java virtual machine, as runtime type enforcement cannot be assured.

The Java collections framework leverages generics to define the specific types of elements contained within collections, thereby improving type safety and minimizing errors during execution.

In 1998, Gilad Bracha, Martin Odersky, David Stoutamire, and Philip Wadler created Generic Java, an extension that introduced support for generic types. Their contributions significantly influenced Java’s eventual integration of generics along with the introduction of wildcards.

Hierarchy and classification

According to the Java Language Specification:

  • A type variable is an identifier without qualification that is introduced through declarations in generic classes, interfaces, methods, or constructors.
  • A class is considered generic if it declares one or more type variables, resulting in parameterized types that correspond to the same runtime class.
  • An interface qualifies as generic when it declares type variables; its parameterized forms represent the same interface at runtime.
  • A method is generic if it introduces type variables as formal parameters, analogous to those found in generic classes or interfaces.
  • A constructor may be independently declared as generic apart from its class, using formal type parameters similar to those in generic classes or interfaces.

Motivation

The following example demonstrates an issue arising in Java code without generics. It creates an Template:Javadoc:SE intended to hold objects of type Template:Javadoc:SE. After inserting a String, it attempts to cast this element to an Integer, which triggers a runtime exception.

<syntaxhighlight lang="Java"> final List v = new ArrayList(); v.add("test"); // Adding a String that cannot be cast to Integer final Integer i = (Integer) v.get(0); // Causes runtime error </syntaxhighlight>

Although this code compiles successfully, it results in a java.lang.ClassCastException during execution. By utilizing generics and specifying type parameters, such errors can be identified at compile time—this is the fundamental reason for adopting generics.

The rewritten version using generics:

<syntaxhighlight lang="Java"> final List<String> v = new ArrayList<String>(); v.add("test"); final Integer i = (Integer) v.get(0); // Compilation error detected </syntaxhighlight>

In this case, declaring the ArrayList with String as its type parameter removes the need for casting. The compiler flags the incompatibility between String and Integer, preventing runtime failures.

Extracts from the Template:Javadoc:SE and Template:Javadoc:SE interfaces:

<syntaxhighlight lang="Java"> interface List<E> {

   void add(E x);
   Iterator<E> iterator();

}

interface Iterator<E> {

   E next();
   boolean hasNext();

} </syntaxhighlight>

Generic class definitions

Here is a sample of a generic Java class that models key-value pairs within a map structure:

<syntaxhighlight lang="Java"> public class Entry<KeyType, ValueType> {

   private final KeyType key;
   private final ValueType value;
   public Entry(KeyType key, ValueType value) {  
       this.key = key;
       this.value = value;
   }
   public KeyType getKey() { return key; }
   public ValueType getValue() { return value; }
   public String toString() { 
       return "(" + key + ", " + value + ")";  
   }

} </syntaxhighlight>

Examples of how to use this class:

<syntaxhighlight lang="Java"> final Entry<String, String> grade = new Entry<>("Mike", "A"); final Entry<String, Integer> mark = new Entry<>("Mike", 100); System.out.println("grade: " + grade); System.out.println("mark: " + mark);

final Entry<Integer, Boolean> prime = new Entry<>(13, true); if (prime.getValue()) {

   System.out.println(prime.getKey() + " is prime.");

} else {

   System.out.println(prime.getKey() + " is not prime.");

} </syntaxhighlight>

Expected output:

grade: (Mike, A)
mark: (Mike, 100)
13 is prime.

Generic method definitions

Below is an example of a generic method that utilizes the generic class defined earlier: <syntaxhighlight lang="Java"> public static <Type> Entry<Type, Type> twice(Type value) {

   return new Entry<Type, Type>(value, value);

} </syntaxhighlight>

Omitting the initial <Type> declaration leads to a compilation failure because it declares the generic type parameter.

In many cases, the compiler can automatically deduce the type parameters from the method call:

<syntaxhighlight lang="Java"> final Entry<String, String> pair = Entry.twice("Hello"); </syntaxhighlight>

Alternatively, type parameters can be explicitly provided:

<syntaxhighlight lang="Java"> final Entry<String, String> pair = Entry.<String>twice("Hello"); </syntaxhighlight>

Note that primitive types cannot be used directly as type arguments; their wrapper classes must be employed instead:

<syntaxhighlight lang="Java"> final Entry<int, int> pair; // Compilation error. Use Integer instead. </syntaxhighlight>

Generic methods can also be created to work with variable arguments:

<syntaxhighlight lang="Java"> public <Type> Type[] toArray(Type... elements) {

   return elements;

} </syntaxhighlight>

Again, primitive types are disallowed here:

<syntaxhighlight lang="Java"> Integer[] array = toArray(1, 2, 3, 4, 5, 6); </syntaxhighlight>

Diamond operator

Starting with Java SE 7, the diamond operator (<>) enables type inference during object instantiation, allowing omission of explicit type arguments when they are clear from context. For instance:

<syntaxhighlight lang="Java"> final Entry<String, String> grade = new Entry<>("Mike", "A"); final Entry<String, Integer> mark = new Entry<>("Mike", 100); System.out.println("grade: " + grade); System.out.println("mark: " + mark);

final Entry<Integer, Boolean> prime = new Entry<>(13, true); if (prime.getValue()) System.out.println(prime.getKey() + " is prime."); else System.out.println(prime.getKey() + " is not prime."); </syntaxhighlight>

Type wildcards

Java supports the use of wildcards in generics denoted by "?", which can optionally include bounds. Wildcards provide greater flexibility by allowing unknown or variable type parameters in generic classes and methods.

Consider this example using a wildcard with a Collection<E>:

<syntaxhighlight lang="java"> final Collection<?> c = new ArrayList<String>(); c.add(new Object()); // compilation error c.add(null); // permitted </syntaxhighlight>

Since the precise element type is unspecified, adding any object other than null is prohibited.

Upper bounded wildcards specify that the unknown type extends a particular class. For example, List<? extends Number> can hold elements of any subclass of Number, such as Float. When retrieving elements from such a list, they are treated as Number. Adding only null is allowed.

Wildcards resolve the issue that parameterized types with different concrete types do not inherit from each other. For instance, although Integer inherits from Number, neither List<Integer> nor List<Number> is a subtype of the other. This prevents unsafe operations like inserting a non-Integer number into a list intended for Integers.

The following snippet demonstrates how allowing such subtyping would break type safety:

<syntaxhighlight lang="java"> final List<Integer> ints = new ArrayList<>(); ints.add(2); final List<Number> nums = ints; // invalid if List<Integer> were subtype of List<Number> nums.add(3.14); final Integer x = ints.get(1); // would incorrectly assign 3.14 to an Integer variable </syntaxhighlight>

Wildcards prevent these unsafe mutations by restricting additions:

<syntaxhighlight lang="java"> final List<? extends Number> nums = ints; // valid nums.add(3.14); // compile-time error nums.add(null); // allowed </syntaxhighlight>

Lower bounded wildcards use the super keyword and specify that the unknown type is a supertype of a given class. For example, List<? super Number> could represent either List<Number> or List<Object>. When reading from such collections, elements are treated as Object. Adding elements requires them to be instances of Number, its subclasses, or null.

The PECS principle—Producer Extends, Consumer Super—introduced by Joshua Bloch in Effective Java serves as a useful guideline for deciding when to apply bounded wildcards to improve API adaptability.

Generics in throws clause

Although exceptions themselves cannot be generic, it is possible to use generic type parameters within a throws clause:

<syntaxhighlight lang="Java"> public <T extends Throwable> void throwMeConditional(boolean conditional, T exception) throws T {

   if (conditional) {
       throw exception;
   }

} </syntaxhighlight>

Challenges arising from type erasure

Generic types undergo compile-time verification to ensure type correctness; however, during type erasure, all generic type details are stripped away. For example, a List<Integer> is converted to a raw List at runtime. Despite this, compile-time checks maintain type safety.

Due to type erasure, it is impossible to retrieve the specific type parameters at runtime. For example, an ArrayList instance cannot disclose whether it was parameterized with Integer or Float. Although some attempts are made to deduce types by examining elements, this method has significant limitations.

The following snippet illustrates this behavior by printing "Equal": <syntaxhighlight lang="java"> final List<Integer> li = new ArrayList<>(); final List<Float> lf = new ArrayList<>(); if (li.getClass() == lf.getClass()) { // true because of type erasure

   System.out.println("Equal");

} </syntaxhighlight>

Type erasure also restricts generic classes from extending Throwable, since the runtime cannot differentiate between various generic exceptions:

<syntaxhighlight lang="java"> public class GenericException<T> extends Exception </syntaxhighlight>

Consider the example: <syntaxhighlight lang="java"> try {

   throw new GenericException<Integer>();

} catch (GenericException<Integer> e) {

   System.err.println("Integer");

} catch (GenericException<String> e) {

   System.err.println("String");

} </syntaxhighlight>

This code is invalid because the runtime cannot resolve which catch block corresponds to the thrown exception.

Unlike C++ templates, Java generics compile into a single class or method version regardless of the specific type arguments. Since parameterized type information is not preserved at runtime but verified during compilation, creating an instance of a generic type parameter directly is prohibited:

<syntaxhighlight lang="java"> <T> T instantiateElementType(List<T> arg) {

    return new T(); // compilation error

} </syntaxhighlight>

Because only one generic class copy exists at runtime, static variables are shared among all instances irrespective of their type parameters. Consequently, static members cannot reference the generic type parameter.

Type erasure was introduced primarily to ensure backward compatibility with legacy Java code predating Java SE5.

Differences from Arrays

There are fundamental differences between arrays (both primitive and Object arrays) and Java generics, especially regarding variance and reification.

Covariance, contravariance and invariance

Arrays exhibit covariance, whereas generics are invariant. This invariance in generics prevents certain runtime errors by enforcing strict type safety during compilation.

For example, assigning an array to a variable compiles successfully because arrays support covariance. However, inserting a element into this array results in an at runtime. Generics avoid such issues by generating compile-time errors when incompatible types are used.

If a developer attempts to declare a and initialize it as an , the compiler will reject this due to generics’ invariance. This restriction helps avoid runtime failures and promotes correct typing practices. Starting with Java SE7, the diamond operator () simplifies generic instantiation syntax, such as .

Reification

Arrays in Java are reified, meaning they retain type information during execution. Conversely, Java generics undergo type erasure, making them non-reifiable: their type details are validated at compile-time but removed when the program runs.

As a result, generic types like or lose their specific type data at runtime, which impacts operations that depend on this information being present during execution.

Project on generics

Project Valhalla represents an experimental effort to improve Java’s generics and related language capabilities for upcoming versions, potentially beginning with Java 10. The main enhancements under consideration include:

See also

References