Generic Class Declaration
A generic class introduces one or more type parameters within angle brackets. For example:
public class Range<T> {
private T lower;
private T upper;
public Range(T low, T high) {
this.lower = low;
this.upper = high;
}
}
Instantiation binds T to a concrete type at compile time:
Range<LocalDateTime> duration = new Range<>(start, end);
Inheritance with Generics
A subclass may retain genericity or fix the type parameter:
-
Generic subclass:
public class TimeInterval<T> extends Range<T> { /* ... */ } -
Concrete subclass:
public class DateSpan extends Range<LocalDate> { /* ... */ }
The following is invalid — <LocalDate> after the class name has no meaning in Java:
// ❌ Compilation error
public class DateSpan<LocalDate> extends Range<LocalDate> {}
Generic Methods
Type parameters for methods appear before the return type:
public static <U> U identity(U value) {
return value;
}
Due to type erasure, the bytecode contains no generic metadata. Instead, a Signature attribute preserves the generic signature for tools like decompilers.
Constructor-Level Type Parameters
Constructors can declare independent type variables, distinct from the enclosing class’s parameters:
class ConfigurableContainer<T> {
public <K> ConfigurableContainer(K config) {
// K is independent of T
}
}
Usage:
ConfigurableContainer<String> container = new <Integer>ConfigurableContainer<>(42);
// K → Integer, T → String
Bounded Type Variables
Constraints restrict allowable types:
-
Single upper bound (class or interface):
public class PositiveValue<T extends Number> { private final T value; public PositiveValue(T v) { this.value = v; } public boolean isPositive() { return value.doubleValue() > 0; } }Only
Numberand its subtypes (e.g.,BigDecimal,Long) are permitted. -
Multiple bounds, where at most one is a class (and must come first):
interface Serializable {} class Base {} class Derived extends Base implements Serializable {} public static <X extends Base & Serializable> void process(X item) { /* ... */ }process(new Base())fails;process(new Derived())succeeds.
Wildcards
Wildcards (?) represent unknown types and support flexible subtyping.
Upper-Bounded Wildcard (? extends T)
Enables safe reading from collections:
public static double sumNumbers(List<? extends Number> nums) {
return nums.stream().mapToDouble(Number::doubleValue).sum();
}
Valid calls:
sumNumbers(new ArrayList<Integer>());
sumNumbers(new ArrayList<Double>());
sumNumbers(new ArrayList<Number>());
But insertion is prohibited — the compiler cannot guarantee type safety:
// ❌ Compile-time error
nums.add(new Integer(1));
Unbounded Wildcard (?)
Indicates total type ignorance:
public static int sizeOfAnyList(List<?> list) {
return list.size(); // only non-generic operations allowed
}
Elements retrieved via get() have type Object. Insertion is only valid for null.
Lower-Bounded Wildcard (? super T)
Enables safe writing:
public static void fillWithZeros(List<? super Integer> target, int count) {
for (int i = 0; i < count; i++) {
target.add(0); // OK: Integer is assignable to any supertype of Integer
}
}
Accepts List<Integer>, List<Number>, or List<Object>.
Wildcard Capture
Direct mutation using ? fails due to type uncertainty:
// ❌ Compile error: incompatible types
public static void swapFirst(List<?> list) {
if (list.size() >= 2) {
list.set(0, list.get(1)); // CAP#1 ≠ CAP#2
}
}
Solution: delegate to a generic helper method that captures the unknown type:
public static void swapFirst(List<?> list) {
swapHelper(list);
}
private static <E> void swapHelper(List<E> list) {
if (list.size() >= 2) {
E temp = list.get(0);
list.set(0, list.get(1));
list.set(1, temp);
}
}
Bridge Methods
After erasure, raw signatures may conflict with intended polymorphism. The compiler synthesizes bridge methods to preserve inheritance contracts.
Given:
class Node<T> {
T data;
void setData(T d) { this.data = d; }
}
class IntNode extends Node<Integer> {
@Override
void setData(Integer d) { /* ... */ }
}
Erasure produces:
class Node {
Object data;
void setData(Object d) { /* ... */ }
}
class IntNode extends Node {
void setData(Integer d) { /* ... */ }
// Compiler injects:
void setData(Object d) { setData((Integer) d); }
}
Without the bridge, IntNode would not override Node.setData(Object).
Key Limitations
-
No primitive type arguments:
List<int>is illegal; use boxed types (List<Integer>). -
No generic array creation:
new List<String>[10]fails — erased tonew List[10], breaking runtime type checks. -
No
instanceofwith parameterized types:list instanceof List<String>is disallowed; only raw types (List.class) exist at runtime. -
No static fields with type parameters:
static T cacheviolates type erasure — static members belong to the class, not parameterized instances. -
No exception classes with type parameters:
class ValidationException<T> extends Exceptionis forbidden — exception handling relies on exact class identity at runtime. -
No overloading by generic parameter alone:
void handle(List<String>)andvoid handle(List<Integer>)erase to identical signatures and cause compilation failure.
Heap Pollution
Occurs when unchecked conversions allow incorrect object assignments into generic structures. A common source is varargs with generic types:
public static <T> void addAll(List<T> dest, T... items) {
Collections.addAll(dest, items);
}
public static void unsafeCall() {
List<String> strings = new ArrayList<>();
// ⚠️ Warning: heap pollution risk
addAll(strings, "a", "b", 42); // 42 is not a String
}
Suppress warnings selectively:
@SafeVarargs: guarantees safety (applicable only tofinal,static, orprivatemethods).@SuppressWarnings({"unchecked", "varargs"}): suppresses both unchecked casts and varargs warnings.
Compiler Diagnositcs
Enable enhanced feedback during compilation:
-Xlint:unchecked: reports unchecked operations (e.g., raw type usage).-Xdiags:verbose: increases diagnostic detail level.-Duser.language=en: forces English locale for messages.