Index

Generics Syntax

Java Syntax

14.1 Generic Classes

Generics are a powerful feature in Java that enable you to write type-safe, reusable code by allowing classes, interfaces, and methods to operate on parameterized types. Instead of working with raw Object types and casting manually, generics allow you to specify a placeholder for the type that the class will handle, making your code cleaner and less error-prone.

Defining a Generic Class

A generic class is declared by adding a type parameter in angle brackets <T> after the class name. The type parameter T is a placeholder that gets replaced by a concrete type when an object of that class is instantiated.

Syntax:

public class Box<T> {
    private T content;

    public Box(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }

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

Here, Box<T> is a generic class with a type parameter T. The field content can hold any type that replaces T. The constructor, getter, and setter also work with this generic type.

Using a Generic Class with Different Types

When creating instances of a generic class, you specify the actual type to use in place of T:

public class Main {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>("Hello");
        System.out.println(stringBox.getContent());  // Output: Hello

        Box<Integer> intBox = new Box<>(123);
        System.out.println(intBox.getContent());     // Output: 123

        Box<Double> doubleBox = new Box<>(45.67);
        System.out.println(doubleBox.getContent());  // Output: 45.67
    }
}

Here we created three Box objects with different types: String, Integer, and Double. The compiler enforces type safety, ensuring that only the declared type is used. For example, you cannot accidentally put an Integer into a Box<String>.

Generic Classes with Multiple Type Parameters

You can also declare generic classes with multiple type parameters, allowing more flexibility:

public 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 example:

Pair<String, Integer> entry = new Pair<>("Age", 30);
System.out.println(entry.getKey() + ": " + entry.getValue());  // Output: Age: 30

Pair<Integer, String> reversedEntry = new Pair<>(100, "Score");
System.out.println(reversedEntry.getKey() + ": " + reversedEntry.getValue());  // Output: 100: Score

This Pair class can hold any two types, named K and V here for key and value, but you can use any valid identifier for type parameters.

Click to view full runnable Code

public class GenericDemo {

    // Generic Box class
    public static class Box<T> {
        private T content;

        public Box(T content) {
            this.content = content;
        }

        public T getContent() {
            return content;
        }

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

    // Generic Pair class with two type parameters
    public static 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;
        }
    }

    public static void main(String[] args) {
        // Using Box with different types
        Box<String> stringBox = new Box<>("Hello");
        Box<Integer> intBox = new Box<>(123);
        Box<Double> doubleBox = new Box<>(45.67);

        System.out.println("Box Contents:");
        System.out.println("String: " + stringBox.getContent());
        System.out.println("Integer: " + intBox.getContent());
        System.out.println("Double: " + doubleBox.getContent());

        // Using Pair with different type combinations
        Pair<String, Integer> entry = new Pair<>("Age", 30);
        Pair<Integer, String> reversedEntry = new Pair<>(100, "Score");

        System.out.println("\nPair Entries:");
        System.out.println(entry.getKey() + ": " + entry.getValue());
        System.out.println(reversedEntry.getKey() + ": " + reversedEntry.getValue());
    }
}

Benefits of Using Generic Classes

  1. Type Safety Generics eliminate the need for explicit casting and reduce the risk of ClassCastException at runtime. The compiler checks that only the correct types are used, catching errors early.

  2. Code Reuse Instead of writing separate classes for each data type (e.g., StringBox, IntegerBox), you write one generic class that works for all types, improving maintainability and reducing boilerplate code.

  3. Expressiveness Generics communicate your intent clearly. When you see Box<String>, you immediately know what kind of data that box holds, improving readability.

  4. Interoperability with Collections Java Collections API heavily uses generics, so understanding generic classes is essential for working effectively with lists, sets, maps, and more.

Important Notes

Summary

Generics transform Java from a language with raw types to a much more type-safe and expressive one, paving the way for modern Java programming.

Index

14.2 Generic Methods

Generic methods extend the power of generics by allowing you to define methods with their own type parameters—independent of whether the enclosing class is generic or not. This means you can write a single method that works with various types, improving flexibility and code reuse without having to make the entire class generic.

Syntax of a Generic Method

A generic method declares its type parameter(s) before the return type, enclosed in angle brackets <>. This signals to the compiler that the method can operate on one or more type parameters.

Basic syntax:

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

Here, <T> declares a generic type parameter T just for the method printArray. The method takes an array of T elements and prints each one. Note that T is a method-level type parameter, separate from any generic types the class might have.

Generic Methods Inside Non-Generic Classes

