1
The Duplication Problem
In Lesson 1, we saw the challenge: if we want a method to print an Integer array, then a String array, then a Student array, we need three separate methods -- identical logic, duplicated code. Let's look at what that actually looks like.
Three overloaded printArray methods
public static void printArray(Integer[] array) {
for (Integer element : array)
System.out.println(element);
}
public static void printArray(String[] array) {
for (String element : array)
System.out.println(element);
}
public static void printArray(Student[] array) {
for (Student element : array)
System.out.println(element);
}
DRY Violation
Same logic, copy-pasted three times. What if we need 100 types? This violates the
DRY principle (Don't Repeat Yourself). Every time we add a new type, we have to write yet another identical method. There has to be a better way.
2
The Object Approach (And Its Limitations)
Your first instinct might be: "Everything in Java extends Object, so why not just use Object[]?" Good thinking! Let's try it.
2a
Printing with Object — It Works!
Using Object[] for printing
public static void printArray(Object[] array) {
for (Object element : array)
System.out.println(element);
}
This works perfectly for printing! Since every class has a toString() method inherited from Object, println can handle anything. But what about returning elements?
2b
Returning with Object — The Problem
A method that returns an element
public static Object getFirst(Object[] array) {
return array[0];
}
// Usage -- casting required!
Integer[] nums = {10, 20, 30};
Integer first = (Integer) getFirst(nums); // Must cast!
String[] names = {"Ali", "Sara"};
String name = (String) getFirst(names); // Must cast!
// DANGER: Wrong cast compiles fine!
Integer wrong = (Integer) getFirst(names); // RUNTIME ERROR!
Casting is Dangerous
When the method returns
Object, we
must cast the result back to the specific type. And if we cast to the wrong type? The code compiles without any warnings, but crashes at runtime with a
ClassCastException. The compiler cannot protect us here.
3
The Generic Method Solution
Here is the big idea: instead of Object, we use a type parameter. The key syntax rule is that <T> goes before the return type. Let's see how the duplicated printArray method becomes a single generic method.
The generic printArray method
public static <T> void printArray(T[] array) {
for (T element : array)
System.out.println(element);
}
Understanding the Syntax
- <T> -- Declares this is a generic method with type parameter T
- void -- The return type (this method returns nothing)
- T[] -- The parameter type; the array can be of any type
- The compiler figures out what T is from the argument you pass
Usage
Calling the Generic Method
One method, many types
Integer[] nums = {10, 20, 30};
String[] names = {"Ali", "Sara", "Ahmad"};
Double[] prices = {9.99, 19.99, 29.99};
printArray(nums); // T is Integer
printArray(names); // T is String
printArray(prices); // T is Double
One Method Handles All Types
ONE method handles
ALL types! No duplication, no casting, complete type safety. The compiler infers the type automatically from the argument you pass.
4
Generic Methods with Return Types
Generic methods are not limited to void. They can also return a value of the generic type. This is where they really shine compared to the Object approach -- no casting required!
A generic method that returns a value
public static <T> T getFirst(T[] array) {
return array[0];
}
Reading the Signature
Let's read <T> T getFirst(T[] array) piece by piece:
- <T> -- "This method has a type parameter called T"
- T (after <T>) -- "The return type is T -- whatever T turns out to be"
- T[] array -- "The parameter is an array of T"
So if you pass an Integer[], T becomes Integer, and the return type becomes Integer.
Usage
No Casting Needed
Using the generic getFirst method
Integer[] nums = {10, 20, 30};
Integer first = getFirst(nums); // Returns Integer -- no cast!
String[] names = {"Ali", "Sara"};
String name = getFirst(names); // Returns String -- no cast!
Type-Safe Returns
No casting needed for the return value! The compiler knows that if you pass
Integer[], the return type is
Integer. If you accidentally try to assign the result to the wrong type, you get a
compile-time error instead of a runtime crash.
5
Side-by-Side Comparison
Let's put the Object approach and the Generic approach next to each other so you can clearly see the difference.
public static Object getFirst(Object[] arr) {
return arr[0];
}
// Must cast -- error-prone!
Integer n = (Integer) getFirst(nums);
public static <T> T getFirst(T[] arr) {
return arr[0];
}
// No cast needed -- type-safe!
Integer n = getFirst(nums);
The Key Difference
With
Object, the compiler forgets the original type -- you have to remember it yourself and cast manually. With generics, the compiler
tracks the type throughout, so it knows exactly what goes in and what comes out.
6
Complete Example
Let's put it all together in a complete, runnable program. You can copy this code into your IDE and run it right away to see generic methods in action.
GenericMethodDemo.java
public class GenericMethodDemo {
// Generic print method
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
// Generic method with return type
public static <T> T getFirst(T[] array) {
return array[0];
}
public static void main(String[] args) {
Integer[] nums = {10, 20, 30};
String[] names = {"Ali", "Sara", "Ahmad"};
System.out.println("Numbers:");
printArray(nums);
System.out.println("Names:");
printArray(names);
Integer firstNum = getFirst(nums);
String firstName = getFirst(names);
System.out.println("First number: " + firstNum);
System.out.println("First name: " + firstName);
}
}
7
Key Takeaways
What You Learned in This Lesson
- Generic methods declare <T> before the return type -- this is the syntax that makes a method generic.
- The compiler infers the type from the arguments -- you do not need to specify the type explicitly when calling the method.
- One method replaces many type-specific methods -- no more copy-pasting the same logic for different types.
- Return types can be generic too (<T> T) -- the return type matches whatever type you pass in.
- No casting needed -- the compiler handles type tracking automatically, catching errors at compile time instead of runtime.
- Generic methods can exist in both generic and non-generic classes -- you do not need a generic class to write a generic method.
Coming Up Next
In
Lesson 4, we will explore
Generic Interfaces -- how to define interfaces with type parameters and implement them in concrete classes. This is the next step toward mastering Java generics.