PYnative

Python Programming

  • Learn Python ▼
    • Python Tutorials
    • Python Basics
    • Python Interview Q&As
  • Exercises ▼
    • Python Exercises
    • C++ Exercises
    • C Programming Exercises
    • Java Exercises
  • Quizzes
  • Code Editor ▼
    • Online Python Code Editor
    • Online C Compiler
    • Online C++ Compiler
    • Online Java Compiler
Home » Java Exercises » Java Generics Exercises: 20 Coding Problems with Solutions

Java Generics Exercises: 20 Coding Problems with Solutions

Updated on: July 6, 2026 | Leave a Comment

This set of 20 Java generics exercises is organized into three progressive levels, taking you from writing your first generic class to confidently using wildcards.

  • Level 1 covers generic classes and methods, including a generic Box, Pair, Stack, and Queue.
  • Level 2 introduces bounded type parameters with extends, used for finding a maximum value, summing numeric types, and binary search.
  • Level 3 tackles wildcards, unbounded, upper-bounded, and lower-bounded, applying the Producer-Extends-Consumer-Super (PECS) principle to real scenarios like copying and filtering lists.

Each exercise includes a Problem Statement, Purpose, Hint, and a Solution with Explanation that clarifies exactly why a particular type parameter or wildcard was chosen.

  • Also, See: Java Exercises with over 20+ topic-wise sets and 575+ coding questions to practice.
  • Practice questions using our Online Java Compiler
+ Table of Contents (20 Exercises)

Table of contents

  • Exercise 1: Generic Box
  • Exercise 2: Generic Pair
  • Exercise 3: Array Element Swapper
  • Exercise 4: List Reverser
  • Exercise 5: Generic Stack
  • Exercise 6: Generic Queue
  • Exercise 7: Array-to-List Converter
  • Exercise 8: Find Maximum Element
  • Exercise 9: Sum of Numbers
  • Exercise 10: Property Counter
  • Exercise 11: Bounded Numeric Box
  • Exercise 12: Generic Binary Search
  • Exercise 13: Range Checker
  • Exercise 14: Print Unknown List
  • Exercise 15: List Copier (PECS Principle)
  • Exercise 16: Sum of Wildcard List
  • Exercise 17: Lower-Bounded Element Adder
  • Exercise 18: Filter and Collect
  • Exercise 19: Intersection Finder
  • Exercise 20: Generic CRUD Repository

Exercise 1: Generic Box

Problem Statement: Create a class Box<T> that can hold a single object of any type. Implement getter and setter methods, along with an isEmpty() method that returns a boolean.

Purpose: This exercise helps you practice defining a generic class with a type parameter, and understanding how a single class definition can safely work with any object type without casting.

Given Input: Box<String> box = new Box<>(); box.set("Hello");

Expected Output:

Is empty: true
Value: Hello
Is empty: false
▼ Hint
  • Declare a private field of type T to store the value.
  • The set() method should accept a parameter of type T and assign it to the field.
  • The get() method should simply return the stored field.
  • isEmpty() can check whether the stored field is null.
▼ Solution & Explanation
class Box<T> {
    private T value;

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

    public T get() {
        return value;
    }

    public boolean isEmpty() {
        return value == null;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<String> box = new Box<>();
        System.out.println("Is empty: " + box.isEmpty());

        box.set("Hello");
        System.out.println("Value: " + box.get());
        System.out.println("Is empty: " + box.isEmpty());
    }
}Code language: Java (java)

Explanation:

  • class Box<T>: Declares a generic class with a type parameter T, which acts as a placeholder for whatever type is supplied when the class is used.
  • private T value: Stores the object of type T inside the box.
  • Box<String> box = new Box<>(): Creates a box that is restricted to holding String values. The diamond operator <> lets the compiler infer the type on the right side.
  • isEmpty(): Returns true when no value has been set yet, since the default value of an object field is null.

Exercise 2: Generic Pair

Problem Statement: Create a class Pair<K, V> to hold a key-value pair. Implement methods to retrieve the key and the value independently, and a toString() method to print them cleanly.

Purpose: This exercise builds on single type parameters by introducing multiple type parameters in one class, a pattern used heavily in map entries and data structures that associate two related values.

Given Input: Pair<String, Integer> pair = new Pair<>("Age", 25);

Expected Output: Pair{key=Age, value=25}

▼ Hint
  • Use two type parameters, commonly named K and V, for the key and value.
  • Store both in private fields and set them through the constructor.
  • Override toString() and build the output string manually using the field values.
▼ Solution & Explanation
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;
    }

    @Override
    public String toString() {
        return "Pair{key=" + key + ", value=" + value + "}";
    }
}

