A generic class is a class that declares one or more type parameters in its header and uses them inside its body like real types. The previous lesson explained why generics exist and what raw types cost; this one shows how to write your own parameterized class so a single definition can hold any kind of element. We'll cover the syntax, multiple type parameters, instantiation with the diamond operator, type parameter scope, and why Box<String> and Box<Integer> are unrelated to each other at the type level.
A generic class looks like a normal class with an extra pair of angle brackets after the class name. The names inside the brackets are type parameters: placeholders that act like types throughout the class body. A single-letter convention is universal: T for "type", E for "element", K and V for "key" and "value". The names are arbitrary, but the convention is widely recognized, so stick with it unless there's a reason not to.
Here's a Box class that can hold any single value:
T appears in four places: the class header, the field type, the constructor parameter, and the getter's return type. Inside the class body, T behaves like any other type name. Fields of type T are valid, methods can return T, methods can take T parameters, and T references can be stored in collections. What is not allowed is treating T as a specific type like String; the class has to work for any choice the caller makes, so T has the powers of Object and nothing more.
The compiler enforces the rule that every use of T inside the class refers to the same type for a given object. A Box<String> instance holds a String in its content field, returns a String from getContent, and only accepts a String in setContent. A Box<Integer> does the same with Integer. Each instance picks one concrete type at construction time and sticks with it.
The diamond <> on the right side of new Box<>("Notebook") tells the compiler "figure out the type argument from context". Since the left side declares the variable as Box<String>, the compiler infers that the diamond expands to <String>. Diamond inference (added in Java 7) is the standard way to write generic constructors today. The pre-Java-7 form new Box<String>("Notebook") still works, just longer.
A type parameter declared in the class header is visible everywhere inside that class: fields, constructors, instance methods, nested classes. It is not visible in static members of the class, and not visible outside the class body. The scope is exactly the class body, just like a field name.
A Container<T> that uses T in several places makes this clear:
Every method that needs to talk about elements uses T. The List<T> items field is itself parameterized by the same T, so a Container<String> holds a List<String> and a Container<Integer> holds a List<Integer>. The two Ts in the field and the methods refer to the same type because they live in the same class.
The T in the class header acts as a binding declaration; every later mention of T in the class is a use of that binding. Renaming the header to Container<E> requires renaming every use to E as well. The compiler doesn't accept a class where the header says T and the body says E, because the two names would be unrelated.
Type parameters live entirely in the type system. After compilation, they're erased to Object. At runtime, a Container<String> and a Container<Integer> look identical to the JVM. The cost of generics is paid by the compiler, not by the running program.
What T cannot do is appear in a static field or static method of the same class:
What's wrong with this code?
Fix:
A static member belongs to the class itself, not to any particular instance, so it can't refer to an instance-level type parameter. To tie a default to a specific type, either keep it on instances or declare a separate generic method with a method-level type parameter:
The instance field defaultValue is fine because every FixedContainer<T> instance carries its own T.
A class can declare more than one type parameter by listing them in the angle brackets, separated by commas. Pair<K, V> is the canonical example: a small holder for two related values that don't have to be the same type.
Both K and V are scoped to the class body. They can be different types or the same type; the caller decides at construction time.
The getValue call on productStock returns an Integer because V is bound to Integer for that instance. On productPrice, the same method returns a Double because V is bound to Double. The same source code produces different return types depending on how the class was parameterized at the call site.
A class diagram of Pair<K, V> makes the structure visible:
Three parameters work the same way. A Triple<A, B, C> extends the idea to three values:
Two practical points about multiple type parameters. First, the order matters: Pair<String, Integer> and Pair<Integer, String> are different types. A method that takes a Pair<String, Integer> will not accept a Pair<Integer, String>. Second, beyond three or four parameters, the angle brackets get hard to read and a dedicated class with named fields is usually clearer than a generic tuple. The standard library doesn't ship a Pair or Triple for this reason; small, named domain classes are clearer.
Every generic class needs a type argument when instantiated. There are two ways to provide it: explicitly or via diamond inference.
Explicit form:
Diamond form (preferred):
The diamond <> tells the compiler to infer the type argument from the target type. The target type is whatever the result is being assigned to, returned from, or passed into. In the line above, the target type of the right-hand expression is Box<String> (the variable's declared type), so the diamond expands to <String>.
A few examples of the diamond doing its job:
In the second example, catalog.add(new Pair<>("Notebook", 42)) passes a Pair<?, ?> whose type arguments are inferred from the list's element type Pair<String, Integer>. In the third, createPriceBox declares its return type as Box<Double>, so the new Box<>(12.99) inside the method picks up <Double> from that return type.
The one place the diamond doesn't help is when there's no target type at all. With var box = new Box<>("Notebook");, the compiler has to fall back on the constructor argument and infer Box<String>, which works for this simple case but can produce unexpected types when the argument is more complicated. With var, an explicit type argument is sometimes clearer:
Diamond inference is a compile-time convenience. The generated bytecode is identical to the explicit form, so there's no runtime impact either way. Pick the form that reads better at the call site.
A single generic class definition produces a family of types when you instantiate it with different type arguments, but those types are not related to each other in the inheritance hierarchy. Box<String> is not a subtype of Box<Object>, and Box<Integer> is not a subtype of Box<Number>. Each parameterized type is its own thing.
Even though String is a subtype of Object, Box<String> is not a subtype of Box<Object>. Consider what would happen if it were:
What's wrong with this code?
If the second line compiled, the fourth line would crash at runtime because stringBox would actually hold an Integer. To prevent this, the compiler rejects the assignment outright. Generic classes are invariant in their type parameters: Box<X> is only assignable to Box<X>, never to Box<Y> for a different Y, even when X is a subtype of Y.
Box<String> and Box<Integer> are siblings under Object, not relatives of each other:
Each parameterized type stands alone. Any of them can be held in an Object reference (everything extends Object), but assignments between them are not allowed.
The same rule applies to multi-parameter classes. Pair<String, Integer> and Pair<String, Number> are unrelated, even though Integer is a subtype of Number. Each type parameter is invariant independently.
A Result<T> wrapper is a common pattern for methods that can either return a value or fail. It carries an outcome flag, the value (when successful), and an error message (when failed), all parameterized by the type of the value the operation produces.
The ok and fail factory methods are static, so they need their own method-level type parameter <T> in front of the return type. This is how static factories on a generic class re-declare a type parameter. The instance methods (isSuccess, getValue, getError) use the class's T without re-declaring it because they're not static.
A small program that uses Result to model a stock lookup:
Two calls, two outcomes, one class. Result<Integer> is the parameterized type used for both because both calls return stock counts; if the operation produced a Double (a price) or a String (a product name), the variable's declared type would change to Result<Double> or Result<String> and the rest of the code would adjust at compile time.
A stack is a classic example of a data structure that benefits from being generic. The same operations (push, pop, peek, isEmpty) make sense for any element type, so writing them once and parameterizing the class is the natural choice.
The backing storage is a List<T>, which inherits its generics from the same T declared on the class. push appends, pop removes from the end, and peek reads the last element without removing it. The runtime cost of each operation is the cost of the underlying ArrayList method.
The same class works just as well for a stack of integers:
Storing primitives in Stack<Integer> triggers autoboxing on every push and unboxing on every pop. For a stack of millions of int values, a dedicated IntStack backed by an int[] is faster. For everyday application code, the cost is invisible.
The standard library does ship a java.util.Stack class, but it's a legacy class from Java 1.0 with synchronized methods. Modern code uses ArrayDeque when a stack-like data structure is needed. Writing a custom Stack<T> is a teaching exercise; the production answer is ArrayDeque<T>.
10 quizzes