Skip to content

Java Generics

Imagine needing to sort arrays of different types: Integers, Strings, custom objects. Without generics, you’d write separate sort methods for each type. This leads to repetitive code and potential errors.

Java Generics allow you to write code that operates on types specified as parameters. With a single generic method declaration, you can define a set of related methods, or with a single generic class declaration, a set of related types. This promotes code reusability and flexibility.

A key benefit of generics is enhanced compile-time type safety. The compiler checks if you’re using the correct types, catching potential ClassCastException errors before your program even runs.

Using generics, we can write one sort method that works on arrays of any type implementing the Comparable interface, ensuring elements can be ordered. This single method can then be invoked with Integer arrays, String arrays, etc.

A generic method is a method declared with type parameters. The compiler infers the actual types based on the arguments passed during invocation. Rules for defining generic methods:

  • The type parameter section, enclosed in angle brackets (< >), precedes the method’s return type (e.g., <T>).
  • It contains one or more type parameters (e.g., <K, V>), separated by commas. A type parameter (or type variable) acts as a placeholder for a specific type.
  • Type parameters can be used in the return type, parameter types, and local variables within the method.
  • The method body is implemented using the type parameters as placeholders for actual types.
  • Important: Type parameters can only represent reference types (like String, Integer, custom classes), not primitive types (int, double, char). Use wrapper classes (Integer, Double, Character) for primitives.

This example demonstrates a single generic method printArray used to print elements of arrays with different reference types:

import java.util.Arrays;
public class GenericMethodTest {
// Generic method printArray
public static <E> void printArray(E[] inputArray) {
// Display array elements using Streams API for conciseness
Arrays.stream(inputArray).forEach(element -> System.out.printf("%s ", element));
System.out.println();
}
public static void main(String[] args) { // Modern array declaration style
// Create arrays of Integer, Double, and Character
Integer[] intArray = {1, 2, 3, 4, 5};
Double[] doubleArray = {1.1, 2.2, 3.3, 4.4};
Character[] charArray = {'H', 'E', 'L', 'L', 'O'};
System.out.println("Array integerArray contains:");
printArray(intArray); // Pass an Integer array
System.out.println("\nArray doubleArray contains:");
printArray(doubleArray); // Pass a Double array
System.out.println("\nArray characterArray contains:");
printArray(charArray); // Pass a Character array
}
}

Output:

Array integerArray contains:
1 2 3 4 5
Array doubleArray contains:
1.1 2.2 3.3 4.4
Array characterArray contains:
H E L L O

Sometimes, you need to restrict the types that can be used as arguments for a type parameter. For instance, a method performing calculations might only accept types that are subtypes of Number. Bounded type parameters achieve this.

Declare a bounded type parameter using the extends keyword followed by the upper bound class or interface. Note: extends is used for both extending classes and implementing interfaces in this context. Example: <T extends Number> means T must be Number or a subclass.

This generic method finds the maximum of three objects. It uses a bounded type parameter <T extends Comparable<T>> to ensure the objects can be compared using the compareTo method.

public class MaximumTest {
// Determines the largest of three Comparable objects
// Uses bounded type parameter T which must implement Comparable<T>
public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
T max = x; // Assume x is initially the largest
if (y.compareTo(max) > 0) {
max = y; // y is the largest so far
}
if (z.compareTo(max) > 0) {
max = z; // z is the largest now
}
return max;
}
public static void main(String[] args) {
System.out.printf("Max of %d, %d and %d is %d%n%n",
3, 4, 5, maximum(3, 4, 5));
System.out.printf("Max of %.1f, %.1f and %.1f is %.1f%n%n",
6.6, 8.8, 7.7, maximum(6.6, 8.8, 7.7));
System.out.printf("Max of %s, %s and %s is %s%n", "pear",
"apple", "orange", maximum("pear", "apple", "orange"));
}
}

Output:

Max of 3, 4 and 5 is 5
Max of 6.6, 8.8 and 7.7 is 8.8
Max of pear, apple and orange is pear

Similar to generic methods, you can declare generic classes. The class name is followed by a type parameter section.

A generic class definition like class Box<T> creates a blueprint. When you create an instance, you provide the actual type, e.g., Box<Integer>. These are called parameterized types.

This example shows a simple generic class Box<T> that can hold an object of any type T:

public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
// Create Box instances with specific types using the diamond operator <>
// The compiler infers the type from the variable declaration.
Box<Integer> integerBox = new Box<>();
Box<String> stringBox = new Box<>();
// Using var for local variable type inference (Java 10+)
// var stringBox = new Box<String>();
// Autoboxing converts int 10 to Integer object
integerBox.set(10);
stringBox.set("Hello World");
// No cast needed due to generics
System.out.printf("Integer Value: %d%n", integerBox.get());
System.out.printf("String Value: %s%n", stringBox.get());
}
}

Output:

Integer Value: 10
String Value: Hello World

Wildcards (?) can provide more flexibility when working with generic types, especially in method parameters. Learn more about ? extends Type (upper bounded wildcard) and ? super Type (lower bounded wildcard) for advanced use cases.