public class Main {
    public static void main(String[] args) {
        Pair<String, Integer> pair = new Pair<>("Age", 25);
        System.out.println(pair);
    }
}Code language: Java (java)

Explanation:

  • class Pair<K, V>: Declares a generic class with two independent type parameters, allowing the key and value to be different types.
  • public Pair(K key, V value): The constructor accepts both values and assigns them to the object’s fields.
  • getKey() and getValue(): Give independent access to each half of the pair without exposing the fields directly.
  • @Override public String toString(): Replaces the default object representation with a readable one, so printing the pair calls this method automatically.

Exercise 3: Array Element Swapper

Problem Statement: Write a generic method swap that takes an array of any type T[] and two indices, then swaps the elements at those positions.

Purpose: This exercise introduces generic methods, which declare their own type parameter separately from any class, and is a common building block for sorting and array-manipulation algorithms.

Given Input: Integer[] arr = {1, 2, 3, 4, 5}; swap(arr, 0, 4);

Expected Output: [5, 2, 3, 4, 1]

▼ Hint
  • Declare the type parameter before the return type, like public static <T> void swap(...).
  • Store one of the two elements in a temporary variable before overwriting it.
  • Use Arrays.toString() to print the array contents.
▼ Solution & Explanation
import java.util.Arrays;

public class Main {
    public static <T> void swap(T[] arr, int i, int j) {
        T temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        Integer[] arr = {1, 2, 3, 4, 5};
        swap(arr, 0, 4);
        System.out.println(Arrays.toString(arr));
    }
}Code language: Java (java)

Explanation:

  • <T> void swap(T[] arr, int i, int j): The <T> before the return type declares this as a generic method, independent of any class-level type parameter.
  • T temp = arr[i]: Saves the first element so it is not lost when it gets overwritten.
  • arr[i] = arr[j]; arr[j] = temp;: Completes the swap by moving the second element into the first position, then placing the saved value into the second position.
  • Arrays.toString(arr): Converts the array to a readable string for printing, since arrays do not override toString() by default.

Exercise 4: List Reverser

Problem Statement: Write a generic method that takes a List<T> and reverses the order of its elements without using the built-in Collections.reverse() utility.

Purpose: This exercise reinforces working with generic collections and practicing an in-place, two-pointer reversal technique commonly used in array and list manipulation problems.

Given Input: List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

Expected Output: [5, 4, 3, 2, 1]

▼ Hint
  • Use two index pointers, one starting at the beginning and one at the end.
  • Swap the elements at those two positions using get() and set().
  • Move the pointers toward the middle and stop once they meet or cross.
▼ Solution & Explanation
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    public static <T> void reverseList(List<T> list) {
        int left = 0;
        int right = list.size() - 1;
        while (left < right) {
            T temp = list.get(left);
            list.set(left, list.get(right));
            list.set(right, temp);
            left++;
            right--;
        }
    }

    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
        reverseList(numbers);
        System.out.println(numbers);
    }
}Code language: Java (java)

Explanation:

  • int left = 0; int right = list.size() - 1;: Sets up two pointers at opposite ends of the list.
  • while (left < right): Keeps swapping until the pointers meet in the middle, at which point the whole list has been reversed.
  • list.set(left, list.get(right)): Uses the generic List interface methods to read and overwrite elements without needing array-style indexing.
  • new ArrayList<>(Arrays.asList(...)): Wraps the fixed-size list from Arrays.asList() in a resizable, mutable ArrayList so set() works correctly.

Exercise 5: Generic Stack

Problem Statement: Implement a classic Stack data structure (push, pop, isEmpty, peek) using generics and an underlying ArrayList or custom linked nodes.

Purpose: This exercise shows how generics let you build a reusable, type-safe data structure once and use it with any object type, a pattern found throughout the Java Collections Framework.

Given Input: Stack<Integer> stack = new Stack<>(); stack.push(10); stack.push(20); stack.push(30);

Expected Output:

Peek: 30
Pop: 30
Stack after pop: [10, 20]
▼ Hint
  • Use a private List<T> field internally to hold the stack elements.
  • push() should add to the end of the list, and pop() should remove from the end.
  • peek() returns the last element without removing it.
  • Always check isEmpty() before popping or peeking to avoid errors.
▼ Solution & Explanation
import java.util.ArrayList;
import java.util.List;

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

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

    public T pop() {
        if (isEmpty()) {
            throw new RuntimeException("Stack is empty");
        }
        return elements.remove(elements.size() - 1);
    }

    public T peek() {
        return elements.get(elements.size() - 1);
    }

    public boolean isEmpty() {
        return elements.isEmpty();
    }

    @Override
    public String toString() {
        return elements.toString();
    }
}

public class Main {
    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();
        stack.push(10);
        stack.push(20);
        stack.push(30);

