Why this matters
A stack, a list or a queue does not care what it holds. A stack of phone numbers and a stack of exam scores need the same push and pop steps.
Without generics, a programmer has two choices. The first is to write one copy of the code for each type. That repeats the same work many times, and every copy can contain its own bug. The second is to write one copy that stores a general value, then cast each value back to the real type when it is read. A cast is an instruction that tells the program "treat this value as a String". With the second choice, nothing stops a wrong value from going in, and the mistake shows up later, far from its cause.
Generics solve this. A generic piece of code has a type parameter, which is a placeholder such as T that stands for a real type. Each time you use the code, you fill in T with a real type such as int or String. The compiler then checks that every use fits.
Java and C++ both have generics, but they work differently. Java checks types when it compiles, then removes most of the type information from the compiled code. This removal is called type erasure, and it is the main reason Java generics have limits. C++ makes a separate, fully typed copy of the code for each type you use. This copy step is called instantiation.
This lesson shows both approaches, the errors each one produces, and the limits you will meet. You need to know what a list is (ArrayList in Java, std::vector in C++) and how to compile and run a program. Every sample was compiled and run on a Linux machine with g++ 15.2 and OpenJDK 25, and the output shown below is copied from those runs.
Java generics: a checker that runs before the program
In Java, a class can take a type parameter inside angle brackets. The type List<String> means a list that the compiler will allow to hold only String values. The compiler checks every add and every get against that rule.
Here is a small program that uses two generic lists.
import java.util.ArrayList;
import java.util.List;
public class ErasureDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
names.add("Ada");
numbers.add(42);
System.out.println(names.getClass() == numbers.getClass());
System.out.println(names.getClass().getName());
System.out.println(names.get(0));
System.out.println(numbers.get(0) + 1);
}
}The program prints this.
true
java.util.ArrayList
Ada
43Two things stand out. The first line is true. The two lists have different element types, yet they are the same class at runtime. The second line shows that the class is java.util.ArrayList, with no String or Integer in its name.
The line numbers.get(0) + 1 works because the compiler knows the value is an Integer. It converts the Integer to a plain int, adds 1, and prints 43. You never write that conversion yourself.
What erasure does
Erasure is a two-step process. First, the compiler reads every <String> and <Integer> and checks that your code follows the rules. Second, the compiler deletes those angle-bracket parts from the compiled code. The compiled names and numbers are both plain ArrayList objects. The program still runs the correct checks because the compiler already did them, but the running program cannot see the type that was given inside the brackets.
Java added generics in version 5. Old code had already been compiled with plain List values, and Java needed old and new code to work together. Erasure is the design that allowed this. The price is the set of limits described below.
Raw types: the way around the checker
A raw type is a generic type used without angle brackets, such as plain List. Raw types still compile, mainly so that older code keeps working. They skip the checker entirely, which means a wrong value can get into a list and show up later as a crash.
import java.util.ArrayList;
import java.util.List;
public class RawDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
List raw = names;
raw.add(7);
System.out.println("added");
String s = names.get(0);
System.out.println(s);
}
}Compiling this with javac -Xlint:unchecked RawDemo.java prints a warning. The -Xlint:unchecked flag asks the compiler to report unsafe operations that it would otherwise skip.
RawDemo.java:8: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
raw.add(7);
^
where E is a type-variable:
E extends Object declared in interface List
1 warningThe program then runs and fails.
added
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 RawDemo.main(RawDemo.java:10)The number 7 went into names on line 8. The crash happened on line 10, where the program read the value back. The compiler inserted a cast to String at that read, and the cast failed. This gap between cause and crash is the main danger of raw types. If you see a ClassCastException in Java, look for a raw type or an unchecked warning somewhere in the code that filled the list.
What erasure forbids
Because the type is gone at runtime, some things that look reasonable are refused by the compiler. This program uses five of them.
import java.util.List;
public class LimitsDemo<T> {
T make() {
return new T();
}
T[] makeArray() {
return new T[10];
}
boolean isStringList(Object o) {
return o instanceof List<String>;
}
List<int> numbers;
static T shared;
}The compiler rejects it with five errors. Here is the complete output of javac LimitsDemo.java. The compiler lists the errors in its own order, not in the order they appear in the file.
LimitsDemo.java:16: error: unexpected type
List<int> numbers;
^
required: reference
found: int
LimitsDemo.java:18: error: non-static type variable T cannot be referenced from a static context
static T shared;
^
LimitsDemo.java:5: error: unexpected type
return new T();
^
required: class
found: type parameter T
where T is a type-variable:
T extends Object declared in class LimitsDemo
LimitsDemo.java:9: error: generic array creation
return new T[10];
^
LimitsDemo.java:13: error: Object cannot be safely cast to List<String>
return o instanceof List<String>;
^
5 errorsEach error has a reason that comes from erasure.
List<int>is refused because type arguments must be reference types such asInteger. The primitiveintis not an object, so it cannot be a type argument. WriteList<Integer>instead.static T sharedis refused because a static field belongs to the class itself, not to one object.Tis chosen per object, so a static field cannot use it.new T()is refused because at runtime there is noT. The program does not know which class to create.new T[10]is refused for the same reason. An array keeps its element type at runtime, and erasure has removed that type.o instanceof List<String>is refused because at runtime the check could only seeList. It could not tell a list of strings from any other list, so the compiler will not pretend it can.
A generic method with a bound
A type parameter can also have a bound, a rule about what the type must be able to do. This method finds the largest item in a list. It needs to compare items, so the bound says that T must implement Comparable.
import java.util.List;
public class MaxDemo {
static <T extends Comparable<T>> T maxOf(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(maxOf(List.of(3, 9, 4)));
System.out.println(maxOf(List.of("pear", "apple", "plum")));
}
}The program prints the following.
9
plumThe <T extends Comparable<T>> part sits before the return type. It says that this method has a type parameter T, and that T must be a type with a compareTo method. Without that bound, item.compareTo(best) would not compile, because the compiler would only know that T is some Object. The word plum wins over pear because l comes after e in the alphabet.
C++ templates: a separate copy for each type
C++ uses a different design. A template is a pattern for a function or a class. When you call a template with a real type, the compiler writes a complete version of that function for that type. The version is then compiled like any other code, with full type information. This means C++ keeps the type at runtime, and the compiled program contains one copy per type used.
A first template
#include <iostream>
#include <string>
template <typename T>
T larger(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << larger(3, 7) << '\n';
std::cout << larger(2.5, 1.5) << '\n';
std::cout << larger(std::string("pear"), std::string("apple")) << '\n';
}Compile it with g++ -std=c++20 -Wall larger.cpp -o larger and run it.
7
2.5
pearThe line template <typename T> says that the function below it has a type parameter named T. The keyword typename here means "a type". You can also write class T with the same meaning.
Nothing in main says which type to use. The compiler reads the arguments and works out T. In larger(3, 7), both arguments are int, so T becomes int. This step is called argument deduction. When the arguments are 2.5 and 1.5, T becomes double. When they are two std::string values, T becomes std::string.
The body (a > b) ? a : b uses the > operator. That works for int, double and std::string, because each of those types has a > operator. The next sections show what happens when a type does not have one.
Each type gets its own copy
To see the separate copies, compile the file to an object file, which is a compiled piece of code that is not yet a full program. Run g++ -std=c++20 -c larger.cpp -o larger.o, then nm -C larger.o | grep larger. The nm tool lists the functions inside an object file. Adding -C makes the C++ names readable, and grep keeps only the lines for larger.
0000000000000000 W std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > larger<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
0000000000000000 W double larger<double>(double, double)
0000000000000000 W int larger<int>(int, int)The three lines are three separate functions, one for each type used. The letter W marks a function that the compiler generated from a template. The name after the return type shows the type that filled in T, for example larger<int>. The std::string line is long because the full name of std::string is long.
A consequence of this design is that each copy has its own static variables. A static variable inside a function keeps its value between calls. In a template, each type gets a separate function, so each type gets a separate static variable.
#include <iostream>
template <typename T>
int calls() {
static int count = 0;
return ++count;
}
int main() {
calls<int>();
calls<int>();
std::cout << calls<int>() << ' ' << calls<double>() << '\n';
}The program prints the following.
3 1The first two calls use calls<int>, so its count goes to 2. The third call returns 3. The call to calls<double> is a different function with its own count, so it returns 1. The notation calls<int>() tells the compiler which copy to call. You can write it explicitly like this, or let the compiler deduce it when a parameter has type T.
Errors come from the copy
Because a template is checked when it is used with a type, the errors appear at the call site or inside the copy. The first error is a type conflict. A template with one T for both arguments needs both arguments to have the same type.
template <typename T>
T larger(T a, T b) {
return (a > b) ? a : b;
}
int main() {
larger(3, 2.5);
}GCC reports the following.
conflict.cpp: In function ‘int main()’:
conflict.cpp:7:11: error: no matching function for call to ‘larger(int, double)’
7 | larger(3, 2.5);
| ~~~~~~^~~~~~~~
conflict.cpp:7:11: note: there is 1 candidate
conflict.cpp:2:3: note: candidate 1: ‘template<class T> T larger(T, T)’
2 | T larger(T a, T b) {
| ^~~~~~
conflict.cpp:2:3: note: template argument deduction/substitution failed:
conflict.cpp:7:11: note: deduced conflicting types for parameter ‘T’ (‘int’ and ‘double’)
7 | larger(3, 2.5);
| ~~~~~~^~~~~~~~The key line is deduced conflicting types for parameter ‘T’ (‘int’ and ‘double’). The compiler saw T as int from the first argument and as double from the second, and it cannot pick both. Exercise 3 below shows one fix, and the other is to write 3.0 so both arguments are double.
The second error comes from the body of the template. A struct with no > operator cannot be used with larger.
#include <iostream>
template <typename T>
T larger(T a, T b) {
return (a > b) ? a : b;
}
struct Plain {
int x;
};
int main() {
Plain p = larger(Plain{1}, Plain{2});
std::cout << p.x << '\n';
}GCC reports the following.
nomatch.cpp: In instantiation of ‘T larger(T, T) [with T = Plain]’:
nomatch.cpp:13:21: required from here
13 | Plain p = larger(Plain{1}, Plain{2});
| ~~~~~~^~~~~~~~~~~~~~~~~~~~
nomatch.cpp:5:15: error: no match for ‘operator>’ (operand types are ‘Plain’ and ‘Plain’)
5 | return (a > b) ? a : b;
| ~~~^~~~Find the error: line first, then read the In instantiation of line at the top. It says the compiler made a copy of larger with T = Plain, and that copy failed. The line no match for ‘operator>’ points to the exact > in the template body. The error refers to line 5, inside the template, but it appears only because the program used the template with Plain.
Concepts: asking for a requirement up front
C++20 added concepts. A concept is a named rule about a type, and a template can require one. This makes the error appear at the call site, in terms the programmer can read. The standard library provides std::totally_ordered, which means the type supports all six comparison operators.
#include <concepts>
#include <iostream>
template <std::totally_ordered T>
T larger(T a, T b) {
return (a > b) ? a : b;
}
struct Plain {
int x;
};
int main() {
Plain p = larger(Plain{1}, Plain{2});
std::cout << p.x << '\n';
}GCC 15 reports the following. The first ten lines are shown here, and the rest of the output lists the chain of checks that failed.
concept_err.cpp: In function ‘int main()’:
concept_err.cpp:14:21: error: no matching function for call to ‘larger(Plain, Plain)’
14 | Plain p = larger(Plain{1}, Plain{2});
| ~~~~~~^~~~~~~~~~~~~~~~~~~~~
concept_err.cpp:14:21: note: there is 1 candidate
concept_err.cpp:5:3: note: candidate 1: ‘template<class T> requires totally_ordered<T> T larger(T, T)’
5 | T larger(T a, T b) {
| ^~~~~~
concept_err.cpp:5:3: note: template argument deduction/substitution failed:
concept_err.cpp:5:3: note: constraints not satisfiedThe line constraints not satisfied tells you that the call failed the requirement, before the body was tried. Here the reason is that Plain has no == operator, and totally_ordered needs one. Adding the comparison operators to Plain would let the call compile.
Java and C++ side by side
The two languages solve the same problem with different trade-offs.
| Question | Java | C++ |
|---|---|---|
| When are types checked | At compile time, then erased | At compile time, for each type used |
| Copies of the code | One copy shared by all types | One copy per type used |
| Can a primitive be a type argument | No, List<int> is an error | Yes, std::vector<int> works |
| Can the program ask the type at runtime | No, the type argument is gone | Yes, the type is part of the compiled code |
Java generics take less space in the compiled program, because all List<String> and List<Integer> objects use the same code. The cost is the set of limits shown earlier. C++ templates can do more, such as use int directly and make separate static variables, but each new type adds a copy of the code. A program with many template uses can grow large and take longer to compile.
Common mistakes
Using a raw type in Java
Writing List names = new ArrayList(); without angle brackets compiles, but it turns off the checker for that variable. The result is a ClassCastException on a read that is far from the bad write. The fix is to write the type argument, List<String>, everywhere. Compile with -Xlint:unchecked so the compiler warns about unsafe calls.
Writing new T() in Java
A beginner often writes T item = new T(); inside a generic class, then gets error: unexpected type ... found: type parameter T. The error is correct, because T does not exist at runtime. The usual fix is to pass in a way to create the value. The exercises include a Supplier, which is a small object with a get method that returns a new value.
Expecting C++ template errors in the definition
A C++ template body is checked when it is used with a type. A template that has a mistake may compile without complaint if you never call it. When you do call it, the error message starts with In instantiation of, which tells you the copy that failed. Find the error: line, then look for the [with T = ...] part of the In instantiation of line to see which type caused it.
Passing two different types to one parameter
larger(3, 2.5) fails because the template says both arguments must have the same T. Fix it by giving one argument the other type, such as 3.0, or by naming the type yourself, such as larger<double>(3, 2.5). Exercise 3 uses the second form.
Practice
Each exercise is followed by its answer. Try each one before you read the answer.
Exercise 1
In the ErasureDemo program, the line names.getClass().getName() prints java.util.ArrayList. Why does it not print something like java.util.ArrayList<java.lang.String>?
Show the answer
Erasure removes the angle-bracket part from the compiled code. At runtime, the list is only an ArrayList. The compiler checked that the list held String values, but the running program does not keep that information in the object's class name.
Exercise 2
Write a C++ template smallest that returns the smaller of two values. Test it with two int values and two std::string values.
Show the answer
#include <iostream>
#include <string>
template <typename T>
T smallest(T a, T b) {
return (a < b) ? a : b;
}
template <typename T>
T larger(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << smallest(4, 9) << '\n';
std::cout << smallest(std::string("pear"), std::string("apple")) << '\n';
std::cout << larger<double>(3, 2.5) << '\n';
}The program prints the following. The third line belongs to exercise 3.
4
apple
3Exercise 3
Why does larger(3, 2.5) fail, and how does the third line of the answer to exercise 2 fix it?
Show the answer
The compiler sees the first argument as int, so it sets T to int. It sees the second argument as double, so it wants T to be double. These two choices conflict, and the call fails. The line larger<double>(3, 2.5) names the type, double, so the compiler does not need to deduce it. The 3 is converted to 3.0, and the program prints 3.
Exercise 4
Java does not allow T item = new T();. Rewrite this method so it can create a new value of any type, using a Supplier. A Supplier<T> is an object with a get method that returns a T.
static <T> T make() {
return new T();
}Show the answer
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class SupplierFix {
static <T> T make(Supplier<T> factory) {
return factory.get();
}
public static void main(String[] args) {
List<String> empty = make(ArrayList::new);
System.out.println(empty.size());
System.out.println(make(() -> "hello"));
}
}The caller now supplies the way to create the value. The method itself does not need to know the type. The program prints the following.
0
helloExercise 5
A program uses the raw List from the RawDemo example. On which line does the ClassCastException appear, and which line put the bad value in the list?
Show the answer
The exception appears on line 10, where names.get(0) is read into a String. The bad value 7 went in on line 8, through the raw variable raw. The gap between the two lines is why raw types are hard to debug.
Summary
- Generics let one piece of code work on many types by using a type parameter such as
T. - Java checks generic types at compile time, then erases them, so a running program cannot see the type argument.
- Erasure causes Java limits: no
new T(), nonew T[n], noinstanceof List<String>, and no primitive type arguments. - A raw type skips the Java checker and can cause a
ClassCastExceptionfar from the bad write. - C++ templates produce a separate, fully typed copy of the code for each type used, so each copy has its own
staticvariables. - C++ errors come from the copy, and concepts such as
std::totally_orderedmove the error to the call site.