Introduction
Hello! In our previous lesson, we explored how interfaces and abstract classes are used to define contracts and create flexible class hierarchies. We saw that they define the shape of an object's behavior—what methods it must have.
Today, we'll build directly on that foundation by introducing Java Generics. Your goal is to learn how to use generics to create classes and methods that are not only reusable but also type-safe. Generics allow you to write code that works with any object type while ensuring that you don't accidentally mix incompatible types, catching errors at compile time instead of at runtime.
For someone with your background in C++, generics will seem conceptually similar to templates. However, as we'll discover, their implementation in Java—a mechanism called "type erasure"—is quite different and has important consequences for how you write and think about generic code.
The Problem: Code Duplication and Lack of Type Safety
Imagine you need a simple class that holds and prints a value. Without generics, you'd have to write a separate class for each data type: an IntegerPrinter, a StringPrinter, a DoublePrinter, and so on. This leads to significant code duplication.
A naive solution might be to create a single ObjectPrinter class that holds a value of type Object. While this works, it destroys type safety. The compiler has no idea what type of object is actually being stored, so you lose the ability to catch errors. For example, you could accidentally retrieve an Integer from the printer and try to assign it to a String variable, which would only fail at runtime with a ClassCastException.
Generics solve both of these problems elegantly. Let's see how.
Generics In Java - Full Simple Tutorial
This video from the 'Coding with John' channel provides an excellent, step-by-step motivation for generics. It starts with the exact 'Printer' problem we just discussed and refactors it into a generic solution.
Please watch from the beginning to 06:14. Focus on: The code duplication problem with IntegerPrinter and DoublePrinter. How the code is refactored into a single, generic Printer<T> class. The syntax for defining the type parameter <T> and using it within the class. How to instantiate the generic class with a specific type argument, like Printer<Integer>.
As the video demonstrates, by declaring class Printer<T>, you create a placeholder T for a type that will be specified later. This makes the class a template that can be instantiated for any type, like Printer<Integer> or Printer<String>, giving you both reusability and compile-time type safety.
The Core Benefit: Compile-Time Type Safety
The most significant advantage of generics is catching type errors during compilation. Let's solidify this concept using Java's List, which you have likely seen. Before generics, a list was just a list of Objects.
// Pre-generics style (raw type) - DON'T DO THIS!
List names = new ArrayList();
names.add("Alice");
names.add("Bob");
names.gadd(Integer.valueOf(42)); // Whoops! A number was added to a list of names.
String lastName = (String) names.get(2); // Throws ClassCastException at RUNTIME
With generics, you declare the type of element the list is intended to hold. The compiler then enforces this contract.
// Modern, generic style - DO THIS!
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add(Integer.valueOf(42)); // COMPILE ERROR! The compiler stops you here.
This simple shift moves error detection from runtime to compile-time, which is fundamental to building robust applications.
The following image shows a simple, complete example of a generic class in action.

The C++ Connection: How Generics Work via Type Erasure
Your experience with C++ templates will be very helpful here. In C++, std::vector<int> and std::vector<double> are two completely different types, generated by the compiler. Java takes a different approach called type erasure.
During compilation, the Java compiler:
- Checks your code for type safety using the generic type information.
- Erases the generic type parameters and replaces them with their bound (or
Objectif unbounded). - Inserts the necessary type casts automatically and invisibly.
This means that at runtime, the JVM has no knowledge of the generic types. For the JVM, ArrayList<String> and ArrayList<Integer> are both just ArrayList.
This has two immediate, practical consequences:
- You can't use primitive types (like
int,double) as generic type arguments because they don't descend fromObject. You must use their wrapper classes (Integer,Double). - You cannot ask an object for its generic type at runtime.
new ArrayList<String>().getClass()is the same asnew ArrayList<Integer>().getClass().
This distinction from C++ templates is crucial. The following reading from a Stack Overflow discussion explains this perfectly.
What makes a template different from a generic? - Stack Overflow
This highly-regarded answer on Stack Overflow directly contrasts C++ templates with Java generics, focusing on the key concept of type erasure.
Please read the short answer starting with "I'll confine my answers to C++ templates vs. Java generics." It contains a small code example (Cell<E>) that demonstrates how different generic instantiations result in the same class at runtime.
Advanced Techniques: Generic Methods, Bounded Types, and Wildcards
Now that you understand the "what" and "why," let's explore some more powerful features you'll frequently encounter.
Generic Methods
You can also make individual methods generic, independent of whether their class is generic. The type parameter is declared right before the method's return type.
public class ArrayUtils {
// This is a generic method
public static <T> List<T> fromArrayToList(T[] a) {
return Arrays.stream(a).collect(Collectors.toList());
}
}
// You can call it with different types
Integer[] intArray = {1, 2, 3};
List<Integer> intList = ArrayUtils.fromArrayToList(intArray);
String[] stringArray = {"A", "B", "C"};
List<String> stringList = ArrayUtils.fromArrayToList(stringArray);
Bounded Type Parameters
Sometimes, you need to ensure that a generic type has certain capabilities. For example, you might write a sorting method that only works for types that can be compared. You can enforce this using an upper bound with the extends keyword.
<T extends Comparable<T>> means "T can be any type as long as it implements the Comparable interface."
This is powerful because it allows you to call methods from the bounding type on your generic objects.
Generics In Java - Full Simple Tutorial
The 'Coding with John' video we saw earlier also has an excellent segment on bounded generics. It uses a clear Animal example to show how this works.
Please watch the segment from 09:40 to 12:01. Notice how using <T extends Animal> allows the print method to call the eat() method on the generic object, something that wouldn't be possible without the bound.
You can also have multiple bounds, such as <T extends Number & Serializable>. If a class is used as a bound, it must be listed first.
Wildcards (?)
Wildcards are used to create more flexible APIs, especially when dealing with collections. Consider a method that prints a list of numbers. You might write:
public void printList(List<Number> list)
This seems correct, but it's overly restrictive. Due to the way generics work, a List<Integer> is not a subtype of List<Number>, so you couldn't pass a list of integers to this method.
The solution is an upper-bounded wildcard: ? extends Type.
public void printList(List<? extends Number> list)
This means "a list of some unknown type that is a subclass of Number." This is now flexible and will accept List<Integer>, List<Double>, etc. The trade-off is that you cannot add elements to a collection defined with ? extends because the compiler doesn't know the exact type (Integer? Double?). You can only safely retrieve elements, which you know will be at least a Number.
This concept is key to understanding many APIs in the Spring ecosystem and the Java Collections Framework.

Conclusion
In this lesson, we've unpacked Java Generics, a cornerstone of modern Java development. You've learned how they provide reusability and, most critically, compile-time type safety.
Here are the key takeaways:
- Purpose: Generics allow you to write classes and methods that operate on objects of various types while providing compile-time type checking.
- Mechanism: They are implemented via type erasure, where type parameters are checked by the compiler and then removed. This is a key difference from C++ templates.
- Syntax: You define generic classes with
<T>and generic methods with<T>before the return type. - Bounded Types: Use
<T extends SomeType>to restrict a generic type parameter and allow you to call methods from the bounding type. - Wildcards: Use
List<? extends SomeType>to create flexible methods that can accept collections of various subtypes.
In our next lesson, we will explore Java annotations. These are a form of metadata you can add to your code. While generics give us compile-time type safety, annotations will give us a powerful way to configure application behavior and drive framework functionality, forming the magic behind Spring's @Component, @Autowired, and many other features.