        System.out.println("Peek: " + stack.peek());
        System.out.println("Pop: " + stack.pop());
        System.out.println("Stack after pop: " + stack);
    }
}Code language: Java (java)

Explanation:

  • class Stack<T>: A custom generic class, separate from java.util.Stack, backed internally by an ArrayList<T>.
  • elements.remove(elements.size() - 1): Removes and returns the last element of the list, which represents the top of the stack.
  • peek(): Reads the last element with get() without removing it, so the stack is unchanged after the call.
  • push(10); push(20); push(30): Adds elements in order, so 30 ends up on top and is the first one returned by pop() or peek().

Exercise 6: Generic Queue

Problem Statement: Implement a FIFO (First-In, First-Out) Queue data structure (enqueue, dequeue, isEmpty, size) using generics.

Purpose: This exercise contrasts with the stack exercise by practicing FIFO ordering instead of LIFO, and reinforces building a type-safe structure that works with any element type.

Given Input: Queue<Integer> queue = new Queue<>(); queue.enqueue(10); queue.enqueue(20); queue.enqueue(30);

Expected Output:

Dequeued: 10
Queue size: 2
Queue: [20, 30]
▼ Hint
  • Use a private List<T> field internally, similar to the stack.
  • enqueue() should add to the end of the list.
  • dequeue() should remove from the front (index 0), not the end.
  • size() simply returns the number of stored elements.
▼ Solution & Explanation
import java.util.ArrayList;
import java.util.List;

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

    public void enqueue(T item) {
        elements.add(item);
    }

    public T dequeue() {
        if (isEmpty()) {
            throw new RuntimeException("Queue is empty");
        }
        return elements.remove(0);
    }

    public boolean isEmpty() {
        return elements.isEmpty();
    }

    public int size() {
        return elements.size();
    }

    @Override
    public String toString() {
        return elements.toString();
    }
}

public class Main {
    public static void main(String[] args) {
        Queue<Integer> queue = new Queue<>();
        queue.enqueue(10);
        queue.enqueue(20);
        queue.enqueue(30);

        System.out.println("Dequeued: " + queue.dequeue());
        System.out.println("Queue size: " + queue.size());
        System.out.println("Queue: " + queue);
    }
}Code language: Java (java)

Explanation:

  • class Queue<T>: A custom generic class, separate from java.util.Queue, backed internally by an ArrayList<T>.
  • elements.remove(0): Removes and returns the first element of the list, which represents the front of the queue, giving FIFO behavior.
  • size(): Delegates to the underlying list’s size() method to report how many elements remain.
  • dequeue(): Since 10 was enqueued first, it is the first one removed, leaving 20 and 30 behind.

Exercise 7: Array-to-List Converter

Problem Statement: Write a generic method that takes an array of type T[] and converts it into a List<T>.

Purpose: This exercise practices moving between the two most common generic containers in Java, arrays and lists, a conversion needed constantly when integrating legacy array-based APIs with collection-based code.

Given Input: String[] languages = {"Java", "Python", "C++"};

Expected Output: [Java, Python, C++]

▼ Hint
  • Create a new empty ArrayList<T> before the loop.
  • Use an enhanced for loop to iterate over the array elements.
  • Add each element to the list, then return the completed list.
▼ Solution & Explanation
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static <T> List<T> arrayToList(T[] array) {
        List<T> list = new ArrayList<>();
        for (T item : array) {
            list.add(item);
        }
        return list;
    }

    public static void main(String[] args) {
        String[] languages = {"Java", "Python", "C++"};
        List<String> list = arrayToList(languages);
        System.out.println(list);
    }
}Code language: Java (java)

Explanation:

  • <T> List<T> arrayToList(T[] array): Declares a generic method whose type parameter T is inferred from whatever array type is passed in.
  • for (T item : array): Iterates over every element of the array in order.
  • list.add(item): Copies each array element into the new list, preserving the original order.
  • arrayToList(languages): Since languages is a String[], the compiler infers T as String, so the returned value is a List<String>.

Exercise 8: Find Maximum Element

Problem Statement: Write a generic method findMax that takes an array of type T and returns the largest element. Restrict T so that it only accepts types implementing Comparable<T>.

Purpose: This exercise introduces bounded type parameters, which restrict a generic type to those that support a specific capability, in this case the ability to be compared to one another.

Given Input: Integer[] numbers = {3, 7, 2, 9, 4};

Expected Output: Maximum value: 9

▼ Hint
  • Bound the type parameter with <T extends Comparable<T>>.
  • Start by assuming the first element is the maximum.
  • Use compareTo() to check each remaining element against the current maximum.
