A lost-and-found desk with labeled bins#
Picture the lost-and-found desk at a busy bus station. Behind the counter is a wall of identical plastic bins, each with a blank label slot. The clerk writes “Scarves” on one and “Keys” on another. From then on, each bin holds only that kind of thing. (Every lost-and-found on earth also has one bin that is mostly umbrellas.)
Java generics work the same way. You write a method or class once, with a blank for a type, and whoever uses it fills in the blank. The blank is a type parameter, usually one capital letter inside angle brackets: <T>. The real type that fills it in, such as String, is the type argument. It stands in for a type the way a parameter stands in for a value.
You have done this before: ArrayList<String> fills in the blank of the library class ArrayList<E>, and the compiler reads that label. This guide builds one program, LostAndFound, for the desk, starting with its log of item descriptions:
import java.util.ArrayList;
public class LostAndFound {
public static void main(String[] args) {
ArrayList<String> log = new ArrayList<>();
log.add("black umbrella");
log.add(42);
}
}
LostAndFound.java:7: error: incompatible types: int cannot be converted to String
log.add(42);
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
The list is labeled String, so the number 42 is refused before the program ever runs. That is the point of generics: write the code once, and let the compiler check every type that goes in and comes out.
One method per type, or just use Object?#
Start from a shape you already know. The desk keeps a shelf of item names, and you want a method that prints a shelf on one line:
LostAndFound.javapublic class LostAndFound {
public static void printShelf(String[] shelf) {
String line = "Shelf:";
for (String item : shelf) {
line += " " + item;
}
System.out.println(line);
}
public static void main(String[] args) {
String[] items = {"umbrella", "scarf", "keys"};
printShelf(items);
}
}
Shelf: umbrella scarf keys
Now the desk adds a shelf of tag numbers, an Integer[]. Copying the method with Integer in place of String works, but every bug fix must then be made twice.
There is an obvious shortcut. Every class is a kind of Object, and Java lets an array of Strings go where an Object[] is expected. So one method that takes Object[] accepts any shelf. Here is that idea in a method that hands back the first item on a shelf.
firstItem returns an Object, so the caller must cast it, which means telling Java “treat this value as a String”. Look at the line that casts the first tag to a String. Does it compile? If so, what happens when it runs?
Show the answer
It compiles. The compiler only knows the result is some Object, which might be a String, so it trusts your cast. At run time the value is an Integer, and the cast fails with a ClassCastException.
public class LostAndFound {
public static Object firstItem(Object[] shelf) {
return shelf[0];
}
public static void main(String[] args) {
String[] items = {"umbrella", "scarf", "keys"};
Integer[] tags = {104, 101, 103};
String first = (String) firstItem(items); // cast needed
System.out.println("First item: " + first);
String wrong = (String) firstItem(tags); // compiles, then fails
System.out.println("Never printed: " + wrong);
}
}
First item: umbrella
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at LostAndFound.main(LostAndFound.java:11)
The method threw away the label, so every result needs a cast, and a wrong cast is found only at run time. Java collections worked this way before Java 5. (Python lists hold anything too, and you find out what is inside when you use it. Java wants the label up front.)
The one change: a type parameter#
Go back to the String version. A generic method is the same method with one change: announce a type parameter, <T>, then write T wherever you wrote String.
The announcement goes in one exact spot: after the modifiers (public static) and just before the return type. Read public static <T> T firstItem(T[] shelf) as “for any type T, return a T from an array of T”. The first T declares the type parameter. Every later T uses it.
public class LostAndFound {
public static <T> void printShelf(T[] shelf) {
String line = "Shelf:";
for (T item : shelf) {
line += " " + item;
}
System.out.println(line);
}
public static <T> T firstItem(T[] shelf) {
return shelf[0];
}
public static <T> void swap(T[] shelf, int i, int j) {
T temp = shelf[i];
shelf[i] = shelf[j];
shelf[j] = temp;
}
public static void main(String[] args) {
String[] items = {"umbrella", "scarf", "keys"};
Integer[] tags = {104, 101, 103};
printShelf(items);
printShelf(tags);
String first = firstItem(items); // no cast
int firstTag = firstItem(tags);
System.out.println(first + " has tag " + firstTag);
swap(items, 0, 2);
printShelf(items);
}
}
Shelf: umbrella scarf keys
Shelf: 104 101 103
umbrella has tag 104
Shelf: keys scarf umbrella
One printShelf printed both shelves, and neither firstItem call needed a cast. The Integer was unboxed, meaning converted to a plain int automatically. swap works on any shelf; index 2 is the third item, since indexes start at 0.
Now the mistake that crashed the Object version:
public class LostAndFound {
public static <T> T firstItem(T[] shelf) {
return shelf[0];
}
public static void main(String[] args) {
Integer[] tags = {104, 101, 103};
String wrong = firstItem(tags);
System.out.println("Never printed: " + wrong);
}
}
LostAndFound.java:8: error: incompatible types: inference variable T has incompatible bounds
String wrong = firstItem(tags);
^
upper bounds: String,Object
lower bounds: Integer
where T is a type-variable:
T extends Object declared in method <T>firstItem(T[])
1 error
The run-time crash is now a compile error, read in the next section.
For a generic method, <T> goes before the return type, every time. Leave it out, as in public static T firstItem(T[] shelf), and Java assumes T is a class you forgot to write. You get cannot find symbol with symbol: class T, once for each T in the heading. That message means: look for a missing <T>.
How Java fills in T: type inference#
You never wrote <String> when calling printShelf. Java worked out what T had to be, which is called type inference. It collects rules that T must obey, called bounds in the messages, then looks for one type that obeys them all.
In the last error, the argument tags made Integer a lower bound: T must be Integer or a type above it. The variable receiving the result, called the target type, made String an upper bound: T must be String or below. (Object is listed because it bounds every plain T.) No type is both “Integer or above” and “String or below”, so the result is incompatible bounds.
You can also name the type yourself with an explicit type argument. It goes after the dot and just before the method name: LostAndFound.<Integer>firstItem(tags). For a static method, the class name and dot are required. Recognize it on an exam.
Inference gets more interesting when two arguments share one T:
public class LostAndFound {
public static <T> T firstItem(T[] shelf) {
return shelf[0];
}
public static <T> T choose(boolean takeFirst, T a, T b) {
if (takeFirst) {
return a;
}
return b;
}
public static void main(String[] args) {
Integer[] tags = {104, 101, 103};
Integer tag = LostAndFound.<Integer>firstItem(tags); // T named by hand
String item = choose(false, "scarf", "keys"); // T is String
Object either = choose(true, "scarf", 104); // String and Integer
System.out.println(tag + " " + item + " " + either);
}
}
104 keys scarf
In choose(true, "scarf", 104), takeFirst is true, so the value returned is the String "scarf". Would String item = choose(true, "scarf", 104); compile?
Show the answer
No. Inference happens at compile time and only looks at types, never at true or false. T must accept a String and an Integer, so it can never be String. javac reports inferred type does not conform to upper bound(s). The Object variable in the listing accepts the result, which is why that line compiled.
Bounded generic methods: T extends something#
The desk wants the largest item on a shelf: the newest tag, or the last name alphabetically. Java compares objects with compareTo.
Write largest with a plain <T> and call shelf[i].compareTo(best) inside it. Will it compile?
Show the answer
No. A plain T could be any type, so Java only allows what every object has: the methods of Object. javac reports cannot find symbol for compareTo, with location: class Object.
The fix is a bound, also called an upper bound. It limits which types may fill the blank, and in exchange it lets you call their methods. Write <T extends Comparable<T>>: “any type T that can be compared with other Ts”. Comparable is itself generic, declared Comparable<T>, which is why T appears twice. This is a bounded type parameter. A class implements Comparable, but a bound always says extends, for classes and interfaces alike.
A second common bound is <T extends Number>. Number is the parent class of Integer, Double and the other wrapper classes (class versions of the primitive number types). It gives them all doubleValue().
Predict all four lines. The cash is 20, 5 and 40 dollars; the weights are 0.5, 1.25 and 0.25 kilograms. Watch the return type of total.
Show the answer
Newest tag: 104, Last by name: umbrella, Cash found: 65.0 and Total weight: 2.0. The cash prints 65.0 because total returns a double.public class LostAndFound {
public static <T extends Comparable<T>> T largest(T[] shelf) {
T best = shelf[0]; // assume the first is largest
for (int i = 1; i < shelf.length; i++) {
if (shelf[i].compareTo(best) > 0) {
best = shelf[i];
}
}
return best;
}
public static <T extends Number> double total(T[] values) {
double sum = 0;
for (T v : values) {
sum += v.doubleValue(); // every Number has this
}
return sum;
}
public static void main(String[] args) {
Integer[] tags = {104, 101, 103};
String[] items = {"umbrella", "scarf", "keys"};
System.out.println("Newest tag: " + largest(tags));
System.out.println("Last by name: " + largest(items));
Integer[] cash = {20, 5, 40}; // dollars found in wallets
Double[] weights = {0.5, 1.25, 0.25}; // kilograms
System.out.println("Cash found: " + total(cash));
System.out.println("Total weight: " + total(weights));
}
}
Newest tag: 104
Last by name: umbrella
Cash found: 65.0
Total weight: 2.0
Does this make sense? 20 + 5 + 40 is 65, with .0 because of the double, and a total below the biggest single value would be obviously wrong. Keep this check when an AI tool hands you an answer too: decide roughly what the result should be, then compare.
One largest served Integers and Strings, and one total served Integers and Doubles. Outside a story, their parameters would get general names like values.
The bound also rejects types. Calling total(items) with the String shelf fails with inference variable T has incompatible bounds. The upper bound Number comes from your bound and the lower bound String comes from the argument. You tried to put a String in a slot labeled “numbers only”.
largest starts best at shelf[0] and loops from i = 1. Shrink it to check. With one tag, {7}, the test 1 < 1 is false, the loop runs zero times, and it returns 7. Correct. With an empty shelf, shelf[0] throws ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0. An empty shelf has no largest item, so check shelf.length first.
Generic classes: one Bin class, many bins#
A bin keeps its label for as long as it exists. That is a generic class: an ordinary class with one change, a type parameter right after the class name, class Bin<T>. Inside, fields, parameters and return values can use T, and methods do not announce it again. Each Bin keeps its items in an ArrayList<T>, so a Bin<String> holds an ArrayList<String>.
Tickets 101 and 1000 go into the tickets bin. What should contains(101) and contains(1000) print? Read contains before you look.
Show the answer
Both should print true. On this run, 1000 does not. The reason is below the output.
import java.util.ArrayList;
class Bin<T> {
private String label;
private ArrayList<T> items = new ArrayList<>();
public Bin(String label) {
this.label = label;
}
public void add(T item) {
items.add(item);
}
public T get(int index) {
return items.get(index);
}
public boolean contains(T target) {
for (T item : items) {
if (item == target) { // on purpose: see below
return true;
}
}
return false;
}
@Override
public String toString() {
return label + " " + items;
}
}
public class LostAndFound {
public static void main(String[] args) {
Bin<String> scarves = new Bin<>("Scarves");
scarves.add("red wool");
scarves.add("striped");
Bin<Integer> tickets = new Bin<Integer>("Tickets");
tickets.add(101);
tickets.add(1000);
String first = scarves.get(0); // no cast
int ticket = tickets.get(1);
System.out.println(scarves);
System.out.println(tickets);
System.out.println(first + " / ticket " + ticket);
System.out.println("Has 101? " + tickets.contains(101));
System.out.println("Has 1000? " + tickets.contains(1000));
}
}
Scarves [red wool, striped]
Tickets [101, 1000]
red wool / ticket 1000
Has 101? true
Has 1000? false
One Bin class made two kinds of bin, and each get needed no cast.
The last line is wrong on purpose, so you meet it here and not at 2 a.m. before a deadline. T values are always objects, and == on objects asks “same object?”, not “same value?”. Each Integer here was autoboxed, meaning converted automatically from an int into an object. Java guarantees one shared object only from -128 to 127. Above that it depends on the JVM. On a standard setup, like the run above, the stored 1000 and the searched-for 1000 are different objects, so == can say true or false. Only equals is safe.
Every time you compare two T values, use equals. Change the one line in contains to if (item.equals(target)), and the last line becomes:
Has 1000? trueNow the creation lines. new Bin<Integer>("Tickets") repeats the type. new Bin<>("Scarves") leaves the angle brackets empty, and inference fills them from the variable on the left. The empty pair is nicknamed the diamond operator. Both are correct; most programmers use the diamond.
Leaving the angle brackets off completely, new Bin("Scarves"), makes a raw type, a Bin with no label at all. The compiler cannot check that line, so it adds a note about “unchecked or unsafe operations”. Declare the variable as plain Bin and every use of it goes unchecked.
Two type parameters, and a bounded class#
Whoever hands in an item gets a claim ticket. A claim joins two values of possibly different types, so it gets two type parameters, separated by a comma: class Claim<K, V>.
class Claim<K, V> {
private K ticket;
private V item;
public Claim(K ticket, V item) {
this.ticket = ticket;
this.item = item;
}
public K getTicket() {
return ticket;
}
public V getItem() {
return item;
}
@Override
public String toString() {
return "#" + ticket + " = " + item;
}
}
public class LostAndFound {
public static void main(String[] args) {
Claim<Integer, String> c1 = new Claim<>(104, "umbrella");
Claim<String, Double> c2 = new Claim<>("B-7", 1.25);
int number = c1.getTicket();
String what = c1.getItem();
System.out.println(number + " is the " + what);
System.out.println(c1);
System.out.println(c2);
}
}
104 is the umbrella
#104 = umbrella
#B-7 = 1.25
One Claim class paired an Integer with a String, and a String with a Double. c1.getItem() is a String because V is String for c1. The diamond filled in both type arguments.
A class can take a bound too. Inside class WeightLog<T extends Number>, the bound allows weight.doubleValue(). Outside, WeightLog<Double> is fine, but WeightLog<String> words; fails with type argument String is not within bounds of type-variable T.
By convention, type parameters are single capital letters: T for type, E for element, K and V for key and value. Any legal name compiles, but one letter shows at a glance that it is not a real class. A general two-slot class is often just called Pair<A, B>.
No primitives as type arguments#
Here is why the tag shelf was never an int[]:
import java.util.ArrayList;
class Bin<T> {
private ArrayList<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
}
}
public class LostAndFound {
public static void main(String[] args) {
Bin<int> tickets = new Bin<>();
}
}
LostAndFound.java:13: error: unexpected type
Bin<int> tickets = new Bin<>();
^
required: reference
found: int
1 error
A type argument must be a reference type: a class, an interface or an array type. The eight primitive types, such as int, double, char and boolean, are not allowed.
Every time you write a type argument, check it is not a primitive. Use the wrapper class: Integer, Double, Character, Boolean. tickets.add(101) still works, because Java autoboxes the 101.
What generics cannot do: new T() and new T[]#
Why does Bin use an ArrayList<T>? Here is a plain-array version, plus a spare item from T’s constructor:
class Bin<T> {
private T[] items = new T[10];
private T spare = new T();
}
public class LostAndFound {
public static void main(String[] args) {
Bin<String> scarves = new Bin<>();
}
}
LostAndFound.java:2: error: generic array creation
private T[] items = new T[10];
^
LostAndFound.java:3: error: unexpected type
private T spare = new T();
^
required: class
found: type parameter T
where T is a type-variable:
T extends Object declared in class Bin
2 errors
Neither line compiles, for one shared reason. Java checks generic types while compiling and then removes them, so the running program no longer knows what T was. This is called type erasure. Creating an object or an array needs the real class, and after erasure there is none. (And T might not even have a no-argument constructor.)
So store many Ts in an ArrayList<T>. The library’s ArrayList<E> meets the same limit from the inside. It keeps its elements in a plain Object[]. When you call get, the compiler quietly adds the cast to your code, so you never write it.
Do not paste this one: typing it is how the two positions of <T> stick. Add a static method binOf(String label, T[] shelf) that returns a new Bin holding every item on the shelf. Test it with items and tags.
Show the answer
The method is generic, so <T> goes before the return type. The return type is a generic class, so T appears twice in a row:
public static <T> Bin<T> binOf(String label, T[] shelf) {
Bin<T> bin = new Bin<>(label);
for (T item : shelf) {
bin.add(item);
}
return bin;
}Desk [umbrella, scarf, keys]
Tags [104, 101, 103]Inference made T a String, then an Integer, so each bin came back already labeled.
What was new in this guide#
- A type parameter
<T>is a blank that a type argument fills in. - An
Objectversion needs casts that can fail at run time. - A generic method puts
<T>before its return type. - Type inference finds
Tfrom argument types and the target type, never values. - A bound such as
<T extends Number>limits the types and unlocks their methods. - A generic class puts
<T>after its name; the diamond<>infers it. - Type arguments are reference types, and
Tvalues are compared withequals. - Type erasure rules out
new T()andnew T[n].
<T> is a blank type that the user fills in and the compiler checks. It goes before the return type of a generic method and after the name of a generic class. Add extends to limit the allowed types so you can call their methods. Type arguments are reference types, so write Integer, never int.
Where these go wrong#
- Forgetting
<T>before the return type of a generic method. The compiler looks for a class calledTand reportscannot find symbol. - Repeating
<T>on a method inside a generic class.public <T> void add(T item)insideBin<T>declares a second, unrelatedT. Thenitems.add(item)fails withT#1 cannot be converted to T#2. - Using a primitive as the type argument.
Bin<int>fails; useBin<Integer>. - Passing an
int[]to a method that takesT[].Tcannot beint, so use anInteger[]. - Calling
compareToon a plainT. A plainTonly hasObject’s methods. Bound it:<T extends Comparable<T>>. - Writing
implementsin a bound. Bounds always useextends. - Comparing
Tvalues with==. It asks “same object?”, and for Integers above 127 the answer depends on the JVM. Useequals. - Leaving the angle brackets off:
new Bin("Scarves"). That is a raw type, aBinwith no label, so the compiler cannot check it. Writenew Bin<>("Scarves"). - Writing
new T()ornew T[n]. Neither compiles because of type erasure; use anArrayList<T>.
On the exam: “write a generic method” means only the method, and marks are often lost on a missing <T> before the return type. For “write a generic class”, <T> goes after the class name only. Read the question twice, answer, then reread to check you did every part.
Practice#
Answer each one on paper before you look at the solutions. Every answer that involves running code was checked by compiling and running it.
- In
public static <T> T first(T[] arr), what does each of the threeTs mean? - Write a generic method
countMatches(T[] items, T target)that returns how many items equaltarget. Test it on Strings and Integers. - Why does this not compile, and how do you fix it?
public static <T> T bigger(T a, T b) { if (a.compareTo(b) > 0) return a; return b; } - Write a generic class
Box<T>with a constructor,get,setandisEmpty(true when the contents arenull). - Write a bounded generic method that returns the smallest element of an array, and show it working on Integers and Strings.
- Which of these compile?
Pair<String, Integer> a = new Pair<>("x", 1);Pair<String, int> b;Stats<String> c;whereclass Stats<T extends Number>.
Solutions#
- The first, inside
<T>, declares the type parameter. The second is the return type: the method returns a T. The third says the parameter is an array of T. When called with aString[], all three becomeString. - Use
equals, which every object has, so no bound is needed:public class QCount { public static <T> int countMatches(T[] items, T target) { int count = 0; for (T item : items) { if (item.equals(target)) { count++; } } return count; } public static void main(String[] args) { String[] votes = {"yes", "no", "yes", "yes"}; Integer[] dice = {6, 2, 6}; System.out.println(countMatches(votes, "yes")); System.out.println(countMatches(dice, 6)); } }3 2 - A plain
Tmight be any type, and not every type hascompareTo, so the compiler reportscannot find symbol. Add a bound so only comparable types are allowed:public static <T extends Comparable<T>> T bigger(T a, T b). - Every place that would have said a specific type says
Tinstead:class Box<T> { private T contents; public Box(T contents) { this.contents = contents; } public T get() { return contents; } public void set(T contents) { this.contents = contents; } public boolean isEmpty() { return contents == null; } } public class QBox { public static void main(String[] args) { Box<String> b = new Box<>("socks"); System.out.println(b.get() + " " + b.isEmpty()); b.set(null); System.out.println(b.isEmpty()); } }socks false true - Bound T to
Comparable<T>socompareTois allowed:public class QMin { public static <T extends Comparable<T>> T smallest(T[] items) { T min = items[0]; for (T item : items) { if (item.compareTo(min) < 0) { min = item; } } return min; } public static void main(String[] args) { Integer[] n = {8, 3, 9}; String[] s = {"kiwi", "fig", "lime"}; System.out.println(smallest(n)); System.out.println(smallest(s)); } }3 fig - Only the first.
intis a primitive and cannot be a type argument, andStringis not aNumber, so it is outside the bound ofStats.
Test-readiness checklist#
These are the items from the course’s Exam 2 topic list that this guide covers. Tick each one only when you can do it without looking.
- ☐ I can explain what generics are and why they catch errors at compile time.
- ☐ I can write and call generic methods, with the type parameter before the return type.
- ☐ I can write bounded generics with
extendsand explain what the bound allows. - ☐ I can write and use generic classes with one or more type parameters.
Test yourself in the free Kestrel Exams app
40 practice questions on generic methods, generic classes and bounds, with every code answer checked by compiling and running it. Free, offline, no account.
Practise generics →Frequently asked questions#
What are generics in Java?
Generics let a class or method be written with a type parameter, usually T, that is filled in with a real type when the class or method is used. The compiler then checks every use against that type, catching mistakes at compile time and removing the need for casts.
How do you write a generic method in Java?
Declare the type parameter in angle brackets just before the return type, then use it like any type: public static <T> void printAll(T[] items). Callers do not usually name the type; Java infers it from the arguments.
What is a bounded generic type?
A type parameter limited to a class or interface and its subtypes, written with extends, for example <T extends Number> or <T extends Comparable<T>>. The bound restricts which types may be used and in return lets the code call that type's methods, such as doubleValue() or compareTo().
How do you write a generic class in Java?
Put the type parameters after the class name, as in class Pair<A, B>, and use A and B as the types of fields, parameters and return values. Create objects with the types filled in: new Pair<String, Integer>("Ana", 20), or new Pair<>("Ana", 20) using the diamond operator.
Why can I not use int with generics?
Type arguments must be reference types, and int is a primitive. Use the wrapper class Integer instead; Java converts between int and Integer automatically through autoboxing.
Why can I not create a generic array with new T[]?
Because of type erasure: Java checks generic types at compile time and then removes them, so at run time it does not know what T is and cannot create an array of that exact type. Use an ArrayList<T> instead.
Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written — several of these exist because somebody asked.