Java Generics Tutorial with Examples

Introduction

Generics in Java provide a way to create classes, interfaces, and methods that operate on types specified by the client code. Generics add stability to your code by making more of your bugs detectable at compile time. They enable types (classes and methods) to be parameters when defining classes, interfaces, and methods.

Table of Contents

  1. What are Generics?
  2. Benefits of Using Generics
  3. Generic Classes
  4. Generic Methods
  5. Bounded Type Parameters
  6. Generic Interfaces
  7. Type Inference
  8. Wildcards in Generics
  9. Restrictions on Generics
  10. Example Programs
  11. Conclusion

1. What are Generics?

Generics allow types (classes and methods) to operate on objects of various types while providing compile-time type safety. They enable you to write more flexible and reusable code without sacrificing type safety.

2. Benefits of Using Generics

  • Type Safety: Generics make errors detectable at compile time rather than at runtime.
  • Code Reusability: Generics enable you to write a method or class that can operate on objects of various types.
  • Elimination of Type Casting: Explicit type casting is not needed when using generics.

3. Generic Classes

A generic class is defined with a type parameter. This type parameter can be used throughout the class for various purposes.

Syntax:

class GenericClass<T> {
    private T value;

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

    public T getValue() {
        return value;
    }
}

Example:

public class GenericClassExample {
    public static void main(String[] args) {
        GenericClass<Integer> intObj = new GenericClass<>();
        intObj.setValue(100);
        System.out.println("Integer Value: " + intObj.getValue());

        GenericClass<String> stringObj = new GenericClass<>();
        stringObj.setValue("Java Generics");
        System.out.println("String Value: " + stringObj.getValue());
    }
}

Output:

Integer Value: 100
String Value: Java Generics

4. Generic Methods

A generic method allows the type to be a parameter to methods.

Syntax:

public <T> void genericMethod(T param) {
    System.out.println(param.getClass().getName() + " = " + param);
}

Example:

public class GenericMethodExample {
    public <T> void print(T param) {
        System.out.println(param.getClass().getName() + " = " + param);
    }

    public static void main(String[] args) {
        GenericMethodExample example = new GenericMethodExample();
        example.print(123);
        example.print("Java Generics");
        example.print(45.67);
    }
}

Output:

java.lang.Integer = 123
java.lang.String = Java Generics
java.lang.Double = 45.67

5. Bounded Type Parameters

You can restrict the types that can be used as type arguments by using bounded type parameters.

Syntax:

class GenericClass<T extends Number> {
    private T value;

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

    public T getValue() {
        return value;
    }
}

Example:

public class BoundedTypeParameterExample {
    public static void main(String[] args) {
        GenericClass<Integer> intObj = new GenericClass<>();
        intObj.setValue(100);
        System.out.println("Integer Value: " + intObj.getValue());

        GenericClass<Double> doubleObj = new GenericClass<>();
        doubleObj.setValue(45.67);
        System.out.println("Double Value: " + doubleObj.getValue());

        // The following line will cause a compile-time error
        // GenericClass<String> stringObj = new GenericClass<>();
    }
}

Output:

Integer Value: 100
Double Value: 45.67

6. Generic Interfaces

An interface can be generic and have type parameters.

Syntax:

interface GenericInterface<T> {
    void display(T value);
}

Example:

class GenericInterfaceImpl<T> implements GenericInterface<T> {
    @Override
    public void display(T value) {
        System.out.println(value);
    }
}

public class GenericInterfaceExample {
    public static void main(String[] args) {
        GenericInterface<String> stringImpl = new GenericInterfaceImpl<>();
        stringImpl.display("Hello Generics");

        GenericInterface<Integer> intImpl = new GenericInterfaceImpl<>();
        intImpl.display(123);
    }
}

Output:

Hello Generics
123

7. Type Inference

Java 7 introduced the diamond operator (<>), which allows the compiler to infer the type parameters.

Example:

public class TypeInferenceExample {
    public static void main(String[] args) {
        GenericClass<Integer> intObj = new GenericClass<>();
        intObj.setValue(100);
        System.out.println("Integer Value: " + intObj.getValue());

        // Using the diamond operator
        GenericClass<String> stringObj = new GenericClass<>();
        stringObj.setValue("Java Generics");
        System.out.println("String Value: " + stringObj.getValue());
    }
}

Output:

Integer Value: 100
String Value: Java Generics

8. Wildcards in Generics

Wildcards (?) allow you to use generics more flexibly. There are three types of wildcards:

  • Unbounded Wildcards (?): Accepts any type.
  • Bounded Wildcards (<? extends Type>): Accepts a type and its subclasses.
  • Lower Bounded Wildcards (<? super Type>): Accepts a type and its superclasses.

Example:

import java.util.ArrayList;
import java.util.List;

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

    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);
        intList.add(3);

        List<String> stringList = new ArrayList<>();
        stringList.add("Hello");
        stringList.add("Generics");

        printList(intList);
        printList(stringList);
    }
}

Output:

1
2
3
Hello
Generics

9. Restrictions on Generics

There are several restrictions on generics in Java:

  1. Cannot Instantiate Generic Types with Primitive Types:

    // This is not allowed
    GenericClass<int> intObj = new GenericClass<>();
    
  2. Cannot Create Instances of Type Parameters:

    class GenericClass<T> {
        // This is not allowed
        // T obj = new T();
    }
    
  3. Cannot Declare Static Fields Whose Types are Type Parameters:

    class GenericClass<T> {
        // This is not allowed
        // static T obj;
    }
    
  4. Cannot Use Casts or instanceof with Parameterized Types:

    class GenericClass<T> {
        // This is not allowed
        // if (obj instanceof T) { }
        // T[] array = (T[]) new Object[10];
    }
    
  5. Cannot Create Arrays of Parameterized Types:

    // This is not allowed
    GenericClass<String>[] stringArray = new GenericClass<String>[10];
    

10. Example Programs

Example 1: Generic Class

Example:

class Box<T> {
    private T value;

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

    public T get() {
        return value;
    }
}

public class GenericClassExample {
    public static void main(String[] args) {
        Box<Integer> intBox = new Box<>();
        intBox.set(123);
        System.out.println("Integer Value: " + intBox.get());

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

Example 2: Generic Method

Example:

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

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] strArray = {"Hello", "Generics", "in", "Java"};

        printArray(intArray);
        printArray(strArray);
    }
}

Example 3: Bounded Type Parameter

Example:

class NumberBox<T extends Number> {
    private T value;

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

    public T get() {
        return value;
    }

    public double doubleValue() {
        return value.doubleValue();
    }
}

public class BoundedTypeParameterExample {
    public

 static void main(String[] args) {
        NumberBox<Integer> intBox = new NumberBox<>();
        intBox.set(123);
        System.out.println("Integer Value: " + intBox.get());
        System.out.println("Double Value: " + intBox.doubleValue());

        NumberBox<Double> doubleBox = new NumberBox<>();
        doubleBox.set(45.67);
        System.out.println("Double Value: " + doubleBox.get());
        System.out.println("Double Value: " + doubleBox.doubleValue());
    }
}

11. Conclusion

Generics in Java provide a powerful and flexible mechanism to write more reusable and type-safe code. By understanding and utilizing generic classes, methods, interfaces, bounded type parameters, and wildcards, you can create robust and maintainable code. Generics help eliminate runtime type errors and reduce the need for explicit type casting, making your code more readable and error-free.

Happy coding!

Comments