Introduction
Welcome to the final lesson of our foundational Java module. In our previous session, we established the principle of "favoring composition over inheritance" as a way to build flexible and maintainable systems from smaller, reusable components. We saw how combining interfaces with composition, as in the Strategy pattern, creates powerful, loosely coupled designs.
Today, we will learn about Java generics, a feature that is absolutely essential for creating the kind of reusable components we've been discussing. Generics allow you to write classes and methods that are not only reusable but also type-safe, meaning the compiler can catch errors that would otherwise only appear at runtime. This lesson directly addresses the learning outcome of using generics to write a type-safe container or method. Mastering this concept is crucial before we move into functional programming and the design patterns that rely heavily on it.
The Problem Before Generics
Before generics were introduced in Java 5, developers faced two major problems when trying to write general-purpose code: a lack of type safety and significant code duplication.
1. Lack of Type Safety
Collections like ArrayList were designed to hold any kind of Object. While this made them general-purpose, it created a dangerous situation. You could put anything into a list, but when you retrieved an item, you had to manually cast it back to its original type. If you made a mistake and added the wrong type of object, the error would only be discovered at runtime, causing your program to crash with a ClassCastException.
Let's examine what this looked like.
Java Programming Tutorial on Generics
This section from a tutorial by NTU Singapore clearly illustrates the pitfalls of pre-generic collections.
Please read the subsection titled "Example 2: Pre-Generic Collections (Pre-JDK 5) are not Compile-Time Type-Safe". Focus on the code example. Notice how an Integer is accidentally added to a list intended for Strings, and how the program compiles without error but crashes at runtime.
This runtime error is exactly what we want to avoid in robust software. The compiler had no information about our intent—that the list should only contain Strings.

2. Code Duplication
The alternative to using Object was to create a separate class for every data type you wanted to support. This led to a massive amount of repetitive, hard-to-maintain code. The following video explains this problem perfectly.
Generics In Java - Full Simple Tutorial
This video from the "Coding with John" channel demonstrates the code duplication issue that existed before generics.
Watch the segment from the beginning, where the presenter creates an IntegerPrinter, then a DoublePrinter, and a StringPrinter to highlight how unmanageable this approach becomes.
The Solution: Generic Classes and Methods
Generics solve both of these problems by allowing you to define classes and methods with a placeholder for a type, known as a type parameter. This lets you write a single, reusable component that is also fully type-safe.
Creating a Generic Class
Let's see how the Printer class from the video can be transformed into a single, generic class.
Generics In Java - Full Simple Tutorial
This part of the video shows the solution to the code duplication problem.
Continue watching from this section. Pay close attention to how the angle brackets <T> are used to introduce a type parameter, making the class generic and reusable for any type like Integer, Double, or String.
The syntax <T> introduces a formal type parameter. By convention, T stands for "Type", E for "Element" (in collections), K for "Key", and V for "Value". When you instantiate the class, you provide an actual type parameter, like String in Printer<String>. The compiler then ensures that only objects of that type can be used with that instance.

Writing a Type-Safe Container
Now, let's apply this to the "write a type-safe container" part of our learning goal. We'll look at the process of building a simplified ArrayList, first without generics to see the problems, and then with generics to see the solution.
Java Programming Tutorial on Generics
This resource walks through the creation of a custom array list, highlighting the power of generics.
First, navigate to the section "Example 1: Non-Type-Safe Non-Generic MyArrayList". Read the introduction and review the code for MyArrayList.java. Then, look at the test code and the drawbacks described, which are the same ones we've discussed: explicit downcasting and runtime errors. Next, scroll down to "Example 3: Type-Safe MyGenericArrayList". Read the brief introduction and examine the code for MyGenericArrayList<E>. Notice how the add method now takes an E and the get method returns an E. Finally, read the explanation that follows. It summarizes how the generic version provides compile-time type checking, eliminating the ClassCastException and the need for manual casts.
This before-and-after comparison is the essence of why generics are so fundamental to modern Java. They allow library creators (and you!) to provide components that are both flexible and safe.
Creating a Generic Method
Generics aren't limited to classes. You can also create standalone generic methods. A generic method declares its own type parameter(s) before the return type. This is useful for static utility methods that operate on different types.
The "Bro Code" channel has a very direct and clear example of creating a generic method.
Watch this segment from the beginning. The presenter shows how, instead of writing four separate displayArray methods for integers, doubles, etc., you can write a single generic method displayArray<T>(T[] array).
Bounded Types: Connecting Generics with Inheritance
What if you want to write a generic class that works with a range of types, but not every possible type? For example, a method that only works on numbers, or, connecting back to our previous lessons, a class that only works with objects that are Animals. This is where bounded types come in.
Using the extends keyword, you can constrain a type parameter to be a subclass of a specific class or an implementation of an interface. This is incredibly powerful because it allows you to call methods from the specified bound, blending the flexibility of generics with the power of polymorphism.
Generics In Java - Full Simple Tutorial
"Coding with John" provides an excellent, practical example of bounded generics.
Please watch the section from this timestamp. The example public class Printer<T extends Animal> is key. Notice how, because T is guaranteed to be a subtype of Animal, you are now allowed to call methods like thingToPrint.eat() from within the generic class—something that was impossible with an unbounded T.
Note that in the context of generics, the extends keyword is used for both classes and interfaces. So, <T extends Runnable> is also valid. This feature is fundamental to many advanced design patterns and library designs.
A brief note on implementation: Java achieves this through a process called type erasure. The compiler uses the generic type information for checks and then "erases" it, replacing the type parameters with Object (or the bounded type, like Animal). It also automatically inserts the necessary casts. This ensures that the generated bytecode is compatible with older versions of Java, but it's a detail you rarely need to worry about in practice.
Conclusion
In this lesson, we've unlocked one of the most powerful features in the Java language. We saw how generics solve the critical issues of type safety and code duplication that plagued early Java code.
Here are the key takeaways:
- Generics provide compile-time type safety, catching errors that would otherwise cause runtime crashes (
ClassCastException). - Generics enable code reuse by allowing you to write a single class or method that can operate on many different data types.
- You create a generic class using a type parameter like
<T>in the class declaration (e.g.,class Box<T>). - You create a generic method by declaring the type parameter before the return type (e.g.,
static <T> void printArray(T[] array)). - Bounded types (e.g.,
<T extends Animal>) constrain the types that can be used and allow you to call methods from the bound, combining generics with polymorphism.
This lesson concludes our foundational module. You now have the essential object-oriented and core Java tools—encapsulation, polymorphism, composition, and generics—needed to tackle design patterns effectively.
Our next lesson will begin a new module, "Functional Java and Reflection." We will start with lambda expressions, a modern Java feature that allows you to treat code as data. You will see immediately that the functional interfaces lambdas implement are themselves generic, making today's lesson a direct prerequisite.