Java • Java-Specific Topics

Using Generic Types — Use generic types (List<String>, Map<K,V>) correctly

Generics let you write safer, reusable code by telling Java exactly what type a collection holds.

Java programming emblem with a coffee cup, code braces, and terminal
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

Historical Context & Motivation

Before generics were added to Java, collections like ArrayList and HashMap stored everything as Object. That meant every time you pulled an item out of a list, you had to cast it back to the type you expected. If you guessed wrong, the program would crash at runtime with a ClassCastException. This was a constant source of bugs that were hard to track down because the compiler couldn't warn you in advance.

The Java community recognized that a language feature was needed to let programmers specify the types stored in a collection at compile time. Several proposals competed, and ultimately type parameters—angle-bracket annotations like <String>—won out. This addition fundamentally changed how Java developers write safe, reusable code.

1998 • Java Collections Framework Released

Java 1.2 introduced the Collections Framework (List, Map, Set), but everything was stored as Object with no type safety.

2001 • GJ Proposal & JSR 14

Researchers proposed Generic Java (GJ), and Sun Microsystems accepted JSR 14 to formally add generics to the language specification.

2004 • Java 5 Ships Generics

Java 5 (JDK 1.5) officially introduced generics. Developers could now write List<String> and get compile-time type checking.

2011 • Diamond Operator Added

Java 7 added the diamond operator (<>), reducing boilerplate by letting the compiler infer type arguments on the right side of assignments.

The central question generics answer is: How can we make collections type-safe without writing a separate class for every possible element type? The answer is type parameters, and learning to use them correctly is one of the most practical skills you'll develop in Java.

SECTION 2

Core Principles of Generics

Generics revolve around a few foundational ideas. Once you understand these, every generic class and method in Java will make sense. Think of generics as a contract between you and the compiler: you promise to put only certain types in, and the compiler promises to catch mistakes before the program ever runs.

1

Type Parameters

Letters like T, K, and V are placeholders for actual types. When you write List<String>, you're filling in T with String.
2

Compile-Time Safety

The compiler checks that you only add the correct type to a generic collection. A List<Integer> will refuse to accept a String — the error shows up before you run the program.
3

No Casting Needed

When the compiler knows the type, you don't need explicit casts. Calling list.get(0) on a List<String> directly returns a String.
4

Type Erasure

At runtime, generic type info is removed (erased). A List<String> and a List<Integer> both become plain List. This is a behind-the-scenes detail, but it explains some limitations.
✦

KEY TAKEAWAY

Think of generic types like labeled bins in a warehouse. A bin labeled "Books Only" prevents workers from accidentally tossing in a wrench. The label (the generic type) doesn't change the bin itself—it just tells everyone what belongs inside. Similarly, List<String> is a labeled bin that only accepts String objects, and the compiler acts as the warehouse manager enforcing the rules.

SECTION 3

How Generics Work — A Visual Explanation

The diagram below shows two approaches to storing data in a list: the old, pre-generics way using raw types (left), and the modern, generics-based way (right). Notice how the raw-type list allows any object in, leading to a runtime crash when you pull out the wrong type. The generic list catches the mistake at compile time, before the program ever runs.

Raw Type (Pre-Generics)Generic TypeList rawList = new ArrayList();"Hello"423.14⚠ Mixed types — no safety!List<String> list = new ArrayList<>();"Hello""World""Java"✓ Only Strings allowedRetrieving an element:String s = (String) rawList.get(1);💥 ClassCastException at runtime!Retrieving an element:String s = list.get(1);✓ No cast needed — returns StringRUNTIME ERRORBug discovered when users run the appCOMPILE-TIME ERRORBug caught before the code even runs
The left panel shows how a raw List accepts any object, leading to a runtime crash. The right panel shows how List<String> only allows String values, catching errors at compile time.

In the raw-type list on the left, a String, an Integer, and a Double are all mixed in the same container. When the programmer tries to pull out element 1 and cast it to a String, the program explodes because element 1 is actually 42. On the right, the generic list refuses to accept anything that isn't a String in the first place, so the bug never happens. This is the fundamental value proposition of generics: shift errors from runtime to compile time.

SECTION 4

How Generic Types Work Under the Hood

Generic types in Java are not just syntactic sugar—they follow a specific mechanism called type erasure. When you compile code that uses List<String>, the Java compiler verifies all your types are correct. Then, in the compiled bytecode, the <String> part is removed (erased). This means at runtime, a List<String> is just a List. The compiler secretly inserts the casts for you, but since it already checked everything, those casts are guaranteed to succeed.

Common Type Parameter Conventions

LetterConventionExample
TType (general purpose)class Box<T>
EElement (used by collections)interface List<E>
KKey (used in maps)interface Map<K, V>
VValue (used in maps)interface Map<K, V>
NNumberclass Stats<N extends Number>

Standard naming conventions for generic type parameters in Java

The Diamond Operator

