-
Hajipur, Bihar, 844101
Generics are one of the most powerful features in Java. They allow you to write flexible, reusable, and type-safe code. In simple terms, Generics mean writing classes, interfaces, and methods that can work with different data types without rewriting the same logic multiple times.
You’ll often see angle brackets like <T> in Java code. That’s where generics come into play. Let’s break it down step by step and understand how generics work, why they’re important, and how you can use them effectively.
Generics enable you to define a class, interface, or method with a placeholder for a data type. Instead of hardcoding a specific data type, you can define a generic type that gets replaced with an actual type when the code runs.
For example:
ArrayList<Integer> list = new ArrayList<>();
Here, ArrayList is a generic class, and <Integer> specifies the type of elements it will hold.
Without generics, you could accidentally insert the wrong type of object into a collection. With generics, the compiler ensures type safety, reducing runtime errors.
Generics are useful for several reasons:
Type Safety: You can ensure that only specific types of objects are used.
Eliminate Type Casting: No need to manually cast objects when retrieving them from collections.
Reusability: Write one version of code that works with multiple data types.
Compile-Time Checking: Type-related errors are caught during compilation, not at runtime.
Before generics were introduced, Java collections used Object type. This meant you had to cast objects manually.
import java.util.ArrayList;
public class WithoutGenerics {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("Hello");
list.add(100); // Allowed, but not type-safe
String text = (String) list.get(0); // Casting required
System.out.println(text);
}
}
If you accidentally cast the wrong type, you’d get a runtime ClassCastException.
Now let’s rewrite the same example using generics.
import java.util.ArrayList;
public class WithGenerics {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
// list.add(100); // Compile-time error
String text = list.get(0); // No casting needed
System.out.println(text);
}
}
Here, ArrayList<String> ensures only strings are added to the list. If you try to add an integer, the compiler will show an error before running the program.
You can also create your own generic classes. Let’s say you want a class that can store and return any type of data.
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public static void main(String[] args) {
Box<Integer> intBox = new Box<>();
intBox.setValue(100);
System.out.println("Integer value: " + intBox.getValue());
Box<String> strBox = new Box<>();
strBox.setValue("Java Generics");
System.out.println("String value: " + strBox.getValue());
}
}
Here, T is a type parameter, which can represent any data type. When you create an object, you specify what T should be.
You can use single or multiple type parameters. Common naming conventions are:
T – Type
E – Element (used in collections)
K – Key
V – Value
Example with two 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; }
public static void main(String[] args) {
Pair<Integer, String> pair = new Pair<>(1, "Java");
System.out.println(pair.getKey() + " = " + pair.getValue());
}
}
You can also make methods generic. This is useful when only one method needs to be generic, not the entire class.
public class GenericMethodExample {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4};
String[] strArray = {"A", "B", "C"};
printArray(intArray);
printArray(strArray);
}
}
Here, <T> before the return type tells Java that this method is generic and can work with any type of array.
Sometimes, you want to restrict what types can be used with generics. For example, a method should work only for numbers.
You can use bounded type parameters with the extends keyword.
public class BoundedType {
public static <T extends Number> void showValue(T value) {
System.out.println("Value: " + value);
}
public static void main(String[] args) {
showValue(100); // int
showValue(45.67); // double
// showValue("Hello"); // Error
}
}
Here, T extends Number means only classes that extend Number (like Integer, Double, Float) are allowed.
Wildcards (?) are used when you don’t know the exact type at compile time or when flexibility is needed.
Example:
import java.util.List;
import java.util.ArrayList;
public class WildcardExample {
public static void displayList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(10);
intList.add(20);
List<String> strList = new ArrayList<>();
strList.add("A");
strList.add("B");
displayList(intList);
displayList(strList);
}
}
The ? wildcard allows the method to accept any type of list.
You can further restrict wildcards using extends and super.
Upper bounded wildcard (? extends Type) → accepts a type or its subclasses.
Lower bounded wildcard (? super Type) → accepts a type or its superclasses.
Example:
import java.util.*;
public class BoundedWildcard {
public static void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
printNumbers(intList);
printNumbers(doubleList);
}
}
Compile-time type checking prevents runtime errors.
Code reusability — write once, use for multiple types.
No explicit casting needed.
Improved readability and cleaner code.
Generics make Java code safer, cleaner, and more reusable.
They help eliminate type casting and runtime errors while ensuring your collections and methods work smoothly across multiple data types.
In short:
Use <T> to define a generic type.
Apply generics to classes, methods, and interfaces.
Use bounded types to restrict allowed data types.
Use wildcards (?) for flexibility when working with collections.
Generics are an essential concept for writing modern Java code, especially when you start working with frameworks like Spring, Hibernate, and Java Collections Framework.
Write a generic Java class named Box<T> that can store and return any data type. Demonstrate it using both String and Integer values.
Create a generic method named printArray() that prints all elements of an array, regardless of its type. Test it with arrays of integers, doubles, and strings.
Write a Java program to create a generic class Pair<K, V> that stores a key-value pair and prints them.
Create a generic method getLastElement() that returns the last element of any array passed to it.
Write a generic program that accepts only subclasses of Number and prints the sum of all numbers in a list.
Create a program that uses an unbounded wildcard (?) to print the contents of different types of lists.
Write a program that demonstrates upper bounded wildcard (? extends Number) by printing numeric lists such as Integer, Double, and Float.
Write a program that demonstrates lower bounded wildcard (? super Integer) by inserting integers into a list and printing them.
Build a generic class DataHolder<T> with methods to set and get data. Create objects using String, Double, and Character types.
Create a Java program that defines a generic method <T extends Comparable<T>> T findMax(T[] array) which returns the maximum element in any array of comparable objects.