Generics are a powerful feature in Java that enable you to write type-safe, reusable code by allowing classes, interfaces, and methods to operate on parameterized types. Instead of working with raw Object
types and casting manually, generics allow you to specify a placeholder for the type that the class will handle, making your code cleaner and less error-prone.
A generic class is declared by adding a type parameter in angle brackets <T>
after the class name. The type parameter T
is a placeholder that gets replaced by a concrete type when an object of that class is instantiated.
Syntax:
public class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
}
Here, Box<T>
is a generic class with a type parameter T
. The field content
can hold any type that replaces T
. The constructor, getter, and setter also work with this generic type.
When creating instances of a generic class, you specify the actual type to use in place of T
:
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>("Hello");
System.out.println(stringBox.getContent()); // Output: Hello
Box<Integer> intBox = new Box<>(123);
System.out.println(intBox.getContent()); // Output: 123
Box<Double> doubleBox = new Box<>(45.67);
System.out.println(doubleBox.getContent()); // Output: 45.67
}
}
Here we created three Box
objects with different types: String
, Integer
, and Double
. The compiler enforces type safety, ensuring that only the declared type is used. For example, you cannot accidentally put an Integer
into a Box<String>
.
You can also declare generic classes with multiple type parameters, allowing more flexibility:
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;
}
}
Usage example:
Pair<String, Integer> entry = new Pair<>("Age", 30);
System.out.println(entry.getKey() + ": " + entry.getValue()); // Output: Age: 30
Pair<Integer, String> reversedEntry = new Pair<>(100, "Score");
System.out.println(reversedEntry.getKey() + ": " + reversedEntry.getValue()); // Output: 100: Score
This Pair
class can hold any two types, named K
and V
here for key and value, but you can use any valid identifier for type parameters.
public class GenericDemo {
// Generic Box class
public static class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
}
// Generic Pair class with two type parameters
public static 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;
}
}
public static void main(String[] args) {
// Using Box with different types
Box<String> stringBox = new Box<>("Hello");
Box<Integer> intBox = new Box<>(123);
Box<Double> doubleBox = new Box<>(45.67);
System.out.println("Box Contents:");
System.out.println("String: " + stringBox.getContent());
System.out.println("Integer: " + intBox.getContent());
System.out.println("Double: " + doubleBox.getContent());
// Using Pair with different type combinations
Pair<String, Integer> entry = new Pair<>("Age", 30);
Pair<Integer, String> reversedEntry = new Pair<>(100, "Score");
System.out.println("\nPair Entries:");
System.out.println(entry.getKey() + ": " + entry.getValue());
System.out.println(reversedEntry.getKey() + ": " + reversedEntry.getValue());
}
}
Type Safety Generics eliminate the need for explicit casting and reduce the risk of ClassCastException
at runtime. The compiler checks that only the correct types are used, catching errors early.
Code Reuse Instead of writing separate classes for each data type (e.g., StringBox
, IntegerBox
), you write one generic class that works for all types, improving maintainability and reducing boilerplate code.
Expressiveness Generics communicate your intent clearly. When you see Box<String>
, you immediately know what kind of data that box holds, improving readability.
Interoperability with Collections Java Collections API heavily uses generics, so understanding generic classes is essential for working effectively with lists, sets, maps, and more.
Generics use type erasure: at runtime, the generic type parameters are erased and replaced with their bounds or Object
. This means you cannot use generics for certain operations (like creating arrays of generic types).
Generic type parameters are invariant: Box<Number>
is not a superclass or subclass of Box<Integer>
, even though Integer
is a subclass of Number
. This is important to understand when working with generic collections.
<T>
, which are placeholders for types specified upon instantiation.Generics transform Java from a language with raw types to a much more type-safe and expressive one, paving the way for modern Java programming.
Generic methods extend the power of generics by allowing you to define methods with their own type parameters—independent of whether the enclosing class is generic or not. This means you can write a single method that works with various types, improving flexibility and code reuse without having to make the entire class generic.
A generic method declares its type parameter(s) before the return type, enclosed in angle brackets <>
. This signals to the compiler that the method can operate on one or more type parameters.
Basic syntax:
public <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
Here, <T>
declares a generic type parameter T
just for the method printArray
. The method takes an array of T
elements and prints each one. Note that T
is a method-level type parameter, separate from any generic types the class might have.
You can define generic methods inside regular (non-generic) classes. This is common when the generic behavior is limited to a few methods rather than the whole class.
Example:
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 getFirstElement(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[0];
}
}
Usage:
public class Main {
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4};
String[] strArray = {"apple", "banana", "cherry"};
Utility.printArray(intArray); // Output: 1 2 3 4
Utility.printArray(strArray); // Output: apple banana cherry
System.out.println("First int: " + Utility.getFirstElement(intArray)); // Output: First int: 1
System.out.println("First string: " + Utility.getFirstElement(strArray)); // Output: First string: apple
}
}
Notice that the Utility
class itself is not generic, but the methods are. This pattern is useful for stateless utility methods that operate on different types.
public class GenericMethodDemo {
// Utility class with generic methods
public static class Utility {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static <T> T getFirstElement(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[0];
}
}
// Main method to demonstrate generic utility methods
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4};
String[] strArray = {"apple", "banana", "cherry"};
System.out.println("Integer Array:");
Utility.printArray(intArray); // Output: 1 2 3 4
System.out.println("\nString Array:");
Utility.printArray(strArray); // Output: apple banana cherry
System.out.println("\nFirst int: " + Utility.getFirstElement(intArray)); // Output: First int: 1
System.out.println("First string: " + Utility.getFirstElement(strArray)); // Output: First string: apple
}
}
Methods can have multiple type parameters, separated by commas:
public class PairUtil {
public static <K, V> void printPair(K key, V value) {
System.out.println("Key: " + key + ", Value: " + value);
}
}
Usage:
PairUtil.printPair("Name", "Alice");
PairUtil.printPair(1001, 99.5);
This method works with any combination of key and value types.
Generic methods can also return values of generic types. The compiler enforces type safety when the method is called, so you get the correct type inferred automatically:
public static <T> T getMiddleElement(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[array.length / 2];
}
Usage:
String[] fruits = {"apple", "banana", "cherry"};
String middle = Utility.getMiddleElement(fruits);
System.out.println(middle); // Output: banana
The method returns a value of the generic type T
, inferred by the compiler based on the argument type.
public class GenericReturnDemo {
// Utility class with a generic method that returns the middle element
public static class Utility {
public static <T> T getMiddleElement(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[array.length / 2];
}
}
public static void main(String[] args) {
String[] fruits = {"apple", "banana", "cherry"};
Integer[] numbers = {10, 20, 30, 40, 50};
String middleFruit = Utility.getMiddleElement(fruits);
Integer middleNumber = Utility.getMiddleElement(numbers);
System.out.println("Middle fruit: " + middleFruit); // Output: banana
System.out.println("Middle number: " + middleNumber); // Output: 30
}
}
Flexibility Without Class-Level Generics You can keep your classes simple and non-generic but still write reusable, type-safe methods that work with any type.
Code Reuse and Reduced Duplication Instead of writing overloaded methods for different types, a generic method handles them all in one place.
Improved Type Safety The compiler ensures you don't accidentally mix incompatible types, eliminating many potential runtime errors.
Clearer APIs Using generic methods makes your APIs expressive about the types they handle, which aids in maintenance and readability.
Utility.<String>printArray(new String[]{"a", "b", "c"});
but this is rarely required.
<T> void methodName(...)
.Generic methods expand your programming toolkit, allowing you to write more generalized code while maintaining strong typing guarantees, making Java development both easier and safer.
T extends Number
)In Java generics, bounded type parameters allow you to restrict the types that can be used as arguments for a generic type. This gives you more control over what types are valid, enabling you to write methods or classes that operate only on a subset of types—while maintaining type safety and flexibility.
A bounded type parameter uses the extends
keyword to limit the types you can use. Despite the keyword extends
, the bound works for both classes and interfaces (including abstract classes), and it means "is a subtype of".
For example, if you write:
<T extends Number>
it means that T
can be any class that is a subclass of Number
or Number
itself. This includes types like Integer
, Double
, Float
, etc., but not types like String
or custom classes unrelated to Number
.
Suppose you want a method that works only with numeric types, so you can perform numeric operations safely. Without bounds, your generic method would accept any type, including types that cannot be used numerically. Bounded type parameters solve this problem by restricting acceptable types, enabling you to use numeric methods like doubleValue()
or intValue()
on the generic parameter.
Here is a simple generic method that calculates the sum of an array of numbers:
public class MathUtils {
// T must extend Number, so only numeric types are allowed
public static <T extends Number> double sumArray(T[] numbers) {
double sum = 0.0;
for (T number : numbers) {
sum += number.doubleValue(); // safe because T extends Number
}
return sum;
}
}
Usage:
public class Main {
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4};
Double[] doubleArray = {1.5, 2.5, 3.5};
System.out.println(MathUtils.sumArray(intArray)); // Output: 10.0
System.out.println(MathUtils.sumArray(doubleArray)); // Output: 7.5
// The following line would cause a compile-time error:
// String[] strArray = {"a", "b", "c"};
// MathUtils.sumArray(strArray); // Not allowed because String doesn't extend Number
}
}
public class NumericSumDemo {
// Math utility class with a generic method to sum numeric arrays
public static class MathUtils {
// T must extend Number to ensure only numeric types are accepted
public static <T extends Number> double sumArray(T[] numbers) {
double sum = 0.0;
for (T number : numbers) {
sum += number.doubleValue(); // safe conversion for all Number subclasses
}
return sum;
}
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4};
Double[] doubleArray = {1.5, 2.5, 3.5};
System.out.println("Sum of intArray: " + MathUtils.sumArray(intArray)); // Output: 10.0
System.out.println("Sum of doubleArray: " + MathUtils.sumArray(doubleArray)); // Output: 7.5
// The following would fail to compile because String is not a subclass of Number
// String[] strArray = {"a", "b", "c"};
// System.out.println(MathUtils.sumArray(strArray));
}
}
Java also allows multiple bounds by using &
to separate interfaces/classes. For example:
<T extends Number & Comparable<T>>
means T
must be a subtype of Number
and implement Comparable<T>
. This is useful when you need both numeric behavior and the ability to compare objects.
Bounds can also be applied at the class level:
public class NumericBox<T extends Number> {
private T value;
public NumericBox(T value) {
this.value = value;
}
public double doubleValue() {
return value.doubleValue();
}
}
Here, NumericBox
can only be instantiated with subclasses of Number
, ensuring safe numeric operations.
Balance Flexibility and Safety Bounded types let you accept a wide range of compatible types (like all numeric types) while preventing invalid types (like String
). This preserves the flexibility generics provide without sacrificing compile-time type safety.
Enable Use of Specific Methods Without bounds, you cannot safely call methods specific to certain classes. For example, without bounding T extends Number
, calling doubleValue()
on T
would cause a compile error because not all types have this method.
Enhance Code Readability and Intent Bounds make your code's intent clearer. When you declare <T extends Number>
, other developers immediately understand the method or class is meant for numeric types.
Avoid Casting and Runtime Errors By restricting types, you reduce the need for unsafe casting and prevent runtime errors that would occur if a method assumed numeric behavior on non-numeric types.
extends
keyword, e.g., <T extends Number>
.Number
) can be used, enabling type-safe numeric operations.Using bounded type parameters effectively helps you harness the power of generics while avoiding pitfalls of overly general or unsafe code. It's a key technique to write reusable, robust, and expressive Java code.
?
, ? extends
, ? super
Generics in Java provide great flexibility and type safety, but sometimes you want to write methods that can work with a range of related types without specifying the exact type parameter. This is where wildcards come into play. Wildcards allow you to express uncertainty or flexibility in generic type arguments, enabling powerful and reusable APIs.
The wildcard ?
represents an unknown type in generics. It means "some type, but I don't know which." For example:
List<?> list = new ArrayList<String>();
Here, list
can hold any kind of List
, but you cannot add elements to it (except null
) because the exact type is unknown.
Wildcards are useful when you want a method to accept generic types of unknown or varying types, especially when the method doesn't need to modify the collection or wants to maintain flexibility.
There are three main wildcard forms:
?
? extends Type
? super Type
?
This represents any type. It's often used when you only need to read data but don't care about the specific type.
Example:
public void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem);
}
}
You can call printList
with a List<String>
, List<Integer>
, or any other List<T>
. But since the type is unknown, you cannot add elements except null
because it might violate type safety.
? extends Type
This means the unknown type is a subtype of Type
(or Type
itself). It is useful when you want to read from a collection and ensure that everything in it is at least of type Type
.
Example:
public double sumNumbers(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
You can pass List<Integer>
, List<Double>
, or any List
of a subclass of Number
. This guarantees safe reading since every element is at least a Number
.
Important: You cannot add anything to list
except null
, because the exact subtype is unknown. For example, if list
is actually a List<Double>
, adding an Integer
would be unsafe.
? super Type
This means the unknown type is a supertype of Type
(or Type
itself). It is useful when you want to write to a collection safely, ensuring you can add elements of a specific type or its subclasses.
Example:
public void addIntegers(List<? super Integer> list) {
list.add(10);
list.add(20);
}
Here, you can pass List<Integer>
, List<Number>
, or List<Object>
. Because the list is guaranteed to accept Integer
objects (or their subclasses), it's safe to add Integer
values.
Note: You cannot safely read specific types out of such a list other than Object
, because the exact type could be any supertype.
To remember when to use extends
or super
, the Java community uses the PECS acronym:
? extends T
.? super T
.public void processNumbers(List<? extends Number> producer) {
Number n = producer.get(0); // safe to read as Number
// producer.add(3); // Compile error! Can't add to ? extends Number
}
public void fillNumbers(List<? super Integer> consumer) {
consumer.add(5); // safe to add Integer
// Integer n = consumer.get(0); // Compile error! get() returns Object
}
import java.util.*;
public class WildcardDemo {
// Unbounded wildcard: can read but not add
public static void printList(List<?> list) {
System.out.println("Printing list:");
for (Object elem : list) {
System.out.println(" " + elem);
}
}
// Upper bounded wildcard: reads from a list of Numbers or subclasses
public static double sumNumbers(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
// Lower bounded wildcard: writes Integers to a list of Integer or supertypes
public static void addIntegers(List<? super Integer> list) {
list.add(10);
list.add(20);
}
public static void main(String[] args) {
List<String> stringList = Arrays.asList("apple", "banana", "cherry");
List<Integer> intList = new ArrayList<>(Arrays.asList(1, 2, 3));
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
List<Number> numberList = new ArrayList<>();
// Unbounded wildcard demo
printList(stringList);
printList(intList);
// Upper bounded wildcard demo
System.out.println("\nSum of intList: " + sumNumbers(intList)); // Output: 6.0
System.out.println("Sum of doubleList: " + sumNumbers(doubleList)); // Output: 6.6
// Lower bounded wildcard demo
addIntegers(numberList); // Safe to add Integers
printList(numberList);
// Demonstrating PECS in action
processNumbers(intList);
fillNumbers(numberList);
}
// Supporting PECS examples
public static void processNumbers(List<? extends Number> producer) {
System.out.println("\nFirst number (producer): " + producer.get(0));
// producer.add(100); // Compile-time error
}
public static void fillNumbers(List<? super Integer> consumer) {
consumer.add(42); // Safe to add Integer
System.out.println("Added 42 to consumer");
}
}
Without wildcards, you might write overly restrictive code or use unsafe casts. Wildcards allow you to express flexible contracts safely.
For example, a method that accepts List<Number>
cannot accept List<Integer>
, because generics are invariant in Java (List<Integer>
is NOT a subtype of List<Number>
). Using List<? extends Number>
relaxes this restriction, allowing covariance.
? extends
supports covariance (safe reading).? super
supports contravariance (safe writing).? extends
to maintain type safety.Wildcard | Meaning | Use case | Can you add elements? | Can you read elements? |
---|---|---|---|---|
? (unbounded) |
Unknown type | Read-only or general APIs | No (except null) | Yes, as Object |
? extends T |
Subtype of T (covariant) |
Reading from collection | No | Yes, as type T or supertype |
? super T |
Supertype of T (contravariant) |
Writing to collection | Yes, type T or subtype |
Yes, only as Object |
Wildcards are a powerful feature of Java generics that let you write flexible, reusable code while preserving type safety. Understanding how to use ?
, ? extends
, and ? super
effectively—and applying the PECS principle—enables you to design APIs that work cleanly across a variety of types, making your code more robust and maintainable.