Skip to content
Java Collections · Lesson 2

Generics, just enough

Why the angle brackets exist, and what happens to your int.

Contents
Part 1 of 7 · What it is

Labels on the lunchbox

Generics make a list say what it holds.

Imagine a rack of lunchboxes in a school. One box has no label. You open it and find rice. The next one holds a pencil case, because someone mixed them up. You only find out when you are hungry, and it is too late.

Now imagine every box has a label: "food only". Anyone who tries to put a pencil case in is stopped at the door. You never open a box and get a surprise.

That label is what generics give a collection. Before Java 5, a List could hold anything at all, and you only found out what you had when the program ran and broke. Now a list carries a label in angle brackets: List<String> means "a list of strings, and nothing else". The part inside the brackets is called a type parameter. A type is the kind of thing a value is, such as String for text or Integer for a whole number.

Here is what the unlabelled version looks like, and why nobody writes it any more.

Java
List things = new ArrayList(); // no <...>: a "raw" list, as before generics
things.add("tea");
things.add(42); // nothing stops this
String second = (String) things.get(1); // compiles, then fails at run time

Paste into jshell to try it

This compiles. A list with no label is called a raw type. It happily accepts the word and the number. The trouble comes on the last line, when we pretend the number is a word. Java stops the program with a ClassCastException while it is running. The mistake was on line three, but the crash is on line four, and in a bigger program it could be in a different file.

Now the labelled version.

Java
List<String> drinks = new ArrayList<>();
drinks.add("tea");
// drinks.add(42); would not compile: int cannot be converted to String
String first = drinks.get(0); // no cast: the list knows it holds Strings
System.out.println(first.toUpperCase());

Paste into jshell to try it

Output
TEA

The commented-out line shows the point. With the label, drinks.add(42) is rejected by javac before the program ever runs. And reading is easier too, because get already knows it returns a String, so there is no cast.

Remember: <String> is a label that the compiler checks for you. Mistakes are caught when you compile, not when your program is running in front of someone.

Part 2 of 7 · The idea

Catch it before it runs

The compiler reads the label for you.

A List of String accepts words and refuses a numberList<String> is a box labelled for words. Add a word and it goes in. Add a number and the compiler stops you before the program runs.List<String>AshaRaviMeera"Kiran"42compile error: int cannot be converted to String
List<String> is a box labelled for words. Add a word and it goes in. Add a number and the compiler stops you before the program runs.

The picture shows a List<String>. The word "Kiran" fits and goes in. The number 42 is turned away at the door, and the message comes from the compiler, not from a crashed program.

Two ideas hide in that picture.

Compile time and run time are different moments. Compile time is when you run javac. Run time is when the program is going. A mistake caught at compile time costs you a few seconds. A mistake found at run time can cost you a failed test or a broken app. Generics move a whole group of mistakes from the second kind to the first kind.

The label is part of the type. List<String> and List<Integer> are different types, even though both are lists. You can write a method that takes one and refuses the other.

Part 3 of 7 · Use it

Using generics daily

The diamond, the wrappers, and two traps.

The diamond. When you build a collection, you write the label on the left and leave the right side empty:

Java
// The type on the left says everything, so the right side can stay empty.
Map<String, List<Integer>> marksByStudent = new HashMap<>();
marksByStudent.put("Asha", new ArrayList<>());
marksByStudent.get("Asha").add(82);
System.out.println(marksByStudent);

Paste into jshell to try it

Output
{Asha=[82]}

The empty <> is called the diamond. It tells Java: "work out the label from the left side". Without it you would write HashMap<String, List<Integer>> twice. The map above holds a list for each student. A type parameter can itself be a generic type, and nothing stops you from nesting them, as here.

Only objects fit inside the brackets. You cannot write List<int>. The brackets accept classes, and int is a primitive, a plain value that is not an object. Each primitive has an object twin, called its wrapper class:

PrimitiveWrapper
intInteger
longLong
doubleDouble
booleanBoolean
charCharacter

So a list of whole numbers is a List<Integer>. The good news is that Java converts for you. Putting an int in is called boxing, and taking it out again is called unboxing.

Java
List<Integer> nums = new ArrayList<>();
nums.add(7); // the int 7 is wrapped into an Integer for you
int back = nums.get(0); // and unwrapped on the way out
System.out.println(back + 1);