You can define generic methods inside regular (non-generic) classes. This is common when the generic behavior is limited to a few methods rather than the whole class.

Example:

public class Utility {

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

    public static <T> T getFirstElement(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        return array[0];
    }
}

Usage:

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

        Utility.printArray(intArray);  // Output: 1 2 3 4 
        Utility.printArray(strArray);  // Output: apple banana cherry 

        System.out.println("First int: " + Utility.getFirstElement(intArray));  // Output: First int: 1
        System.out.println("First string: " + Utility.getFirstElement(strArray));  // Output: First string: apple
    }
}

Notice that the Utility class itself is not generic, but the methods are. This pattern is useful for stateless utility methods that operate on different types.

Click to view full runnable Code

public class GenericMethodDemo {

    // Utility class with generic methods
    public static class Utility {

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

        public static <T> T getFirstElement(T[] array) {
            if (array == null || array.length == 0) {
                return null;
            }
            return array[0];
        }
    }

    // Main method to demonstrate generic utility methods
    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4};
        String[] strArray = {"apple", "banana", "cherry"};

        System.out.println("Integer Array:");
        Utility.printArray(intArray);  // Output: 1 2 3 4

        System.out.println("\nString Array:");
        Utility.printArray(strArray);  // Output: apple banana cherry

        System.out.println("\nFirst int: " + Utility.getFirstElement(intArray));     // Output: First int: 1
        System.out.println("First string: " + Utility.getFirstElement(strArray));    // Output: First string: apple
    }
}

Generic Methods with Multiple Type Parameters

Methods can have multiple type parameters, separated by commas:

public class PairUtil {

    public static <K, V> void printPair(K key, V value) {
        System.out.println("Key: " + key + ", Value: " + value);
    }
}

Usage:

PairUtil.printPair("Name", "Alice");
PairUtil.printPair(1001, 99.5);

This method works with any combination of key and value types.

Generic Return Types

Generic methods can also return values of generic types. The compiler enforces type safety when the method is called, so you get the correct type inferred automatically:

public static <T> T getMiddleElement(T[] array) {
    if (array == null || array.length == 0) {
        return null;
    }
    return array[array.length / 2];
}

Usage:

String[] fruits = {"apple", "banana", "cherry"};
String middle = Utility.getMiddleElement(fruits);
System.out.println(middle);  // Output: banana

The method returns a value of the generic type T, inferred by the compiler based on the argument type.

Click to view full runnable Code

public class GenericReturnDemo {

    // Utility class with a generic method that returns the middle element
    public static class Utility {

        public static <T> T getMiddleElement(T[] array) {
            if (array == null || array.length == 0) {
                return null;
            }
            return array[array.length / 2];
        }
    }

    public static void main(String[] args) {
        String[] fruits = {"apple", "banana", "cherry"};
        Integer[] numbers = {10, 20, 30, 40, 50};

        String middleFruit = Utility.getMiddleElement(fruits);
        Integer middleNumber = Utility.getMiddleElement(numbers);

        System.out.println("Middle fruit: " + middleFruit);   // Output: banana
        System.out.println("Middle number: " + middleNumber); // Output: 30
    }
}

Benefits of Generic Methods

  1. Flexibility Without Class-Level Generics You can keep your classes simple and non-generic but still write reusable, type-safe methods that work with any type.

  2. Code Reuse and Reduced Duplication Instead of writing overloaded methods for different types, a generic method handles them all in one place.

  3. Improved Type Safety The compiler ensures you don't accidentally mix incompatible types, eliminating many potential runtime errors.

  4. Clearer APIs Using generic methods makes your APIs expressive about the types they handle, which aids in maintenance and readability.

Important Notes

Utility.<String>printArray(new String[]{"a", "b", "c"});

but this is rarely required.

Summary

Generic methods expand your programming toolkit, allowing you to write more generalized code while maintaining strong typing guarantees, making Java development both easier and safer.

Index

14.3 Bounded Type Parameters (T extends Number)

In Java generics, bounded type parameters allow you to restrict the types that can be used as arguments for a generic type. This gives you more control over what types are valid, enabling you to write methods or classes that operate only on a subset of types—while maintaining type safety and flexibility.

What Are Bounded Type Parameters?

A bounded type parameter uses the extends keyword to limit the types you can use. Despite the keyword extends, the bound works for both classes and interfaces (including abstract classes), and it means "is a subtype of".

For example, if you write:

<T extends Number>

it means that T can be any class that is a subclass of Number or Number itself. This includes types like Integer, Double, Float, etc., but not types like String or custom classes unrelated to Number.

