Generics
Generics
Introduction
In this lesson, you'll learn about generics in Java. Coming from C#, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In C#, you're familiar with generics.
Java has its own approach to generics, which we'll explore step by step.
The Java Way
Let's see how Java handles this concept. Here's a typical example:
class Box<T> {
T value;
}
// Wildcards instead of variance annotations
List<? extends Number> upperBounded; // covariant read
List<? super Integer> lowerBounded; // contravariant writeComparing to C#
Here's how you might have written similar code in C#:
class Box<T> where T : class {
public T Value { get; set; }
}
// Covariance on interface
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // covariantYou may be used to different syntax or behavior.
C# constraints use 'where T : constraint'; Java uses 'T extends Type' inline
You may be used to different syntax or behavior.
C# supports covariance/contravariance via in/out on interfaces; Java uses ? extends/? super wildcards
You may be used to different syntax or behavior.
Both use type erasure at runtime — generic type info is lost
You may be used to different syntax or behavior.
C# has struct/class/new() constraints; Java has no equivalent for value types
You may be used to different syntax or behavior.
Java wildcards (?) allow flexible API design without needing variance annotations
Step-by-Step Breakdown
1. Generic Constraints
C# uses 'where T : SomeType' after the class/method signature. Java puts the bound inline with 'T extends SomeType'.
class Sorter<T> where T : IComparable<T> {
T Max(T a, T b) => a.CompareTo(b) > 0 ? a : b;
}class Sorter<T extends Comparable<T>> {
T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}2. Wildcards vs Variance
Java uses ? extends T for covariant (read-only) positions and ? super T for contravariant (write-only) positions — the PECS rule.
// C# out = covariant, in = contravariant
IEnumerable<out T> // can read T
IComparer<in T> // can write T// Java wildcard PECS: Producer Extends, Consumer Super
void printAll(List<? extends Number> list) {
for (Number n : list) System.out.println(n); // read OK
}
void addInts(List<? super Integer> list) {
list.add(42); // write OK
}3. Multiple Bounds
Both languages support multiple type bounds. Syntax differs slightly.
class Repo<T> where T : class, IEntity, new() { }// Java: class bound first, then interfaces
class Repo<T extends Entity & Serializable> { }
// Note: only one class bound allowed, rest must be interfaces4. Type Erasure Similarity
Both C# and Java erase generic type parameters at runtime for reference types. You cannot do 'new T()' or 'typeof(T)' without workarounds in either language.
// C# workaround with new() constraint
T Create<T>() where T : new() => new T();// Java workaround: pass Class<T> explicitly
<T> T create(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}Common Mistakes
When coming from C#, developers often make these mistakes:
- C# constraints use 'where T : constraint'; Java uses 'T extends Type' inline
- C# supports covariance/contravariance via in/out on interfaces; Java uses ? extends/? super wildcards
- Both use type erasure at runtime — generic type info is lost
Key Takeaways
- Java bounds are inline (T extends Type) vs C# where clause
- Java wildcards (? extends/? super) replace C# in/out variance annotations
- PECS: Producer Extends, Consumer Super
- Both erase generic type info at runtime for reference types