Generics in Java
Introduced in JDK 5, generics are a key feature in Java that allowed for the introduction of parameterised types, radically changing the language. This implies that classes, interfaces, and methods can be defined with parameters indicating the kind of data they work with. A new grammar element was added, and numerous fundamental API classes and functions underwent modifications as a result.
Benefits of Generics in Java
Significant benefits are offered by generics, chief among them being improved code reuse and type safety.

- Type Safety:
Object
references were frequently used by developers to build generalised code before generics. However, this method was not type safe because any object may be stored, which could result in type mismatch problems at runtime and necessitate the use of explicit, error-prone casts. By enabling robust compile-time type checking, generics make sure that incompatible types are detected at compilation time as opposed to runtime. This makes the code cleaner and more reliable by doing away with the requirement for manual casting. - Code Reusability: No matter what kind of data an algorithm processes, it usually maintains its logical consistency. With the help of generics, programmers can create these algorithms only once, regardless of the type of data, and then use them with little effort on a wide range of data. The creation, testing, and debugging cycle is greatly shortened because a single solution may be created and applied to several data types.
Creating Generic Classes
After the class name, type parameters included in angle brackets (< >
) are used to declare a generic class. These type parameters serve as stand-ins for the actual types that are given during the creation of an object of the generic class. It is crucial to remember that primitive types like int
or char
cannot be used to substitute type parameters; only reference types can. Java’s autoboxing and auto-unboxing features, however, make it easy to use type wrappers with primitive types.
Here’s a simple generic class Gen<T>
and its usage:
// A simple generic class.
class Gen<T> {
T ob; // Declare an object of type T
// Pass the constructor a reference to an object of type T.
Gen(T o) {
ob = o;
}
// Return ob.
T getob() {
return ob;
}
// Show type of T.
void showType() {
System.out.println("Type of T is " + ob.getClass().getName());
}
}
class GenDemo {
public static void main(String args[]) {
// Create a Gen reference for Integers.
Gen<Integer> iOb;
// Create a Gen<Integer> object. Autoboxing encapsulates 88.
iOb = new Gen<Integer>(88);
iOb.showType();
int v = iOb.getob(); // No cast needed
System.out.println("Value: " + v);
System.out.println();
// Create a Gen object for Strings.
Gen<String> strOb = new Gen<String>("Generics Test");
strOb.showType();
String str = strOb.getob(); // No cast needed
System.out.println("Value: " + str);
}
}
Output:
Type of T is java.lang.Integer
Value: 88
Type of T is java.lang.String
Value: Generics Test
Additionally, you can specify a generic class with several type arguments separated by commas (e.g., TwoGen<T, V>
).
Bounded Type Parameters
The types that can be used in place of a type parameter can be limited with bounded type parameters. This is helpful when you need to make that the generic type can do specified things, such being a number or implementing a particular interface. The type argument must be that type or a subclass of it as the extends
keyword specifies an upper bound. For interfaces, extends
means “implements”.
For instance, a class designed to compute the average of an array of numbers can be bounded by Number
:
// T must be Number or a subclass of Number.
class Stats<T extends Number> {
T[] nums; // array of Number or subclass
Stats(T[] o) {
nums = o;
}
double average() {
double sum = 0.0;
for (int i = 0; i < nums.length; i++)
sum += nums[i].doubleValue();
return sum / nums.length;
}
}
class BoundsDemo {
public static void main(String args[]) {
Integer inums[] = {1, 2, 3, 4, 5};
Stats<Integer> iob = new Stats<Integer>(inums);
double v = iob.average();
System.out.println("iob average is " + v);
Double dnums[] = {1.1, 2.2, 3.3, 4.4, 5.5};
Stats<Double> dob = new Stats<Double>(dnums);
double w = dob.average();
System.out.println("dob average is " + w);
// This won't compile because String is not a subclass of Number.
// String strs[] = {"1", "2", "3", "4", "5"};
// Stats<String> strob = new Stats<String>(strs);
// double x = strob.average();
// System.out.println("strob average is " + v);
}
}
Output:
iob average is 3.0
dob average is 3.3
You can also specify multiple bounds using the ampersand (&
) symbol, for example, <T extends MyClass & MyInterface>
, requiring the type argument to extend a class and implement one or more interfaces.
Wildcard Arguments
The ?
symbol for wildcard arguments indicates an unknown type. They are essential in situations when a method must function on several instances of a generic type, but the precise type argument is either irrelevant or subject to change. For example, to compare the averages of Stats<Integer>
and Stats<Double>
objects.
It is also possible to limit wildcards:
- Upper-bounded wildcards (
? extends T
):T
or a subtype ofT
must be the unknown type. This usually applies to “read” access. - Lower-bounded wildcards (
? super T
): Wildcards with lower bounds (? super T) require that the unknown type be eitherT
or a supertype ofT
. This usually applies to “write” access.
Here’s an example demonstrating a wildcard in a method:
// Use a wildcard.
class Stats<T extends Number> {
T[] nums;
Stats(T[] o) {
nums = o;
}
double average() {
double sum = 0.0;
for (int i = 0; i < nums.length; i++)
sum += nums[i].doubleValue();
return sum / nums.length;
}
// Determine if two averages are the same.
// Notice the use of the wildcard.
boolean sameAvg(Stats<?> ob) {
if (average() == ob.average())
return true;
return false;
}
}
class WildcardDemo {
public static void main(String args[]) {
Integer inums[] = {1, 2, 3, 4, 5};
Stats<Integer> iob = new Stats<Integer>(inums);
System.out.println("iob average is " + iob.average());
Double dnums[] = {1.1, 2.2, 3.3, 4.4, 5.5};
Stats<Double> dob = new Stats<Double>(dnums);
System.out.println("dob average is " + dob.average());
Float fnums[] = {1.0F, 2.0F, 3.0F, 4.0F, 5.0F};
Stats<Float> fob = new Stats<Float>(fnums);
System.out.println("fob average is " + fob.average());
System.out.print("Averages of iob and dob ");
if (iob.sameAvg(dob))
System.out.println("are the same.");
else
System.out.println("differ.");
System.out.print("Averages of iob and fob ");
if (iob.sameAvg(fob))
System.out.println("are the same.");
else
System.out.println("differ.");
}
}
Output:
iob average is 3.0
dob average is 3.3
fob average is 3.0
Averages of iob and dob differ.
Averages of iob and fob are the same.
Generic Methods
Methods are generic regardless of whether the enclosing class is generic since they can have their own type parameters. The return type of the method comes after the type parameter list. In this way, type safety is maintained yet a single method can work on various types.
// A non-generic class with a generic method.
class GenMethDemo {
// Determine if an object is in an array.
static <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y) {
for (int i = 0; i < y.length; i++)
if (x.equals(y[i])) return true;
return false;
}
public static void main(String args[]) {
Integer nums[] = {1, 2, 3, 4, 5};
if (isIn(2, nums))
System.out.println("2 is in nums");
if (!isIn(7, nums))
System.out.println("7 is not in nums");
System.out.println();
String strs[] = {"one", "two", "three", "four", "five"};
if (isIn("two", strs))
System.out.println("two is in strs");
if (!isIn("seven", strs))
System.out.println("seven is not in strs");
}
}
Output:
2 is in nums
7 is not in nums
two is in strs
seven is not in strs
Generic Constructors
Additionally, a constructor may be generic even if its class is not. This is helpful for initialising objects of different numeric types since, for example, the constructor can be parameterised to accept any type that extends Number
.
// Use a generic constructor.
class GenCons {
private double val;
<T extends Number> GenCons(T arg) { // Generic constructor
val = arg.doubleValue();
}
void showval() {
System.out.println("val: " + val);
}
}
class GenConsDemo {
public static void main(String args[]) {
GenCons test = new GenCons(100);
GenCons test2 = new GenCons(123.5F);
test.showval();
test2.showval();
}
}
Output:
val: 100.0
val: 123.5
Generic Interfaces
Similar to classes, interfaces can be generic. A generic interface is declared with type parameters, just as a generic class. A class must usually be generic or provide a concrete type for the interface’s type parameter if it implements a generic interface.
Here’s an example of a generic interface MinMax
and its implementation:
// A generic Min/Max interface.
interface MinMax<T extends Comparable<T>> {
T min();
T max();
}
// Implement MinMax
class MyClass<T extends Comparable<T>> implements MinMax<T> {
T[] vals;
MyClass(T[] o) {
vals = o;
}
public T min() {
T v = vals;
for (int i = 1; i < vals.length; i++)
if (vals[i].compareTo(v) < 0) v = vals[i];
return v;
}
public T max() {
T v = vals;
for (int i = 1; i < vals.length; i++)
if (vals[i].compareTo(v) > 0) v = vals[i];
return v;
}
}
class GenIFDemo {
public static void main(String args[]) {
Integer inums[] = {3, 6, 2, 8, 6};
Character chs[] = {'b', 'r', 'p', 'w'};
MyClass<Integer> iob = new MyClass<Integer>(inums);
System.out.println("Max value in inums: " + iob.max());
System.out.println("Min value in inums: " + iob.min());
MyClass<Character> cob = new MyClass<Character>(chs);
System.out.println("Max value in chs: " + cob.max());
System.out.println("Min value in chs: " + cob.min());
}
}
Output:
Max value in inums: 8
Min value in inums: 2
Max value in chs: w
Min value in chs: b
Type Inference (Diamond Operator )
The diamond operator (<>
) was introduced in JDK 7 and makes it easier to create instances of generic types. It enables the compiler to determine the type arguments that the constructor requires from the context, removing the need for duplicate specification.
For example, instead of: MyClass<Integer, String> mcOb = new MyClass<Integer, String>(98, "A String");
You can write: MyClass<Integer, String> mcOb = new MyClass<>(98, "A String");
It is also possible to use this shorthand for argument passing in method calls.
These characteristics make generics a strong and adaptable framework for creating Java code that is clean, reusable, and type-safe.