Java Generic Classes and Methods
You have used List<String>; now define your own reusable types and methods. Learn what T means, when a bound helps, and how wildcards connect producers and consumers without giving up compiler checks.
Before you start: review typed collections and wrappers, interfaces, and reference parameters. Every runner is a complete Java 21 Main.java program with no input required. It needs the existing isolated sandbox; there is no website-host execution fallback.
What you will be able to do
- Define generic classes and substitute reference types for parameters.
- Read generic method declarations and type inference.
- Use bounds for operations the implementation actually needs.
- Distinguish invariant containers and producer/consumer wildcards.
- Recognize raw-type risks and erasure restrictions.
- Build a typed container that validates bulk changes before committing.
1. Give one reusable class a checked value type
You already used List<String> and Map<String, Integer>. Now define a small generic type yourself. Box<T> says that each parameterized use chooses a type for its value. Box<String>.get returns String and its set expects String. Box<Integer> reuses the same source definition with Integer instead. No caller-written cast belongs at normal retrieval.
T is a type variable, not a value variable or a class you need to import. The letter is a convention; a descriptive name such as Element also works. The diamond <> in new Box<>(...) asks the compiler to infer suitable constructor type arguments from the surrounding context and arguments. It does not mean any value can be inserted afterward.
Type arguments must be reference types, so Box<int> does not compile; use Integer and recognize boxing at the boundary. Generic typing does not reject null or make a Box immutable. This example intentionally accepts null, while the later Batch lab defines a separate non-null rule. Type compatibility and business validation answer different questions.
One class, two checked value types
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Topic: SQL
Minutes: 30
Try a deliberate compiler error
Replace topic.set("SQL") with topic.set(42). Compilation should reject the Integer write into a String box. Restore a String to run successfully. This is the guarantee you lose when you introduce a raw alias.
2. Give independent roles independent type variables
A progress pair connects a topic name to a completed count without forcing both values to share a type. Pair<K, V> declares two independent variables. With Pair<String, Integer>, first returns String and second returns Integer. Another use can choose different types while keeping the same class behavior.
The final fields stop replacement of the stored references after construction. They do not deep-freeze referenced objects. Our String and Integer values are immutable, but a Pair containing a mutable List could still expose changes to that List. Generic syntax adds no automatic copying, null validation, or ownership policy.
Do not introduce a generic class merely because two fields exist. A domain-specific Progress class may communicate intent better when its fields have important rules. Use type parameters for genuinely reusable behavior, and let their names describe distinct roles rather than replacing every concrete type with an unexplained letter.
Different type parameters express different roles
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Java: 3
3. A generic method declares its parameters before the return type
Read static <T> T identity(T value) in order: static method, declaration of T, return type T, then parameter type T. The repeated name connects what enters to what comes back. Main itself is not generic; a method can introduce its own type parameter independently of its surrounding class.
The first call lets the compiler infer the type using the argument and assignment context. Main.<Integer>identity(80) writes it explicitly so you can recognize that syntax; ordinary code usually omits it. Inference is still static checking, not a promise that an incompatible assignment or insertion will be accepted at runtime.
A method variable is useful when it relates input, output, or several parameters. A static method cannot use an enclosing class instance’s T as though that instance existed; it needs an appropriate method-level parameter of its own. Avoid confusing readers by accidentally shadowing a class type variable with a different method variable of the same name.
A method declares its own type parameter
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Java
80
4. A bound permits the operations an implementation needs
An unconstrained T exposes Object-level operations, so the compiler cannot assume it has doubleValue or a domain-specific method. T extends Number grants the numeric capability used here. In a type bound, extends also names an interface requirement; the syntax is not limited to class inheritance.
The calls accept boxed Integer and Double values, and the method reads Number.doubleValue before multiplying. A String call is rejected. The bound does not enable arbitrary arithmetic directly on T, guarantee exact decimal precision, or reject null. These examples supply known non-null values; reusable application code must define those policies separately.
Choose constraints because they express a useful relationship. If all you need is a Number’s methods and the exact subtype is not connected to another signature position, a plain Number parameter may be clearer than <T extends Number>. More generic syntax is not automatically a better API. The lab’s factory uses T because its input and returned batch really must relate.
A bound grants known operations
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
42.0
5.0
5. A producer wildcard allows compatible reads
Integer is a Number, but List<Integer> is not List<Number>. If that assignment worked, a wider alias could insert a Double into the Integer list. This invariant relationship protects writes. Our sum helper accepts List<? extends Number> so different numeric lists can instead be used as compatible producers.
Read ? extends Number as ‘some unknown type that is Number or a subtype’. Reading as Number is safe, but adding a new Integer or Double is not: the hidden type might be another Number subtype. The helper can use both lists without pretending their element types are identical or forcing an unchecked cast.
An upper bound does not imply immutability. Null is type-compatible for insertion, subject to implementation support, and removal may still be possible. This helper promises only to read. Its known non-null values and approximate double arithmetic make it a focused wildcard example, not an exact monetary summation API.
Read different numeric lists through an upper bound
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
6.0
4.0
6. A consumer accepts values without promising a narrow read type
Read ? super Integer as ‘some unknown supertype of Integer, including Integer itself’. The destination might be List<Integer>, List<Number>, or List<Object>. Inserting an Integer is compatible with each, so addPracticeCounts can write its values without demanding exactly one destination declaration.
Retrieval only promises Object because a List<Object> destination could already contain Strings or other unrelated values. Do not cast entries to Integer merely because this helper inserts Integers. The example deliberately stores its retrieved entry in an Object variable, making the safe read type visible.
A useful mnemonic is producer extends, consumer super, from the helper’s point of view. A reusable copy helper can use a named T to relate List<? extends T> to Collection<? super T>. When one list must both accept and produce a particular type, List<T> may express the requirement more clearly. Wildcards are roles, not decoration to add to every parameter.
Write Integers to compatible destinations
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Read as Object: 2
Read as Object: 2
Numbers: [2, 3]
Objects: [2, 3]
7. A wildcard neither copies nor freezes a collection
The view and original references point to the same editable list. Adding through original is visible through view. Calling the supported clear operation through view then removes every entry from original. The type argument restricts compatible element writes; it does not create a snapshot or strip all mutation operations away.
List<?> means that the element type is unknown and ordinary reads can be treated as Object. It is not a list into which you may safely insert every possible Object. For a separate editable snapshot, explicitly copy the container. For an unmodifiable API, choose and document that different policy. Neither one follows from wildcard syntax alone.
Keep these responsibilities separate in the lab: Batch<T> defines compatibility, List<? extends T> admits producers, validation enforces null/capacity rules, and a new ArrayList provides container independence. A shallow copy still shares element references. Deep copying and concurrent ownership need additional domain rules, not an extra angle bracket.
An upper bound does not freeze shared data
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Visible through view: [1, 2, 3]
Original after clear: []
8. Preserve compiler checks; do not rely on erased runtime types
Java generally implements generics through erasure. ArrayList<String> and ArrayList<Integer> share a runtime class, while the compiler checks their declared uses and may insert casts at typed retrieval sites. Our example checks the class relationship without pretending it can discover String elements from an empty list at runtime.
You cannot directly write new T(), new T[], or instanceof List<String>. instanceof List<?> can check that an object is a list, not that all its elements are Strings. Overloads differing only by List<String> versus List<Integer> also clash after erasure. These are reasons to design with the real generic model, not to bypass it with raw types.
A raw Box or List discards useful checks and can permit a bad value into an otherwise typed object. Do not silence an unchecked warning and add a later cast as a substitute for fixing the write. Keep parameters on aliases and signatures so incompatible insertions fail near their source. The repair exercise demonstrates exactly this boundary.
Type arguments do not create separate runtime classes
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Same runtime class: true
A list: true
Repair exercise: a raw alias bypassed the String contract
A raw alias allows an Integer write into a String box. Run it, restore a Box<String> alias, and replace the incompatible write with a String. Repair the write contract rather than adding a later cast.
Main.java · Find the unchecked write
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
The intentional raw-type example can show compiler warnings and produces:
Raw write broke String access.
Reveal the explained repair
The raw reference permits an unchecked write into the shared object. Typed retrieval fails at a compiler-inserted String cast. Parameterizing the alias makes alias.set(42) a compiler error; replacing it with a String restores the contract without suppressing warnings or casting away the mistake.
Main.java · Restore the typed alias
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Value: SQL
Independent lab: a validated typed batch
Build a reusable Batch<T> with capacity 0..1000. A bulk addition accepts compatible producer lists, rejects invalid data or overflow before changing contents, and offers independent editable container snapshots. Demonstrate it through a generic factory method.
- Implement Batch<T> with capacity in 0..1000 and no raw types or unchecked casts.
- Implement addAll(List<? extends T> source); reject null input or null values with IllegalArgumentException.
- Validate every incoming value before checking space and committing. Capacity overflow throws IllegalStateException.
- Preserve order and duplicates; failed null/capacity checks leave the existing batch unchanged.
- Implement size() and snapshot(); each snapshot is a new editable List<T>, sharing element references shallowly.
- Implement static <T> Batch<T> batchOf(int capacity, List<? extends T> source) and show Batch<Number> accepting Integer and Double lists.
The starter compiles but prints nothing. Define the class and factory in the same Main.java runner, then demonstrate this output:
Batch: [10, 20, 2.5]
Size: 3
Batch after snapshot edit: [10, 20, 2.5]
Original: [10, 20]
Main.java · Define Batch and its generic factory
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Stage 1 hints
Stage 1: define compatibility. Store List<T> internally and accept List<? extends T>. Validate capacity in the constructor and expose no raw aliases or internal lists.
Stage 2 hints
Stage 2: prepare before committing. Stage incoming references in a temporary List<T> while rejecting nulls. Check staged size against capacity minus current size, then append only after both checks succeed.
Stage 3 hints
Stage 3: separate snapshots from ownership. Return a fresh ArrayList<T>. Put the factory’s <T> before its Batch<T> return type, and connect its producer input to the new typed batch.
Reveal the complete explained solution
Main.java · Validate before committing a typed addition
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Batch<Number> accepts Integer and Double producers but rejects String additions at compilation. Staging prevents a later null or capacity error from partially changing stored contents. Each snapshot is a new container, not a deep copy. The unchanged-content guarantee covers explicit validation/capacity failures with stable single-threaded input, not rollback from virtual-machine resource failure or concurrent mutation.
Test your contract
- Capacity zero accepts empty additions and rejects nonempty additions without changing contents.
- Exact capacity, duplicate entries, and empty additions preserve order and size.
- A null after a valid candidate and an oversized addition leave the existing batch unchanged.
- Snapshot edits and later additions do not change stored membership or earlier snapshot containers.
- Batch<Number> accepts Integer/Double sources; Batch<String> rejects Integer sources at compilation. Test capacities -1 and 1001.
For a null-entry test, add null to an ArrayList explicitly; List.of rejects it before Batch sees the source. Seed an existing batch, attempt an addition containing one valid value followed by null, catch the validation exception, and compare the batch snapshot before and after. Separately test overflow with a fully valid source.
Common mistakes and how to diagnose them
Using primitive type arguments
Use Box<Integer>, not Box<int>. Boxing adapts primitive values, but generic type arguments must still be reference types.
Assuming containers inherit element subtyping
List<Integer> is not List<Number>. Use a justified producer wildcard for compatible reads rather than forcing the relationship with raw aliases or casts.
Calling an unknown operation on T
An unbounded T exposes Object operations. Add a meaningful bound for the actual capability required, or choose a simpler parameter when no exact type relationship is needed.
Treating ? extends as immutable
It restricts safe element writes, not all changes. Supported clear/removal and writes through another alias may still affect the collection.
Reading a consumer as a narrow type
List<? super Integer> may be a List<Object> with unrelated entries. Integer insertion is safe, but ordinary retrieval only guarantees Object.
Suppressing an unchecked write warning
Parameterize aliases and helper signatures so the compiler rejects the bad insertion. A warning can indicate a later typed retrieval failure, not merely untidy syntax.
Frequently asked questions
Is T a keyword?
No. It is a conventional type-parameter identifier declared in angle brackets. Use names that communicate roles, such as E for an element or K/V for keys and values.
Does a bound prevent null?
No. It grants operations for non-null compatible values. Null validation, numeric precision, and application rules need separate contracts.
Should every helper be generic?
No. A type parameter should express useful reusable behavior or a relationship between signature positions. A plain Number parameter may be simpler when its exact subtype is irrelevant.
How is T different from a wildcard?
T is a named type variable that can connect several class or method positions. A wildcard is an unknown type argument at a use site. Name a parameter when inputs and output must relate.
Can I use instanceof List<String>?
No. The element argument is not generally available as a runtime class check. instanceof List<?> establishes list membership, not that every element is a String.
Are snapshots deep copies?
No. The Batch snapshot has a separate list container but shares element references. Mutable elements require a separate domain-specific copying policy if deep isolation is needed.
Use generics to express a useful relationship
A class parameter types its instances; a method parameter connects one invocation. Bounds grant capabilities, extends reads compatible producers, and super accepts compatible writes. Raw types lose checks, erasure limits runtime tests, and snapshot/null policies still need explicit implementation.
Before moving on, explain why Batch<Number> accepts integer sources, why a consumer read only promises Object, and why staging prevents partial additions after validation failure. Next: annotations and the metadata they express.
Can you explain the type relationship?
Answer six questions and read the explanations. Quiz answers are temporary and do not save course progress.
References and related learning
This original SovranCode lesson uses the Java 21 specification and Oracle's foundational generics tutorials. The older tutorials are used for enduring language concepts, not as a claim about the latest Java release.
- Java 21 specification: type variables, parameterized types, and erasure
- Oracle tutorial: generic types
- Oracle tutorial: generic methods
- Oracle tutorial: wildcard guidelines
Review typed collections · Previous lesson: sorting · Java course roadmap.
Last updated: