Understanding Generics in Java: Classes, Methods, Interfaces, and Primitive Types

Generics in Java enable developers to create classes, interfaces, and methods with type parameters, providing compile-time type safety and reducing the need for explicit type casting. By using generics, developers can write reusable and flexible code that works with various data types.

This article explores the usage of generics in Java, including their application in classes, methods, and interfaces, handling primitive types via wrapper classes, the concept of autoboxing, and the dangers of using raw types. Additionally, it covers multiple type parameters, upper and lower bounds, multiple bounds using &, and wildcard types using ?.

Generic Classes

A generic class defines a type parameter that can be specified when creating an instance of the class. The type parameter allows the class to work with different types without compromising type safety.

Example of a Generic Class

class Box<T> {
    private T value;

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

    public T getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<Integer> intBox = new Box<>();
        intBox.setValue(10);
        System.out.println("Integer Value: " + intBox.getValue());

        Box<String> strBox = new Box<>();
        strBox.setValue("Hello Generics");
        System.out.println("String Value: " + strBox.getValue());
    }
}

Explanation

  • T is a type parameter that will be replaced with a concrete type when the class is instantiated.

  • Box<Integer> ensures type safety, preventing incorrect assignments.

  • Box<String> provides the same functionality for string values.

Generic Methods

Generic methods allow type parameters to be used within a method independently of the class-level type parameters.

Example of a Generic Method

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

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

        Util.printArray(intArray);
        Util.printArray(strArray);
    }
}

Explanation

  • <T> before the return type indicates a generic method.

  • The method printArray works with any array type.

Multiple Type Parameters

A generic class can define multiple type parameters, allowing more flexibility.

Example of a Class with Multiple Type Parameters

class Pair<K, V> {
    private K key;
    private V value;

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

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

Usage

Pair<String, Integer> student = new Pair<>("Alice", 95);
System.out.println("Student: " + student.getKey() + ", Score: " + student.getValue());

Upper and Lower Bounds

Java generics allow restrictions on the types using extends (upper bound) and super (lower bound).

Upper Bounded Wildcard

public static void printNumbers(List<? extends Number> list) {
    for (Number num : list) {
        System.out.println(num);
    }
}
  • ? extends Number ensures the list contains elements that are instances of Number or its subclasses.

Lower Bounded Wildcard

public static void addNumbers(List<? super Integer> list) {
    list.add(10);
    list.add(20);
}
  • ? super Integer ensures the list can contain Integer or its superclasses.

Multiple Bounds

Java allows specifying multiple bounds using &. The first type must be a class (if any), followed by interfaces.

Example of Multiple Bounds

class Data<T extends Number & Comparable<T>> {
    private T value;

    public Data(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}
  • T extends Number & Comparable<T> ensures T is a subclass of Number and implements Comparable.

Wildcards (?)

Wildcards (?) provide flexibility when dealing with generic types.

Unbounded Wildcard

public static void displayList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}
  • List<?> accepts any type of list.

Handling Primitive Types: Wrapper Classes & Autoboxing

Java generics do not support primitive types directly. Instead, they require wrapper classes such as Integer, Double, and Character.

Example of Wrapper Classes in Generics

Box<Integer> intBox = new Box<>();  // Correct
Box<int> intBox = new Box<>();      // Compilation Error

Autoboxing & Unboxing

Autoboxing automatically converts primitive types into their corresponding wrapper classes, and unboxing converts them back.

public class Main {
    public static void main(String[] args) {
        Box<Integer> intBox = new Box<>();
        intBox.setValue(100);  // Autoboxing: int -> Integer
        int num = intBox.getValue();  // Unboxing: Integer -> int
        System.out.println(num);
    }
}

Raw Types and Why to Avoid Them

A raw type is a generic class or interface used without specifying a type parameter. This can lead to type safety issues.

Example of Raw Types

Box rawBox = new Box();  // Raw type
rawBox.setValue("String");  // No compile-time check
Integer value = (Integer) rawBox.getValue(); // Runtime error: ClassCastException

Why Avoid Raw Types?

  1. Loss of Type Safety: The compiler does not enforce type checking, leading to potential runtime errors.

  2. Possible ClassCastException: Casting might fail if the stored value does not match the expected type.

  3. Reduced Code Readability: The intent of the type is not clear when raw types are used.

Conclusion

Generics in Java provide a powerful way to write flexible and type-safe code. By utilizing generic classes, methods, interfaces, multiple type parameters, bounds, and wildcards, developers can build reusable components. Since Java generics do not support primitive types directly, wrapper classes and autoboxing handle the conversion seamlessly. However, using raw types should be avoided due to the risk of runtime errors and loss of type safety.

By understanding and properly implementing generics, Java developers can create robust and maintainable applications.

0
Subscribe to my newsletter

Read articles from Ali Rıza Şahin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ali Rıza Şahin
Ali Rıza Şahin

Product-oriented Software Engineer with a solid understanding of web programming fundamentals and software development methodologies such as agile and scrum.