Understand generics in Java, type parameters, and generic methods.
Generics enable you to create classes, interfaces, and methods that operate on types that are specified at compilation time. They provide stronger type checks, eliminate the need for casting, and enable programmers to implement generic algorithms.
// Non-generic collection public class Box { private Object object; public void set(Object object) { this.object = object; } public Object get() { return object; } } // Client code - requires casting and is not type-safe Box box = new Box(); box.set("Hello World"); // Store a String // Type casting is required - potential runtime error String message = (String) box.get(); // This would compile but fail at runtime Integer wrongType = (Integer) box.get(); // ClassCastException
// Generic collection public class Box{ private T t; public void set(T t) { this.t = t; } public T get() { return t; } } // Client code - type-safe and no casting required Box stringBox = new Box<>(); stringBox.set("Hello World"); // No casting required, and type safety is guaranteed String message = stringBox.get(); // This won't compile - caught at compile time // stringBox.set(10); // Error: incompatible types
Generic methods allow type parameters to be used in a single method declaration, making it possible to implement algorithms that operate on multiple types while providing type safety.
public class Util { // Generic method to find maximum of two comparable values public static> T findMax(T a, T b) { if (a.compareTo(b) > 0) { return a; } else { return b; } } // Generic method to print array elements public static void printArray(E[] array) { for (E element : array) { System.out.print(element + " "); } System.out.println(); } } // Client code public class TestGenerics { public static void main(String[] args) { // Using the generic method with Integers Integer max = Util.findMax(10, 20); System.out.println("Maximum of 10 and 20: " + max); // Using the generic method with Strings String maxString = Util.findMax("apple", "orange"); System.out.println("Maximum of 'apple' and 'orange': " + maxString); // Using the generic printArray method Integer[] intArray = {1, 2, 3, 4, 5}; String[] stringArray = {"Hello", "World"}; Util.printArray(intArray); Util.printArray(stringArray); } }
Bounded type parameters restrict the types that can be used as type arguments in a generic class, interface, or method. This allows you to invoke methods of the bound type without casting.
// Upper bounded type parameter public class NumericCalculator{ private T number; public NumericCalculator(T number) { this.number = number; } // Can call methods of Number class without casting public double getDoubleValue() { return number.doubleValue(); } public double square() { return number.doubleValue() * number.doubleValue(); } } // Example with wildcards public class WildcardExample { // Producer - use "extends" (read-only) public static void printList(List extends Number> list) { for (Number n : list) { System.out.print(n + " "); } System.out.println(); } // Consumer - use "super" (write-only) public static void addNumbers(List super Integer> list) { for (int i = 1; i <= 5; i++) { list.add(i); } } public static void main(String[] args) { // Using bounded type parameter NumericCalculator intCalc = new NumericCalculator<>(5); System.out.println("Square: " + intCalc.square()); NumericCalculator doubleCalc = new NumericCalculator<>(2.5); System.out.println("Double value: " + doubleCalc.getDoubleValue()); // Using wildcards List intList = new ArrayList<>(); List doubleList = new ArrayList<>(); addNumbers(intList); // Works fine // addNumbers(doubleList); // Won't compile - Double is not a supertype of Integer printList(intList); // Works fine - Integer extends Number printList(doubleList); // Works fine - Double extends Number } }