AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

13 Generics / Lesson 1 of 6

Why generics exist

12 min · +10 XP · 2 exercises · 5 questions

Read the text12 min
Exercises0/2
Quiz5 q

Quick answer

What are generics in Java?

Generics in Java let a class, interface or method take type parameters, so List<String> is a list the compiler knows holds only strings. Adding the wrong type becomes a compile error on the line that is wrong, reading needs no cast, and one implementation serves every element type. Added in Java 5, generics are checked at compile time and erased at run time.

Before Java 5, a List held Objects: you put a String in and got an Object out, cast it back, and found out at run time if you were wrong. Generics let you say List<String> — a list of strings — so the compiler checks what goes in and knows what comes out. This lesson is the problem generics solve, the vocabulary (type parameter, type argument, parameterised type, raw type), and the first look at the design decision — erasure — that explains every limitation you will meet later in the module.

The problem

List names = new ArrayList();            // pre-generics: a list of Object
names.add("Ada");
names.add(42);                           // nothing stops this
String first = (String) names.get(0);    // cast required…
String second = (String) names.get(1);   // …and this one throws ClassCastException at RUN time

Three costs: casts everywhere, no documentation of what the list holds, and errors that surface far from their cause — the 42 was added in one file and exploded in another.

The solution

List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42);                           // compile error: incompatible types: int cannot be converted to String
String first = names.get(0);             // no cast: the compiler knows it is a String

List<String> is a parameterised type: the generic interface List<E> with the type argument String supplied for its type parameter E. Inside List, every E now means String, so add(E) accepts only strings and get(int) returns one. The error moves to compile time, to the line that is actually wrong.

Vocabulary

TermExampleMeaning
Generic typeList<E>, Map<K, V>A type declared with type parameters
Type parameterE, K, V, TA placeholder in the declaration
Type argumentString in List<String>The actual type supplied at use
Parameterised typeList<String>A generic type with arguments
Raw typeListA generic type used without arguments — legacy, unsafe
Generic methodstatic <T> T first(List<T> xs)A method with its own type parameters
Bounded type<T extends Comparable<T>>A parameter restricted to subtypes of something
WildcardList<?>, List<? extends Number>An unknown type argument, possibly bounded

Conventions: single capital letters — T (type), E (element), K/V (key/value), R (result), N (number). Two-letter names are unusual; descriptive names are not idiomatic.

Type arguments must be reference types

List<int> is a compile error; List<Integer> with autoboxing is how primitives get in. This costs boxing (Module 2) and is the reason IntStream, int[] and primitive-specialised functional interfaces exist. Project Valhalla is working on lifting this; for now, wrappers.

The diamond

Map<String, List<Integer>> index = new HashMap<String, List<Integer>>();   // Java 5–6
Map<String, List<Integer>> index = new HashMap<>();                        // Java 7+: infer from the left side
var index = new HashMap<String, List<Integer>>();                          // Java 10+: infer the variable's type instead

<> tells the compiler to infer the constructor's type arguments from the target type. Combine it with var and the arguments must be on the right instead — never var x = new HashMap<>(), which infers HashMap<Object, Object>.

Raw types: why they still exist and why you avoid them

List without arguments is a raw type. It compiles for backwards compatibility with pre-2004 code, with warnings ("unchecked call"), and it turns off checking:

List<String> safe = new ArrayList<>();
List raw = safe;                         // allowed, warning
raw.add(42);                             // allowed, warning — pollutes the String list
String s = safe.get(0);                  // ClassCastException here, far from the raw add

Never write a raw type in new code. The one legitimate appearance is in class literals and instanceof (List.class, x instanceof List), because those cannot carry type arguments — the next section explains why.

Erasure, in one paragraph

Generics were added to a language with ten years of compiled libraries and a JVM that knew nothing about them. The design chosen was erasure: type arguments exist for the compiler only. After checking, List<String> compiles to plain List, T becomes Object (or its bound), and casts are inserted where the code reads elements. At run time there is one List class, no List<String>, and new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() is true. This kept old code running unchanged and is the reason for every "you cannot do that with generics" rule in lesson 5. Understand it now and the rules will feel inevitable rather than arbitrary.

What you gain, concretely

  • Compile-time safety: wrong-type insertions are errors at the line that is wrong.
  • No casts in reading code.
  • Documentation: Map<CustomerId, List<Order>> says what a structure holds; Map says nothing.
  • Reusable algorithms: one sort, one max, one filter for every element type, without Object and casts.
  • APIs that describe themselves: Function<String, Integer> tells you the whole contract.

Reading generic signatures

public static <T extends Comparable<? super T>> void sort(List<T> list) — read it left to right: a generic method with parameter T, where T must be comparable to itself or a supertype; takes a list of T; returns nothing. By the end of this module every part of that line will be obvious. Until then, the skill is to not panic: find the type parameters, their bounds, and where they appear in the parameters and return.

