Generic Classes: Generics, Type Safety & Type Parameters
This article offers a comprehensive guide to generic classes, covering Generics, Type Safety, Wildcards, and Bounded Type Parameters with practical examples.
In a Nutshell
Generic classes are type templates that use type parameters to provide reusable, type-safe data structures and algorithms. Common examples include List<T>, Map<K,V>, and C++ Templates.
Core Technical Overview
Generic classes encapsulate behavior independent of specific data types and are instantiated through type arguments. They enable type-safe reuse without code duplication.
Implementation across languages:
Java Generics
- Type Erasure: Type information is removed at compile time
- Runtime: Only raw types exist; no generic type information persists
- Wildcards:
? extends Number,? super Number - Bounded Types:
<T extends Number>
C# Generics
- Reified Generics: Type information is preserved at runtime
- Constraints:
where T : class, new() - Covariance/Contravariance:
out T,in T - Performance: No boxing-unboxing overhead
C++ Templates
- Compile-Time: A specialized version is generated for each type
- Zero Cost: No runtime overhead
- Template Metaprogramming: Turing-complete
- SFINAE: Substitution Failure Is Not An Error
Core Concepts:
- Invariance:
List<String>is not aList<Object> - Covariance:
Producer extends(can supply) - Contravariance:
Consumer super(can consume)
Key Topics for Assessment
- Generics: Type templates for reusable code
- Type Safety: Compile-time type checking instead of runtime errors
- Type Parameters: Placeholders for concrete types
- Bounded Types: Constraints on type parameters
- Wildcards: Unknown types with restrictions
- Type Erasure: Java’s approach without runtime type information
- PECS Rule: Producer Extends, Consumer Super
- IHK-relevant: Essential for type-safe programming
Core Components
- Type Parameters: Placeholders like
<T>for concrete types - Bounded Types:
<T extends Number>with constraints - Wildcards:
<?>for unknown types - Generic Methods:
<T> T max(T a, T b) - Generic Classes:
class Box<T> - Type Safety: Compile-time type checking
- Covariance:
Producer extendsrelationship - Contravariance:
Consumer superrelationship
Practical Examples
1. Simple Generic Class in Java
// Generic Box class
public class Box<T> {
private T content;
public Box() {
this.content = null;
}
public Box(T content) {
this.content = content;
}
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public boolean isEmpty() {
return content == null;
}
@Override
public String toString() {
return "Box[" + (content != null ? content.toString() : "empty") + "]";
}
}
// Using the generic class
public class BoxDemo {
public static void main(String[] args) {
// Box for Strings
Box<String> stringBox = new Box<>("Hello World");
System.out.println(stringBox); // Box[Hello World]
// Box for Integer
Box<Integer> integerBox = new Box<>(42);
System.out.println(integerBox); // Box[42]
// Box for custom objects
class Person {
String name;
Person(String name) { this.name = name; }
@Override public String toString() { return name; }
}
Box<Person> personBox = new Box<>(new Person("Max"));
System.out.println(personBox); // Box[Max]
// Type safety at compile time
// stringBox.setContent(123); // Error: cannot assign Integer to String
}
}
2. Generic Methods
public class GenericMethods {
// Generic method for swapping
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// Generic method with bounded type parameter
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// Generic method with wildcard
public static void printList(List<?> list) {
for (Object element : list) {
System.out.print(element + " ");
}
System.out.println();
}
// PECS: Producer Extends
public static double sum(List<? extends Number> list) {
double sum = 0.0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum;
}
// PECS: Consumer Super
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
public static void main(String[] args) {
// Swap method
String[] names = {"Alice", "Bob", "Charlie"};
swap(names, 0, 2);
System.out.println(Arrays.toString(names)); // [Charlie, Bob, Alice]
// Max method
System.out.println(max(5, 10)); // 10
System.out.println(max("Apple", "Banana")); // Banana
// Wildcard
List<String> strings = Arrays.asList("A", "B", "C");
List<Integer> numbers = Arrays.asList(1, 2, 3);
printList(strings); // A B C
printList(numbers); // 1 2 3
// PECS examples
List<Integer> ints = Arrays.asList(1, 2, 3);
System.out.println(sum(ints)); // 6.0
List<Object> objects = new ArrayList<>();
addNumbers(objects);
System.out.println(objects); // [1, 2, 3]
}
}
3. Generic Class with Bounded Types
// Generic class with multiple bounded type parameters
public class Pair<T extends Comparable<T>, U> {
private T first;
private U second;
public Pair(T first, U second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public U getSecond() {
return second;
}
public void setFirst(T first) {
this.first = first;
}
public void setSecond(U second) {
this.second = second;
}
// Method that benefits from the bounded type
public int compareFirst(T other) {
return this.first.compareTo(other);
}
@Override
public String toString() {
return "Pair[" + first + ", " + second + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Pair<T, U> pair = (Pair<T, U>) obj;
return first.equals(pair.first) && second.equals(pair.second);
}
@Override
public int hashCode() {
return Objects.hash(first, second);
}
}
// Usage
public class PairDemo {
public static void main(String[] args) {
// Valid: String implements Comparable
Pair<String, Integer> nameAge = new Pair<>("Alice", 25);
System.out.println(nameAge); // Pair[Alice, 25]
// Error: StringBuilder does not implement Comparable
// Pair<StringBuilder, Integer> invalid = new Pair<>(new StringBuilder(), 25);
// Using comparison
Pair<String, Integer> alice = new Pair<>("Alice", 25);
Pair<String, Integer> bob = new Pair<>("Bob", 30);
int comparison = alice.compareFirst(bob.getFirst());
System.out.println("Comparison: " + comparison); // negative (Alice < Bob)
}
}
4. C# Generics with Constraints
using System;
// Generic class with constraints
public class Repository<T> where T : class, new()
{
private List<T> items = new List<T>();
public void Add(T item)
{
items.Add(item);
}
public T GetById(int id)
{
return items.FirstOrDefault(item => GetId(item) == id);
}
public IEnumerable<T> GetAll()
{
return items;
}
// Method that benefits from the new() constraint
public T CreateNew()
{
return new T(); // Only possible because of new() constraint
}
private int GetId(T item)
{
// Assumption: T has an Id property
var property = typeof(T).GetProperty("Id");
if (property != null && property.PropertyType == typeof(int))
{
return (int)property.GetValue(item);
}
return 0;
}
}
// Entity class
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public Person() { }
public Person(int id, string name)
{
Id = id;
Name = name;
}
}
// Usage
public class RepositoryDemo
{
public static void Main(string[] args)
{
var personRepo = new Repository<Person>();
// Create a new person (uses new() constraint)
var person1 = personRepo.CreateNew();
person1.Id = 1;
person1.Name = "Alice";
var person2 = new Person(2, "Bob");
personRepo.Add(person1);
personRepo.Add(person2);
// Print all persons
foreach (var person in personRepo.GetAll())
{
Console.WriteLine($"ID: {person.Id}, Name: {person.Name}");
}
// Find person by ID
var foundPerson = personRepo.GetById(1);
Console.WriteLine($"Found: {foundPerson?.Name}");
}
}
5. C++ Templates
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
// Generic class (template)
template<typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& element) {
elements.push_back(element);
}
T pop() {
if (elements.empty()) {
throw std::runtime_error("Stack is empty");
}
T element = elements.back();
elements.pop_back();
return element;
}
bool empty() const {
return elements.empty();
}
size_t size() const {
return elements.size();
}
// Template method within the class
void print() const {
std::cout << "Stack [";
for (size_t i = 0; i < elements.size(); ++i) {
std::cout << elements[i];
if (i < elements.size() - 1) {
std::cout << ", ";
}
}
std::cout << "]" << std::endl;
}
};
// Template function
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
// Template with bounded type parameter (Concept in C++20)
template<typename T>
requires std::is_arithmetic_v<T>
T calculateAverage(const std::vector<T>& numbers) {
if (numbers.empty()) {
return T{};
}
T sum = T{};
for (const T& num : numbers) {
sum += num;
}
return sum / static_cast<T>(numbers.size());
}
int main() {
// Stack for different types
Stack<int> intStack;
intStack.push(1);
intStack.push(2);
intStack.push(3);
std::cout << "Int-Stack: ";
intStack.print();
Stack<std::string> stringStack;
stringStack.push("Hello");
stringStack.push("World");
std::cout << "String-Stack: ";
stringStack.print();
// Template function
std::cout << "Max(5, 10): " << max(5, 10) << std::endl;
std::cout << "Max(3.14, 2.71): " << max(3.14, 2.71) << std::endl;
// Bounded template
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::cout << "Average: " << calculateAverage(numbers) << std::endl;
return 0;
}
PECS Rule: Producer Extends, Consumer Super
Producer (supplies data)
// Producer: reads from the list, returns data
public static void processList(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num.doubleValue()); // Can call Number methods
}
// list.add(42); // Error: cannot write to producer
}
Consumer (consumes data)
// Consumer: writes to the list, consumes data
public static void fillList(List<? super Integer> list) {
list.add(1); // Integer can be added
list.add(2); // Number can be added
list.add(3); // Object can be added
}
Type Erasure in Java
What happens at compile time?
// Source code
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// After type erasure (what the JVM sees)
public class Box {
private Object content;
public void setContent(Object content) {
this.content = content;
}
public Object getContent() {
return content;
}
}
Type casts at runtime
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
// Implicit cast on return
String content = stringBox.getContent(); // Cast from Object to String
// Type safety at compile time
// stringBox.setContent(123); // Compile-time error
Advantages and Disadvantages
Advantages of generics
- Type Safety: Compile-time errors instead of runtime errors
- Reusability: One implementation for multiple types
- Readability: Explicit type information
- Maintainability: Less code duplication
- Performance: No boxing/unboxing for value types (C#)
Disadvantages
- Complexity: Learning curve for wildcards and bounds
- Type Erasure: Java loses type information at runtime
- Compilation Overhead: Longer compile times (C++ templates)
- Binary Compatibility: Changes can break existing code
Common Exam Questions
-
What is type erasure in Java? Type information is removed at compile time; only raw types exist at runtime.
-
Explain the PECS rule! Producer Extends (can only read), Consumer Super (can only write).
-
What’s the difference between
List<?>andList<Object>?List<?>is an unknown type,List<Object>is specifically for Object. -
Why do you need bounded type parameters? To define constraints and use methods of that type.
Key Resources
- https://docs.oracle.com/javase/tutorial/java/generics/
- https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/
- https://en.cppreference.com/w/cpp/language/templates



