Java · lesson 61 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Java Generics: Bounded Types, Wildcards, and Generic Methods
Master generics beyond basic <T> — bounded types, wildcards (? extends vs ? super), and generic methods that work across collections.
By SkillByExample · Updated
This lesson goes past List<String>: bounds that let generic code call methods on T, the
wildcards ? extends and ? super, generic methods, and what type erasure takes away at
runtime. Every example is one file run with the JDK 21 source launcher (java File.java), and
several show the compile error the generic rules exist to produce.
Bounded type parameters
<T extends Comparable<T>> promises the compiler that every T has compareTo, so the method
can call it.
import java.util.List;
public class Bounded {
static <T extends Comparable<T>> T max(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) best = item;
}
return best;
}
public static void main(String[] args) {
System.out.println(max(List.of(3, 1, 4, 1, 5, 9, 2, 6)));
System.out.println(max(List.of("apple", "zebra", "mango")));
System.out.println(max(List.of(new Object(), new Object())));
}
}
$ java Bounded.java
Bounded.java:15: error: method max in class Bounded cannot be applied to given types;
System.out.println(max(List.of(new Object(), new Object())));
^
required: List<T>
found: List<Object>
reason: inference variable E has incompatible bounds
upper bounds: Comparable<T>,Object
lower bounds: Object
where T,E are type-variables:
T extends Comparable<T> declared in method <T>max(List<T>)
E extends Object declared in method <E>of(E,E)
1 error
error: compilation failed
Object doesn’t implement Comparable, so the call is rejected at compile time rather than
failing inside max. Delete that line and the program prints 9 and zebra. Without the bound,
item.compareTo(best) wouldn’t compile at all — a plain T only has Object’s methods.
Multiple bounds
A type variable can require a class and several interfaces, joined with &; the class comes first.
public class MultiBound {
interface Named { String name(); }
static final class Score extends Number implements Named, Comparable<Score> {
private final String name; private final int value;
Score(String name, int value) { this.name = name; this.value = value; }
public String name() { return name; }
public int intValue() { return value; }
public long longValue() { return value; }
public float floatValue() { return value; }
public double doubleValue() { return value; }
public int compareTo(Score o) { return Integer.compare(value, o.value); }
}
static <T extends Number & Named & Comparable<T>> String describe(T a, T b) {
T winner = a.compareTo(b) >= 0 ? a : b;
return winner.name() + " wins with " + winner.doubleValue();
}
public static void main(String[] args) {
System.out.println(describe(new Score("ann", 82), new Score("bo", 91)));
}
}
$ java MultiBound.java
bo wins with 91.0
Inside describe, T has doubleValue() from Number, name() from Named and compareTo
from Comparable — all three bounds apply at once.
Generics are invariant
A List<Integer> is not a List<Number>, even though an Integer is a Number.
import java.util.ArrayList;
import java.util.List;
public class Invariance {
static double sum(List<Number> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>(List.of(1, 2, 3));
System.out.println(sum(ints));
}
}
$ java Invariance.java
Invariance.java:13: error: method sum in class Invariance cannot be applied to given types;
System.out.println(sum(ints));
^
required: List<Number>
found: List<Integer>
reason: argument mismatch; List<Integer> cannot be converted to List<Number>
1 error
error: compilation failed
If this were allowed, sum could call nums.add(2.5) and put a Double into a list the caller
believes holds only Integers. The compiler forbids the conversion instead.
? extends T: read-only access for producers
An upper-bounded wildcard accepts a list of T or any subtype. You can read elements as T, but
you cannot add to it.
import java.util.ArrayList;
import java.util.List;
public class UpperBound {
static double sum(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3)));
System.out.println(sum(List.of(1.5, 2.5)));
List<? extends Number> view = new ArrayList<Integer>();
view.add(4);
}
}
$ java UpperBound.java
UpperBound.java:16: error: no suitable method found for add(int)
view.add(4);
^
method List.add(CAP#1) is not applicable
(argument mismatch; int cannot be converted to CAP#1)
method List.add(int,CAP#1) is not applicable
(actual and formal argument lists differ in length)
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number
1 error
error: compilation failed
sum now takes List<Integer> and List<Double>. But through a List<? extends Number> the
compiler doesn’t know which subtype the list holds (CAP#1), so it rejects every add except
null — the list could be a List<Double>. Remove the last two lines and the program prints
6.0 and 4.0.
? super T: write access for consumers
A lower-bounded wildcard accepts a list of T or any supertype. You can add Ts, but reading
gives you only Object.
import java.util.ArrayList;
import java.util.List;
public class LowerBound {
static void addCountdown(List<? super Integer> sink) {
for (int i = 3; i >= 1; i--) sink.add(i);
}
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>();
List<Number> nums = new ArrayList<>(List.of(0.5));
List<Object> objs = new ArrayList<>(List.of("start"));
addCountdown(ints);
addCountdown(nums);
addCountdown(objs);
System.out.println(ints + " " + nums + " " + objs);
List<? super Integer> sink = objs;
Object first = sink.get(0);
System.out.println("read back as Object: " + first);
}
}
$ java LowerBound.java
[3, 2, 1] [0.5, 3, 2, 1] [start, 3, 2, 1]
read back as Object: start
Any list that can hold an Integer is accepted. Reading through ? super Integer gives Object,
because the list might be a List<Object> holding a String — exactly what happened here.
PECS: producer extends, consumer super
When a method reads from one collection and writes to another, use both wildcards. This is the
signature of Collections.copy.
import java.util.*;
public class Pecs {
static <T> void copyInto(List<? super T> dst, List<? extends T> src) {
for (T item : src) dst.add(item);
}
public static void main(String[] args) {
List<Integer> source = List.of(10, 20, 30);
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
copyInto(numbers, source);
Pecs.<Number>copyInto(objects, List.of(1.5, 2.5));
System.out.println(numbers + " " + objects);
List<Integer> nums = new ArrayList<>(List.of(5, 3, 8));
Collections.sort(nums, Comparator.<Number>comparingDouble(Number::doubleValue));
System.out.println(nums);
}
}
$ java Pecs.java
[10, 20, 30] [1.5, 2.5]
[3, 5, 8]
src produces Ts (extends); dst consumes them (super). The last example uses a
Comparator<Number> to sort a List<Integer>: sort declares Comparator<? super T>, so a
comparator for any supertype works. Pecs.<Number>copyInto shows an explicit type argument when
you want to choose T yourself.
Generic methods and inference
A generic method declares its own type parameter before the return type. The compiler infers T
from the arguments.
import java.util.*;
public class GenericMethods {
static <T> void swap(List<T> list, int i, int j) {
T tmp = list.get(i);
list.set(i, list.get(j));
list.set(j, tmp);
}
static <K, V> Map<V, K> invert(Map<K, V> map) {
Map<V, K> out = new TreeMap<>();
map.forEach((k, v) -> out.put(v, k));
return out;
}
public static void main(String[] args) {
List<String> words = new ArrayList<>(List.of("first", "middle", "last"));
swap(words, 0, 2);
System.out.println(words);
System.out.println(invert(Map.of("one", 1, "two", 2, "three", 3)));
var mixed = List.of(1, 2.5);
System.out.println(mixed.get(0).getClass().getSimpleName() + " " + mixed.get(1).getClass().getSimpleName());
}
}
$ java GenericMethods.java
[last, middle, first]
{1=one, 2=two, 3=three}
Integer Double
invert has two type parameters and swaps them in the result type. For List.of(1, 2.5) the
compiler infers the closest common type of Integer and Double — a Number & Comparable<...>
intersection — and the elements keep their own runtime classes.
Type erasure
Generic type arguments exist only at compile time. At runtime a List<String> and a
List<Integer> are the same class, which rules out some code.
import java.util.*;
public class Erasure {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
System.out.println(strings.getClass() == ints.getClass());
System.out.println(strings.getClass().getName());
Object o = strings;
if (o instanceof List<?> l) System.out.println("a List of something, size " + l.size());
if (o instanceof List<String>) System.out.println("a List of String");
}
}
$ java Erasure.java
Erasure.java:12: error: Object cannot be safely cast to List<String>
if (o instanceof List<String>) System.out.println("a List of String");
^
1 error
error: compilation failed
The runtime can’t tell a List<String> from any other List, so instanceof List<String> is
rejected; List<?> is fine. Removing that line prints true, java.util.ArrayList and
a List of something, size 0. Erasure is also why new T[n] and new T() don’t compile — there
is no T at runtime to instantiate.
Practice
- Write
<T extends Comparable<T>> boolean isSorted(List<T> list)and test it on a sorted and an unsorted list.
Solution
import java.util.List;
public class IsSorted {
static <T extends Comparable<T>> boolean isSorted(List<T> list) {
for (int i = 1; i < list.size(); i++) {
if (list.get(i - 1).compareTo(list.get(i)) > 0) return false;
}
return true;
}
public static void main(String[] args) {
System.out.println(isSorted(List.of(1, 2, 3, 4)));
System.out.println(isSorted(List.of("b", "a")));
System.out.println(isSorted(List.<Integer>of()));
}
}
$ java IsSorted.java
true
false
true
An empty list is sorted: the loop never runs.
- Write
addAll(Collection<? super T> dst, T... items)and use it to addIntegers to aList<Number>.
Solution
import java.util.*;
public class AddAll {
@SafeVarargs
static <T> void addAll(Collection<? super T> dst, T... items) {
for (T item : items) dst.add(item);
}
public static void main(String[] args) {
List<Number> nums = new ArrayList<>(List.of(0.5));
addAll(nums, 1, 2, 3);
System.out.println(nums);
}
}
$ java AddAll.java
[0.5, 1, 2, 3]
dst consumes Ts, so it is ? super T. @SafeVarargs silences the warning about a generic
varargs array; it is safe here because the method only reads the array.
- Write
sumAll(List<? extends List<? extends Number>> lists)that totals a list of lists, and call it with aList<List<Integer>>.
Solution
import java.util.List;
public class SumAll {
static double sumAll(List<? extends List<? extends Number>> lists) {
double total = 0;
for (List<? extends Number> inner : lists)
for (Number n : inner) total += n.doubleValue();
return total;
}
public static void main(String[] args) {
List<List<Integer>> grid = List.of(List.of(1, 2), List.of(3, 4, 5));
System.out.println(sumAll(grid));
}
}
$ java SumAll.java
15.0
Both levels need a wildcard: List<List<? extends Number>> would not accept a
List<List<Integer>>, for the same invariance reason as before.
- Two overloads
print(List<String>)andprint(List<Integer>)in one class: predict whether they compile, then try it.
Solution
import java.util.List;
public class Overloads {
static void print(List<String> list) { System.out.println("strings " + list); }
static void print(List<Integer> list) { System.out.println("ints " + list); }
public static void main(String[] args) {
print(List.of("a"));
}
}
$ java Overloads.java
Overloads.java:5: error: name clash: print(List<Integer>) and print(List<String>) have the same erasure
static void print(List<Integer> list) { System.out.println("ints " + list); }
^
1 error
error: compilation failed
After erasure both become print(List), so the class would contain two identical methods. Give
them different names, such as printStrings and printInts.