Featured resource
pluralsight tech forecast
2025 Tech Forecast

Which technologies will dominate in 2025? And what skills do you need to keep up?

Check it out
Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

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.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 1h 7m
Published
Clock icon Dec 11, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. 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, and com.pluralsight.model.Newspaper: Concrete classes extending LibraryItem.
    • 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 the Main class, are fully implemented. The LibraryCollection and LibraryUtils 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 named step2, 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 in step2), where [file] represents the file name, the first number indicates the step, and the second indicates the task.

  2. 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 type
    • E for element type
    • K and V for key and value types
    • U, S for additional types when T 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 parameter T, but its fields and methods don't use it yet. You'll address this in the next task, except for the addToCollection method, which will be handled in a later task. ## Type Bounds

    Type 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 with Number 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 of LibraryItem. You'll add this restriction in the next task.

  3. 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:

    1. Upper-bounded wildcards (? extends Type): Allow read-only access to a collection that produces values
    2. 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 other Number subtype. The second method can write to lists of Integer, Number, or Object.

    In the LibraryCollection class, the addItems method adds items from another collection to the library collection. Currently, it accepts a Collection<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 the LibraryCollection class, the addToCollection method in LibraryCollection transfers all items from the library collection to another collection. Currently, it accepts a raw Collection type, which isn't type-safe. Since this method writes items to the destination collection, it needs a proper type bound.

  4. 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:

    1. Declare it in the angle brackets before the return type
    2. Use it in the parameter list and return type
    3. Use it in the method body when creating new collections or working with the type

    The filterByTitle method in LibraryUtils 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.

  5. 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 parameter T would be replaced with Number during type erasure, rather than Object.
    • 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:

    1. You cannot create arrays of generic types
    2. Runtime type information is limited
    3. Method overloading with different generic types isn't possible
    4. 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 standard new 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:

    1. Create an array of the appropriate base type
    2. Cast it safely to the generic array type
    3. Use the @SuppressWarnings("unchecked") annotation to handle the unavoidable unchecked cast warning

    In the next task, you'll modify the createArray method of the LibraryCollection class to use this technique. ### Runtime Type Checking

    Due 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 a List<String>, you can check if an object is a List but not if it's specifically a List<String>.

    However, when you store a Class object representing the type parameter (like the LibraryCollection class does with the type field), you can use its isInstance 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 the LibraryCollection class to use this approach. ### Type Safety Risks

    Type 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.

  6. 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:

    1. Compile and package the application:

      mvn clean package
      
    2. 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:
      1. Generic array creation and its runtime type
      2. Runtime type checking with stored type information
      3. Type safety risks, showing how type erasure can lead to runtime errors if you bypass generic type checking

    Review the Mainclass 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:

    1. 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.
    2. 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.
    3. 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!

Esteban Herrera has more than twelve years of experience in the software development industry. Having worked in many roles and projects, he has found his passion in programming with Java and JavaScript. Nowadays, he spends all his time learning new things, writing articles, teaching programming, and enjoying his kids.

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.