Interview angle

  • "What are generics for?" Compile-time type safety and reuse without casts; errors move to the source line.
  • "What is a raw type and why avoid it?" A generic type without arguments; it disables checking and pollutes typed collections.
  • "Can you have a List<int>?" No — type arguments must be reference types; use Integer (boxing) or int[].
  • "What is erasure?" Type arguments are compile-time only; at run time List<String> is List.

Key takeaways

  • Generics move type errors from run time (casts) to compile time (the wrong line) and document what collections hold.
  • Vocabulary: type parameter E, type argument String, parameterised type List<String>, raw type List.
  • Reference types only; the diamond <> infers constructor arguments from the target.
  • Raw types are legacy — never in new code except class literals and instanceof.
  • Erasure: generics are for the compiler; run time sees the raw class.

Common questions

What is a raw type in Java?

A raw type is a generic type used without type arguments, such as List instead of List<String>. It compiles only for compatibility with pre-generics code, gives "unchecked" warnings and switches off type checking, so a wrong element can pollute a typed list. Never write one in new code.

Can you have a List<int> in Java?

No. Type arguments must be reference types, so List<int> is a compile error; use List<Integer> and let autoboxing convert, or an int[]. That boxing cost is why IntStream and the primitive-specialised functional interfaces exist.

What is the diamond operator in Java?

The diamond, <>, tells the compiler to infer a constructor's type arguments from the target type: Map<String, List<Integer>> index = new HashMap<>(); compiles since Java 7. With var, put the arguments on the right instead, because var x = new HashMap<>() infers HashMap<Object, Object>.

What is the difference between a type parameter and a type argument?

A type parameter is the placeholder in a generic declaration, such as E in List<E>. A type argument is the actual type supplied when the generic type is used, such as String in List<String>, which makes it a parameterised type.

Why does Java use type erasure for generics?

For backward compatibility. Generics arrived in a language with ten years of compiled libraries and a JVM that knew nothing about them, so type arguments are checked by the compiler and then erased: at run time List<String> and List<Integer> are the same List class.

Mark as readSign in to keep your place.

What Are Generics in Java? Type Safety and Raw Types

Java generics let the compiler check what a collection holds, so wrong types fail at compile time with no casts. Vocabulary, the diamond, raw types, erasure.

  • Course: Java study plan
  • Module: Generics
  • Kind: Lesson
  • Reading time: 12 min
  • Runtime: OpenJDK 18

What are generics in Java?

Generics in Java let a class, interface or method take type parameters, so List<String> is a list the compiler knows holds only strings. Adding the wrong type becomes a compile error on the line that is wrong, reading needs no cast, and one implementation serves every element type. Added in Java 5, generics are checked at compile time and erased at run time.

Lesson

Before Java 5, a List held Objects: you put a String in and got an Object out, cast it back, and found out at run time if you were wrong. Generics let you say List<String> — a list of strings — so the compiler checks what goes in and knows what comes out. This lesson is the problem generics solve, the vocabulary (type parameter, type argument, parameterised type, raw type), and the first look at the design decision — erasure — that explains every limitation you will meet later in the module.

The problem

List names = new ArrayList();            // pre-generics: a list of Object
names.add("Ada");
names.add(42);                           // nothing stops this
String first = (String) names.get(0);    // cast required…
String second = (String) names.get(1);   // …and this one throws ClassCastException at RUN time

Three costs: casts everywhere, no documentation of what the list holds, and errors that surface far from their cause — the 42 was added in one file and exploded in another.

The solution

List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42);                           // compile error: incompatible types: int cannot be converted to String
String first = names.get(0);             // no cast: the compiler knows it is a String

List<String> is a parameterised type: the generic interface List<E> with the type argument String supplied for its type parameter E. Inside List, every E now means String, so add(E) accepts only strings and get(int) returns one. The error moves to compile time, to the line that is actually wrong.

Vocabulary

TermExampleMeaning
Generic typeList<E>, Map<K, V>A type declared with type parameters
Type parameterE, K, V, TA placeholder in the declaration
Type argumentString in List<String>The actual type supplied at use
Parameterised typeList<String>A generic type with arguments
Raw typeListA generic type used without arguments — legacy, unsafe
Generic methodstatic <T> T first(List<T> xs)A method with its own type parameters
Bounded type<T extends Comparable<T>>A parameter restricted to subtypes of something
WildcardList<?>, List<? extends Number>An unknown type argument, possibly bounded

Conventions: single capital letters — T (type), E (element), K/V (key/value), R (result), N (number). Two-letter names are unusual; descriptive names are not idiomatic.

Type arguments must be reference types

List<int> is a compile error; List<Integer> with autoboxing is how primitives get in. This costs boxing (Module 2) and is the reason IntStream, int[] and primitive-specialised functional interfaces exist. Project Valhalla is working on lifting this; for now, wrappers.

The diamond

Map<String, List<Integer>> index = new HashMap<String, List<Integer>>();   // Java 5–6
Map<String, List<Integer>> index = new HashMap<>();                        // Java 7+: infer from the left side
var index = new HashMap<String, List<Integer>>();                          // Java 10+: infer the variable's type instead

