Generic Methods in Java
Learn how to create methods that work with any data type
Think of generic methods like a universal remote control that works with ANY brand of TV! You don't need a different remote for Samsung, LG, or Sony - one remote works with all of them. Generic methods let you write ONE method that works with any data type instead of writing separate methods for String, Integer, Double, and every other type!
The Problem Without Generic Methods
Without generic methods, you have to write the same method multiple times for different types:
public class OldWayPrinter { // Print String public void printString(String value) { System.out.println("String: " + value); } // Print Integer public void printInteger(Integer value) { System.out.println("Integer: " + value); } // Print Double public void printDouble(Double value) { System.out.println("Double: " + value); } // Print Boolean public void printBoolean(Boolean value) { System.out.println("Boolean: " + value); } // Print Person public void printPerson(Person value) { System.out.println("Person: " + value); } // Problem: So much code duplication! // Each method does the same thing!}public class WithoutGenericMethods { public static void main(String[] args) { OldWayPrinter printer = new OldWayPrinter(); printer.printString("Hello"); // Method 1 printer.printInteger(42); // Method 2 printer.printDouble(3.14); // Method 3 printer.printBoolean(true); // Method 4 printer.printPerson(new Person("Alice")); // Method 5 // We need a method for EVERY type! 😫 // This is not scalable! // Solution: Generic Methods! 👇 }}class Person { String name; Person(String name) { this.name = name; } public String toString() { return "Person{" + name + "}"; }}What is a Generic Method?
A generic method is a method that can work with any type. You specify the type in angle brackets <T> before the return type. The method then works with that type parameter!
// Generic method - works with ANY type!// Syntax: <T> comes BEFORE the return typepublic class GenericPrinter { // Generic print method public <T> void print(T value) { System.out.println("Value: " + value); } // Generic swap method public <T> void swap(T[] arr, int i, int j) { T temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } // Generic get max method public <T extends Comparable<T>> T getMax(T[] arr) { if (arr.length == 0) return null; T max = arr[0]; for (int i = 1; i < arr.length; i++) { if (arr[i].compareTo(max) > 0) { max = arr[i]; } } return max; }}public class GenericMethodIntro { public static void main(String[] args) { GenericPrinter printer = new GenericPrinter(); // Same method, different types! printer.print("Hello"); // Works with String printer.print(42); // Works with Integer printer.print(3.14); // Works with Double printer.print(true); // Works with Boolean printer.print(new Person("Alice")); // Works with Person // One method to rule them all! // No duplicated code! // Type safe! // Type inference in action // printer.print("Hello") - Java infers T is String // printer.print(42) - Java infers T is Integer }}class Person { String name; Person(String name) { this.name = name; } public String toString() { return "Person{" + name + "}"; }}Static Generic Methods
Static methods can also be generic. This is very useful for utility methods:
import java.util.*;public class GenericUtilities { // Static generic method for printing public static <T> void printArray(T[] arr) { System.out.print("["); for (int i = 0; i < arr.length; i++) { System.out.print(arr[i]); if (i < arr.length - 1) System.out.print(", "); } System.out.println("]"); } // Static generic swap method public static <T> void swap(T[] arr, int i, int j) { T temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } // Static generic method for finding minimum public static <T extends Comparable<T>> T findMin(T[] arr) { if (arr.length == 0) return null; T min = arr[0]; for (T element : arr) { if (element.compareTo(min) < 0) { min = element; } } return min; } // Static generic method for finding maximum public static <T extends Comparable<T>> T findMax(T[] arr) { if (arr.length == 0) return null; T max = arr[0]; for (T element : arr) { if (element.compareTo(max) > 0) { max = element; } } return max; } // Static generic method for converting to list public static <T> List<T> arrayToList(T[] arr) { return Arrays.asList(arr); } // Static generic method for counting occurrences public static <T> int count(T[] arr, T value) { int count = 0; for (T element : arr) { if (element.equals(value)) { count++; } } return count; }}public class StaticGenericMethods { public static void main(String[] args) { // String array String[] words = {"apple", "banana", "cherry"}; GenericUtilities.printArray(words); // [apple, banana, cherry] // Integer array Integer[] numbers = {5, 2, 8, 1, 9}; GenericUtilities.printArray(numbers); // [5, 2, 8, 1, 9] System.out.println("Min: " + GenericUtilities.findMin(numbers)); // 1 System.out.println("Max: " + GenericUtilities.findMax(numbers)); // 9 // Double array Double[] prices = {19.99, 5.50, 12.75, 8.25}; GenericUtilities.printArray(prices); // Swap test Integer[] nums = {1, 2, 3}; GenericUtilities.swap(nums, 0, 2); GenericUtilities.printArray(nums); // [3, 2, 1] // Convert to list List<String> wordList = GenericUtilities.arrayToList(words); System.out.println("List: " + wordList); // Count occurrences Integer[] repeating = {1, 2, 2, 3, 2, 4}; int count = GenericUtilities.count(repeating, 2); System.out.println("Count of 2: " + count); // 3 }}Instance Generic Methods
Even in non-generic classes, you can have generic methods:
// Non-generic class with generic methodspublic class DataHandler { private String name; public DataHandler(String name) { this.name = name; } // Instance generic method public <T> void process(T data) { System.out.println(name + " processing: " + data); } // Instance generic method that returns type public <T> T getFirstElement(T[] arr) { if (arr.length == 0) return null; return arr[0]; } // Instance generic method with multiple operations public <T> void printWithType(T value) { System.out.println("Type: " + value.getClass().getSimpleName()); System.out.println("Value: " + value); System.out.println("Hash: " + value.hashCode()); } // Instance generic method with local type variable public <T extends Number> double convert(T number) { return number.doubleValue(); }}// Another example: Repository pattern without genericspublic class SimpleRepository { // Generic find method that works with any type public <T> T findById(List<T> items, String id) { for (T item : items) { if (item.toString().contains(id)) { return item; } } return null; } // Generic filter method public <T> List<T> filterByType(List<Object> items, Class<T> type) { List<T> result = new ArrayList<>(); for (Object item : items) { if (type.isInstance(item)) { result.add(type.cast(item)); } } return result; } // Generic clone method (simplified) public <T> List<T> cloneList(List<T> original) { return new ArrayList<>(original); }}public class InstanceGenericMethods { public static void main(String[] args) { // Using DataHandler DataHandler handler = new DataHandler("Handler1"); handler.process("Hello"); // Works with String handler.process(42); // Works with Integer handler.process(3.14); // Works with Double // Using generic method that returns String[] fruits = {"apple", "banana", "cherry"}; String first = handler.getFirstElement(fruits); System.out.println("First fruit: " + first); Integer[] numbers = {10, 20, 30}; Integer firstNum = handler.getFirstElement(numbers); System.out.println("First number: " + firstNum); // Using print with type handler.printWithType("Java"); handler.printWithType(123); // Using SimpleRepository SimpleRepository repo = new SimpleRepository(); List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); String found = repo.findById(names, "Bob"); System.out.println("Found: " + found); // Filter method List<Object> mixed = new ArrayList<>(); mixed.add("String"); mixed.add(123); mixed.add(45.67); mixed.add("Another"); List<String> strings = repo.filterByType(mixed, String.class); System.out.println("Strings: " + strings); // Clone method List<Integer> original = Arrays.asList(1, 2, 3, 4, 5); List<Integer> cloned = repo.cloneList(original); System.out.println("Original: " + original); System.out.println("Cloned: " + cloned); }}class User { String name; User(String name) { this.name = name; } public String toString() { return "User{" + name + "}"; }}Type Inference
Java can often figure out what type you mean without you having to specify it:
public class TypeInferenceDemo { // Generic method public static <T> T identity(T value) { return value; } // Generic method with two parameters public static <T> void swap(T[] arr, int i, int j) { T temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } // Generic method that returns first element public static <T> T getFirst(T... elements) { return elements.length > 0 ? elements[0] : null; } // Generic method with multiple types public static <K, V> void printPair(K key, V value) { System.out.println("Key: " + key + ", Value: " + value); }}public class TypeInferenceExamples { public static void main(String[] args) { // Type inference in action! // Explicit type specification (not needed, but allowed) String str1 = TypeInferenceDemo.<String>identity("Hello"); Integer num1 = TypeInferenceDemo.<Integer>identity(42); // Type inference - Java figures it out! String str2 = TypeInferenceDemo.identity("Hello"); // T inferred as String Integer num2 = TypeInferenceDemo.identity(42); // T inferred as Integer Double dbl = TypeInferenceDemo.identity(3.14); // T inferred as Double Boolean bool = TypeInferenceDemo.identity(true); // T inferred as Boolean // Java infers from the assignment target String result = TypeInferenceDemo.identity("World"); // T is String // Java infers from the argument type Integer[] numbers = {1, 2, 3}; TypeInferenceDemo.swap(numbers, 0, 2); // T inferred as Integer String[] words = {"apple", "banana"}; TypeInferenceDemo.swap(words, 0, 1); // T inferred as String // Varargs with type inference String first = TypeInferenceDemo.getFirst("one", "two", "three"); System.out.println("First: " + first); Integer firstNum = TypeInferenceDemo.getFirst(10, 20, 30, 40); System.out.println("First number: " + firstNum); // Multiple type parameters - inference for both K and V TypeInferenceDemo.printPair("name", "Alice"); // K=String, V=String TypeInferenceDemo.printPair(1, "One"); // K=Integer, V=String TypeInferenceDemo.printPair("age", 25); // K=String, V=Integer TypeInferenceDemo.printPair("pi", 3.14); // K=String, V=Double // Type inference is smart! System.out.println("--- Type Inference Benefits ---"); System.out.println("Cleaner code - no need to specify types"); System.out.println("Type safe - compiler still checks types"); System.out.println("Flexible - works with any type"); }}Multiple Type Parameters
Methods can work with multiple type parameters:
import java.util.*;public class MultipleTypeParameters { // Two type parameters public static <K, V> void printPair(K key, V value) { System.out.println("Key: " + key + ", Value: " + value); } // Three type parameters public static <A, B, C> void printTriple(A first, B second, C third) { System.out.println("First: " + first); System.out.println("Second: " + second); System.out.println("Third: " + third); } // Generic method to convert between types public static <T, U> U convert(T input, Class<U> targetType) { if (input instanceof String) { String str = (String) input; if (targetType == Integer.class) { return (U) Integer.valueOf(str); } else if (targetType == Double.class) { return (U) Double.valueOf(str); } } return null; } // Transform one type to another public static <T, R> List<R> transformList(List<T> input, Transformer<T, R> transformer) { List<R> result = new ArrayList<>(); for (T item : input) { result.add(transformer.transform(item)); } return result; } // Find common type behavior public static <T, U> void compareTypes(T obj1, U obj2) { System.out.println("Type 1: " + obj1.getClass().getSimpleName()); System.out.println("Type 2: " + obj2.getClass().getSimpleName()); System.out.println("Same? " + obj1.getClass().equals(obj2.getClass())); } // Complex: Map transformation public static <K, V, N> Map<K, N> transformMapValues(Map<K, V> source, Transformer<V, N> transformer) { Map<K, N> result = new HashMap<>(); for (Map.Entry<K, V> entry : source.entrySet()) { result.put(entry.getKey(), transformer.transform(entry.getValue())); } return result; } interface Transformer<T, R> { R transform(T input); }}public class MultipleTypeParametersExample { public static void main(String[] args) { // Two type parameters MultipleTypeParameters.printPair("name", "Alice"); // K=String, V=String MultipleTypeParameters.printPair(1, "One"); // K=Integer, V=String MultipleTypeParameters.printPair("count", 42); // K=String, V=Integer MultipleTypeParameters.printPair("pi", 3.14); // K=String, V=Double // Three type parameters System.out.println("--- Three Parameters ---"); MultipleTypeParameters.printTriple("John", 30, 6.1); // A=String, B=Integer, C=Double MultipleTypeParameters.printTriple(1, 2.5, true); // A=Integer, B=Double, C=Boolean // Type conversion System.out.println("--- Type Conversion ---"); Integer num = MultipleTypeParameters.convert("123", Integer.class); System.out.println("Converted: " + num); // 123 Double dbl = MultipleTypeParameters.convert("3.14", Double.class); System.out.println("Converted: " + dbl); // 3.14 // Transform list System.out.println("--- Transform List ---"); List<String> numbers = Arrays.asList("1", "2", "3", "4", "5"); List<Integer> transformed = MultipleTypeParameters.transformList(numbers, input -> Integer.parseInt(input)); System.out.println("Transformed: " + transformed); // Transform map values System.out.println("--- Transform Map ---"); Map<String, String> nameMap = new HashMap<>(); nameMap.put("user1", "alice"); nameMap.put("user2", "bob"); nameMap.put("user3", "charlie"); Map<String, Integer> lengthMap = MultipleTypeParameters.transformMapValues( nameMap, input -> input.length() ); System.out.println("Name lengths: " + lengthMap); // Compare types System.out.println("--- Compare Types ---"); MultipleTypeParameters.compareTypes("string", 123); MultipleTypeParameters.compareTypes("hello", "world"); }}Real-World Examples
See how generic methods are used in practical applications:
import java.util.*;import java.util.function.*;public class RealWorldGenericMethods { // 1. Generic swap - very common! public static <T> void swap(T[] array, int i, int j) { T temp = array[i]; array[i] = array[j]; array[j] = temp; } // 2. Generic search public static <T> int indexOf(T[] array, T value) { for (int i = 0; i < array.length; i++) { if (array[i] != null && array[i].equals(value)) { return i; } } return -1; } // 3. Generic filter public static <T> List<T> filter(List<T> list, Predicate<T> predicate) { List<T> result = new ArrayList<>(); for (T item : list) { if (predicate.test(item)) { result.add(item); } } return result; } // 4. Generic map/transform public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) { List<R> result = new ArrayList<>(); for (T item : list) { result.add(mapper.apply(item)); } return result; } // 5. Generic reduce public static <T> T reduce(List<T> list, T identity, BinaryOperator<T> accumulator) { T result = identity; for (T item : list) { result = accumulator.apply(result, item); } return result; } // 6. Generic cache pattern public static class Cache<K, V> { private Map<K, V> data = new HashMap<>(); private Map<K, Long> timestamps = new HashMap<>(); public void put(K key, V value) { data.put(key, value); timestamps.put(key, System.currentTimeMillis()); } public V get(K key) { return data.get(key); } public boolean containsKey(K key) { return data.containsKey(key); } public void clear() { data.clear(); timestamps.clear(); } } // 7. Generic API response wrapper public static class ApiResponse<T> { private boolean success; private T data; private String error; public ApiResponse(boolean success, T data, String error) { this.success = success; this.data = data; this.error = error; } public static <T> ApiResponse<T> ok(T data) { return new ApiResponse<>(true, data, null); } public static <T> ApiResponse<T> error(String error) { return new ApiResponse<>(false, null, error); } @Override public String toString() { return success ? "Success: " + data : "Error: " + error; } }}public class RealWorldGenericMethodsExample { public static void main(String[] args) { // 1. Swap System.out.println("--- Swap ---"); Integer[] numbers = {1, 2, 3, 4, 5}; System.out.println("Before: " + Arrays.toString(numbers)); RealWorldGenericMethods.swap(numbers, 0, 4); System.out.println("After: " + Arrays.toString(numbers)); // 2. Search System.out.println("--- Search ---"); int index = RealWorldGenericMethods.indexOf(numbers, 3); System.out.println("Index of 3: " + index); // 3. Filter System.out.println("--- Filter ---"); List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> evens = RealWorldGenericMethods.filter(list, n -> n % 2 == 0); System.out.println("Even numbers: " + evens); // 4. Map/Transform System.out.println("--- Transform ---"); List<String> words = Arrays.asList("hello", "world", "java"); List<Integer> lengths = RealWorldGenericMethods.map(words, String::length); System.out.println("Lengths: " + lengths); List<String> upper = RealWorldGenericMethods.map(words, String::toUpperCase); System.out.println("Uppercase: " + upper); // 5. Reduce System.out.println("--- Reduce ---"); Integer sum = RealWorldGenericMethods.reduce(list, 0, (a, b) -> a + b); System.out.println("Sum: " + sum); // 6. Cache System.out.println("--- Cache ---"); RealWorldGenericMethods.Cache<String, String> cache = new RealWorldGenericMethods.Cache<>(); cache.put("key1", "value1"); cache.put("key2", "value2"); System.out.println("Cached value: " + cache.get("key1")); // 7. API Response System.out.println("--- API Response ---"); RealWorldGenericMethods.ApiResponse<String> response = RealWorldGenericMethods.ApiResponse.ok("Data loaded"); System.out.println(response); RealWorldGenericMethods.ApiResponse<Integer> errorResponse = RealWorldGenericMethods.ApiResponse.error("Not found"); System.out.println(errorResponse); }}Key Concepts
Type Safety
Generic methods catch type errors at compile time, ensuring type safety without needing to cast!
Code Reusability
Write one generic method instead of multiple overloaded methods for different types!
Type Parameters
Type parameters in methods work like variables that represent actual types at runtime!
Syntax
Place <T> between visibility modifier and return type: public <T> T genericMethod(T param)
Best Practices
- ✓Use meaningful type parameter names (T for Type, E for Element, K for Key, V for Value)
- ✓Generic methods are best for utility methods that should work with any type
- ✓Type parameters are inferred automatically - let Java figure out the types
- ✓Use wildcards (?) for more flexible method signatures when appropriate
- ✓Combine generic methods with bounded types when you need type constraints
- ✓Document what types are expected and any method constraints
Common Mistakes
✗ Forgetting to declare type parameter before return type
Why it's wrong: Generic method syntax requires <T> between visibility modifier and return type: public <T> T method(T param). Forgetting this makes the method non-generic!
✗ Mixing generic methods with generic classes incorrectly
Why it's wrong: A generic method in a non-generic class has its own type parameters. Class<T> and method <E> are different!
✗ Using raw types when calling generic methods
Why it's wrong: Always provide type information when calling generic methods for type safety.
✗ Not understanding type inference
Why it's wrong: Java infers types from the method arguments, so method<String>("hello") is usually unnecessary - just call method("hello")!
Interview Tips
- 💡Explain that generic methods are different from generic classes - method type parameters are local to that method
- 💡Know how to declare a generic method with <T> before the return type
- 💡Understand type inference - Java figures out types from arguments
- 💡Be able to create methods with multiple type parameters
- 💡Know the difference between static generic methods and instance generic methods
- 💡Explain real-world use cases: swap methods, search methods, transform methods