Generics allow you to create classes, interfaces, and methods with type parameters, which provides type checking at compile time and helps avoid ClassCastException.
Key features and pitfalls:
new List<String>[10] — compile-time error.T obj = new T();if(obj instanceof List<String>) — error.? extends T — covariance (reading), ? super T — contravariance (writing).Example:
// Covariant approach for reading void printNumbers(List<? extends Number> numbers) { for (Number n : numbers) { System.out.println(n); } } // Contravariant approach for writing void addIntegers(List<? super Integer> list) { list.add(10); } }
Question: "What is the difference between List<Object> and List<?>? Can you put any object in List<?>?"
Answer: No, you cannot add anything to List<?> (except for null), because the compiler does not know what type parameter is there. However, in List<Object> you can add any objects.
Example:
List<?> list1 = new ArrayList<String>(); // list1.add("test"); // Compile-time error! List<Object> list2 = new ArrayList<>(); list2.add("test"); // OK
Story
The development team attempted to implement a cache based on an array of parameterized type
T[]. Due to type erasure and the inability to create arrays of generic type, the solution did not work as expected: it resulted in anObject[]array, leading to ClassCastException during runtime casts.
Story
In one of the microservices, a developer tried to implement a receiver using List<?> as a parameter and attempted to modify the collection. This caused a compilation error and delayed the release timeline, as it was necessary to refactor the logic considering PECS.
Story
In a project integrating with an external system, a developer made an error overriding one collection type with another through an unchecked raw type: List list = new ArrayList<String>(), which led to ClassCastException and service crashes in production when attempting to cast elements to other types.