Java

Understanding Java Generics: A Deep Dive into Type-Safe Code

Java Generics, a key feature introduced in Java 5, enables developers to create type-safe and reusable code. It allows you to define classes, interfaces, and methods using type parameters, which provides compile-time type checking and eliminates the need for type casting.

We will look at why generics exist, what issues they answer, the benefits they provide, and various types of generics in Java.

Why Generics? The Problem It Solves

Prior to generics, Java depended largely on raw types (non-generic collections), resulting in runtime errors caused by unchecked type conversions. Let's look at the following code

import java.util.*;

public class NonGenericList {
    public static void main(String[] args) {
        List numbers = new ArrayList(); // Raw type List
        numbers.add(10);
        numbers.add("String"); // No compile-time error

        for (Object obj : numbers) {
            int num = (int) obj; // Runtime error (ClassCastException)
            System.out.println(num);
        }
    }
}

The Problem:

  1. Lack of Type Safety: Different types can be added to the same collection.
  2. Runtime Errors: Type mismatches are caught at runtime instead of compile time
  3. Manual Type Casting: Every time an element is retrieved, explicit casting is required.

Generics address these issues by enforcing type safety during compile time.


Benefits of Generics

  1. Compile-Time Type Safety: Generics enforce type constraints, preventing runtime errors
  2. Elimination of Type Casting: Code becomes cleaner as explicit type casting is unnecessary.
  3. Code Reusability: Generic classes and methods work with multiple data types.
  4. Improved Readability and Maintainability: Strongly-typed collections and methods lead to self-documenting code.

Example with Generics Using List.

import java.util.*;

public class GenericList {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(); // Type-safe list
        numbers.add(10);
        numbers.add(20);
        // numbers.add("String"); // Compile-time error
        
        for (int num : numbers) {
            System.out.println(num); // No type casting required
        }
    }
}

Types of Generics in Java

Generics can be used in a number of ways, including:

  1. Generic classes:

A class can be parameterized using generics to create reusable, type-safe components

class Box<T> {
    private T value;
    
    public void setValue(T value) {
        this.value = value;
    }
    
    public T getValue() {
        return value;
    }
}

public class GenericClassDemo {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.setValue("Hello Generics");
        System.out.println(stringBox.getValue());

        Box<Integer> intBox = new Box<>();
        intBox.setValue(100);
        System.out.println(intBox.getValue());
    }
}

T indicates the type that will be allocated to this class. The class takes the type String, so essentially replace T with String.

Another example with multiple Types:

public class Box<K,V>{
    
    private K key;
    private V value;

    public Box(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public V getValue() {
        return value;
    }

    public void setValue(V value) {
        this.value = value;
    }
}


 public static void main(String[] args) {
       Box<String,Integer> boxPair = new Box<String,Integer>("Box",1);
        System.out.println(boxPair.getKey());
        System.out.println(boxPair.getValue());
        boxPair.setValue(2);
        boxPair.setKey("Key2");
       
    }

Note: You can use Any alphabet to represent the generic type but the commonly used type variables are T,K,V,E,S,U,V etc.

  1. Generic Methods:

A method can be declared generic regardless of whether the class is generic.

class Utility {
    public static <T> void printArray(T[] array) {
        for (T item : array) {
            System.out.print(item + " ");
        }
        System.out.println();
    }
}

public class GenericMethodDemo {
    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] strArray = {"A", "B", "C"};
        
        Utility.printArray(intArray);
        Utility.printArray(strArray);
    }
}

Another example with a return value

 public static <T> T showValue(T t){
        return t;
    }
  1. Class implementing generic interface

Interface

public interface IPerson<K,V> {
    K getName();
    V getAge();
}

Class

public class Person<K,V> implements IPerson<K,V>{

    private K name;
    private V age;

    public Person(K name, V age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public K getName() {
        return this.name;
    }

    @Override
    public V getAge() {
        return this.age;
    }
}

