Java Generics & Type Safety
Write reusable, type-safe Java code. Learn generic classes, methods, wildcards, bounded parameters, and type erasure rules.
Downloaded from PrepIQ (https://prepiq.online)
Q1. What are Generics in Java and why were they introduced?
Generics allow classes, interfaces, and methods to operate on a type specified at compile time, providing compile-time type safety and eliminating the need for manual casting.
// BEFORE generics (Java 1.4 and earlier) - raw types, unsafe
List list = new ArrayList();
list.add('Hello');
list.add(42); // no compile-time check - mixing types allowed
String s = (String) list.get(0); // manual cast required
String s2 = (String) list.get(1); // ClassCastException at RUNTIME! 42 isn't a String
// WITH generics (Java 5+) - type-safe
List<String> typedList = new ArrayList<>();
typedList.add('Hello');
// typedList.add(42); // COMPILE ERROR - caught immediately, not at runtime
String value = typedList.get(0); // no cast needed, compiler knows the type
| Benefit | Explanation |
|---|---|
| Compile-time type checking | Type errors caught before running the program |
| No explicit casting | Compiler inserts casts automatically and safely |
| Generic algorithms | Write one method that works for multiple types |
Q2. How do you create a generic class in Java?
// Generic class with a single type parameter T
class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
Box<String> stringBox = new Box<>();
stringBox.set('Hello');
String value = stringBox.get(); // no cast needed
Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer num = intBox.get();
// Generic class with MULTIPLE type parameters
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
Pair<String, Integer> pair = new Pair<>('age', 30);
System.out.println(pair.getKey() + ': ' + pair.getValue()); // age: 30
// Common type parameter naming conventions:
// T - Type, E - Element, K - Key, V - Value, N - Number, R - Return type
// Generic class can have bounded type parameters too
class NumericBox<T extends Number> {
private T value;
public void set(T value) { this.value = value; }
public double doubleValue() { return value.doubleValue(); } // Number methods available
}
Q3. What are bounded type parameters in Java Generics?
Bounded type parameters restrict what types can be used, using 'extends' to require the type to be a subtype of a specific class or interface (or implement it, for interfaces).
// Upper bound - T must be Number or a subclass of Number
class Calculator<T extends Number> {
private T value;
public Calculator(T value) { this.value = value; }
public double square() {
return value.doubleValue() * value.doubleValue(); // Number's method available
}
}
Calculator<Integer> intCalc = new Calculator<>(5);
System.out.println(intCalc.square()); // 25.0
// Calculator<String> stringCalc = new Calculator<>('x'); // COMPILE ERROR
// String is not a subtype of Number
// Multiple bounds - class first, then interfaces, separated by &
interface Printable {
void print();
}
class Item implements Comparable<Item>, Printable {
public int compareTo(Item o) { return 0; }
public void print() { System.out.println('Item'); }
}
class Container<T extends Comparable<T> & Printable> {
private T item;
public Container(T item) { this.item = item; }
public void show() {
item.print(); // available from Printable
item.compareTo(item); // available from Comparable
}
}
// Generic method with a bounded type parameter
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
System.out.println(max(10, 20)); // 20 - works for any Comparable type
Q4. What is the difference between >, extends T>, and super T> wildcards?
| Wildcard | Meaning | Can read? | Can write? |
|---|---|---|---|
| > | Unknown type | Yes, as Object | No (except null) |
| extends T> | T or any subtype (upper bound) | Yes, as T | No (except null) |
| super T> | T or any supertype (lower bound) | Yes, as Object only | Yes, as T |
// Unbounded wildcard - accepts a List of ANY type
public static void printList(List<?> list) {
for (Object o : list) {
System.out.println(o); // can only treat elements as Object
}
// list.add('x'); // COMPILE ERROR - can't add anything except null
}
// Upper bounded wildcard - <? extends Number> for READING
public static double sumNumbers(List<? extends Number> list) {
double sum = 0;
for (Number n : list) { // safe to read as Number
sum += n.doubleValue();
}
// list.add(5); // COMPILE ERROR - can't add, compiler doesn't know exact subtype
return sum;
}
sumNumbers(List.of(1, 2, 3)); // works with List<Integer>
sumNumbers(List.of(1.5, 2.5)); // works with List<Double>
// Lower bounded wildcard - <? super Integer> for WRITING
public static void addNumbers(List<? super Integer> list) {
list.add(1); // safe to add Integer or its subtypes
list.add(2);
// Integer i = list.get(0); // COMPILE ERROR - can only read as Object
}
List<Number> numberList = new ArrayList<>();
addNumbers(numberList); // works, Number is a supertype of Integer
// PECS mnemonic: Producer Extends, Consumer Super
// - use 'extends' when the collection PRODUCES data you read
// - use 'super' when the collection CONSUMES data you write
Q5. What is type erasure in Java Generics?
Type erasure means generic type information exists only at COMPILE time - the compiler uses it for type checking, then removes (erases) it, replacing type parameters with their bounds (or Object) in the compiled bytecode.
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// At RUNTIME, both have the SAME class - generic type info is erased
System.out.println(stringList.getClass() == intList.getClass()); // true
System.out.println(stringList.getClass()); // class java.util.ArrayList (no <String>)
// Consequences of type erasure:
// 1. Cannot create an instance of a type parameter
class Box<T> {
// T item = new T(); // COMPILE ERROR - T is erased, no info at runtime
}
// 2. Cannot use instanceof with a parameterized type
// if (list instanceof List<String>) { } // COMPILE ERROR
if (stringList instanceof List<?>) { } // OK - unbounded wildcard works
// 3. Cannot create generic arrays directly
// T[] array = new T[10]; // COMPILE ERROR
// 4. Static context cannot use a class's type parameter
class Container<T> {
// static T value; // COMPILE ERROR - static members don't have T
}
// Why erasure exists: for BACKWARD COMPATIBILITY with pre-Java 5
// code that used raw types - the bytecode format didn't change,
// generics are purely a compile-time / source-level feature
Q6. How do you write a generic method in Java?
A generic method declares its own type parameter(s), independent of the class it's in, using angle brackets right before the return type.
public class Utility {
// Generic method - <T> before the return type declares the type parameter
public static <T> void printArray(T[] array) {
for (T item : array) {
System.out.println(item);
}
}
// Generic method with a return type using the type parameter
public static <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
// Multiple type parameters
public static <K, V> Map<V, K> invertMap(Map<K, V> original) {
Map<V, K> inverted = new HashMap<>();
for (Map.Entry<K, V> entry : original.entrySet()) {
inverted.put(entry.getValue(), entry.getKey());
}
return inverted;
}
// Bounded generic method
public static <T extends Comparable<T>> T findMax(List<T> list) {
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) max = item;
}
return max;
}
}
// Usage - type is usually INFERRED, no need to specify explicitly
Integer[] numbers = {1, 2, 3};
Utility.printArray(numbers); // T inferred as Integer
String first = Utility.getFirst(List.of('a', 'b', 'c')); // T inferred as String
// Explicit type witness (rarely needed)
Utility.<String>printArray(new String[]{'x', 'y'});
Q7. Why can't you create generic arrays in Java, and what are the workarounds?
Arrays in Java are covariant and retain their component type at runtime, but generics use type erasure - combining these would allow type-unsafe operations to slip past the compiler, so Java disallows creating generic arrays directly.
class Box<T> {
// T[] items = new T[10]; // COMPILE ERROR: Cannot create a generic array
}
// Why this restriction exists - illustrating the danger it prevents:
Object[] objArray = new String[3]; // arrays ARE covariant, this compiles
objArray[0] = 42; // would compile if T[] were allowed like this,
// but throws ArrayStoreException at runtime since the array is really a String[]
// Generics avoid this entire class of bugs by disallowing generic array creation
// WORKAROUND 1: use a List<T> instead of an array (most common solution)
class Box<T> {
private List<T> items = new ArrayList<>();
public void add(T item) { items.add(item); }
}
// WORKAROUND 2: create an Object[] internally, cast when accessing
// (unchecked but works because you control access carefully)
class GenericArrayBox<T> {
private Object[] items;
public GenericArrayBox(int size) {
items = new Object[size];
}
@SuppressWarnings('unchecked')
public T get(int index) {
return (T) items[index]; // unchecked cast, but safe if used correctly
}
public void set(int index, T value) {
items[index] = value;
}
}
// WORKAROUND 3: pass a Class<T> token to create arrays reflectively
import java.lang.reflect.Array;
@SuppressWarnings('unchecked')
public static <T> T[] createArray(Class<T> type, int size) {
return (T[]) Array.newInstance(type, size);
}
String[] strings = createArray(String.class, 5);
Q8. What are raw types in Java, and why should you avoid them?
A raw type is a generic class or interface used WITHOUT specifying its type parameter, e.g. using List instead of List<String>. It exists for backward compatibility but bypasses all compile-time type checking.
// Raw type usage - legal but discouraged
List rawList = new ArrayList(); // no type parameter specified
rawList.add('Hello');
rawList.add(42); // no compile error - mixed types allowed!
rawList.add(true);
for (Object o : rawList) {
// must cast manually and carefully, error-prone
if (o instanceof String) {
String s = (String) o;
}
}
// Parameterized type - the correct, safe approach
List<String> typedList = new ArrayList<>();
typedList.add('Hello');
// typedList.add(42); // COMPILE ERROR - caught immediately
// Mixing raw and generic types causes 'unchecked' warnings
List<String> list = new ArrayList<>();
List rawRef = list; // raw reference to a generic list
rawRef.add(42); // compiles with an 'unchecked' warning, but corrupts 'list'
String s = list.get(0); // ClassCastException at RUNTIME - 42 isn't a String!
// The compiler emits: 'unchecked call to add(E) as a member of the raw type List'
// warnings like this should NEVER be ignored
// Rule of thumb: ALWAYS specify a type parameter (or use <> diamond
// operator to infer it) - raw types exist ONLY for legacy code compatibility
// and should never appear in new code
Q9. Can you overload methods that differ only by generic type parameters?
No - because of type erasure, two methods that differ only in their generic type parameter end up with the IDENTICAL erased signature at compile time, causing a 'duplicate method' compile error.
class Processor {
public void process(List<String> list) {
System.out.println('Processing strings');
}
// COMPILE ERROR: 'process(List<Integer>)' has the same erasure
// as 'process(List<String>)' - after erasure, BOTH become
// process(List) with no distinguishing information
// public void process(List<Integer> list) {
// System.out.println('Processing integers');
// }
}
// WORKAROUND 1: use different method names
class ProcessorFixed {
public void processStrings(List<String> list) {
System.out.println('Processing strings');
}
public void processIntegers(List<Integer> list) {
System.out.println('Processing integers');
}
}
// WORKAROUND 2: pass a Class<T> token to distinguish at runtime
class ProcessorWithToken {
public <T> void process(List<T> list, Class<T> type) {
System.out.println('Processing ' + type.getSimpleName());
}
}
new ProcessorWithToken().process(List.of('a', 'b'), String.class);
// Note: methods DIFFERING in the RAW type (not just generic parameter)
// CAN coexist - this is true overloading, not affected by erasure
class ValidOverload {
public void process(List<String> list) { }
public void process(Set<String> set) { } // OK - List vs Set are different raw types
}
Q10. How do generics improve type safety in the Java Collections Framework?
Before generics (pre-Java 5), collections stored Object references, requiring manual casts and risking ClassCastException at runtime. Generics let the compiler enforce type consistency, catching mismatches before the program ever runs.
// BEFORE generics - unsafe, error discovered at RUNTIME
List list = new ArrayList();
list.add('John');
list.add(25); // accidentally added an Integer instead of a String
for (Object o : list) {
String name = (String) o; // ClassCastException when it hits the Integer!
System.out.println(name.toUpperCase());
}
// WITH generics - unsafe operation caught at COMPILE time
List<String> names = new ArrayList<>();
names.add('John');
// names.add(25); // COMPILE ERROR - caught immediately, before running
for (String name : names) {
System.out.println(name.toUpperCase()); // guaranteed to be a String
}
// Generics also improve API clarity and self-documentation
Map<String, List<Order>> ordersByCustomer = new HashMap<>();
// Immediately clear: keys are customer names (String), values are
// lists of Order objects - no need to check documentation or guess
// Generic methods in the Collections API enforce type-safe algorithms
List<Integer> numbers = List.of(5, 3, 8, 1);
Integer max = Collections.max(numbers); // type-safe, returns Integer directly
// Comparator<T> ensures sort logic matches the collection's element type
List<String> words = new ArrayList<>(List.of('banana', 'apple'));
words.sort(Comparator.naturalOrder()); // compiler verifies String is Comparable
Java Generics & Type Safety
Write reusable, type-safe Java code. Learn generic classes, methods, wildcards, bounded parameters, and type erasure rules.
What are Generics in Java and why were they introduced?
Generics allow classes, interfaces, and methods to operate on a type specified at compile time, providing comp...
How do you create a generic class in Java?
// Generic class with a single type parameter T class Box<T> { private T content; public void set(T c...
What are bounded type parameters in Java Generics?
Bounded type parameters restrict what types can be used, using 'extends' to require the type to be a subtype o...
What is the difference between >, extends T>, and super T> wildcards?
WildcardMeaningCan read?Can write?Unknown typeYes, as ObjectNo (except null)
What is type erasure in Java Generics?
Type erasure means generic type information exists only at COMPILE time - the compiler uses it for type checki...
How do you write a generic method in Java?
A generic method declares its own type parameter(s), independent of the class it's in, using angle brackets ri...
Why can't you create generic arrays in Java, and what are the workarounds?
Arrays in Java are covariant and retain their component type at runtime, but generics use type erasure - combi...
What are raw types in Java, and why should you avoid them?
A raw type is a generic class or interface used WITHOUT specifying its type parameter, e.g. using List instead...
Can you overload methods that differ only by generic type parameters?
No - because of type erasure, two methods that differ only in their generic type parameter end up with the IDE...
How do generics improve type safety in the Java Collections Framework?
Before generics (pre-Java 5), collections stored Object references, requiring manual casts and risking ClassCa...