
Your learning drop for cleaner APIs and safer code. Below are 7 field-tested habits (with tiny snippets) to make your generics rock.
TL;DR
- ▪️ Avoid raw types, kill unchecked warnings, and prefer List over arrays.
- ▪️ Make your types and methods generic to push errors to compile time.
- ▪️ Use bounded wildcards (? extends / ? super) to keep APIs flexible.
- ▪️ For advanced cases, typesafe heterogeneous containers give you compile-time safety without casts.
🔸 DON’T USE RAW TYPES IN NEW CODE
Always parameterize collections and generics—raw types drop type safety.
// ❌ Raw type — loses type info List raw = new ArrayList(); raw.add(42); // ✅ Parameterized List<Integer> nums = new ArrayList<>(); nums.add(42); // type-checked
Why: Raw types bypass generics checking → casts, ClassCastException at runtime.
🔸 ELIMINATE UNCHECKED WARNINGS
Make the compiler happy—warnings often hide real type risks.
// ✅ Narrow the warning with a helper (into a tiny, well-named method (the “helper”)) static <T> List<T> asListSafe(T... items) { return List.of(items); // no unchecked cast } // If you *must* suppress, do it locally and prove correctness: @SuppressWarnings("unchecked") static <T> T[] toArray(List<T> list, IntFunction<T[]> factory) { return list.toArray(factory.apply(list.size())); // safe boundary }
Tip: Prefer designs that avoid @SuppressWarnings; if used, document why it’s safe.
🔸 PREFER LISTS TO ARRAYS
Choose List
over T[]—because arrays are reified & covariant (checks at runtime), while generics are erased & invariant (checks at compile time).
// ❌ Array-based API (covariant, runtime surprises) void addAll(Number[] dst, Integer[] src) { /* ... */ } // ✅ List-based API (invariant, compile-time checks) void addAll(List<Number> dst, List<Integer> src) { /* ... */ }
Reason: Arrays are covariant + runtime checks; Lists are invariant + compile-time checks.
🔸 FAVOR GENERIC TYPES
Make your classes generic instead of storing Object.
// ❌ Not generic final class Box { private Object value; Object get() { return value; } void set(Object v) { value = v; } } // ✅ Generic type final class Box<T> { private T value; T get() { return value; } void set(T v) { value = v; } } Box<String> b = new Box<>(); b.set("hello"); // type-safe
Outcome: Clearer APIs, no casting, fewer runtime errors.
🔸 FAVOR GENERIC METHODS
Let methods declare their own
to reduce casting and overload noise.
// ✅ Generic static factory public static <T> Set<T> union(Set<T> a, Set<T> b) { Set<T> r = new HashSet<>(a); r.addAll(b); return r; } Set<String> s = union(Set.of("a"), Set.of("b"));
Bonus: Generic methods compose nicely in fluent APIs.
🔸 USE BOUNDED WILDCARDS FOR FLEXIBLE APIS
PECS: Producer Extends, Consumer Super.
// Reads/produces T => ? extends T static <T> void copyAll(List<? extends T> src, List<? super T> dst) { for (T t : src) dst.add(t); } List<Integer> src = List.of(1, 2, 3); List<Number> dst = new ArrayList<>(); copyAll(src, dst); // ✅ flexible
Rule:
- ▪️ If param produces values → ? extends T
- ▪️ If param consumes values → ? super T
🔸 CONSIDER TYPESAFE HETEROGENEOUS CONTAINERS
Key the values by type to store different types safely—no casts.
final class TypeSafeMap { private final Map<Class<?>, Object> map = new HashMap<>(); public <T> void put(Class<T> type, T value) { map.put(type, type.cast(value)); } public <T> T get(Class<T> type) { return type.cast(map.get(type)); } } interface Handler<T> { void handle(T msg); } var registry = new TypeSafeMap(); registry.put(UserCreated.class, (Handler<UserCreated>) msg -> {/*...*/}); registry.put(OrderPaid.class, (Handler<OrderPaid>) msg -> {/*...*/}); Handler<UserCreated> h = registry.get(UserCreated.class);
Use cases: Registries, context bags, DI-lite containers.
TAKEAWAYS
- ▪️ Generics push errors left (compile time > runtime).
- ▪️ Lists beat arrays for API design.
- ▪️ PECS makes wildcard choices straightforward.
- ▪️ Avoid @SuppressWarnings unless you can prove safety.
- ▪️ Advanced: typesafe heterogeneous containers keep flexibility and safety.
Go further with Java certification:
Java👇
Spring👇