<> tells the compiler to infer the constructor's type arguments from the target type. Combine it with var and the arguments must be on the right instead — never var x = new HashMap<>(), which infers HashMap<Object, Object>.

Raw types: why they still exist and why you avoid them

List without arguments is a raw type. It compiles for backwards compatibility with pre-2004 code, with warnings ("unchecked call"), and it turns off checking:

List<String> safe = new ArrayList<>();
List raw = safe;                         // allowed, warning
raw.add(42);                             // allowed, warning — pollutes the String list
String s = safe.get(0);                  // ClassCastException here, far from the raw add

Never write a raw type in new code. The one legitimate appearance is in class literals and instanceof (List.class, x instanceof List), because those cannot carry type arguments — the next section explains why.

Erasure, in one paragraph

Generics were added to a language with ten years of compiled libraries and a JVM that knew nothing about them. The design chosen was erasure: type arguments exist for the compiler only. After checking, List<String> compiles to plain List, T becomes Object (or its bound), and casts are inserted where the code reads elements. At run time there is one List class, no List<String>, and new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() is true. This kept old code running unchanged and is the reason for every "you cannot do that with generics" rule in lesson 5. Understand it now and the rules will feel inevitable rather than arbitrary.

What you gain, concretely

  • Compile-time safety: wrong-type insertions are errors at the line that is wrong.
  • No casts in reading code.
  • Documentation: Map<CustomerId, List<Order>> says what a structure holds; Map says nothing.
  • Reusable algorithms: one sort, one max, one filter for every element type, without Object and casts.
  • APIs that describe themselves: Function<String, Integer> tells you the whole contract.

Reading generic signatures

public static <T extends Comparable<? super T>> void sort(List<T> list) — read it left to right: a generic method with parameter T, where T must be comparable to itself or a supertype; takes a list of T; returns nothing. By the end of this module every part of that line will be obvious. Until then, the skill is to not panic: find the type parameters, their bounds, and where they appear in the parameters and return.

Interview angle

  • "What are generics for?" Compile-time type safety and reuse without casts; errors move to the source line.
  • "What is a raw type and why avoid it?" A generic type without arguments; it disables checking and pollutes typed collections.
  • "Can you have a List<int>?" No — type arguments must be reference types; use Integer (boxing) or int[].
  • "What is erasure?" Type arguments are compile-time only; at run time List<String> is List.

Key takeaways

  • Generics move type errors from run time (casts) to compile time (the wrong line) and document what collections hold.
  • Vocabulary: type parameter E, type argument String, parameterised type List<String>, raw type List.
  • Reference types only; the diamond <> infers constructor arguments from the target.
  • Raw types are legacy — never in new code except class literals and instanceof.
  • Erasure: generics are for the compiler; run time sees the raw class.

Common questions

What is a raw type in Java?

A raw type is a generic type used without type arguments, such as List instead of List<String>. It compiles only for compatibility with pre-generics code, gives "unchecked" warnings and switches off type checking, so a wrong element can pollute a typed list. Never write one in new code.

Can you have a List<int> in Java?

No. Type arguments must be reference types, so List<int> is a compile error; use List<Integer> and let autoboxing convert, or an int[]. That boxing cost is why IntStream and the primitive-specialised functional interfaces exist.

What is the diamond operator in Java?

The diamond, <>, tells the compiler to infer a constructor's type arguments from the target type: Map<String, List<Integer>> index = new HashMap<>(); compiles since Java 7. With var, put the arguments on the right instead, because var x = new HashMap<>() infers HashMap<Object, Object>.

What is the difference between a type parameter and a type argument?

A type parameter is the placeholder in a generic declaration, such as E in List<E>. A type argument is the actual type supplied when the generic type is used, such as String in List<String>, which makes it a parameterised type.

Why does Java use type erasure for generics?

For backward compatibility. Generics arrived in a language with ten years of compiled libraries and a JVM that knew nothing about them, so type arguments are checked by the compiler and then erased: at run time List<String> and List<Integer> are the same List class.

Exercises

Typed from the start

Read an integer n and n tokens. Put the integers into a List<Integer> and everything else into a List<String> (a token is an integer if matches("-?\\d+")). Print ints=<sum of the integers> and words=<the strings joined by ,> (or words= when there are none), then a third line same class=<whether the two lists' getClass() are ==> to observe erasure.

Example: 4 then 3 a 4 b →

ints=7
words=a,b
same class=true

A typed pair

Write class Pair<A, B> with first(), second(), Pair<B, A> swap() and a toString of (first, second). Read n lines <word> <int>; build a Pair<String, Integer> for each, print it, print its swap(), and add second() to a running total without a cast — the type parameter makes second() an Integer. Finally print total=<sum>.

Example: 2 then ab 3, cd 4 →

(ab, 3) swapped (3, ab)
(cd, 4) swapped (4, cd)
total=7

In this module: Generics

← Checkpoint — Exceptions · Writing generic classes and methods →