▼ Solution & Explanation
public class Main {
    public static <T extends Comparable<T>> T findMax(T[] array) {
        T max = array[0];
        for (T item : array) {
            if (item.compareTo(max) > 0) {
                max = item;
            }
        }
        return max;
    }

    public static void main(String[] args) {
        Integer[] numbers = {3, 7, 2, 9, 4};
        System.out.println("Maximum value: " + findMax(numbers));
    }
}Code language: Java (java)

Explanation:

  • <T extends Comparable<T>>: Bounds T so only types that implement Comparable<T> can be used, guaranteeing that compareTo() is available.
  • T max = array[0]: Initializes the running maximum with the first element before scanning the rest.
  • item.compareTo(max) > 0: Returns a positive number when item is greater than max, in which case max is updated.
  • findMax(numbers): Since Integer implements Comparable<Integer>, the method compiles and correctly returns 9, the largest value in the array.

Exercise 9: Sum of Numbers

Problem Statement: Write a generic method that takes a List<T> where T is restricted to Number or its subclasses, and returns the sum of all elements as a double.

Purpose: This exercise practices bounding a type parameter to an abstract class rather than an interface, and using the shared doubleValue() method that all Number subclasses provide.

Given Input: List<Integer> numbers = Arrays.asList(10, 20, 30);

Expected Output: Sum: 60.0

▼ Hint
  • Bound the type parameter with <T extends Number>.
  • Every Number subclass, such as Integer or Double, provides a doubleValue() method.
  • Accumulate the sum in a double variable while looping through the list.
▼ Solution & Explanation
import java.util.Arrays;
import java.util.List;

public class Main {
    public static <T extends Number> double sumOfNumbers(List<T> list) {
        double sum = 0.0;
        for (T item : list) {
            sum += item.doubleValue();
        }
        return sum;
    }

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(10, 20, 30);
        System.out.println("Sum: " + sumOfNumbers(numbers));
    }
}Code language: Java (java)

Explanation:

  • <T extends Number>: Bounds T to Number and its subclasses, which include Integer, Double, Long, and others.
  • item.doubleValue(): A method defined on the abstract Number class that converts any numeric wrapper type to a double, letting the method work with mixed numeric types.
  • sum += item.doubleValue(): Accumulates the running total across every element in the list.
  • sumOfNumbers(numbers): Adds 10, 20, and 30 together, producing 60.0 as a double even though the input list held Integer values.

Exercise 10: Property Counter

Problem Statement: Write a generic method to count the number of elements in a collection that match a specific condition. Use a custom functional interface (e.g., UnaryPredicate<T>) to check properties like “is even,” “is prime,” or “is a palindrome.”

Purpose: This exercise combines generics with a custom functional interface and a lambda expression, showing how generic code can accept behavior as a parameter instead of hardcoding a single condition.

Given Input: List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Expected Output: Count of even numbers: 5

▼ Hint
  • Define a functional interface with a single abstract method, such as boolean test(T item).
  • The counting method should accept both a List<T> and an instance of the functional interface as parameters.
  • Loop through the list and call the interface’s method on each element to decide whether to increment the count.
  • Pass a lambda expression like n -> n % 2 == 0 at the call site to define the actual condition.
▼ Solution & Explanation
import java.util.Arrays;
import java.util.List;

interface UnaryPredicate<T> {
    boolean test(T item);
}

public class Main {
    public static <T> int countMatching(List<T> list, UnaryPredicate<T> predicate) {
        int count = 0;
        for (T item : list) {
            if (predicate.test(item)) {
                count++;
            }
        }
        return count;
    }

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        int evenCount = countMatching(numbers, n -> n % 2 == 0);
        System.out.println("Count of even numbers: " + evenCount);
    }
}Code language: Java (java)

Explanation:

  • interface UnaryPredicate<T>: A custom functional interface with one abstract method, test(), that returns a boolean condition for a given element.
  • countMatching(List<T> list, UnaryPredicate<T> predicate): Accepts the condition as a parameter, so the same method can count elements matching any rule without being rewritten.
  • predicate.test(item): Calls whatever logic was passed in at the call site to decide whether the current element satisfies the condition.
  • n -> n % 2 == 0: A lambda expression that implements UnaryPredicate<Integer> inline, checking whether a number is even, and matches 5 of the 10 numbers in the list.

Exercise 11: Bounded Numeric Box

Problem Statement: Create a class NumericBox<T extends Number> that holds a single numeric value. Implement a method isInteger() that returns true if the held value has no fractional part.

Purpose: This exercise practices bounding a class-level type parameter to Number, so every method inside the class can rely on numeric operations like doubleValue() being available.

Given Input: NumericBox<Double> box1 = new NumericBox<>(10.0); NumericBox<Double> box2 = new NumericBox<>(10.5);

Expected Output:

box1 isInteger: true
box2 isInteger: false
▼ Hint
  • Bound the class with <T extends Number> so the field always has numeric methods.
  • Convert the value to a double using doubleValue().
  • Compare the value to its own floor using Math.floor(). If they are equal, there is no fractional part.
▼ Solution & Explanation
class NumericBox<T extends Number> {
    private T value;

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

    public T getValue() {
        return value;
    }

    public boolean isInteger() {
        return value.doubleValue() == Math.floor(value.doubleValue());
    }
}

public class Main {
    public static void main(String[] args) {
        NumericBox<Double> box1 = new NumericBox<>(10.0);
        NumericBox<Double> box2 = new NumericBox<>(10.5);

        System.out.println("box1 isInteger: " + box1.isInteger());
        System.out.println("box2 isInteger: " + box2.isInteger());
    }
}Code language: Java (java)

Explanation:

  • class NumericBox<T extends Number>: Restricts T to Number and its subclasses, so only numeric wrapper types like Integer or Double can be used.
  • value.doubleValue(): Converts the stored value to a primitive double, which is guaranteed to exist because of the Number bound.
  • Math.floor(value.doubleValue()): Rounds the value down to the nearest whole number. If the value already equals its floor, it has no fractional part.
  • box1.isInteger(): Returns true for 10.0 since it equals its floor, and false for 10.5 since it does not.

Exercise 12: Generic Binary Search

Problem Statement: Implement a generic binary search algorithm that works on any sorted array of T, where T extends Comparable<T>.

Purpose: This exercise combines the bounded type parameter pattern with a classic divide-and-conquer algorithm, showing that generic algorithms are not limited to simple utility methods.

Given Input: Integer[] numbers = {2, 5, 8, 12, 16, 23, 38, 45};

Expected Output: Index of 23: 5

▼ Hint
  • Bound the type parameter with <T extends Comparable<T>>.
  • Track a low and high index, and repeatedly check the middle element.
  • Use compareTo() to decide whether to search the left half or the right half.
  • Return -1 if the loop finishes without finding the target.
▼ Solution & Explanation
public class Main {
    public static <T extends Comparable<T>> int binarySearch(T[] arr, T target) {
        int low = 0;
        int high = arr.length - 1;

        while (low <= high) {
            int mid = (low + high) / 2;
            int cmp = arr[mid].compareTo(target);

            if (cmp == 0) {
                return mid;
            } else if (cmp < 0) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        Integer[] numbers = {2, 5, 8, 12, 16, 23, 38, 45};
        int index = binarySearch(numbers, 23);
        System.out.println("Index of 23: " + index);
    }
}Code language: Java (java)

Explanation:

  • <T extends Comparable<T>>: Requires that T can be compared to itself, which is what makes ordering, and therefore binary search, possible.
  • int mid = (low + high) / 2: Calculates the middle index of the current search range on each iteration.
  • arr[mid].compareTo(target): Compares the middle element to the target. A result of zero means a match, negative means the target is further right, and positive means it is further left.
  • low = mid + 1 / high = mid - 1: Narrows the search range based on the comparison, halving the remaining elements to check on each pass.

Exercise 13: Range Checker

Problem Statement: Create a class Range<T extends Comparable<T>> with low and high bounds. Include a method contains(T value) to check if a given value falls within the specified range (inclusive).

Purpose: This exercise applies bounded type parameters to a small reusable class, a pattern useful for validation logic, date ranges, and numeric range checks across many kinds of comparable data.

Given Input: Range<Integer> range = new Range<>(10, 20);

Expected Output:

Contains 15: true
Contains 25: false
▼ Hint
  • Store low and high as fields of type T, set through the constructor.
  • Use compareTo() twice inside contains(): once against low and once against high.
  • Remember that “inclusive” means the value can equal either bound and still count as contained.
▼ Solution & Explanation
class Range<T extends Comparable<T>> {
    private T low;
    private T high;

    public Range(T low, T high) {
        this.low = low;
        this.high = high;
    }

    public boolean contains(T value) {
        return value.compareTo(low) >= 0 && value.compareTo(high) <= 0;
    }
}

public class Main {
    public static void main(String[] args) {
        Range<Integer> range = new Range<>(10, 20);
        System.out.println("Contains 15: " + range.contains(15));
        System.out.println("Contains 25: " + range.contains(25));
    }
}Code language: Java (java)

Explanation:

  • class Range<T extends Comparable<T>>: Bounds T so compareTo() is available for checking a value against the stored bounds.
  • value.compareTo(low) >= 0: True when value is greater than or equal to the lower bound.
  • value.compareTo(high) <= 0: True when value is less than or equal to the upper bound.
  • &&: Combines both checks, so contains() only returns true when the value satisfies both bounds at once, making the range inclusive on both ends.

Exercise 14: Print Unknown List

Problem Statement: Write a method printList that can accept and print elements of a list containing any object type using an unbounded wildcard (List<?>).

Purpose: This exercise introduces the unbounded wildcard, used when a method only needs to read elements generically and does not care what the actual element type is.

Given Input: List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

Expected Output:

Alice
Bob
Charlie
▼ Hint
  • Use List<?> as the parameter type instead of a specific generic type.
  • Inside the method, elements can only be read as Object, since the exact type is unknown.
  • Loop through the list with an enhanced for loop and print each element.
▼ Solution & Explanation
import java.util.Arrays;
import java.util.List;

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

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        printList(names);
    }
}Code language: Java (java)

Explanation:

  • List<?>: An unbounded wildcard meaning the method accepts a list of any element type, whether it holds String, Integer, or any other object.
  • for (Object item : list): Since the exact element type is unknown, each element can only be treated as a generic Object.
  • printList(names): Works even though names is a List<String>, because List<?> accepts a list of any type.
  • Alternative: A generic method <T> void printList(List<T> list) would also work here, but the wildcard version is simpler when the type is never referenced inside the method body.

Exercise 15: List Copier (PECS Principle)

Problem Statement: Write a generic method copyList that copies elements from a source list to a destination list. Apply the Producer Extends, Consumer Super principle to make the method as flexible as possible.

Purpose: This exercise practices the PECS rule directly: the source list only produces values, so it uses extends, while the destination list only consumes values, so it uses super.

Given Input: List<Integer> source = Arrays.asList(1, 2, 3); List<Number> destination = new ArrayList<>();

Expected Output: [1, 2, 3]

▼ Hint
  • The source parameter should be List<? extends T>, since it only produces values to read.
  • The destination parameter should be List<? super T>, since it only consumes values to store.
  • Loop through the source and add each element to the destination.
▼ Solution & Explanation
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    public static <T> void copyList(List<? extends T> source, List<? super T> destination) {
        for (T item : source) {
            destination.add(item);
        }
    }

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

        copyList(source, destination);
        System.out.println(destination);
    }
}Code language: Java (java)

Explanation:

  • List<? extends T> source: The source only produces elements that get read, so it uses extends, allowing any list of T or a subtype of T.
  • List<? super T> destination: The destination only consumes elements being written into it, so it uses super, allowing any list of T or a supertype of T.
  • destination.add(item): Safe to call because the compiler knows the destination can hold at least a T, even though its exact type parameter is unknown.
  • copyList(source, destination): Works even though source is a List<Integer> and destination is a List<Number>, since Integer is a subtype of Number.

Exercise 16: Sum of Wildcard List

Problem Statement: Write a method that calculates the sum of a List<? extends Number>. Inside the method, try to add a new number to this list and observe/explain why the compiler forbids it.

Purpose: This exercise demonstrates why an upper-bounded wildcard list is read-only in practice: the compiler cannot guarantee what specific type the list actually holds, so it blocks any insertion.

Given Input: List<Integer> numbers = Arrays.asList(10, 20, 30);

Expected Output: Sum: 60.0

▼ Hint

Read each element as a Number and accumulate its doubleValue(). Then try adding a literal like 10 to the list and check what error the compiler reports.

▼ Solution & Explanation
import java.util.Arrays;
import java.util.List;

public class Main {
    public static double sumList(List<? extends Number> list) {
        double sum = 0.0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        // list.add(10);  Compiler error: cannot add to a "? extends Number" list
        return sum;
    }

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(10, 20, 30);
        System.out.println("Sum: " + sumList(numbers));
    }
}Code language: Java (java)

Explanation:

  • List<? extends Number>: Accepts a list of Number or any subtype, such as List<Integer> or List<Double>, but the exact subtype is unknown inside the method.
  • for (Number num : list): Reading is always safe, since every possible element type is guaranteed to be at least a Number.
  • list.add(10): The compiler rejects this because it cannot confirm the list is actually a List<Integer>. It could just as easily be a List<Double>, and inserting an Integer into that list would corrupt it.
  • Alternative: If insertion is required, the parameter would need a lower-bounded wildcard like List<? super Integer> instead, as shown in the next exercise.

Exercise 17: Lower-Bounded Element Adder

Problem Statement: Write a method that takes a List<? super Integer> and adds a sequence of integers (e.g., 1 to 10) to it, demonstrating how lower bounds allow insertions.

Purpose: This exercise is the mirror image of the previous one: a lower-bounded wildcard guarantees the list can safely accept Integer values, since every valid type is Integer or one of its supertypes.

Given Input: List<Number> numbers = new ArrayList<>();