 public static void main(String[] args) {
      Person<String,Integer> person = new Person<String,Integer>("Milo",25);
        System.out.println(person.getAge());
        System.out.println(person.getName());

    }

Generic Subtyping Is Not Covariant.

Dog is a subclass of Animal but ArrayList<Dog> is NOT a subclass of ArrayList<Animal>

Array subtyping is covariant

Dog is a subclass of Animal. Dog[ ] IS a subclass of Animal[ ]

The ? extends bounded Wildcards (Covariance)

The fact that the above is not covariant in ArrayList<Dog> is not a subtype ofArrayList<Animal> is fixed to a large extend with the extends bounded wildcards. Using this List<? extends Animal> resolves that . As now List<Dog> is covariant with List<? extends Animal>.

simpler comprehension. List<? extends Animal> is read as anything that is a subtype of Animal

Upper-Bounded Wildcard (<? extends T>) : Allows you to specify that a type parameter must be a subclass of a specific type.

public double sumOfNumbers(List<? extends Number> numbers) {
    double sum = 0;
    for (Number num : numbers) {
        sum += num.doubleValue();
    }
    return sum;
}

Here, List<? extends Number> means the list can hold any type that is a subclass of Number (e.g., Integer, Double). However, because the exact type is unknown, you can only read from the list, not write to it.

 public static void main(String[] args) {
        List<Integer> ints = new ArrayList<Integer>();
        ints.add(1);
        ints.add(2);
        List<? extends Number> nums = ints;
        //compiler error
        //nums.add(3);
        // nums.add(null); valid
        System.out.println(nums);
    }

The above code gives a compiler error when adding to the nums list. As previously indicated, the extends wildcard may only be read from, not written to. The reason is that the precise type is unknown; it might be Double, Integer, or any other Number subclass.

However, the exception to this is that adding of null to the list does not result in an error.

The ? super Bounded Wildcard (Contravariance)

Lower-Bounded Wildcard (<? super T>) : Allows you to specify that a type parameter must be a superclass of a specific type. In simpler English, it implies everything that is a superclass of T.

In this case List<Animal> is covariant with List<? super Dog>. As Animal is a superclass of Dog.

public void addNumbers(List<? super Integer> list) {
    list.add(10); // Valid
}

Here, List<? super Integer> means the list can hold any type that is a superclass of Integer (e.g., Number, Object). In this case, you can only write to the list, not read from it.

List<? super Integer> intNums = new ArrayList<>();
       intNums.add(1);
       intNums.add(2);
       intNums.get(0); // no compiler error

However, when intNums is assigned a variable it gives an error. As indicated , the super wildcard supports only write only.

List<? super Integer> intNums = new ArrayList<>();
       intNums.add(1);
       intNums.add(2);
       intNums.get(0); // no compiler error
       // Number n = intNums.get(0); compiler error
       // Integer intN = intNums.get(0); compiler error

However, the exception is that intNums can be read using the Object Type

List<? super Integer> intNums = new ArrayList<>();
       intNums.add(1);
       intNums.add(2);
       Object object = intNums.get(1); // valid
       System.out.println(object); // outputs 2

Unbounded Wildcard (?)

The wildcard(?) without the super or extends qualifier is known as the unbounded wildcard. Wildcards (?) are a powerful feature of Java Generics that allow you to work with unknown types. They are particularly useful when you want to write flexible and reusable code.

It represents an unknown type. It is used when the type is irrelevant or when you want to write code that works with any type.

public void printList(List<?> list) {
    for (Object elem : list) {
        System.out.println(elem);
    }
}

Note: Generics is used with wrapper classes and not primitive types

Java Generics are a key component of modern Java programming, allowing developers to build clean, reusable, and type-safe code. Understanding the concepts of generic classes, methods, bounded types, wildcards, and the link between generics and primitive types (via wrapper classes) will allow you to fully utilize this feature. Furthermore, limited types and wildcards give the flexibility required to handle covariance and contravariance effectively.
Begin experimenting with generics in your projects immediately to see firsthand how they may improve your Java programming abilities!

About

At DevelopersMonk, we share tutorials, tips, and insights on modern programming frameworks like React, Next.js, Spring Boot, and more. Join us on our journey to simplify coding and empower developers worldwide!

Email: developersmonks@gmail.com

Phone: +23359*******

Quick Links

  • Home
  • About
  • Contact Us
  • Categories

  • Java
  • TypeScript
  • JavaScript
  • React
  • Nextjs
  • Spring Boot
  • DevOps