Since Java 7, you can use the diamond operator (<>) to avoid repeating the type on the right side of an assignment. Instead of writing List<String> names = new ArrayList<String>();, you can write List<String> names = new ArrayList<>();. The compiler infers the type from the left side of the equals sign. This reduces clutter without sacrificing type safety.

Primitives vs. Wrapper Classes

An important rule: generics only work with reference types, not primitive types. You cannot write List<int>; you must use the wrapper class List<Integer> instead. Java's autoboxing feature automatically converts between int and Integer for you, so in practice the code looks nearly the same.

SECTION 5

Common Generic Collections in Java

The Java Collections Framework makes heavy use of generics. The three most important generic interfaces you'll encounter are List<E>, Set<E>, and Map<K, V>. Each has its own purpose and common implementations.

Java Generic Collections OverviewList<E>Ordered, allows duplicates[0] "Alice"[1] "Bob"[2] "Alice"↑ Index-based access↑ Duplicates OKSet<E>Unordered, no duplicates "Alice" "Bob" "Carol"↑ No index access↑ No duplicatesMap<K, V>Key-value pairs"Alice" → 95"Bob" → 87"Carol" → 92↑ Keys unique↑ Values may repeatCommon ImplementationsArrayList<E>LinkedList<E>HashSet<E>TreeSet<E>HashMap<K,V>TreeMap<K,V>Each interface has specialized implementations suited to different performance needs.
The three core generic collection interfaces: List<E> (ordered, indexed, allows duplicates), Set<E> (unique elements only), and Map<K, V> (key-value pairs with unique keys). Below each are their most common implementing classes.

List<E> — Ordered and Indexed

A List<E> maintains the order in which elements were added and allows you to access them by index. ArrayList<E> is the most commonly used implementation. You create one with List<String> names = new ArrayList<>(); and use methods like add(), get(index), remove(index), and size().

Map<K, V> — Key-Value Pairs

A Map<K, V> associates keys with values. Each key must be unique. HashMap<K, V> is the most popular implementation. You use put(key, value) to insert entries and get(key) to retrieve values. For example, Map<String, Integer> grades = new HashMap<>(); maps student names to their scores.

SECTION 6

Worked Example — Building a Student Grade Tracker

Let's build a small program that uses a Map<String, List<Integer>> to track multiple test scores for each student. This example combines a Map and a List together, showing how generics nest inside each other.

Student Grade Tracker Using Map<String, List<Integer>>

  1. 1

    Step 1 — Declare the Map

    Create a HashMap where each key is a student name (String) and each value is a list of test scores (List<Integer>).
    Map<String, List<Integer>> gradeBook = new HashMap<>();
  2. 2

    Step 2 — Add a Student with Scores

    Create a new ArrayList<Integer> for the student's scores, add some values, then put the entry into the map.
    List<Integer> aliceScores = new ArrayList<>(); aliceScores.add(95); aliceScores.add(88); aliceScores.add(92); gradeBook.put("Alice", aliceScores);
  3. 3

    Step 3 — Retrieve and Use the Scores

    Use get() on the map to retrieve Alice's list, then loop through her scores. Notice how no casts are needed anywhere—the generics handle everything.
    List<Integer> scores = gradeBook.get("Alice"); for (int score : scores) { System.out.println(score); }
  4. 4

    Step 4 — Calculate the Average

    Loop through the list to sum all scores, then divide by the number of scores. The size() method gives you the count.
    int total = 0; for (int s : scores) { total += s; } double avg = (double) total / scores.size(); // avg = 91.666...
  5. 5

    Step 5 — Observe the Compile-Time Safety

    If you accidentally try aliceScores.add("A+");, the compiler immediately produces an error: incompatible types: String cannot be converted to Integer. The bug is caught before the program runs.
    Compile error: error: incompatible types — generics prevent the mistake!

SECTION 7

Raw Types vs. Generic Types — Strengths and Pitfalls

You might wonder: if the old way worked, why bother with generics? The table below compares the two approaches side by side. While raw types still compile (for backward compatibility), they generate compiler warnings and should be avoided in all new code.

FeatureRaw Type (List)Generic Type (List<String>)
Type checkingNone at compile time; errors appear at runtimeFull compile-time checking; errors caught immediately
CastingManual cast required on every retrievalNo cast needed; compiler handles it
ReadabilityUnclear what the collection containsType is documented right in the declaration
Mixed typesAllows mixing any objects — riskyOnly the declared type is permitted
Compiler warningsGenerates "unchecked" warningsClean compilation with no warnings

Raw types vs. generic types comparison

✦

KEY TAKEAWAY

Using raw types in modern Java is like driving without a seatbelt—the car still moves, but you've disabled a critical safety feature. Generics are the seatbelt: they cost you a few extra characters of code (<String>) and in return they prevent crashes (ClassCastException). Always use generics in new code.
⚠

Common Mistakes to Avoid

1. Using primitives: List<int> won't compile — use List<Integer> instead. 2. Forgetting type parameters: Writing List myList = new ArrayList(); compiles but gives warnings and loses all type safety. 3. Ignoring compiler warnings: "Unchecked" warnings almost always mean you're using a raw type somewhere.