Expected Output: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

▼ Hint
  • Use List<? super Integer> as the parameter type.
  • A simple for loop from 1 to 10 can add each value with add().
  • Note that reading elements back out of this list only gives you Object, not Integer.
▼ Solution & Explanation
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }
    }

    public static void main(String[] args) {
        List<Number> numbers = new ArrayList<>();
        addNumbers(numbers);
        System.out.println(numbers);
    }
}Code language: Java (java)

Explanation:

  • List<? super Integer>: Accepts a list of Integer or any supertype, such as Number or Object, guaranteeing it is always safe to insert an Integer.
  • list.add(i): Safe here because every type accepted by the wildcard is known to be a valid container for Integer values.
  • addNumbers(numbers): Works with a List<Number> because Number is a supertype of Integer, satisfying the lower bound.
  • Alternative: Passing a List<Object> instead of List<Number> would also compile, since Object is likewise a supertype of Integer.

Exercise 18: Filter and Collect

Problem Statement: Write a method that takes a List<? extends Number>, filters out numbers greater than a specific threshold, and inserts the passing elements into a destination List<? super Number>.

Purpose: This exercise combines both wildcard directions in a single method, reinforcing the PECS principle in a more realistic scenario where one list is read from and a different list is written to.

Given Input: List<Integer> source = Arrays.asList(5, 15, 25, 35, 45); List<Number> destination = new ArrayList<>(); with threshold 20.0

Expected Output: [5, 15]

▼ Hint
  • The source parameter reads values, so it should use ? extends Number.
  • The destination parameter writes values, so it should use ? super Number.
  • Compare each element’s doubleValue() against the threshold before adding it to the destination.
▼ Solution & Explanation
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    public static void filterAndCollect(List<? extends Number> source, List<? super Number> destination, double threshold) {
        for (Number num : source) {
            if (num.doubleValue() <= threshold) {
                destination.add(num);
            }
        }
    }

    public static void main(String[] args) {
        List<Integer> source = Arrays.asList(5, 15, 25, 35, 45);
        List<Number> destination = new ArrayList<>();

        filterAndCollect(source, destination, 20.0);
        System.out.println(destination);
    }
}Code language: Java (java)

Explanation:

  • List<? extends Number> source: A producer, read-only role, so an upper bound is used to allow any list of Number or a subtype.
  • List<? super Number> destination: A consumer, write-only role, so a lower bound is used to allow any list that can safely hold Number values.
  • num.doubleValue() <= threshold: Filters out elements above the threshold before they get added to the destination list.
  • filterAndCollect(source, destination, 20.0): Only 5 and 15 are at or below 20.0, so those two values end up in the destination list.

Exercise 19: Intersection Finder

Problem Statement: Write a method that takes two lists (List<? extends T> and List<? extends T>) and returns a new List<T> containing only the elements present in both lists.

Purpose: This exercise shows how a generic type parameter T can be combined with wildcards on the input parameters, keeping the return type precise while still accepting flexible input lists.

Given Input: List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5); List<Integer> list2 = Arrays.asList(3, 4, 5, 6, 7);

Expected Output: [3, 4, 5]

▼ Hint
  • Declare the method with its own type parameter T, and accept both lists as List<? extends T>.
  • Create a new List<T> to collect the results.
  • Loop through the first list and use contains() to check whether each element also exists in the second list.
▼ Solution & Explanation
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    public static <T> List<T> findIntersection(List<? extends T> list1, List<? extends T> list2) {
        List<T> result = new ArrayList<>();
        for (T item : list1) {
            if (list2.contains(item)) {
                result.add(item);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
        List<Integer> list2 = Arrays.asList(3, 4, 5, 6, 7);

        System.out.println(findIntersection(list1, list2));
    }
}Code language: Java (java)

Explanation:

  • <T> List<T> findIntersection(...): Declares a type parameter T for the method that determines the element type of the returned list.
  • List<? extends T> list1, List<? extends T> list2: Both parameters are producers, so they use an upper bound, allowing lists of T or any subtype.
  • list2.contains(item): Checks whether the current element from list1 also appears in list2, before adding it to the result.
  • findIntersection(list1, list2): Returns a new List<Integer> containing only 3, 4, and 5, since those are the values shared by both input lists.

Exercise 20: Generic CRUD Repository

Problem Statement: Design a data-access interface Repository<T, ID> mimicking frameworks like Spring Data. Include method signatures for save(T entity), findById(ID id), delete(T entity), and findAll().

Purpose: This exercise applies generics to interface design with two type parameters, one for the entity type and one for its identifier type, the same pattern used by real-world persistence frameworks.

Given Input: UserRepository repo = new UserRepository(); repo.save(new User(1, "Alice")); repo.save(new User(2, "Bob"));

Expected Output:

Optional[User{id=1, name=Alice}]
[User{id=1, name=Alice}, User{id=2, name=Bob}]
▼ Hint
  • Declare the interface with two type parameters: Repository<T, ID>.
  • Use Optional<T> as the return type of findById(), since a matching entity might not exist.
  • Implement the interface with a class backed by an in-memory ArrayList<T> to test it.
▼ Solution & Explanation
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

interface Repository<T, ID> {
    T save(T entity);
    Optional<T> findById(ID id);
    void delete(T entity);
    List<T> findAll();
}

class User {
    int id;
    String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{id=" + id + ", name=" + name + "}";
    }
}

