Java Generic Methods
Java Generic Methods
A generic method declares its own type parameter, independent of any class-level type parameter. This is the mechanism that makes Collections.sort(), Arrays.asList(), and Collections.unmodifiableList() work with any element type — the method itself carries the <T> declaration, so the caller never needs to specify a type explicitly, and the compiler figures out what T is from the arguments passed. The difference from a generic class is scope: a class-level type parameter is fixed for the lifetime of an object, but a method-level type parameter is resolved fresh for every single call.
What Is a Generic Method?
A generic method is any method — static or instance, in a generic class or a non-generic class — that declares one or more type parameters in angle brackets placed before the return type in the method signature.
SYNTAX — type parameter list goes BEFORE the return type:
[modifier] <T> ReturnType methodName(ParameterType parameter) {
// T is available here
}
Examples:
public static <T> void swap(T[] array, int i, int j) { ... }
public <T, R> R convert(T input, Converter<T, R> converter) { ... }
public static <T extends Comparable<T>> T max(T first, T second) { ... }
Compare with a REGULAR method (no type parameter):
public static void swap(String[] array, int i, int j) { ... }
The <T> before the return type is the ONLY difference in syntax.
Everything else — modifiers, return type, name, parameters — is identical.
Basic Overview - What Generic Methods Do That Non-Generic Methods Cannot
1. THE TYPE PARAMETER LIVES ON THE METHOD, NOT THE CLASS
Fresher view : a generic method is a method that works with
ANY type, decided by whoever calls it.
Collections.sort(list) works with List<String>,
List<Integer>, or List<Order> - same method, any type.
Deeper view : the <T> before the return type creates a NEW type
variable scoped to this method call only. When the
method returns, T is gone. The next call to the
same method resolves a completely fresh T. This is
fundamentally different from a class type parameter,
which is fixed for the whole object lifetime.
2. TYPE INFERENCE - the caller usually does not specify T
Fresher view : you call Collections.sort(myList) not
Collections.<String>sort(myList). The compiler
looks at what you passed in and figures out T itself.
Deeper view : the compiler performs TYPE ARGUMENT INFERENCE by
examining the method's argument types and, in
assignment context, the expected return type.
If inference is ambiguous, the caller can explicitly
supply the type argument before the method name:
Collections.<String>sort(myList). This is rare
but occasionally necessary when inference fails.
3. GENERIC METHODS IN NON-GENERIC CLASSES
Fresher view : a class does not need to be generic for its
methods to be generic. Most utility classes
(like Collections, Arrays, Objects) are not
generic classes - but their static methods are
generic methods.
Deeper view : this is the most important distinction for
production code. A generic method in a non-generic
class has its own independent type variable with
no relationship to any enclosing class parameter.
It compiles to a method that accepts Object with
implicit casts, exactly like a method in a
generic class - type erasure applies the same way.
4. BOUNDED TYPE PARAMETERS ON METHODS
Fresher view : <T extends Comparable<T>> means T must be a type
that can compare itself to other T values - like
String, Integer, or LocalDate. The method can then
call compareTo() on values of type T.
Deeper view : the bound is erased to Comparable at runtime -
T becomes Comparable in bytecode, not Object.
This means the CHECKCAST inserted at call sites
verifies the Comparable contract, and the method
body can call Comparable methods without any cast.
Why Generic Methods Exist
The alternative to a generic method is either accepting Object (losing all type safety) or writing one method per type (sortStrings, sortIntegers, sortOrders). Neither scales.
1// File: WhyGenericMethodsDemo.java
2
3import java.util.Arrays;
4
5public class WhyGenericMethodsDemo {
6
7 // WITHOUT generics: one method per type - unacceptable duplication
8 static void swapStrings(String[] array, int i, int j) {
9 String temp = array[i];
10 array[i] = array[j];
11 array[j] = temp;
12 }
13
14 static void swapIntegers(Integer[] array, int i, int j) {
15 Integer temp = array[i];
16 array[i] = array[j];
17 array[j] = temp;
18 }
19 // ... and swapOrders, swapProducts, swapCategories for every type ever needed
20
21 // WITH generics: one method works for every reference type
22 static <T> void swap(T[] array, int i, int j) {
23 T temp = array[i];
24 array[i] = array[j];
25 array[j] = temp;
26 }
27
28 public static void main(String[] args) {
29
30 System.out.println("=== Generic swap with String[] ===");
31 String[] cities = {"Mumbai", "Delhi", "Bengaluru", "Hyderabad"};
32 System.out.println("Before: " + Arrays.toString(cities));
33 swap(cities, 0, 3); // T inferred as String from the argument
34 System.out.println("After : " + Arrays.toString(cities));
35
36 System.out.println();
37
38 System.out.println("=== Same generic method with Integer[] ===");
39 Integer[] prices = {799, 1299, 499, 2499};
40 System.out.println("Before: " + Arrays.toString(prices));
41 swap(prices, 1, 2); // T inferred as Integer - same method, different type
42 System.out.println("After : " + Arrays.toString(prices));
43 }
44}Output:
=== Generic swap with String[] ===
Before: [Mumbai, Delhi, Bengaluru, Hyderabad]
After : [Hyderabad, Delhi, Bengaluru, Mumbai]
=== Same generic method with Integer[] ===
Before: [799, 1299, 499, 2499]
After : [799, 499, 1299, 2499]
How Generic Methods Work
Declaring a Generic Method
The type parameter list appears between the method's access modifiers and its return type. A static generic method and an instance generic method use the same syntax.
1// File: GenericMethodSyntaxDemo.java
2
3import java.util.ArrayList;
4import java.util.List;
5import java.util.Optional;
6
7public class GenericMethodSyntaxDemo {
8
9 // Static generic method - T declared before void
10 public static <T> void printAll(List<T> items) {
11 for (T item : items) {
12 System.out.println(" " + item);
13 }
14 }
15
16 // Instance generic method - T declared before the return type List<T>
17 public <T> List<T> repeat(T element, int times) {
18 List<T> result = new ArrayList<>();
19 for (int i = 0; i < times; i++) {
20 result.add(element);
21 }
22 return result;
23 }
24
25 // Generic method with TWO type parameters
26 public static <K, V> String formatEntry(K key, V value) {
27 return key + " -> " + value;
28 }
29
30 // Generic method returning Optional<T>
31 public static <T> Optional<T> firstOrEmpty(List<T> items) {
32 if (items == null || items.isEmpty()) return Optional.empty();
33 return Optional.of(items.get(0));
34 }
35
36 public static void main(String[] args) {
37
38 System.out.println("=== printAll with List<String> ===");
39 List<String> categories = List.of("Electronics", "Clothing", "Home & Kitchen");
40 GenericMethodSyntaxDemo.printAll(categories); // T inferred as String
41
42 System.out.println();
43
44 System.out.println("=== printAll with List<Integer> ===");
45 List<Integer> scores = List.of(95, 87, 72);
46 GenericMethodSyntaxDemo.printAll(scores); // T inferred as Integer
47
48 System.out.println();
49
50 GenericMethodSyntaxDemo demo = new GenericMethodSyntaxDemo();
51
52 System.out.println("=== repeat with String ===");
53 List<String> repeated = demo.repeat("Zepto", 3);
54 System.out.println(repeated);
55
56 System.out.println();
57
58 System.out.println("=== formatEntry with different types ===");
59 System.out.println(GenericMethodSyntaxDemo.formatEntry("productId", "PROD-101"));
60 System.out.println(GenericMethodSyntaxDemo.formatEntry(1001, true));
61
62 System.out.println();
63
64 System.out.println("=== firstOrEmpty ===");
65 System.out.println(GenericMethodSyntaxDemo.firstOrEmpty(categories));
66 System.out.println(GenericMethodSyntaxDemo.firstOrEmpty(List.of()));
67 }
68}Output:
=== printAll with List<String> ===
Electronics
Clothing
Home & Kitchen
=== printAll with List<Integer> ===
95
87
72
=== repeat with String ===
[Zepto, Zepto, Zepto]
=== formatEntry with different types ===
productId -> PROD-101
1001 -> true
=== firstOrEmpty ===
Optional[Electronics]
Optional.empty
Bounded Type Parameters on Methods
A bounded type parameter <T extends SomeSupertype> restricts what types the caller can use and, crucially, lets the method call methods declared on SomeSupertype directly on values of type T.
1// File: BoundedGenericMethodDemo.java
2
3import java.util.List;
4
5public class BoundedGenericMethodDemo {
6
7 // T must implement Comparable<T> - so compareTo() is available on T values
8 // without any cast. This works for String, Integer, Double, LocalDate, and
9 // any class that implements Comparable<T> with itself.
10 public static <T extends Comparable<T>> T max(T first, T second) {
11 return first.compareTo(second) >= 0 ? first : second;
12 }
13
14 public static <T extends Comparable<T>> T min(T first, T second) {
15 return first.compareTo(second) <= 0 ? first : second;
16 }
17
18 // Multiple bounds: T must extend Number AND implement Comparable<T>
19 // The & operator separates multiple bounds - only ONE class bound allowed,
20 // interfaces can be listed with &
21 public static <T extends Number & Comparable<T>> T clamp(T value, T low, T high) {
22 if (value.compareTo(low) < 0) return low;
23 if (value.compareTo(high) > 0) return high;
24 return value;
25 }
26
27 // Finding the maximum value in a list - T must be Comparable
28 public static <T extends Comparable<T>> T maxInList(List<T> items) {
29 if (items == null || items.isEmpty()) {
30 throw new IllegalArgumentException("List must not be null or empty");
31 }
32 T result = items.get(0);
33 for (T item : items) {
34 if (item.compareTo(result) > 0) {
35 result = item;
36 }
37 }
38 return result;
39 }
40
41 public static void main(String[] args) {
42
43 System.out.println("=== max() with String ===");
44 System.out.println(max("Ananya", "Rahul")); // T = String, String comparison
45 System.out.println(max("Bengaluru", "Mumbai")); // lexicographic
46
47 System.out.println();
48
49 System.out.println("=== max() with Integer ===");
50 System.out.println(max(2499, 1299)); // T = Integer, numeric comparison
51 System.out.println(min(2499, 1299));
52
53 System.out.println();
54
55 System.out.println("=== clamp() with Integer ===");
56 System.out.println(clamp(150, 100, 200)); // within range -> 150
57 System.out.println(clamp(50, 100, 200)); // below low -> 100
58 System.out.println(clamp(300, 100, 200)); // above high -> 200
59
60 System.out.println();
61
62 System.out.println("=== maxInList() with Double (order amounts) ===");
63 List<Double> orderAmounts = List.of(1499.0, 3299.0, 799.0, 4999.0, 2100.0);
64 System.out.println("Highest order: Rs." + maxInList(orderAmounts));
65 }
66}Output:
=== max() with String ===
Rahul
Mumbai
=== max() with Integer ===
2499
1299
=== clamp() with Integer ===
150
100
200
=== maxInList() with Double (order amounts) ===
Highest order: Rs.4999.0
Type Inference - When the Compiler Resolves T
Type inference is what makes generic methods usable without verbose type specifications. The compiler examines the arguments you pass to determine what T must be, and in return-type contexts it also uses the declared type of the variable being assigned.
1// File: TypeInferenceDemo.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class TypeInferenceDemo {
7
8 // The compiler infers T from the argument list passed at each call site
9 static <T> List<T> listOf(T first, T second, T third) {
10 List<T> result = new ArrayList<>();
11 result.add(first);
12 result.add(second);
13 result.add(third);
14 return result;
15 }
16
17 static <T> T identity(T value) {
18 return value;
19 }
20
21 public static void main(String[] args) {
22
23 System.out.println("=== Type inference from arguments ===");
24
25 // T inferred as String - all three arguments are String
26 List<String> cities = listOf("Mumbai", "Pune", "Nagpur");
27 System.out.println("Cities: " + cities);
28
29 // T inferred as Integer - all three arguments are Integer (auto-boxed)
30 List<Integer> codes = listOf(400001, 411001, 440001);
31 System.out.println("Codes : " + codes);
32
33 System.out.println();
34
35 System.out.println("=== Explicit type witness when inference needs help ===");
36
37 // Without the explicit type witness, the compiler would infer
38 // T as the common supertype - here Object, since null has no type
39 // The explicit <String> before the method name forces T = String
40 String result = TypeInferenceDemo.<String>identity(null);
41 System.out.println("Explicit witness result: " + result);
42
43 System.out.println();
44
45 System.out.println("=== Inference vs explicit ===");
46 // Inference: T = String (from the String argument)
47 String inferred = identity("CRED Rewards");
48
49 // Explicit witness: same result, just more verbose
50 String explicit = TypeInferenceDemo.<String>identity("CRED Rewards");
51
52 System.out.println("Inferred : " + inferred);
53 System.out.println("Explicit : " + explicit);
54 System.out.println("Results identical: " + inferred.equals(explicit));
55 }
56}Output:
=== Type inference from arguments ===
Cities: [Mumbai, Pune, Nagpur]
Codes : [400001, 411001, 440001]
=== Explicit type witness when inference needs help ===
Explicit witness result: null
=== Inference vs explicit ===
Inferred : CRED Rewards
Explicit : CRED Rewards
Results identical: true
Generic Methods in Non-Generic Classes
This is the pattern the entire java.util.Collections class uses. The class itself has no type parameter — but methods like sort(), reverse(), shuffle(), and unmodifiableList() are all generic methods. Every generic utility class in production follows this pattern.
1// File: GenericUtilityDemo.java
2
3import java.util.*;
4
5public class GenericUtilityDemo {
6
7 // A non-generic utility class with generic static methods -
8 // the exact pattern java.util.Collections uses
9 static class CollectionUtils {
10
11 // No class-level type parameter - only method-level <T>
12
13 static <T> List<T> immutableCopy(List<T> source) {
14 return List.copyOf(source);
15 }
16
17 static <T> Optional<T> findFirst(List<T> items,
18 java.util.function.Predicate<T> predicate) {
19 for (T item : items) {
20 if (predicate.test(item)) return Optional.of(item);
21 }
22 return Optional.empty();
23 }
24
25 static <T> void fill(List<T> list, T value) {
26 list.replaceAll(ignored -> value);
27 }
28
29 // Two type parameters - transforms every element from one type to another
30 static <S, T> List<T> mapList(List<S> source,
31 java.util.function.Function<S, T> mapper) {
32 List<T> result = new ArrayList<>(source.size());
33 for (S item : source) {
34 result.add(mapper.apply(item));
35 }
36 return result;
37 }
38 }
39
40 public static void main(String[] args) {
41
42 System.out.println("=== immutableCopy ===");
43 List<String> mutable = new ArrayList<>(List.of("Swiggy", "Zomato", "Dunzo"));
44 List<String> immutable = CollectionUtils.immutableCopy(mutable);
45 System.out.println("Copy: " + immutable);
46 mutable.add("Blinkit");
47 System.out.println("Original grown: " + mutable.size() + " items");
48 System.out.println("Copy unchanged: " + immutable.size() + " items");
49
50 System.out.println();
51
52 System.out.println("=== findFirst ===");
53 List<Integer> amounts = List.of(499, 1299, 799, 3499, 199);
54 Optional<Integer> firstHighValue = CollectionUtils.findFirst(
55 amounts, amount -> amount > 1000);
56 System.out.println("First amount above Rs.1000: " + firstHighValue.orElse(-1));
57
58 System.out.println();
59
60 System.out.println("=== mapList - Integer prices to formatted Strings ===");
61 List<Integer> prices = List.of(499, 1299, 799);
62 List<String> formatted = CollectionUtils.mapList(
63 prices, price -> "Rs." + price);
64 System.out.println(formatted);
65 }
66}Output:
=== immutableCopy ===
Copy: [Swiggy, Zomato, Dunzo]
Original grown: 4 items
Copy unchanged: 3 items
=== findFirst ===
First amount above Rs.1000: 1299
=== mapList - Integer prices to formatted Strings ===
[Rs.499, Rs.1299, Rs.799]
Generic Constructors
Constructors can also declare their own type parameters, independently of the class's type parameter. This is less common than generic methods but follows the same syntax rules - the type parameter goes before the constructor name (which is the class name, so effectively before the implicit return type).
1// File: GenericConstructorDemo.java
2
3import java.util.Arrays;
4import java.util.List;
5
6public class GenericConstructorDemo {
7
8 // A non-generic wrapper that can be constructed from any source
9 static class DataWrapper {
10 private final String description;
11 private final int elementCount;
12 private final String firstElementStr;
13
14 // Generic constructor - <T> declared before the constructor name
15 // The class is NOT generic, but this constructor accepts any List<T>
16 <T> DataWrapper(List<T> sourceData, String description) {
17 this.description = description;
18 this.elementCount = sourceData.size();
19 this.firstElementStr = sourceData.isEmpty()
20 ? "(empty)" : sourceData.get(0).toString();
21 }
22
23 // Second generic constructor - accepts an array of any type
24 <T> DataWrapper(T[] sourceArray, String description) {
25 this.description = description;
26 this.elementCount = sourceArray.length;
27 this.firstElementStr = sourceArray.length == 0
28 ? "(empty)" : sourceArray[0].toString();
29 }
30
31 @Override
32 public String toString() {
33 return "DataWrapper[" + description + ", count=" + elementCount
34 + ", first=" + firstElementStr + "]";
35 }
36 }
37
38 public static void main(String[] args) {
39
40 System.out.println("=== Generic constructor - from List<String> ===");
41 List<String> productNames = List.of("Laptop", "Tablet", "Headphones");
42 DataWrapper fromList = new DataWrapper<>(productNames, "ProductNames");
43 System.out.println(fromList);
44
45 System.out.println();
46
47 System.out.println("=== Generic constructor - from Integer[] ===");
48 Integer[] stockLevels = {120, 45, 200, 8, 67};
49 DataWrapper fromArray = new DataWrapper<>(stockLevels, "StockLevels");
50 System.out.println(fromArray);
51
52 System.out.println();
53
54 System.out.println("=== Generic constructor - from Double[] ===");
55 Double[] prices = {499.0, 1299.0, 799.0};
56 DataWrapper fromPrices = new DataWrapper<>(prices, "Prices");
57 System.out.println(fromPrices);
58 }
59}Output:
=== Generic constructor - from List<String> ===
DataWrapper[ProductNames, count=3, first=Laptop]
=== Generic constructor - from Integer[] ===
DataWrapper[StockLevels, count=5, first=120]
=== Generic constructor - from Double[] ===
DataWrapper[Prices, count=3, first=499.0]
Generic Method vs Generic Class - When to Use Which
USE A GENERIC METHOD when:
- The type parameter is only needed for ONE or a few methods
- The class itself does not hold any state of type T
- You are writing a utility or helper method that should work
with any type
- The type relationship spans only the method signature and body,
not the object's lifetime
Examples: Collections.sort(), swap(), max(), findFirst(),
any transformation or predicate utility
USE A GENERIC CLASS when:
- The type parameter defines what the class HOLDS or STORES
- Multiple methods in the class need to share the same type variable
- The type relationship must persist across multiple method calls
on the same object
Examples: List<E>, Map<K,V>, Optional<T>, Box<T>, Stack<T>
THE TELLTALE QUESTION:
"Does this class keep any state of type T between method calls?"
YES -> generic class (the type variable needs object lifetime scope)
NO -> generic method in a possibly non-generic class
Real-World Example - Flipkart Data Pipeline Utilities
A product data pipeline at Flipkart needs several generic utility operations — transforming raw feed data, filtering records by configurable predicates, grouping into batches, and safely extracting a first-valid value from a sequence of suppliers. All of these operations are type-agnostic: they work exactly the same way whether the data is Product, Seller, Order, or Category. Writing them as generic methods in a non-generic utility class is the standard pattern for this kind of shared pipeline infrastructure.
1// File: PipelineUtils.java
2
3import java.util.*;
4import java.util.function.*;
5
6public class PipelineUtils {
7
8 // Splits a large list into smaller fixed-size batches.
9 // Works for any element type - the batch logic does not care about T.
10 public static <T> List<List<T>> partition(List<T> source, int batchSize) {
11 if (batchSize <= 0) {
12 throw new IllegalArgumentException("batchSize must be positive: " + batchSize);
13 }
14 List<List<T>> batches = new ArrayList<>();
15 int total = source.size();
16 for (int start = 0; start < total; start += batchSize) {
17 int end = Math.min(start + batchSize, total);
18 batches.add(List.copyOf(source.subList(start, end)));
19 }
20 return batches;
21 }
22
23 // Transforms a List<S> into a List<T> using a provided mapping function.
24 // The result list is the same size; elements at matching indices correspond.
25 public static <S, T> List<T> transform(List<S> source, Function<S, T> mapper) {
26 Objects.requireNonNull(source, "source must not be null");
27 Objects.requireNonNull(mapper, "mapper must not be null");
28 List<T> result = new ArrayList<>(source.size());
29 for (S item : source) {
30 result.add(mapper.apply(item));
31 }
32 return result;
33 }
34
35 // Filters a list to only those elements satisfying the predicate.
36 // Returns a new list - never modifies the source.
37 public static <T> List<T> filter(List<T> source, Predicate<T> predicate) {
38 Objects.requireNonNull(source, "source must not be null");
39 Objects.requireNonNull(predicate, "predicate must not be null");
40 List<T> result = new ArrayList<>();
41 for (T item : source) {
42 if (predicate.test(item)) result.add(item);
43 }
44 return result;
45 }
46
47 // Returns the first non-null value produced by any supplier in the list.
48 // This is the "fallback chain" pattern: try the primary source, then
49 // secondary, then tertiary, until something returns a value.
50 public static <T> Optional<T> firstNonNull(List<Supplier<T>> suppliers) {
51 for (Supplier<T> supplier : suppliers) {
52 T value = supplier.get();
53 if (value != null) return Optional.of(value);
54 }
55 return Optional.empty();
56 }
57
58 // Groups elements of a list into a map by a key extracted from each element.
59 // Multiple elements can map to the same key - the value is a List<T>.
60 public static <T, K> Map<K, List<T>> groupBy(List<T> source,
61 Function<T, K> keyExtractor) {
62 Map<K, List<T>> groups = new LinkedHashMap<>();
63 for (T item : source) {
64 K key = keyExtractor.apply(item);
65 groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(item);
66 }
67 return groups;
68 }
69}1// File: Product.java
2
3public record Product(String id, String name, String category, double price, boolean inStock) {}1// File: PipelineDemo.java
2
3import java.util.*;
4import java.util.function.*;
5
6public class PipelineDemo {
7
8 public static void main(String[] args) {
9 List<Product> catalog = List.of(
10 new Product("P001", "Wireless Mouse", "Electronics", 799.0, true),
11 new Product("P002", "Laptop Stand", "Accessories", 1299.0, true),
12 new Product("P003", "USB-C Hub", "Electronics", 1799.0, false),
13 new Product("P004", "Mechanical Keyboard", "Electronics", 3499.0, true),
14 new Product("P005", "Webcam HD", "Electronics", 999.0, false),
15 new Product("P006", "Monitor Light", "Accessories", 549.0, true),
16 new Product("P007", "Cable Organiser", "Accessories", 299.0, true)
17 );
18
19 System.out.println("=== partition into batches of 3 ===");
20 List<List<Product>> batches = PipelineUtils.partition(catalog, 3);
21 for (int i = 0; i < batches.size(); i++) {
22 System.out.println("Batch " + (i + 1) + ":");
23 batches.get(i).forEach(p -> System.out.println(" " + p.id() + " " + p.name()));
24 }
25
26 System.out.println();
27
28 System.out.println("=== filter - in-stock products only ===");
29 List<Product> inStock = PipelineUtils.filter(catalog, Product::inStock);
30 inStock.forEach(p -> System.out.println(" " + p.id() + " " + p.name()));
31
32 System.out.println();
33
34 System.out.println("=== transform - Product to display name string ===");
35 List<String> displayNames = PipelineUtils.transform(
36 inStock, p -> p.name() + " (Rs." + (int) p.price() + ")");
37 displayNames.forEach(name -> System.out.println(" " + name));
38
39 System.out.println();
40
41 System.out.println("=== groupBy category ===");
42 Map<String, List<Product>> byCategory =
43 PipelineUtils.groupBy(catalog, Product::category);
44 byCategory.forEach((category, products) -> {
45 System.out.println(category + ":");
46 products.forEach(p -> System.out.println(" " + p.name()));
47 });
48
49 System.out.println();
50
51 System.out.println("=== firstNonNull - fallback chain for featured banner ===");
52 List<Supplier<String>> bannerSources = List.of(
53 () -> null, // external CMS - unavailable
54 () -> null, // A/B test variant - not assigned
55 () -> "Flipkart Big Billion Days - Up to 80% Off" // default banner
56 );
57 Optional<String> banner = PipelineUtils.firstNonNull(bannerSources);
58 System.out.println("Banner: " + banner.orElse("No banner configured"));
59 }
60}Output:
=== partition into batches of 3 ===
Batch 1:
P001 Wireless Mouse
P002 Laptop Stand
P003 USB-C Hub
Batch 2:
P004 Mechanical Keyboard
P005 Webcam HD
P006 Monitor Light
Batch 3:
P007 Cable Organiser
=== filter - in-stock products only ===
P001 Wireless Mouse
P002 Laptop Stand
P004 Mechanical Keyboard
P006 Monitor Light
P007 Cable Organiser
=== transform - Product to display name string ===
Wireless Mouse (Rs.799)
Laptop Stand (Rs.1299)
Mechanical Keyboard (Rs.3499)
Monitor Light (Rs.549)
Cable Organiser (Rs.299)
=== groupBy category ===
Electronics:
Wireless Mouse
USB-C Hub
Mechanical Keyboard
Webcam HD
Accessories:
Laptop Stand
Monitor Light
Cable Organiser
=== firstNonNull - fallback chain for featured banner ===
Banner: Flipkart Big Billion Days - Up to 80% Off
Every method in PipelineUtils is a generic method in a non-generic class. partition works on any List<T>, transform converts any List<S> to List<T>, groupBy groups any list by any key type, and firstNonNull handles any fallback chain of any value type. The class itself holds no state and has no class-level type parameter - it is pure generic method utility, the same structural pattern used by java.util.Collections, java.util.Arrays, and java.util.Objects in the JDK.
Best Practices
Prefer generic methods over Object-accepting methods when type safety matters. A method that returns Object forces the caller to cast and accept ClassCastException risk. A generic method returning T lets the compiler verify the entire interaction at compile time with no cast required.
Let type inference work - do not specify type witnesses unless inference fails. Writing Collections.<String>sort(list) when Collections.sort(list) would work equally well is unnecessary noise. Reserve explicit type witnesses for the rare cases where the compiler genuinely cannot infer T - typically when null is the only argument or when two overloads produce an ambiguity.
Place utility generic methods in non-generic utility classes. A class that has no fields of type T does not need to be generic. class PipelineUtils with all-static generic methods is cleaner than class PipelineUtils<T> with instance methods, because the former does not require instantiating the class with a specific type, and different static method calls can use different T independently.
Use bounded type parameters when the method body needs to call a specific method on T. If the method needs compareTo(), bound to Comparable<T>. If it needs toString() and hashCode() (which everything has), no bound is needed because those are on Object. If you find yourself casting inside the method body to call something, the bound is missing.
Keep the type parameter name conventional. T for a single general type, E for element, K and V for key-value pairs, R for return/result, S for source. A method named <Foo> Foo doSomething(Foo input) is technically correct but reads as if Foo is a concrete class, not a type variable.
Common Mistakes
Mistake 1 - Putting the Type Parameter in the Wrong Position
1// WRONG - the return type is NOT where the type parameter is declared.
2// <T> after the return type is a syntax error or mis-declared.
3// This compiles only because T might be in scope from the class,
4// not because this is a correct generic method declaration.
5public List<T> wrongPosition() { // T from class level - or compile error
6 return new java.util.ArrayList<>();
7}
8
9// CORRECT - <T> goes BEFORE the return type
10public <T> List<T> correctPosition() {
11 return new java.util.ArrayList<>();
12}
13
14// ALSO CORRECT for a static method
15public static <T> List<T> emptyListOf() {
16 return new java.util.ArrayList<>();
17}Mistake 2 - Expecting the Type Parameter to Work With Primitives
1// WRONG - type parameters only work with reference types.
2// int, double, boolean cannot be used as T.
3// swap(int[], 0, 1) would need a separate method for int[].
4static <T> void swap(T[] array, int i, int j) {
5 T temp = array[i];
6 array[i] = array[j];
7 array[j] = temp;
8}
9
10int[] primitiveArray = {1, 2, 3};
11// swap(primitiveArray, 0, 1); // COMPILE ERROR - int[] is not T[]
12// int[] cannot be used as T[] - T must be a reference type
13
14// CORRECT - use the wrapper type (autoboxing handles the rest)
15Integer[] boxedArray = {1, 2, 3};
16swap(boxedArray, 0, 1); // works - T = Integer
17
18// Or write a specialized method for primitive arrays if performance matters
19static void swapPrimitive(int[] array, int i, int j) {
20 int temp = array[i];
21 array[i] = array[j];
22 array[j] = temp;
23}Mistake 3 - Confusing the Class Type Parameter With a Method Type Parameter
1// A generic class with its own T
2class Container<T> {
3 private T value;
4
5 Container(T value) { this.value = value; }
6
7 // WRONG ASSUMPTION - this method's <T> is NOT the same T as the class.
8 // The method declares a NEW, SHADOWING type variable also called T.
9 // This compiles but is confusing and almost certainly a mistake.
10 public <T> void printWithDefault(T defaultValue) {
11 // 'this.value' is the class's T, 'defaultValue' is the method's T
12 // They can be completely different types - Container<String>.printWithDefault(42)
13 // is valid and calls this method with String for the class's T
14 // and Integer for the method's T - confusing but legal
15 System.out.println(value + " | default=" + defaultValue);
16 }
17
18 // CORRECT - if the method should work with the class's T, use T directly
19 // without re-declaring it on the method
20 public void printWithDefault2(T defaultValue) {
21 // Here T is the class's T - consistent and clear
22 System.out.println(value + " | default=" + defaultValue);
23 }
24}Mistake 4 - Using Object Instead of a Generic Method and Getting Unsafe Casts Back
1import java.util.List;
2import java.util.ArrayList;
3
4// WRONG - returns Object, caller must cast, ClassCastException risk reintroduced
5static Object firstElement(List<?> items) {
6 if (items == null || items.isEmpty()) return null;
7 return items.get(0);
8}
9
10// At the call site:
11List<String> names = List.of("Priya", "Ananya");
12String first = (String) firstElement(names); // unsafe cast
13
14// CORRECT - generic method, no cast at the call site
15// Note: the wildcard capture here is simplified; this shows the intent
16static <T> T safeFirst(List<T> items, T defaultValue) {
17 if (items == null || items.isEmpty()) return defaultValue;
18 return items.get(0); // T is known - no cast
19}
20
21String safeResult = safeFirst(names, "Unknown"); // no cast neededInterview Questions
Q1. What is a generic method in Java, and how is its syntax different from a regular method?
A generic method declares one or more type parameters in angle brackets placed before its return type in the method signature. A regular method public String findFirst(List<String> items) only works with String. A generic method public <T> T findFirst(List<T> items) works with any type - the caller's argument determines what T is for that call. The only syntactic difference is the <T> before the return type. The method can be static or instance, in a generic class or a plain one, and the type variable T is scoped entirely to that method call.
Q2. What is type inference in generic methods, and when must you provide an explicit type witness?
Type inference is the compiler's ability to determine the type argument T from the arguments you pass to a generic method. When you call swap(stringArray, 0, 1), the compiler sees that stringArray is String[], infers T = String, and verifies the rest of the call accordingly. An explicit type witness — the <String> in MyClass.<String>swap(array, 0, 1) — is only required when the compiler cannot infer T unambiguously. Common cases: when null is the only argument (null has no type), when two overloads both match and the compiler needs help picking one, or when the method is assigned to a variable in a context where the target type is needed to resolve T.
Q3. What is the difference between a generic method and a generic class, and when do you choose each?
A class-level type parameter is fixed for the lifetime of an object — new Box<String>() creates a Box that is String-typed for all its method calls. A method-level type parameter is resolved fresh for every call — swap(intArray, 0, 1) makes T = Integer for that call, while swap(stringArray, 0, 1) makes T = String for the next call, even though both calls go to the same method. Choose a generic method when the type relationship exists only within the method's scope — the method does not store any state of type T between calls. Choose a generic class when the object must remember its type parameter across multiple method calls — anything that stores elements of type T internally.
Q4. How does a bounded type parameter like <T extends Comparable
Without a bound, T erases to Object, and the method can only call methods declared on Object — equals(), hashCode(), toString(). With <T extends Comparable<T>>, T erases to Comparable, and the method can call compareTo() on any value of type T without a cast. The bound also restricts what types the caller can use — passing a List<Object> to a method bounded to <T extends Comparable<T>> is a compile error, because Object does not implement Comparable. This gives both safety (only sortable types can be sorted) and capability (the method can actually perform the comparison).
Q5. Can a method have multiple bounds on a type parameter, and what are the rules for combining them?
Yes. A type parameter can have one class bound and multiple interface bounds, separated by &: <T extends Number & Comparable<T> & Serializable>. The class bound must come first if one is present — the order matters because it determines the erased type (the first bound is what T becomes in bytecode). Only one class can appear in the bound; multiple classes would require multiple inheritance, which Java does not support. Multiple interface bounds are unrestricted in count. Each bound adds to what the method can assume about T — every bound's methods are available inside the method body without casting.
Q6. How does a generic method in a non-generic class work, and why is this pattern used in JDK utility classes?
A generic method in a non-generic class declares its own type parameter independently of any class-level parameter — the class itself has no <T>, but each individual method does. This is the design of java.util.Collections, java.util.Arrays, and java.util.Objects: the classes hold no state parameterized by a type, so there is no reason to make the class generic. Each static method call resolves its own T from its own arguments. The advantage is that different calls can use different types — Collections.sort(stringList) and Collections.sort(integerList) both work without any object instantiation or type binding at the class level. For utility code that is purely algorithmic with no state, this pattern is both cleaner and more flexible than making the whole class generic.
FAQs
Can a generic method be abstract?
Yes. An abstract method in a generic class or interface can declare its own type parameter. If the type parameter is on the enclosing class or interface, the abstract method simply uses it without re-declaring. If the method needs its own additional type variable, it can declare one on the method signature - even abstract ones.
Does type inference work for the return type of a generic method?
In some cases, yes. When a generic method's return type is T and T is also constrained by the method's parameters, the compiler can infer T from the arguments. When T appears only in the return type and not in any parameter, the compiler may use the target variable's declared type to infer T — this is called target-type inference and was improved in Java 8. If inference remains ambiguous, the explicit type witness before the method name is required.
What is the difference between a generic method and a wildcard method parameter?
A generic method declares a named type parameter T that can be referenced multiple times — in the parameter list, the return type, and the method body. A wildcard (?) in a method parameter like void print(List<?> items) says "some unknown type" but gives it no name, so you cannot use it in the return type or as a specific type inside the method body. Use a generic method when you need to name the type — to use it in the return type, to create objects of that type, or to relate two parameter types. Use a wildcard when you only need to say "any type is fine here" without needing to name or return it.
Can a generic method throw a generic exception type?
With a bounded type parameter, yes — <T extends Exception> void process() throws T is syntactically valid and compiles. This is rare in practice but appears in functional interfaces and retry utilities that need to propagate the specific checked exception type rather than catching and wrapping it. The bound is essential: without extends Exception, the compiler cannot verify that T is throwable.
Is there a performance penalty for calling a generic method versus a non-generic one?
No. Type erasure means the compiled bytecode is essentially identical — the type parameter becomes Object (or its declared bound), and any necessary casts are inserted by the compiler. The JIT compiler treats the resulting bytecode the same as hand-written Object-based code. The performance characteristics of generic method calls and non-generic equivalents are identical at runtime.
Can lambdas be used as arguments to generic method parameters?
Yes, and this is one of the most common patterns in production Java. When a generic method accepts a Function<T, R>, Predicate<T>, or Supplier<T> parameter, the caller can pass a lambda directly — the compiler infers both T and R from the lambda's parameter types and return type. The transform, filter, and groupBy methods in the real-world example above all accept functional interface parameters and are called with lambdas — the type inference happens automatically from the lambda signature.
Summary
A generic method declares its own type parameter before its return type — independent of any class-level parameter, resolved fresh on every call, and inferred automatically by the compiler from the arguments the caller passes. The scope of a method-level type parameter is narrow by design: it covers one call and nothing else. This makes generic methods the right tool for algorithmic utility code that is type-agnostic but needs to preserve type relationships between parameters and return values.
The pattern that appears most often in production is exactly what PipelineUtils demonstrates: a non-generic class full of static generic methods. The class has no state to parameterize, so it needs no class-level type parameter — but each method independently works with whatever type the caller needs. Collections, Arrays, and Objects from the JDK are all built this way.
Bounded type parameters narrow what types are accepted and widen what methods can be called inside the method body — <T extends Comparable<T>> lets a sort or max method call compareTo() on T values without a single cast. Multiple bounds with & stack these capabilities: <T extends Number & Comparable<T>> lets the method treat T as both a Number and something that can be compared.
The interview test is almost always the same question in different forms: what is the difference between a generic method and a generic class, and when do you use each? The answer is scope and state. Methods are for stateless, algorithmic operations. Classes are for objects that hold and manage typed state.
What to Read Next
Learn how to limit a generic type to a specific family of classes.