
You’ve used generics every time you created an ArrayList. The angle brackets in ArrayList<String> specify what type the list holds. Without this, you’d have to cast objects and risk runtime errors. Generics let you write type-safe, reusable code.
The Problem Generics Solve
Before generics (pre-Java 5), collections held Object references:
// Old way - no type safety
ArrayList list = new ArrayList();
list.add("Hello");
list.add(123); // No compile error - but this is probably a bug
String s = (String) list.get(0); // Must cast
String s2 = (String) list.get(1); // ClassCastException at runtime!
Errors appeared only at runtime. With generics:
// With generics - type safe
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // Compile error! Can't add Integer to String list
String s = list.get(0); // No cast needed
The compiler catches type errors immediately. No casting required.
Using Generic Classes
Specify the type parameter in angle brackets:
List<String> names = new ArrayList<>();
Map<String, Integer> ages = new HashMap<>();
Set<Double> prices = new HashSet<>();
The diamond operator <> on the right lets Java infer the type from the left side.
Type parameters must be reference types. Use wrapper classes for primitives:
List<Integer> numbers = new ArrayList<>(); // Not List<int>
List<Double> values = new ArrayList<>(); // Not List<double>
Creating a Generic Class
Define your own generic class with a type parameter:
public class Box<T> {
private T content;
public void put(T item) {
this.content = item;
}
public T get() {
return content;
}
public boolean isEmpty() {
return content == null;
}
}
The T is a placeholder for any type. When you create a Box, you specify what T becomes:
Box<String> stringBox = new Box<>();
stringBox.put("Hello");
String s = stringBox.get(); // No cast needed
Box<Integer> intBox = new Box<>();
intBox.put(42);
int n = intBox.get();
Box<List<String>> listBox = new Box<>();
listBox.put(new ArrayList<>());
One class definition works with any type.
Common Type Parameter Names
By convention, single uppercase letters represent type parameters:
T– Type (general purpose)E– Element (used in collections)K– KeyV– ValueN– NumberS, U, V– Additional type parameters
These are conventions, not requirements. Box<ContentType> works but Box<T> is standard.
Multiple Type Parameters
Classes can have multiple type parameters:
public 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;
}
@Override
public String toString() {
return "(" + key + ", " + value + ")";
}
}
Pair<String, Integer> age = new Pair<>("Alice", 25);
System.out.println(age.getKey()); // Alice
System.out.println(age.getValue()); // 25
Pair<Integer, String> lookup = new Pair<>(404, "Not Found");
System.out.println(lookup); // (404, Not Found)
Generic Methods
Methods can have their own type parameters, independent of any class parameters:
public class Utility {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static <T> T getFirst(List<T> list) {
if (list.isEmpty()) {
return null;
}
return list.get(0);
}
}
The <T> before the return type declares the method’s type parameter:
String[] names = {"Alice", "Bob", "Charlie"};
Integer[] numbers = {1, 2, 3, 4, 5};
Utility.printArray(names); // Alice Bob Charlie
Utility.printArray(numbers); // 1 2 3 4 5
List<String> list = Arrays.asList("X", "Y", "Z");
String first = Utility.getFirst(list); // X
Java infers the type from the arguments.
Bounded Type Parameters
Sometimes you need to restrict what types are allowed. Use extends to set an upper bound:
public class NumberBox<T extends Number> {
private T number;
public NumberBox(T number) {
this.number = number;
}
public double getDoubleValue() {
return number.doubleValue(); // Can call Number methods
}
}
Only Number and its subclasses (Integer, Double, etc.) are allowed:
NumberBox<Integer> intBox = new NumberBox<>(42);
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
// NumberBox<String> stringBox = new NumberBox<>("Hi"); // Error!
Bounds let you call methods of the bound type inside the generic class.
Multiple Bounds
A type parameter can have multiple bounds:
public class Processor<T extends Comparable<T> & Serializable> {
// T must implement both Comparable and Serializable
}
If one bound is a class, it must come first. Interfaces follow with &.
Wildcards
Sometimes you need flexibility in what types a method accepts.
Unbounded Wildcard (?)
Accepts any type:
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
List<String> strings = Arrays.asList("A", "B", "C");
List<Integer> numbers = Arrays.asList(1, 2, 3);
printList(strings); // Works
printList(numbers); // Works
Upper Bounded Wildcard (? extends)
Accepts a type or any subtype:
public static double sumList(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum;
}
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
System.out.println(sumList(ints)); // 6.0
System.out.println(sumList(doubles)); // 7.5
Lower Bounded Wildcard (? super)
Accepts a type or any supertype:
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers); // Works
addNumbers(objects); // Works
Use ? extends when you read from a structure (producer). Use ? super when you write to it (consumer). This is the PECS principle: Producer Extends, Consumer Super.
Generic Interfaces
Interfaces can be generic too:
public interface Repository<T> {
void save(T entity);
T findById(int id);
List<T> findAll();
void delete(T entity);
}
public class UserRepository implements Repository<User> {
private List<User> users = new ArrayList<>();
@Override
public void save(User user) {
users.add(user);
}
@Override
public User findById(int id) {
return users.stream()
.filter(u -> u.getId() == id)
.findFirst()
.orElse(null);
}
@Override
public List<User> findAll() {
return new ArrayList<>(users);
}
@Override
public void delete(User user) {
users.remove(user);
}
}
Practical Example: Generic Stack
public class Stack<E> {
private ArrayList<E> elements;
public Stack() {
elements = new ArrayList<>();
}
public void push(E item) {
elements.add(item);
}
public E pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return elements.remove(elements.size() - 1);
}
public E peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return elements.get(elements.size() - 1);
}
public boolean isEmpty() {
return elements.isEmpty();
}
public int size() {
return elements.size();
}
@Override
public String toString() {
return elements.toString();
}
}
Stack<String> browserHistory = new Stack<>();
browserHistory.push("google.com");
browserHistory.push("github.com");
browserHistory.push("stackoverflow.com");
System.out.println("Current: " + browserHistory.peek());
browserHistory.pop();
System.out.println("Back to: " + browserHistory.peek());
Stack<Integer> undoStack = new Stack<>();
undoStack.push(100);
undoStack.push(200);
System.out.println("Undo value: " + undoStack.pop());
Output:
Current: stackoverflow.com
Back to: github.com
Undo value: 200
Type Erasure
Java implements generics through type erasure. The compiler checks types, then removes generic information from bytecode. At runtime, ArrayList<String> and ArrayList<Integer> are both just ArrayList.
This has implications:
// Cannot create generic arrays
// T[] array = new T[10]; // Error
// Cannot use instanceof with generic types
// if (obj instanceof List<String>) // Error
// Cannot catch generic exceptions
// catch (GenericException<T> e) // Error
These limitations exist because the runtime doesn’t have type parameter information.
Common Mistakes
Using raw types. Don’t use List when you mean List<String>. Raw types lose type safety.
Mixing generic and raw types. Leads to unchecked warnings and potential runtime errors.
Creating arrays of generic types. Use ArrayList instead. new T[10] doesn’t work due to type erasure.
Overcomplicating with wildcards. Start simple. Add wildcards only when needed for flexibility.
Wrapping Up
You’ve completed the core Java curriculum. From variables and loops to object-oriented programming, exceptions, file I/O, collections, and generics. These fundamentals prepare you for frameworks like Spring, Android development, or any Java project.
Keep building. The best way to solidify these concepts is to write real programs. Pick a project that interests you and start coding.
Related: Java Collections Framework Overview | 10 Java Interview Questions Every Entry-Level Developer Should Know
Sources
- Oracle. “Generics.” The Java Tutorials. docs.oracle.com
- Bloch, Joshua. “Effective Java.” 3rd Edition. Addison-Wesley, 2018.


