Index

Generics and Collections

Java Collections

12.1 Using Generics with Collections

Generics are a fundamental part of modern Java, introduced in Java 5 to add compile-time type safety to collections and other container classes. Before generics, Java collections stored objects as raw types, meaning everything was treated as Object. This often required explicit casting and increased the risk of runtime errors. Generics solve this by allowing developers to specify the type of objects stored in a collection, leading to safer and cleaner code.

Why Use Generics?

Generics offer several important benefits:

  1. Type Safety: You can restrict a collection to store only a certain type of object. This prevents accidental insertion of incompatible types.
  2. Eliminates Casting: With generics, you don’t need to cast objects when retrieving them from collections.
  3. Code Clarity: By declaring the intended data type, you make your code more readable and self-documenting.

Basic Syntax

Declaring a collection with generics is straightforward:

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
String firstName = names.get(0); // No casting needed

In this example, List<String> declares that names is a list of String objects. The compiler ensures only String elements can be added.

Without generics, the same code might look like:

List names = new ArrayList();
names.add("Alice");
names.add("Bob");
String firstName = (String) names.get(0); // Explicit cast

This version is less safe, as you might mistakenly add a non-string object and only find out at runtime.

Generics with Other Collections

Generics work with all collection types:

Set<Integer> numbers = new HashSet<>();
Map<String, Integer> scores = new HashMap<>();
Queue<Double> prices = new LinkedList<>();

Here, the generic type ensures only the correct data types are added.

Type Inference

Since Java 7, you can use the diamond operator <> to let the compiler infer the type:

List<String> cities = new ArrayList<>(); // Type inferred as ArrayList<String>

This makes declarations cleaner while maintaining type safety.

Compile-Time Errors for Safety

The compiler will flag incorrect types:

List<Integer> ids = new ArrayList<>();
ids.add(10);
ids.add("abc"); // Compile-time error: incompatible types

Such early feedback prevents many bugs that would otherwise occur at runtime.

Summary

Generics are essential for writing robust, maintainable Java code with collections. By specifying type parameters, you get strong compile-time checks, eliminate the need for casting, and produce cleaner, more self-explanatory code. Understanding and using generics effectively will greatly enhance your experience working with Java’s Collections Framework.

Index

12.2 Wildcards, Bounded Types, and Type Safety

Java generics offer powerful flexibility, especially when working with collections, but there are situations where specifying exact types becomes too restrictive. This is where wildcards (?) and bounded types (extends, super) come in. Wildcards help generalize method parameters and return types, improving reusability and allowing safe polymorphic behavior with generics.

The Need for Wildcards

Consider this basic generic method:

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

This method seems generic, but surprisingly, it won’t accept a List<String> or List<Integer> due to type invariance in Java generics. Even though String is a subtype of Object, List<String> is not a subtype of List<Object>. To solve this, we use a wildcard:

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

Here, ? represents an unknown type. Now, printList can accept List<String>, List<Integer>, or any other List<T>. However, you can only read from such lists—not add new elements (except null)—because the exact type is unknown.

Bounded Wildcards

Bounded wildcards restrict the range of allowable types and are commonly used to increase flexibility while preserving type safety.

? extends T (Upper Bound)

Use ? extends T when you want to read from a collection but don’t need to write to it:

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

This method can accept List<Integer>, List<Double>, etc., because all extend Number. But adding to numbers is disallowed, as the actual subtype is unknown.

? super T (Lower Bound)

Use ? super T when you want to write to a collection:

public void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}

This method accepts List<Integer>, List<Number>, or List<Object>—any list that can safely store Integer values.

Covariance and Contravariance

These terms describe how type relationships behave under inheritance:

Java's wildcard system enforces these constraints to avoid runtime type errors.

Practical Examples

// Covariant read
List<? extends Number> nums = new ArrayList<Integer>();
Number n = nums.get(0); // okay
// nums.add(3); // compile error

// Contravariant write
List<? super Integer> objects = new ArrayList<Number>();
objects.add(42); // okay
// Integer i = objects.get(0); // compile error – returns Object

Summary

Wildcards and bounded types are essential tools for writing flexible, reusable, and type-safe generic code in Java. Use ? extends when you only need to read from a collection, and ? super when you need to write. These constructs, together with careful method design, help you safely navigate complex generic scenarios while preserving robust compile-time type checking.

Index

12.3 Runnable Examples: Creating generic collection methods

Generic methods let us write flexible, reusable code that works with different types of collections without sacrificing type safety. In this section, we’ll walk through several runnable examples showing how to define and use generic methods with collections, wildcards, and bounded types.

Example 1: Copying Elements Between Collections

Let’s write a method that copies elements from one collection to another. This method will use wildcards and generics to ensure type safety.

import java.util.*;

public class GenericCopyExample {
    // Copy from a source Collection of type T or its subtype into a destination Collection of T or its supertype
    public static <T> void copyElements(Collection<? extends T> source, Collection<? super T> destination) {
        for (T item : source) {
            destination.add(item);
        }
    }

    public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(1, 2, 3);
        List<Number> numbers = new ArrayList<>();

        // Copy integers into a list of Numbers
        copyElements(integers, numbers);

        System.out.println("Numbers: " + numbers); // Output: Numbers: [1, 2, 3]
    }
}

Explanation:

Example 2: Finding the Maximum Element in a Collection

Now we’ll implement a generic method that returns the maximum element from a collection, using bounded types to ensure the elements are comparable.

import java.util.*;

public class MaxFinder {
    // T must be a subtype of Comparable<T> to ensure elements can be compared
    public static <T extends Comparable<T>> T findMax(Collection<T> collection) {
        if (collection.isEmpty()) {
            throw new IllegalArgumentException("Collection is empty");
        }

        Iterator<T> iterator = collection.iterator();
        T max = iterator.next();
        while (iterator.hasNext()) {
            T current = iterator.next();
            if (current.compareTo(max) > 0) {
                max = current;
            }
        }
        return max;
    }

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "David", "Bob");
        String maxName = findMax(names);
        System.out.println("Max (lexicographically): " + maxName); // Output: Max (lexicographically): David

        List<Integer> numbers = Arrays.asList(10, 20, 5);
        int maxNum = findMax(numbers);
        System.out.println("Max number: " + maxNum); // Output: Max number: 20
    }
}

Explanation:

Summary

These examples demonstrate:

Using generics in this way helps you write more reusable, type-safe utilities for collections in Java.

Index