Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Fundamentals › Java

Generics

What does the <String> in List<String> do?

The <String> in List<String> is a marker that tells the compiler, "this list holds only String." With this marker, the compiler blocks any line that tries to add something that is not a String, and it also knows that what you take out of the list is a String, so it lets you store it straight into a String variable. Without the marker, the list accepts any object, and the compiler does not know what you take out. So you must write (String) yourself every time you take something out, and a wrong item is not found when you compile but only at run time, at the moment you take it out. In this lab you change <String>, the value you add, and (String), then compile and run the same code, and you see what the compiler reports and what is printed.

Checked against real execution · Eight combinations were compiled with javax.tools on JDK 21, run, and checked

Concepts in this lab

Generics
This feature lets a class or method take the type it works with as a parameter. When you specify a type, as in List<String>, the type is checked at compile time, which prevents the mistake of adding a value of the wrong type and removes the need for casts. The type information is erased after compilation (type erasure), so at run time List<String> and List<Integer> cannot be told apart.
Type argument (<String>)
A type argument is the type that you write inside angle brackets <> after the name of a generic class or interface, as in List<String>. List<String> means "a List that holds String," and a type formed by attaching type arguments in this way is called a parameterized type. The compiler uses this marker to check that the value you add with add is a String, and it treats the value you take out with get as a String, so you do not have to write (String) by hand when you take it out. Only reference types can be used as type arguments, so List<int> is an error, and you must write List<Integer>. If you leave out the type argument and write only List, it becomes a raw type.
Raw type
This is a generic class, one that lets you write a type as in List<String>, written as just List, without <...>. It is a form that was kept so that code from before Java 5, when there were no generics, can still be used. The compiler cannot check what is added through a raw type variable and gives only an unchecked warning, so even code that adds a String to the list that a List<Integer> refers to compiles. That mistake shows up later as a ClassCastException at the place where the value is taken out. It is not used in newly written code, and when the element type is unknown, List<?> is used.
ClassCastException
ClassCastException is an exception that occurs at run time when you try to convert an object to a type that the object is not. When you write a cast such as (String) names.get(0), the compiler checks only that the types are related in a way that allows the conversion and lets it through, and the JVM checks at run time whether the object you took out really is that type. If the object you took out is an Integer and you try to convert it with (String), this exception occurs, and if no code handles it, the program stops on that line. What is typical is that it shows up not on the line that added the wrong object but on the line that takes it out and converts it. When you use the type argument of generics, the compiler blocks the line that adds the wrong item in advance, so you meet this exception less often.

Try these

Running with the default settings

  1. The compiler reads the source from the top. On the line List<String> names = new ArrayList<>();, it reads the <String> after List and remembers, "names holds only String." <String> is a marker that tells the compiler the type of what this list holds.
  2. The compiler checks this line: names.add("Bob"); The "Bob" you are adding is a String, and names is a list that holds only String, so it passes.
  3. The compiler checks this line: String first = names.get(0); names is supposed to hold only String, so the compiler knows that what you take out is a String. So it is fine to store it straight in the String variable first. You do not need to write (String).
  4. Compilation succeeds and the Main class file to run is created.
  5. You run it with java Main. main starts, and new ArrayList creates an empty list and makes names refer to it.
  6. The line names.add("Bob"); runs. "Bob" is added to the list.
  7. The line String first = names.get(0); runs. It takes "Bob" out of the list and stores it in first.
  8. The line System.out.println(first.length()); runs. first is "Bob", and length() returns the number of characters, 3, so it prints 3.

Result of this run

Code

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // <String>: a marker that this list holds only String
        List<String> names = new ArrayList<>();
        names.add("Bob");
        String first = names.get(0);
        System.out.println(first.length());
    }
}

Assumptions of this model

The compiler and runtime are JDK 21.0.12.1 (javac 21), and compilation used the default options, with no warning options turned on. For all eight combinations, the source that is the same as the code on the screen (identical text line by line, including the import lines) was compiled with javax.tools, and for those that compiled, the class file was loaded with a new class loader and Main.main was run, and the results were checked against the standard output and exceptions that were captured. What was checked is whether compilation succeeded, the diagnostic code and English message of the first error (the "int cannot be converted to String" and "Object cannot be converted to String" of compiler.err.prob.found.req were restated as the two error descriptions on the screen), the number of errors and the line where each error occurred, whether the Main class file was created, the notes the compiler left other than errors, the output when run, and the kind, message, and line of the exception at run time. The facts that the step sentences rely on were also run separately and checked. A line that adds 123 to a list without <String> passes without an exception even at run time, 1 item is held in the list, and ClassCastException occurred only on the (String) line that takes it out (message: Integer cannot be cast to String). Even without <String>, a line that stores what you take out in an Object variable compiles, and a line that calls length() on what you take out without a cast does not compile. When <String> is present, you can call length() directly on what you take out. With the default options, the combinations that contain a raw type (both those that compiled and those that had an error) left not an error but two lines of notes (that an unsafe operation is used, and that you should recompile with -Xlint:unchecked), and they did not appear as warnings. When compiled separately with the warning options -Xlint:unchecked and -Xlint:rawtypes turned on, a raw type warning occurred on the declaration line that uses List and ArrayList without <>, an unchecked call warning occurred on the names.add line, and there was no warning on the line that takes out with (String). The combination with <String> and also (String) compiled with the default options without even a note, and with -Xlint:all turned on, a warning about an unnecessary cast occurred. The error of adding 123 to a list with <String> was reported by javac in a shortened form (note: Some messages have been simplified), and when recompiled with -Xdiags:verbose, it was a compiler.err.cant.apply.symbols error saying that no method matches add(int). The documentation was opened with WebFetch and checked. I followed the Java Language Specification (Java SE 21), Section 4.5 and Section 4.5.1 (a generic class with type arguments attached, such as List<String>, is a parameterized type, and a type argument must be a reference type, so List<int> cannot be used), Section 4.8 (a name without <> is a raw type, which is allowed only for compatibility with old code, and some accesses to the members of a raw type give an unchecked warning), Sections 5.5 and 5.1.6 (a conversion between reference types that needs a check examines the class of the object at run time and may throw ClassCastException if it is wrong), and the Java Tutorials "Why Use Generics?" (generics give stronger checks at compile time and remove the casts when taking out). The pages that were opened came back as summarized answers, so I could not read all of the original text directly, and jls-4.html was read only up to the first 100000 characters (Sections 4.5 and 4.8 were within that). That 123 is added to the list as an Integer object was not checked by opening Section 5.1.7 (boxing) of the specification; it was observed in the exception message at run time. The kind of error, the number of errors, the line where it occurs, and whether a note is left are not defined by the specification; they were observed on this javac. The code on the screen has the form of one class with only main. There are ways to get around the compiler's check even with <String>, and this lab did not cover writing your own generic classes or methods, wildcards, or the fact that <String> does not remain at run time (type erasure). They are covered in the in-depth lab "Generics".

Previous lab: Exceptions · Next lab: List and Map

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these