Java Generics: Create Flexible and Type-Safe Code

Discover Java Generics, a powerful feature that allows you to create methods and classes capable of handling different data types while ensuring type safety. Learn how to implement a single sort method that can work with various types of arrays, such as Integer and String, simplifying your code and enhancing reusability. Explore the benefits of using generics for cleaner, more efficient Java programming.



Java Generics

Imagine if we could create a single sort method that sorts elements in an Integer array, a String array, or any array of a type that supports ordering. This is possible with Java Generics.

What are Generics in Java?

Generics allow us to define classes, interfaces, and methods that can operate on different data types using a single declaration. This feature was introduced in Java 5.

Java Generics provide compile-time type safety, allowing programmers to catch type-related errors during compilation rather than at runtime. With Generics, we can write a generic method for sorting arrays of different types, such as Integer, Double, and String.

Advantages of Java Generics

  • No sacrifice of type safety.
  • No need for type casting.
  • Compile-time type checking.
  • Improved code reusability and performance.

Types of Java Generics

Generic Methods

You can define a single generic method that can be called with arguments of various types. The compiler manages each method call based on the types of the arguments passed.

Rules for Defining Generic Methods

  • Generic method declarations have a type parameter section enclosed in angle brackets (< and >) before the method's return type (e.g., <E>).
  • The type parameter section can contain one or more type parameters separated by commas.
  • Type parameters serve as placeholders for the types of the arguments passed to the generic method, known as actual type arguments.
  • Generic methods can only represent reference types, not primitive types (like int, double, and char).

Example of Java Generic Methods

The following example demonstrates how to print arrays of different types using a single generic method:

Syntax

public class GenericMethodTest {
// Generic method printArray
public static <E> void printArray(E[] inputArray) {
    // Display array elements
    for (E element : inputArray) {
        System.out.printf("%s ", element);
    }
    System.out.println();
}

public static void main(String args[]) {
    // 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 intArray contains:");
    printArray(intArray);   // Pass an Integer array

    System.out.println("\nArray doubleArray contains:");
    printArray(doubleArray);   // Pass a Double array

    System.out.println("\nArray charArray contains:");
    printArray(charArray);   // Pass a Character array
}
}
Output

Array intArray contains:
1 2 3 4 5 

Array doubleArray contains:
1.1 2.2 3.3 4.4 

Array charArray contains:
H E L L O

Bounded Type Parameters

Sometimes, you may want to restrict the types that can be passed to a type parameter. Bounded type parameters allow you to define such restrictions. For instance, a method that works on numbers might only accept instances of the Number class or its subclasses.

Declaring Bounded Type Parameters

To declare a bounded type parameter, specify the type parameter's name followed by the extends keyword, followed by its upper bound.

Example of Bounded Type Parameters

The following example illustrates how to return the largest of three Comparable objects using a bounded type parameter:

Syntax

public class MaximumTest {
// Determines the largest of three Comparable objects
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;   // Returns the largest object
}

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

Generic Classes

A generic class declaration is similar to a non-generic class declaration, with the class name followed by a type parameter section. These classes are known as parameterized classes or parameterized types because they accept one or more parameters.

Example of Generic Classes

The following example illustrates how to define a generic class:

Syntax

public class Box<T> {
private T t;

public void add(T t) {
    this.t = t;
}

public T get() {
    return t;
}

public static void main(String[] args) {
    Box<Integer> integerBox = new Box<Integer>();
    Box<String> stringBox = new Box<String>();

    integerBox.add(new Integer(10));
    stringBox.add(new String("Hello World"));

    System.out.printf("Integer Value: %d\n\n", integerBox.get());
    System.out.printf("String Value: %s\n", stringBox.get());
}
}
Output

Integer Value: 10
String Value: Hello World

Conclusion

In this tutorial, we explored the concept of Java Generics, including generic methods, bounded type parameters, and generic classes. By using generics, we can create more flexible, type-safe code that is reusable across different data types.