class UserRepository implements Repository<User, Integer> {
    private List<User> users = new ArrayList<>();

    @Override
    public User save(User entity) {
        users.add(entity);
        return entity;
    }

    @Override
    public Optional<User> findById(Integer id) {
        for (User user : users) {
            if (user.id == id) {
                return Optional.of(user);
            }
        }
        return Optional.empty();
    }

    @Override
    public void delete(User entity) {
        users.remove(entity);
    }

    @Override
    public List<User> findAll() {
        return users;
    }
}

public class Main {
    public static void main(String[] args) {
        UserRepository repo = new UserRepository();
        repo.save(new User(1, "Alice"));
        repo.save(new User(2, "Bob"));

        System.out.println(repo.findById(1));
        System.out.println(repo.findAll());
    }
}Code language: Java (java)

Explanation:

  • interface Repository<T, ID>: Uses two type parameters so the same interface can describe a repository for any entity type T identified by any key type ID.
  • Optional<T> findById(ID id): Wraps the result in an Optional so callers must explicitly handle the case where no entity matches the given id.
  • class UserRepository implements Repository<User, Integer>: Fixes both type parameters to concrete types, producing a repository specifically for User entities keyed by Integer.
  • repo.findById(1): Returns Optional.of(user) wrapping the matching User, which prints as Optional[User{id=1, name=Alice}] when passed to println().

Filed Under: Java Exercises

Did you find this page helpful? Let others know about it. Sharing helps me continue to create free Python resources.

TweetF  sharein  shareP  Pin

About Vishal

I’m Vishal Hule, the Founder of PYnative.com. As a Python developer, I enjoy assisting students, developers, and learners. Follow me on Twitter.

Related Tutorial Topics:

Java Exercises

All Coding Exercises:

C Exercises
C++ Exercises
Python Exercises
Java Exercises

Python Exercises and Quizzes

Free coding exercises and quizzes cover Python basics, data structure, data analytics, and more.

  • 15+ Topic-specific Exercises and Quizzes
  • Each Exercise contains 25+ questions
  • Each Quiz contains 25 MCQ
Exercises
Quizzes

Leave a Reply Cancel reply

your email address will NOT be published. all comments are moderated according to our comment policy.

Use <pre> tag for posting code. E.g. <pre> Your entire code </pre>

In: Java Exercises
TweetF  sharein  shareP  Pin

  Java Exercises

  • All Java Exercises
  • Java Exercise for Beginners
  • Java Loops Exercise
  • Java String Exercise
  • Java ArrayList Exercise
  • Java LinkedList Exercise
  • Java HashMap and TreeMap Exercise
  • Java HashSet and TreeSet Exercise
  • Java OOP Exercise
  • Java Methods Exercise
  • Java Enums Exercise
  • Java Exception Handling Exercise
  • Java File Handling Exercise
  • Java Date and Time Exercise
  • Java Data Structures Exercise
  • Java Sorting and Searching Exercise
  • Java Lambda and Functional Interfaces Exercise
  • Java Regex Exercise
  • Java Random Data Generation Exercise
  • Java Generics Exercise
  • Java Reflection Exercise
  • Java JDBC Exercise

All Coding Exercises

Python Exercises C Exercises C++ Exercises Java Exercises

About PYnative

PYnative.com is for Python lovers. Here, You can get Tutorials, Exercises, and Quizzes to practice and improve your Python skills.

Follow Us

To get New Python Tutorials, Exercises, and Quizzes

  • Twitter
  • Facebook
  • Sitemap

Explore Python

  • Learn Python
  • Python Basics
  • Python Databases
  • Python Exercises
  • Python Quizzes
  • Online Python Code Editor
  • Python Tricks

Coding Exercises

  • C Exercises
  • C++ Exercises
  • Python Exercises
  • Java Exercises

Legal Stuff

  • About Us
  • Contact Us

We use cookies to improve your experience. While using PYnative, you agree to have read and accepted our:

  • Terms Of Use
  • Privacy Policy
  • Cookie Policy

Copyright © 2018–2026 pynative.com