Why Use Bounded Type Parameters?

Suppose you want a method that works only with numeric types, so you can perform numeric operations safely. Without bounds, your generic method would accept any type, including types that cannot be used numerically. Bounded type parameters solve this problem by restricting acceptable types, enabling you to use numeric methods like doubleValue() or intValue() on the generic parameter.

Example: Generic Method with Numeric Bound

Here is a simple generic method that calculates the sum of an array of numbers:

public class MathUtils {

    // T must extend Number, so only numeric types are allowed
    public static <T extends Number> double sumArray(T[] numbers) {
        double sum = 0.0;
        for (T number : numbers) {
            sum += number.doubleValue();  // safe because T extends Number
        }
        return sum;
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4};
        Double[] doubleArray = {1.5, 2.5, 3.5};

        System.out.println(MathUtils.sumArray(intArray));    // Output: 10.0
        System.out.println(MathUtils.sumArray(doubleArray)); // Output: 7.5

        // The following line would cause a compile-time error:
        // String[] strArray = {"a", "b", "c"};
        // MathUtils.sumArray(strArray); // Not allowed because String doesn't extend Number
    }
}
Click to view full runnable Code

public class NumericSumDemo {

    // Math utility class with a generic method to sum numeric arrays
    public static class MathUtils {

        // T must extend Number to ensure only numeric types are accepted
        public static <T extends Number> double sumArray(T[] numbers) {
            double sum = 0.0;
            for (T number : numbers) {
                sum += number.doubleValue();  // safe conversion for all Number subclasses
            }
            return sum;
        }
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4};
        Double[] doubleArray = {1.5, 2.5, 3.5};

        System.out.println("Sum of intArray: " + MathUtils.sumArray(intArray));      // Output: 10.0
        System.out.println("Sum of doubleArray: " + MathUtils.sumArray(doubleArray)); // Output: 7.5

        // The following would fail to compile because String is not a subclass of Number
        // String[] strArray = {"a", "b", "c"};
        // System.out.println(MathUtils.sumArray(strArray));
    }
}

Multiple Bounds

Java also allows multiple bounds by using & to separate interfaces/classes. For example:

<T extends Number & Comparable<T>>

means T must be a subtype of Number and implement Comparable<T>. This is useful when you need both numeric behavior and the ability to compare objects.

Bounded Type Parameters in Classes

Bounds can also be applied at the class level:

public class NumericBox<T extends Number> {
    private T value;

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

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

Here, NumericBox can only be instantiated with subclasses of Number, ensuring safe numeric operations.

Reflection on When to Use Bounds

  1. Balance Flexibility and Safety Bounded types let you accept a wide range of compatible types (like all numeric types) while preventing invalid types (like String). This preserves the flexibility generics provide without sacrificing compile-time type safety.

  2. Enable Use of Specific Methods Without bounds, you cannot safely call methods specific to certain classes. For example, without bounding T extends Number, calling doubleValue() on T would cause a compile error because not all types have this method.

  3. Enhance Code Readability and Intent Bounds make your code's intent clearer. When you declare <T extends Number>, other developers immediately understand the method or class is meant for numeric types.

  4. Avoid Casting and Runtime Errors By restricting types, you reduce the need for unsafe casting and prevent runtime errors that would occur if a method assumed numeric behavior on non-numeric types.

Summary

Using bounded type parameters effectively helps you harness the power of generics while avoiding pitfalls of overly general or unsafe code. It's a key technique to write reusable, robust, and expressive Java code.

Index

14.4 Wildcards: ?, ? extends, ? super

Generics in Java provide great flexibility and type safety, but sometimes you want to write methods that can work with a range of related types without specifying the exact type parameter. This is where wildcards come into play. Wildcards allow you to express uncertainty or flexibility in generic type arguments, enabling powerful and reusable APIs.

What Is a Wildcard?

The wildcard ? represents an unknown type in generics. It means "some type, but I don't know which." For example:

List<?> list = new ArrayList<String>();

Here, list can hold any kind of List, but you cannot add elements to it (except null) because the exact type is unknown.

Why Use Wildcards?

Wildcards are useful when you want a method to accept generic types of unknown or varying types, especially when the method doesn't need to modify the collection or wants to maintain flexibility.

Wildcard Variants and Their Meanings

There are three main wildcard forms:

  1. Unbounded wildcard: ?
  2. Upper bounded wildcard: ? extends Type
  3. Lower bounded wildcard: ? super Type

Unbounded Wildcard: ?

This represents any type. It's often used when you only need to read data but don't care about the specific type.

Example:

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

You can call printList with a List<String>, List<Integer>, or any other List<T>. But since the type is unknown, you cannot add elements except null because it might violate type safety.

Upper Bounded Wildcard: ? extends Type

This means the unknown type is a subtype of Type (or Type itself). It is useful when you want to read from a collection and ensure that everything in it is at least of type Type.

Example:

public double sumNumbers(List<? extends Number> list) {
    double sum = 0.0;
    for (Number num : list) {
        sum += num.doubleValue();
    }
    return sum;
}

You can pass List<Integer>, List<Double>, or any List of a subclass of Number. This guarantees safe reading since every element is at least a Number.

Important: You cannot add anything to list except null, because the exact subtype is unknown. For example, if list is actually a List<Double>, adding an Integer would be unsafe.

Lower Bounded Wildcard: ? super Type

This means the unknown type is a supertype of Type (or Type itself). It is useful when you want to write to a collection safely, ensuring you can add elements of a specific type or its subclasses.

Example:

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

Here, you can pass List<Integer>, List<Number>, or List<Object>. Because the list is guaranteed to accept Integer objects (or their subclasses), it's safe to add Integer values.

Note: You cannot safely read specific types out of such a list other than Object, because the exact type could be any supertype.

Practical Summary: PECS Rule

To remember when to use extends or super, the Java community uses the PECS acronym:

Example: Reading vs Writing

public void processNumbers(List<? extends Number> producer) {
    Number n = producer.get(0);  // safe to read as Number
    // producer.add(3); // Compile error! Can't add to ? extends Number
}

public void fillNumbers(List<? super Integer> consumer) {
    consumer.add(5);   // safe to add Integer
    // Integer n = consumer.get(0); // Compile error! get() returns Object
}
Click to view full runnable Code

import java.util.*;

public class WildcardDemo {

    // Unbounded wildcard: can read but not add
    public static void printList(List<?> list) {
        System.out.println("Printing list:");
        for (Object elem : list) {
            System.out.println("  " + elem);
        }
    }

    // Upper bounded wildcard: reads from a list of Numbers or subclasses
    public static double sumNumbers(List<? extends Number> list) {
        double sum = 0.0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }

    // Lower bounded wildcard: writes Integers to a list of Integer or supertypes
    public static void addIntegers(List<? super Integer> list) {
        list.add(10);
        list.add(20);
    }

    public static void main(String[] args) {
        List<String> stringList = Arrays.asList("apple", "banana", "cherry");
        List<Integer> intList = new ArrayList<>(Arrays.asList(1, 2, 3));
        List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
        List<Number> numberList = new ArrayList<>();

        // Unbounded wildcard demo
        printList(stringList);
        printList(intList);

        // Upper bounded wildcard demo
        System.out.println("\nSum of intList: " + sumNumbers(intList));      // Output: 6.0
        System.out.println("Sum of doubleList: " + sumNumbers(doubleList)); // Output: 6.6

        // Lower bounded wildcard demo
        addIntegers(numberList);  // Safe to add Integers
        printList(numberList);

        // Demonstrating PECS in action
        processNumbers(intList);
        fillNumbers(numberList);
    }

    // Supporting PECS examples
    public static void processNumbers(List<? extends Number> producer) {
        System.out.println("\nFirst number (producer): " + producer.get(0));
        // producer.add(100); // Compile-time error
    }

    public static void fillNumbers(List<? super Integer> consumer) {
        consumer.add(42); // Safe to add Integer
        System.out.println("Added 42 to consumer");
    }
}

Why Wildcards Matter: Avoiding Unsafe Operations

Without wildcards, you might write overly restrictive code or use unsafe casts. Wildcards allow you to express flexible contracts safely.

For example, a method that accepts List<Number> cannot accept List<Integer>, because generics are invariant in Java (List<Integer> is NOT a subtype of List<Number>). Using List<? extends Number> relaxes this restriction, allowing covariance.

Reflection on Wildcards

Summary Table

Wildcard Meaning Use case Can you add elements? Can you read elements?
? (unbounded) Unknown type Read-only or general APIs No (except null) Yes, as Object
? extends T Subtype of T (covariant) Reading from collection No Yes, as type T or supertype
? super T Supertype of T (contravariant) Writing to collection Yes, type T or subtype Yes, only as Object

Conclusion

Wildcards are a powerful feature of Java generics that let you write flexible, reusable code while preserving type safety. Understanding how to use ?, ? extends, and ? super effectively—and applying the PECS principle—enables you to design APIs that work cleanly across a variety of types, making your code more robust and maintainable.

Index