Paste into jshell to try it

Output
8

We added the plain 7, and got back a plain int we could do sums with. The wrapping and unwrapping happened out of sight. Out of sight is nice, but it hides two traps.

Trap 1: == on wrapper objects. Two Integers can hold the same number and still be different objects.

Java
Integer x = 1000;
Integer y = 1000;
System.out.println(x.equals(y)); // compares the numbers
System.out.println(x == y); // compares the two wrapper objects

Paste into jshell to try it

Output
true
false

equals asks "do these hold the same number?". == asks "are these the very same object?". Java guarantees that the numbers from -128 to 127 are shared, so == works for them, which is how the bug sneaks into code that passes a quick test. For 1000 it makes no promise, and the second line is usually false. For anything that is an object, use equals.

Trap 2: unboxing null. A map answers null for a missing key. A plain int cannot hold null, so unboxing it fails.

Java
Map<String, Integer> marks = new HashMap<>();
int mark = marks.get("Kiran"); // get gives null, and null cannot become an int

Paste into jshell to try it

That line throws a NullPointerException, because marks.get("Kiran") is null and Java tries to turn it into an int. If the key might be missing, keep the answer as an Integer, check it, or use getOrDefault.

Writing your own. You can use generics in your own code with a placeholder letter, by convention T for "type". Here is a method that works for lists of anything:

Java
static <T> T firstOr(List<T> items, T fallback) {
    // <T> before the return type declares a placeholder for this method.
    return items.isEmpty() ? fallback : items.get(0);
}

Paste into jshell to try it

Java
System.out.println(firstOr(List.of("tea", "coffee"), "none"));
List<Integer> empty = new ArrayList<>();
System.out.println(firstOr(empty, -1));

Paste into jshell to try it

Output
tea
-1

The <T> before the return type announces "T is a placeholder in this method". The same method handled a list of strings and a list of integers, and the compiler checked each call. And here is a tiny class with the same idea.

Java
static class Crate<T> {
    private T item;

    void put(T item) {
        this.item = item;
    }

    T take() {
        return item;
    }
}

Paste into jshell to try it

Java
Crate<String> mangoes = new Crate<>();
mangoes.put("mango");
String fruit = mangoes.take(); // no cast needed
System.out.println(fruit);

Paste into jshell to try it

Output
mango

Crate<String> is a crate that holds strings. Crate<Integer> would hold integers. The code of Crate is written once.

Remember: == compares objects, equals compares what is inside. For anything inside a collection, reach for equals.

Part 4 of 7 · How it works

What boxing really does

And why the labels vanish.

Boxing is a method call. When you write nums.add(7), javac quietly rewrites it to nums.add(Integer.valueOf(7)). Unboxing becomes a call to intValue(). This is why unboxing null throws a NullPointerException: the program is calling a method on nothing.

Integer.valueOf keeps ready-made objects for the numbers from -128 to 127 and hands out the same one each time. Outside that range it usually makes a new object. That is exactly why == looked fine for small numbers and failed for 1000.

The labels disappear before the program runs. After javac has checked your labels, it removes them from the bytecode. This is called type erasure. At run time a List<String> and a List<Integer> are the same class, plain List. Java did it this way so that code written before generics kept working.

That choice explains two rules you may bump into:

  • You can't write new T() inside a generic class, because at run time there is no T to build.
  • You can't use a primitive as a type parameter, because after erasure everything is stored as an object reference.

You don't need more than that for the rest of this course. Generics have deeper corners, such as wildcards, which are written ?. They matter when you write libraries, not when you use them.

Part 5 of 7 · Cost

What boxing costs

Small, but a list of Integer is not an int array.

Generics themselves cost nothing at run time, because the labels are erased. Boxing is where the price shows up.

OperationWhat happensCost
Compile-time label checkjavac reads the labelsNone at run time
Boxing (nums.add(7))Calls Integer.valueOf; may make an objectO(1)
Unboxing (int x = nums.get(0))Calls intValue()O(1)
a.equals(b) on wrappersCompares the numbersO(1)
a == b on wrappersCompares the objectsO(1), but usually the wrong question