SECTION 8

Looking Ahead — Wildcards and Bounded Types

Once you're comfortable with basic generics like List<String> and Map<String, Integer>, Java offers more advanced features for flexible type constraints. These are concepts you'll explore in AP Computer Science or college-level courses, but it's helpful to know they exist.

ConceptSyntaxWhat It Does
WildcardList<?>Accepts a list of any type — useful for methods that only read, not write.
Upper boundedList<? extends Number>Accepts lists of Number or any subclass (Integer, Double, etc.).
Lower boundedList<? super Integer>Accepts lists of Integer or any superclass (Number, Object).
Generic methods<T> T max(List<T> list)A method that declares its own type parameter, separate from the class.

Advanced generic features preview

For now, the most important takeaway is that basic generic types like List<String> and Map<String, Integer> are the foundation that all these advanced features build upon. Master the basics first, and the advanced concepts will feel like natural extensions. You'll encounter wildcards when you start writing methods that need to accept collections of related types—for example, a method that can print any List of numbers, whether it's List<Integer> or List<Double>.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain in your own words why List<String> is safer than a raw List. What kind of error does it prevent, and when is that error caught?
Show answer
A List<String> tells the compiler that only String objects may be stored in the list. If you accidentally try to add an Integer or any other non-String type, the compiler produces an error before the program runs. A raw List accepts any Object, which means type mismatches aren't discovered until runtime, when a ClassCastException crashes the program. Generics shift this error from runtime to compile time, making the code much safer.

Question 2: BASIC

Write the Java declaration for a HashMap that maps city names (String) to their populations (Integer). Use the diamond operator. Then show how to add the entry "Tokyo" → 14000000 and retrieve it.
Show answer
The declaration uses Map<String, Integer> as the interface type and HashMap<> with the diamond operator: Map<String, Integer> cityPop = new HashMap<>(); To add Tokyo: cityPop.put("Tokyo", 14000000); To retrieve: int pop = cityPop.get("Tokyo"); — autoboxing converts the Integer to int automatically.

Question 3: INTERMEDIATE

You have a List<Double> temps containing daily temperatures. Write a code snippet that iterates through the list, finds the highest temperature, and prints it. What would happen if you accidentally declared it as a raw List and someone added a String to it?
Show answer
With generics: double max = temps.get(0); for (double t : temps) { if (t > max) max = t; } System.out.println(max); This works safely because the compiler guarantees every element is a Double. If you used a raw List and someone added the string "hot", the enhanced for-loop would throw a ClassCastException at runtime when it tries to unbox the String as a double. With generics, the bad add() call would be caught at compile time.

Question 4: APPLIED

Design a data structure using generic collections to represent a school with multiple classes, where each class has a list of student names. Declare the structure using Map and List, add two classes with a few students each, and write code to print all students in "Biology 101".
Show answer
Use a Map<String, List<String>> where the key is the class name and the value is the student list: Map<String, List<String>> school = new HashMap<>(); Create lists: List<String> bio = new ArrayList<>(); bio.add("Alice"); bio.add("Bob"); school.put("Biology 101", bio); Then: List<String> history = new ArrayList<>(); history.add("Carol"); school.put("History 201", history); To print Biology students: for (String name : school.get("Biology 101")) { System.out.println(name); } This works cleanly because both the outer Map and inner List have explicit type parameters, eliminating any need for casts.

Question 5: CRITICAL THINKING

Java uses type erasure, meaning List<String> and List<Integer> become the same class at runtime. Given this, explain why you cannot write if (myList instanceof List<String>) in Java. How does this limitation relate to the design decision behind type erasure? What trade-off did Java's designers accept?
Show answer
Because of type erasure, the generic type information (like <String>) is removed from the bytecode at compilation. At runtime, the JVM only sees a plain List — it has no way to tell whether it was originally a List<String> or a List<Integer>. The instanceof operator works at runtime, so it cannot check erased type parameters. Java's designers chose erasure to maintain backward compatibility — code written before generics (Java 1.4 and earlier) could run alongside generic code without changes to the JVM. The trade-off is that certain runtime checks and generic array creation are impossible. Despite this limitation, compile-time checking still catches the vast majority of type errors, which is where generics provide the most value.

SUMMARY

Summary — Using Generic Types in Java

Summary

Generic types let you specify exactly what kind of data a collection holds, such as List<String> for a list of strings or Map<String, Integer> for a map of names to numbers. By using type parameters (letters like T, K, V), Java shifts type-checking from runtime to compile time, eliminating ClassCastException bugs and removing the need for manual casting.

The key collections to know are List<E> (ordered, indexed, allows duplicates), Set<E> (unique elements), and Map<K, V> (key-value pairs). Use the diamond operator (<>) to reduce boilerplate, always use wrapper classes instead of primitives (e.g., Integer not int), and never use raw types in new code. Mastering these basics prepares you for advanced topics like wildcards and bounded types.

Varsity Tutors • Java • Using Generic Types — Use generic types (List<String>, Map<K,V>) correctly