- Lab
- Core Tech

Guided: Java Generics and Wildcards
This code lab will teach you how to effectively use Java generics and wildcards to build type-safe and flexible systems. You'll learn essential patterns like bounded type parameters, wildcard bounds (PECS principle), and generic methods, while understanding critical concepts like type erasure. By completing this lab, you'll have practical experience creating robust generic components that can be applied to your own Java applications.

Path Info
Table of Contents
-
Challenge
Introduction
Welcome to the lab Guided: Java Generics and Wildcards.
Java generics provide a way to create classes, interfaces, and methods that can work with different types while maintaining type safety at compile time.
Consider this scenario:
List items = new ArrayList(); items.add("Hello"); items.add("world!"); items.add(1); // Mixed types allowed! String text = (String) items.get(2); // Runtime error!
This code compiles but fails at runtime with a
ClassCastException
. Generics prevent such errors by enforcing type safety at compile time:List<String> items = new ArrayList<>(); items.add("Hello"); items.add("world!"); items.add(42); // Compile error - type safety enforced!
In this lab, you'll work with a Library Collection Management application that includes the following functionality:
- Managing different types of library items (books, magazines, newspapers) using generic collections
- Adding items to collections while maintaining type safety
- Transferring items between collections using wildcard bounds
- Filtering items using generic utility methods
- Preventing type erasure and runtime behavior problems
By the end of the lab, you will understand how to leverage Java's generic type system to build type-safe and flexible collection-based solutions. ### Familiarizing with the Program Structure
The application includes the following classes in the
src/main/java
directory:com.pluralsight.model.LibraryItem
: An abstract base class representing any item that can be stored in the library.com.pluralsight.model.Book
,com.pluralsight.model.Magazine
, andcom.pluralsight.model.Newspaper
: Concrete classes extendingLibraryItem
.com.pluralsight.collection.LibraryCollection
: A generic collection class that manages items of a specific type.com.pluralsight.util.LibraryUtils
: A utility class containing a generic method for filtering collections.com.pluralsight.Main
: A class demonstrating the usage of generic collections and methods.
All model classes, including
LibraryItem
and its subclasses, as well as theMain
class, are fully implemented. TheLibraryCollection
andLibraryUtils
classes are partially implemented and include placeholders for you to complete.You can compile and run the application using the Run button located at the bottom-right corner of the Terminal. Initially, the application will compile but will not work correctly in some cases.
Begin by examining the code to understand the program's structure. Once you're ready, start coding. If you encounter issues, a
solution
directory is available for reference. It contains subdirectories namedstep2
,step3
, and so on, each corresponding to a step in the lab. Within each subdirectory, solution files follow the naming convention[file]-[step]-[task].java
(e.g.,LibraryCollection-2-1.java
instep2
), where[file]
represents the file name, the first number indicates the step, and the second indicates the task. -
Challenge
Generic Classes with Type Bounds
Generic Types
A generic type (or type parameter) is represented by a single uppercase letter, typically starting with
T
. This naming convention helps distinguish type parameters from regular class names. Common type parameter names include:T
for a general typeE
for element typeK
andV
for key and value typesU
,S
for additional types whenT
is already in use
You can use any valid identifier, but following these conventions makes your code more readable and consistent with standard Java practices.
When creating generic classes, you define the type parameter in angle brackets after the class name:
public class Box<T> { private T content; public void store(T item) { this.content = item; } public T retrieve() { return content; } }
To use this generic class, you specify the actual type when creating instances:
Box<String> stringBox = new Box<>(); stringBox.store("Hello"); String text = stringBox.retrieve(); Box<Integer> numberBox = new Box<>(); numberBox.store(42); int number = numberBox.retrieve();
The
LibraryCollection
class already defines the type parameterT
, but its fields and methods don't use it yet. You'll address this in the next task, except for theaddToCollection
method, which will be handled in a later task. ## Type BoundsType bounds restrict what types can be used with a generic class or method. The
extends
keyword sets an upper bound, meaning the type parameter must be or extend the specified type:public class NumberBox<T extends Number> { private T value; public double getValue() { return value.doubleValue(); // Safe because T extends Number } }
In this example,
NumberBox
can only work withNumber
and its subclasses (Integer
,Double
, etc.):NumberBox<Integer> intBox = new NumberBox<>(); // Valid NumberBox<Double> doubleBox = new NumberBox<>(); // Valid NumberBox<String> stringBox = new NumberBox<>(); // Won't compile!
Currently, the
LibraryCollection
class can work with any type, but it should only manage subclasses ofLibraryItem
. You'll add this restriction in the next task. -
Challenge
Working with Wildcard Bounds
While type bounds let you restrict a generic class or method to certain types, wildcards provide flexibility when working with different type arguments. Wildcards use the
?
symbol and can be bounded in two ways:- Upper-bounded wildcards (
? extends Type
): Allow read-only access to a collection that produces values - Lower-bounded wildcards (
? super Type
): Allow write-only access to a collection that consumes values
This leads to the Producer Extends Consumer Super (PECS) principle:
- Use
extends
when you want to get values out of a collection (producer) - Use
super
when you want to put values into a collection (consumer)
Here's a simple example:
// Producer (extends) - can read Numbers from the list public double sum(List<? extends Number> numbers) { double sum = 0; for (Number n : numbers) { // Safe to read sum += n.doubleValue(); } return sum; } // Consumer (super) - can write Integers to the list public void addNumbers(List<? super Integer> list) { list.add(1); // Safe to write list.add(2); // Safe to write }
The first method can read from lists of
Integer
,Double
, or any otherNumber
subtype. The second method can write to lists ofInteger
,Number
, orObject
.In the
LibraryCollection
class, theaddItems
method adds items from another collection to the library collection. Currently, it accepts aCollection<T>
type. However, since this method reads from the input collection to add items to the library collection, it needs a proper type bound. In theLibraryCollection
class, theaddToCollection
method inLibraryCollection
transfers all items from the library collection to another collection. Currently, it accepts a rawCollection
type, which isn't type-safe. Since this method writes items to the destination collection, it needs a proper type bound. - Upper-bounded wildcards (
-
Challenge
Generic Utility Methods
Generic methods let you introduce type parameters specific to that method, regardless of whether the class itself is generic. You declare the type parameter before the return type using the syntax
<T>
:public static <T> List<T> firstTwo(List<T> list) { return list.stream() .limit(2) .collect(Collectors.toList()); }
And here's an example of how to call this method:
// Create a list of Strings List<String> names = List.of("A", "B", "C", "D"); // Call the firstTwo method wit the String list List<String> firstTwoNames = firstTwo(names); // Create a list of Integers List<Integer> numbers = List.of(10, 20, 30, 40); // Call the firstTwo method with the Integer list List<Integer> firstTwoNumbers = firstTwo(numbers);
Like class-level type parameters, method type parameters can have bounds:
public static <T extends Comparable<T>> T findMax(Collection<T> items) { return items.stream() .max(T::compareTo) .orElseThrow(); }
When using a bounded type parameter in a method:
- Declare it in the angle brackets before the return type
- Use it in the parameter list and return type
- Use it in the method body when creating new collections or working with the type
The
filterByTitle
method inLibraryUtils
searches for items whose titles contain a specific keyword. Currently, it uses raw types, which isn't type-safe. The method needs a generic type parameter with an appropriate bound since it works with library items. -
Challenge
Understanding Type Erasure
Type erasure is a fundamental aspect of Java generics. While generics provide compile-time type safety, Java removes (erases) the type information at runtime. This means generic types exist only during compilation.
Here's what happens during type erasure:
- Generic type parameters are replaced with their bounds or
Object
if unbounded. For example, if a type parameter is declared as<T extends Number>
, the type parameterT
would be replaced withNumber
during type erasure, rather thanObject
. - Type casts are inserted where necessary
- Bridge methods are created to maintain polymorphism
For example, this code:
List<String> strings = new ArrayList<>(); strings.add("Hello"); String first = strings.get(0);
Effectively becomes this at runtime:
List strings = new ArrayList(); strings.add((Object)"Hello"); String first = (String)strings.get(0);
Type erasure introduces several challenges, for example:
- You cannot create arrays of generic types
- Runtime type information is limited
- Method overloading with different generic types isn't possible
- Reflection can't fully access generic type information
Understanding these limitations is important for working effectively with generics and avoiding potential runtime errors. Let's explore three of them. ### Generic Array Creation
Creating arrays of generic types poses a unique challenge in Java. Due to type erasure, the Java Virtual Machine doesn't know the actual type
T
at runtime, making it impossible to create an array using the standardnew T[]
syntax.For example:
public class Box<T> { // The following line won't compile: private T[] array = new T[10]; }
To work around this limitation, you must create an array of the erasure type and then safely cast it. In other words:
- Create an array of the appropriate base type
- Cast it safely to the generic array type
- Use the
@SuppressWarnings("unchecked")
annotation to handle the unavoidable unchecked cast warning
In the next task, you'll modify the
createArray
method of theLibraryCollection
class to use this technique. ### Runtime Type CheckingDue to type erasure, checking if an object matches a generic type at runtime is challenging. The
instanceof
operator can only check against concrete types, not type parameters. For example, given aList<String>
, you can check if an object is aList
but not if it's specifically aList<String>
.However, when you store a
Class
object representing the type parameter (like theLibraryCollection
class does with thetype
field), you can use itsisInstance
method to perform runtime type checking:Class<String> stringClass = String.class; // Create a List of Strings List<String> stringList = new ArrayList<>(); stringList.add("Hi"); // Check the type of the first element if (stringClass.isInstance(stringList.get(0))) { System.out.println("The first element of stringList is a String."); } else { System.out.println("The first element of stringList is not a String."); }
This approach provides a way to verify if an object matches the exact type parameter of your generic class.
In the next task, you'll modify the
isItemOfType
method of theLibraryCollection
class to use this approach. ### Type Safety RisksType erasure can lead to potential type safety risks when working with raw types. While generic types provide compile-time safety, you can bypass these checks using raw types due to type erasure. Here's an example:
List<String> strings = new ArrayList<>(); List rawList = strings; // Create raw type reference rawList.add(true); // Compiles but breaks type safety String s = strings.get(0); // Runtime ClassCastException!
In the next task, you'll complete the implementation of the
addItemUnsafe
method to demonstrate this vulnerability by intentionally bypassing generic type checking. This will help you understand why avoiding raw types is important for maintaining type safety in your code. - Generic type parameters are replaced with their bounds or
-
Challenge
Conclusion
Congratulations on successfully completing this Code Lab!
To compile and run the application, you can either click the Run button in the bottom-right corner of the screen or use the Terminal with the following commands:
-
Compile and package the application:
mvn clean package
-
Execute the application:
java -cp target/library-collection-manager-1.0-SNAPSHOT.jar com.pluralsight.Main
The program output demonstrates each concept covered:
- The Basic Collection Usage section shows type-safe operations with a collection of books
- The Wildcard Bounds section demonstrates how you can work with different types of library items using bounded wildcards
- The Filtering section shows the generic utility method in action
- The Type Erasure section illustrates three key concepts:
- Generic array creation and its runtime type
- Runtime type checking with stored type information
- Type safety risks, showing how type erasure can lead to runtime errors if you bypass generic type checking
Review the
Main
class to see how the output is generated. ---Extending the Program
Here are some ideas to further enhance your skills and extend the application's capabilities:
-
Sorting Capabilities:
- Introduce a generic
Comparator<T extends LibraryItem>
for flexible sorting. Add methods to sort by title, date, or type, and implement a composite comparator to combine multiple sorting criteria.
- Introduce a generic
-
Advanced Search Functionality:
- Develop a generic search system using the criteria pattern. Enable multi-field searches and build type-safe query builders for constructing complex queries.
-
Collection Operations:
- Implement generic methods for set operations like union, intersection, and difference. Add collection transformations using mapping functions and create type-safe views with bounded wildcards. ---
Related Courses in Pluralsight's Library
If you'd like to continue building your Java skills or explore related topics, check out these courses available on Pluralsight:
These courses cover a wide range of Java programming topics. Explore them to further your learning journey in Java!
-
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.