The real cost is memory and speed in big programs. A List<Integer> holds a reference to a separate Integer object for each number, and an int[] holds the numbers themselves, side by side. For a few thousand items nobody notices. For tens of millions, the plain array is far lighter. Collections give you convenience, and boxing is the fee.

Part 6 of 7 · Practise

Three problems, one label

Add, look at the end, and compare properly.

In all three, the method signature uses the labels you just learned. Read the types first. They tell you most of what the method does.

Problem 1

Add up a list of Integers

Write sumAll, which returns the sum of all the numbers in the list. For [1, 2, 3, 4] the answer is 10. An empty list gives 0.

Start here

Java
static int sumAll(List<Integer> nums) {
    throw new UnsupportedOperationException("add up every number in the list");
}

Paste into jshell to try it

Stuck?

Show a solution
Java
static int sumAll(List<Integer> nums) {
    int total = 0;
    for (int n : nums) { // each Integer is unwrapped into an int here
        total += n;
    }
    return total;
}

Paste into jshell to try it

The loop variable is declared int, so each Integer is unboxed once as it is read. Starting total at 0 makes the empty list correct without a special case. One pass gives O(n) time and O(1) extra space. If any item in the list were null, the unboxing would throw a NullPointerException, which is the trap from earlier.

Problem 2

The last item, or a fallback

Write lastOr, which returns the last item in the list, or the fallback if the list is empty. lastOr(List.of("a", "b", "c"), "none") gives "c". lastOr(emptyListOfStrings, "none") gives "none".

Start here

Java
static <T> T lastOr(List<T> items, T fallback) {
    throw new UnsupportedOperationException("the last item, or the fallback if the list is empty");
}

Paste into jshell to try it

Stuck?

Show a solution
Java
static <T> T lastOr(List<T> items, T fallback) {
    if (items.isEmpty()) {
        return fallback;
    }
    return items.get(items.size() - 1);
}

Paste into jshell to try it

Because T is a placeholder, one method serves lists of strings, numbers or anything else, and javac still checks that the fallback matches the list. The empty check comes first so that items.size() - 1 is never -1. get on a list is O(1) for an ArrayList, so the whole method is O(1) there.

Problem 3

Count one item

Write countOccurrences, which returns how many items in the list are equal to target. For ["a", "b", "a"] and the target "a" the answer is 2. It must work for a list of any type, including a list holding several Integer 1000s.

Start here

Java
static <T> int countOccurrences(List<T> items, T target) {
    throw new UnsupportedOperationException("how many items equal the target?");
}

Paste into jshell to try it

Stuck?

Show a solution
Java
static <T> int countOccurrences(List<T> items, T target) {
    int count = 0;
    for (T item : items) {
        // equals, not ==: == would compare object identity, which
        // breaks for two equal Strings or Integers above 127.
        if (item.equals(target)) {
            count++;
        }
    }
    return count;
}

Paste into jshell to try it

equals compares what is inside the objects, so it works for strings, wrapper numbers and any class that defines equals properly (lesson 7 explains how). Using == would pass small tests and fail on large numbers or on strings built at run time, which is a classic way to lose marks. The loop visits each item once, so it is O(n) time and O(1) space.

Part 7 of 7 · Interview notes

Generics in the interview

Short answers to the usual questions.

Generics are a favourite of interviewers because the answers are short and the traps are real.

  • "Why can't you write List<int>?" Type parameters must be objects, and int is a primitive. Use List<Integer>, and Java boxes and unboxes for you.
  • "What is boxing?" Turning a primitive into its wrapper object, such as int to Integer, which the compiler does by calling Integer.valueOf.
  • "What goes wrong with Integer a = 1000, b = 1000; a == b?" It compares objects and is usually false. Values from -128 to 127 are cached, so small numbers hide the bug. Use equals.
  • "What is type erasure?" The compiler checks the labels, then removes them. At run time a List<String> is just a List.
  • "Why avoid raw types?" They turn off the label check, so mistakes surface at run time as a ClassCastException, far from where they were made.
  • "What does the diamond <> do?" It lets the compiler fill in the type parameters from the left-hand side, so you don't repeat them.
  • "What can go wrong when you unbox?" A null wrapper throws a NullPointerException. Map lookups for missing keys are the usual source.

Updated 2026-10-17

Made by VeraStack Labs