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, andQueue. - 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
Tto store the value. - The
set()method should accept a parameter of typeTand assign it to the field. - The
get()method should simply return the stored field. isEmpty()can check whether the stored field isnull.
▼ Solution & Explanation
Explanation:
class Box<T>: Declares a generic class with a type parameterT, which acts as a placeholder for whatever type is supplied when the class is used.private T value: Stores the object of typeTinside the box.Box<String> box = new Box<>(): Creates a box that is restricted to holdingStringvalues. The diamond operator<>lets the compiler infer the type on the right side.isEmpty(): Returnstruewhen no value has been set yet, since the default value of an object field isnull.
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
KandV, 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
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()andgetValue(): 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
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 overridetoString()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()andset(). - Move the pointers toward the middle and stop once they meet or cross.
▼ Solution & Explanation
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 genericListinterface methods to read and overwrite elements without needing array-style indexing.new ArrayList<>(Arrays.asList(...)): Wraps the fixed-size list fromArrays.asList()in a resizable, mutableArrayListsoset()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, andpop()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
Explanation:
class Stack<T>: A custom generic class, separate fromjava.util.Stack, backed internally by anArrayList<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 withget()without removing it, so the stack is unchanged after the call.push(10); push(20); push(30): Adds elements in order, so30ends up on top and is the first one returned bypop()orpeek().
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
Explanation:
class Queue<T>: A custom generic class, separate fromjava.util.Queue, backed internally by anArrayList<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’ssize()method to report how many elements remain.dequeue(): Since10was enqueued first, it is the first one removed, leaving20and30behind.
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
forloop to iterate over the array elements. - Add each element to the list, then return the completed list.
▼ Solution & Explanation
Explanation:
<T> List<T> arrayToList(T[] array): Declares a generic method whose type parameterTis 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): Sincelanguagesis aString[], the compiler infersTasString, so the returned value is aList<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
Explanation:
<T extends Comparable<T>>: BoundsTso only types that implementComparable<T>can be used, guaranteeing thatcompareTo()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 whenitemis greater thanmax, in which casemaxis updated.findMax(numbers): SinceIntegerimplementsComparable<Integer>, the method compiles and correctly returns9, 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
Numbersubclass, such asIntegerorDouble, provides adoubleValue()method. - Accumulate the sum in a
doublevariable while looping through the list.
▼ Solution & Explanation
Explanation:
<T extends Number>: BoundsTtoNumberand its subclasses, which includeInteger,Double,Long, and others.item.doubleValue(): A method defined on the abstractNumberclass that converts any numeric wrapper type to adouble, letting the method work with mixed numeric types.sum += item.doubleValue(): Accumulates the running total across every element in the list.sumOfNumbers(numbers): Adds10,20, and30together, producing60.0as adoubleeven though the input list heldIntegervalues.
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 == 0at the call site to define the actual condition.
▼ Solution & Explanation
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 implementsUnaryPredicate<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
doubleusingdoubleValue(). - Compare the value to its own floor using
Math.floor(). If they are equal, there is no fractional part.
▼ Solution & Explanation
Explanation:
class NumericBox<T extends Number>: RestrictsTtoNumberand its subclasses, so only numeric wrapper types likeIntegerorDoublecan be used.value.doubleValue(): Converts the stored value to a primitivedouble, which is guaranteed to exist because of theNumberbound.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(): Returnstruefor10.0since it equals its floor, andfalsefor10.5since 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
lowandhighindex, 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
Explanation:
<T extends Comparable<T>>: Requires thatTcan 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
lowandhighas fields of typeT, set through the constructor. - Use
compareTo()twice insidecontains(): once againstlowand once againsthigh. - Remember that “inclusive” means the value can equal either bound and still count as contained.
▼ Solution & Explanation
Explanation:
class Range<T extends Comparable<T>>: BoundsTsocompareTo()is available for checking a value against the stored bounds.value.compareTo(low) >= 0: True whenvalueis greater than or equal to the lower bound.value.compareTo(high) <= 0: True whenvalueis less than or equal to the upper bound.&&: Combines both checks, socontains()only returnstruewhen 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
forloop and print each element.
▼ Solution & Explanation
Explanation:
List<?>: An unbounded wildcard meaning the method accepts a list of any element type, whether it holdsString,Integer, or any other object.for (Object item : list): Since the exact element type is unknown, each element can only be treated as a genericObject.printList(names): Works even thoughnamesis aList<String>, becauseList<?>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
Explanation:
List<? extends T> source: The source only produces elements that get read, so it usesextends, allowing any list ofTor a subtype ofT.List<? super T> destination: The destination only consumes elements being written into it, so it usessuper, allowing any list ofTor a supertype ofT.destination.add(item): Safe to call because the compiler knows the destination can hold at least aT, even though its exact type parameter is unknown.copyList(source, destination): Works even thoughsourceis aList<Integer>anddestinationis aList<Number>, sinceIntegeris a subtype ofNumber.
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
Explanation:
List<? extends Number>: Accepts a list ofNumberor any subtype, such asList<Integer>orList<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 aNumber.list.add(10): The compiler rejects this because it cannot confirm the list is actually aList<Integer>. It could just as easily be aList<Double>, and inserting anIntegerinto 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
forloop from 1 to 10 can add each value withadd(). - Note that reading elements back out of this list only gives you
Object, notInteger.
▼ Solution & Explanation
Explanation:
List<? super Integer>: Accepts a list ofIntegeror any supertype, such asNumberorObject, guaranteeing it is always safe to insert anInteger.list.add(i): Safe here because every type accepted by the wildcard is known to be a valid container forIntegervalues.addNumbers(numbers): Works with aList<Number>becauseNumberis a supertype ofInteger, satisfying the lower bound.- Alternative: Passing a
List<Object>instead ofList<Number>would also compile, sinceObjectis likewise a supertype ofInteger.
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
Explanation:
List<? extends Number> source: A producer, read-only role, so an upper bound is used to allow any list ofNumberor a subtype.List<? super Number> destination: A consumer, write-only role, so a lower bound is used to allow any list that can safely holdNumbervalues.num.doubleValue() <= threshold: Filters out elements above the threshold before they get added to the destination list.filterAndCollect(source, destination, 20.0): Only5and15are at or below20.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 asList<? 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
Explanation:
<T> List<T> findIntersection(...): Declares a type parameterTfor 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 ofTor any subtype.list2.contains(item): Checks whether the current element fromlist1also appears inlist2, before adding it to the result.findIntersection(list1, list2): Returns a newList<Integer>containing only3,4, and5, 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 offindById(), 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
Explanation:
interface Repository<T, ID>: Uses two type parameters so the same interface can describe a repository for any entity typeTidentified by any key typeID.Optional<T> findById(ID id): Wraps the result in anOptionalso 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 forUserentities keyed byInteger.repo.findById(1): ReturnsOptional.of(user)wrapping the matchingUser, which prints asOptional[User{id=1, name=Alice}]when passed toprintln().

Leave a Reply