Generics in Java are a powerful feature that allows you to write flexible, reusable, and type-safe code. They enable you to define classes, interfaces, and methods with a placeholder for the type of data they operate on. This helps in reducing code duplication and increasing type safety by catching errors at compile time.

Key Concepts

  1. Type Parameters: Generics use type parameters to specify the type of data they operate on. These parameters are usually denoted by single uppercase letters like T, E, K, V, etc.
  2. Generic Classes: Classes that can operate on any data type.
  3. Generic Methods: Methods that can operate on any data type.
  4. Bounded Type Parameters: Restricting the types that can be used as type arguments.
  5. Wildcards: Represent an unknown type and are used to specify a range of acceptable types.

Generic Classes

A generic class is defined with a type parameter in angle brackets <> after the class name.

Example

public class Box<T> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

Explanation

  • Box<T>: T is a type parameter that will be replaced with a specific type when an object of Box is created.
  • setContent(T content): A method that sets the content of the box.
  • getContent(): A method that returns the content of the box.

Usage

public class Main {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.setContent("Hello, Generics!");
        System.out.println(stringBox.getContent());

        Box<Integer> integerBox = new Box<>();
        integerBox.setContent(123);
        System.out.println(integerBox.getContent());
    }
}

Generic Methods

A generic method is a method that can operate on any data type. It is defined with a type parameter before the return type.

Example

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

Explanation

  • <T>: Declares a type parameter T for the method.
  • printArray(T[] array): A method that prints all elements of an array.

Usage

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

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

Bounded Type Parameters

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

Example

public class NumberBox<T extends Number> {
    private T number;

    public void setNumber(T number) {
        this.number = number;
    }

    public T getNumber() {
        return number;
    }
}

Explanation

  • T extends Number: Restricts T to be a subclass of Number.

Usage

public class Main {
    public static void main(String[] args) {
        NumberBox<Integer> intBox = new NumberBox<>();
        intBox.setNumber(10);
        System.out.println(intBox.getNumber());

        NumberBox<Double> doubleBox = new NumberBox<>();
        doubleBox.setNumber(10.5);
        System.out.println(doubleBox.getNumber());
    }
}

Wildcards

Wildcards are used to specify a range of acceptable types.

Example

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

Explanation

  • List<?>: A list of unknown type.

Usage

public class Main {
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        List<String> stringList = Arrays.asList("A", "B", "C");

        WildcardDemo.printList(intList);
        WildcardDemo.printList(stringList);
    }
}

Practical Exercises

Exercise 1: Create a Generic Pair Class

Create a generic class Pair that holds two values of any type.

public class Pair<T, U> {
    private T first;
    private U second;

    public Pair(T first, U second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public U getSecond() {
        return second;
    }
}

Solution

public class Main {
    public static void main(String[] args) {
        Pair<String, Integer> pair = new Pair<>("Age", 30);
        System.out.println("First: " + pair.getFirst());
        System.out.println("Second: " + pair.getSecond());
    }
}

Exercise 2: Implement a Generic Stack

Create a generic stack class with methods to push, pop, and peek elements.

public class Stack<T> {
    private List<T> elements = new ArrayList<>();

    public void push(T element) {
        elements.add(element);
    }

    public T pop() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() - 1);
    }

    public T peek() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.get(elements.size() - 1);
    }
}

Solution

public class Main {
    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();
        stack.push(1);
        stack.push(2);
        stack.push(3);

        System.out.println("Peek: " + stack.peek());
        System.out.println("Pop: " + stack.pop());
        System.out.println("Peek: " + stack.peek());
    }
}

Common Mistakes and Tips

  • Type Erasure: Remember that Java uses type erasure to implement generics, which means that generic type information is removed at runtime. This can lead to some unexpected behavior if not understood properly.
  • Raw Types: Avoid using raw types (e.g., List instead of List<T>). They can lead to runtime errors and defeat the purpose of generics.
  • Bounded Wildcards: Use bounded wildcards (<? extends T> and <? super T>) to increase the flexibility of your code.

Conclusion

Generics are a fundamental feature in Java that enhance code reusability and type safety. By understanding and utilizing generics, you can write more flexible and robust code. In the next topic, we will explore annotations, another powerful feature in Java that allows you to add metadata to your code.

Java Programming Course

Module 1: Introduction to Java

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File I/O

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2024. All rights reserved