Java Collections, Generics, and Wrapper Classes
A collection holds a group of values. Generics tell the compiler which values belong; wrapper classes let primitive numbers participate. Learn these ideas together, one small program at a time, then write a safe score summary.
Before you start: review arrays, interfaces, and overloading. All runnable examples are complete Java 21 Main.java programs with no input required. Changes run in the existing isolated Java sandbox.
What you will be able to do
- Read a generic collection declaration one part at a time.
- Distinguish a collection interface from its implementation.
- Explain why primitive values use wrapper types in collections.
- Find implicit unboxing and handle missing values deliberately.
- Replace raw collections with compile-time type checks.
- Build a validated, non-mutating score report from a typed list.
1. A collection holds a group, not a single value
Imagine a student saving lesson names as they study. With one String variable, each assignment replaces the previous name. An array can store several names, but its length is fixed once created. An ArrayList can grow as the student adds entries. Choose the tool for the job: arrays are still useful when a fixed-length sequence is exactly what you need.
Read List<String> as ‘a list whose elements are String references’. Each add appends an entry, size tells you how many entries exist, and get(0) reads the first entry. List positions start at zero. The example stores Variables twice on purpose: a list preserves position and allows duplicates; it does not automatically remove repeated values.
Run the program before changing it. Predict what happens if you add a fourth lesson, then inspect the count and first value. Unlike an array’s length field, size() is a method. Reading get(size()) is out of bounds because the last valid index is size() - 1. An empty list has no valid element index at all.
A growing list of lesson names
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
[Variables, Methods, Variables]
Count: 3
First: Variables
2. The left side is a contract; the right side builds the object
In List<String> names = new ArrayList<>(), List describes the operations the variable promises to expose. ArrayList is the concrete class of the created object. You cannot construct new List<>() because List is an interface. The diamond <> asks the compiler to infer the element type from this declaration; it does not mean ‘any type’.
The printFirst helper accepts List<String>, so both an ArrayList and a LinkedList can be passed to it. The helper needs only the List contract, not implementation-specific methods. Its example calls always supply a non-empty list; a reusable helper would need to define what an empty input means before reading index zero.
The Collections Framework is a family, not just ArrayList. List models an ordered sequence, Set models uniqueness, and Map associates keys with values. Map belongs to the framework but is not a subtype of Collection. Collection is an interface; Collections, with an s, is a utility class. Later lessons explore the individual structures and their costs.
One List contract, two implementations
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Java
SQL
3. Generics move a mistake to the compiler
The deliberately raw List in this example leaves off its element type. It accepts a String and then an Integer. The cast on the second retrieval promises something that is not true, so the runtime raises ClassCastException. The catch makes the failure visible without terminating this teaching program; it does not make the design safe.
Replace the declaration with List<String> values = new ArrayList<>(), then remove both String casts. Now values.add(42) is a compile-time error. Delete that insertion or replace it with a genuine String to restore a successful program. Generics let the compiler reject the bad insertion near its source, rather than leaving a distant reader to discover it.
A type parameter describes the element type, not a runtime validation rule for every possible property. List<String> can still contain an empty String, and an ArrayList<String> can contain null. Also, List<Integer> is not a subtype of List<Number>: if it were, a caller could insert a Double into an Integer-only list. Generic classes, bounds, and wildcards come later.
Why a raw list fails too late
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Java
An Integer is not a String.
Try the compiler check yourself
Change the raw declaration to List<String> values = new ArrayList<>();. Keep values.add(42) and press Run code: compilation should fail at that insertion. Replace 42 with a String, remove the casts, and run again. A compiler error here is evidence that the type contract is working.
4. Wrapper types let primitive values enter a generic collection
Generic type arguments must be reference types, so List<int> does not compile. Use Integer for int, Long for long, Double for double, Boolean for boolean, Character for char, Byte for byte, Short for short, and Float for float. These wrappers represent primitive values as objects. They are not interchangeable spellings of the primitive types.
In scores.add(earned), Java performs autoboxing: it converts the primitive int to an Integer reference. When stored + 5 is evaluated, Java unboxes stored to obtain an int for arithmetic. Those conversions are implicit, which makes short programs readable but can hide a null dereference. The original stored score remains 80; arithmetic produces a new value rather than mutating the Integer object.
Boxing is not parsing. Integer.parseInt("42") reads numeric text and returns an int, while Integer.valueOf(42) obtains an Integer for an existing number. Non-numeric text can cause NumberFormatException. Prefer primitive variables for ordinary arithmetic when you do not need a nullable reference or a generic container; wrappers are useful, but not free of tradeoffs.
From int to Integer and back
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Stored: 80
Next: 85
Parsed: 42
5. Missing is different from zero
Suppose an assessment has not been submitted. A score of zero means a recorded result; null can represent no result yet. ArrayList permits that null reference even when its type is List<Integer>. The collection’s generic type tells you the kind of non-null element expected, but does not promise that every slot holds a value.
A loop written for (int score : scores) unboxes each entry before entering the body. If an entry is null, the loop fails before your body can check it. Instead, loop with Integer score, check score == null, and only then do arithmetic. Our example counts missing scores separately so that a missing assessment is not silently reported as an earned zero.
Decide the policy for your application. A progress display might skip missing entries and count them separately, while a published grade report might reject incomplete data. Neither policy is automatic. NullPointerException is not a useful substitute for explaining the rule to the caller. In the lab, incomplete data is deliberately rejected before a total is produced.
Check for null before arithmetic
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Recorded total: 80
Missing: 1
Trace the missing entry
- The first Integer holds 80, so the non-null branch adds it.
- The second entry is null, so only the missing counter changes.
- The reported total is 80, and the missing count is 1.
Changing the null to 0 produces the same total but zero missing entries. Those reports have different meanings.
6. Compare wrapper values deliberately
With two reference operands, == compares whether the references identify the same object. Wrapper caching can make that comparison appear to work for some numbers, so a few successful tests are misleading. Use equals for same-type wrapper value equality, or Objects.equals when either reference may be null. Do not teach yourself a rule from a cache coincidence.
Integer.equals also checks the wrapper type: Integer.valueOf(1000) is not equal to Long.valueOf(1000). If your domain genuinely compares different numeric types, define a conversion policy rather than assuming all Number objects share one equality rule. The example uses the same Integer type for the first comparison and intentionally different types for the final one.
Comparing an Integer to a primitive int with == is different: numeric comparison unboxes the Integer. That can throw when the reference is null. A safe value comparison therefore starts with knowing the operand types, not merely choosing a symbol. Try Objects.equals(null, null), which returns true, and contrast that with calling equals on a null receiver.
Compare values rather than wrapper identity
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Same value: true
Null-safe comparison: false
Different wrapper types: false
7. A List variable does not guarantee an editable list
List.of creates an unmodifiable list and rejects null elements. It is excellent for a small fixed plan. Calling add, remove, or set on that list throws UnsupportedOperationException. The List interface includes optional mutation operations, so a variable declared as List<String> tells you its contract, not whether its current object supports every change.
new ArrayList<>(plan) creates a separate editable container initialized with the plan’s elements. Adding HTML changes that new container, not the original plan. This is a shallow copy: element references are copied, not recursively cloned. Our String elements are immutable, but mutable objects shared between two containers could still reflect the same internal changes.
Arrays.asList is another distinct case: its list has fixed size, normally permits set, and is backed by the supplied reference array. It does not become resizable just because its variable says List. Avoid memorizing ‘all lists behave alike’. State whether your method borrows an input, changes it, copies it, or exposes an unmodifiable result.
Copy a fixed plan into an editable list
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Plan: [Java, SQL]
Editable: [Java, SQL, HTML]
Plan cannot be changed.
8. Wrappers can change which overloaded method is called
List has both remove(int index) and remove(Object value). With a List<Integer>, remove(1) selects the index overload and removes the second entry, not necessarily the number one. Integer.valueOf(10) selects the object overload in the example and removes the first matching value. The static argument type matters even though the list contains numbers.
Trace the list after each statement: [10, 20, 30] becomes [10, 30], then [30]. The index operation returns the removed element; the object operation returns whether a matching element was removed. This is a small example of why understanding primitives, wrappers, and overloading together is more useful than learning them as isolated definitions.
You now have enough background to read typed list code without inventing your own generic class. Keep the next step small: declare the element type, choose an implementation that matches the needed behavior, make null and mutation rules explicit, and use a simple enhanced for loop. The next lesson studies everyday List operations and ArrayList versus LinkedList in more detail.
An int argument is an index, not a value
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
[10, 30]
[30]
Repair exercise: a missing score was unboxed
The program promises a total, but one score is missing. Run it, locate the implicit unboxing, then replace the loop with a deliberate skip-and-count policy. Do not replace missing data with a hidden zero.
Main.java · Find implicit unboxing
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Current result:
Missing score was unboxed.
Reveal the explained repair
The primitive loop variable unboxes before the loop body executes. Using Integer keeps the reference available for a null check. Only the non-null branch performs arithmetic. The repaired report exposes both the known total and missing count; the lab uses a different, explicitly rejecting policy.
Main.java · Keep missing data visible
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Recorded total: 80
Missing: 1
Independent lab: a validated score summary
Build a reusable score summary for a teacher. The input is borrowed, not edited. Every entry must be a present Integer from 0 through 100. An empty list is valid and has total zero. Reject invalid data rather than publishing a partial report.
- Implement summarize(List<Integer> scores) returning a String.
- Reject a null input list with IllegalArgumentException.
- Reject a null, negative, or greater-than-100 entry before producing a result.
- Use a long accumulator, with boxing only at the collection boundary.
- Preserve order, duplicates, and every input entry without mutation.
- Return exactly Count: N; total: T, including the empty-list case.
The starter compiles but prints nothing until you implement and call the helper. Aim for this exact output:
Count: 3; total: 260
Original: [80, 100, 80]
Empty: Count: 0; total: 0
Main.java · Implement the score contract
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Stage 1 hints
Stage 1: choose the contract. Accept List<Integer>, not a raw List. Reject a null list before iteration. Return a String so the caller decides where to display it.
Stage 2 hints
Stage 2: inspect one entry safely. Keep the loop variable Integer. Use short-circuit || to test null before the numeric comparisons that unbox it.
Stage 3 hints
Stage 3: accumulate without editing. Add validated scores to a long, then build the String after the loop. Test both mutable ArrayList and unmodifiable List.of inputs.
Reveal the complete explained solution
Main.java · Validate before reporting
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
The first check protects the list reference. The loop’s null check is evaluated before either numeric comparison, so missing entries are never unboxed. Valid Integer values unbox for addition to a long. No setter, remover, or sorting operation is used. The method borrows its input and returns a new summary only after all entries have passed validation. This is not thread-safe snapshot processing: callers must not concurrently modify the input.
Test your contract
- Empty input → Count: 0; total: 0.
- Single boundary scores 0 and 100 are both valid.
- Duplicates count as separate assessments; [80, 100, 80] totals 260.
- Null list, null entry, -1, and 101 must each be rejected.
- Use an unmodifiable input and verify an editable input is unchanged afterward.
For a missing entry test, create an ArrayList and add null explicitly; List.of(null) rejects null before your helper is called. Wrap an intentional invalid call in try/catch to print its validation message. Do not treat an early factory failure as proof that your own validation works.
Common mistakes and how to diagnose them
List<int> does not compile
Use List<Integer>. Primitive types cannot be generic type arguments. Boxing adapts a primitive value at insertion; it does not change the grammar of a type declaration.
A raw list compiles with warnings
Do not silence the warning and rely on casts. Add the intended type parameter to both the variable and construction, then repair the insertion that no longer compiles.
An unexpected NullPointerException in a loop
Inspect implicit unboxing in primitive loop variables, comparisons, and arithmetic. Read an Integer reference first and apply your missing-data policy before conversion.
Mutation fails despite a List declaration
Inspect the object’s construction. List.of is unmodifiable; Arrays.asList has fixed size; new ArrayList<>(source) is an editable copy. The interface name alone does not guarantee resizability.
The wrong number disappears
Check which remove overload is selected. An int means an index, while an Integer argument can select remove(Object) to remove a value. Make the intended operation explicit.
Two equal numbers compare inconsistently
Reference == is not a wrapper value test. Use equals for a known non-null same-type value, Objects.equals for nullable references, and an explicit conversion policy for different numeric types.
Frequently asked questions
Is ArrayList better than an array?
Not universally. Arrays have fixed length and can store primitives directly. ArrayList provides a resizable list abstraction using reference elements. Choose based on the required operations rather than replacing every array automatically.
Why declare List instead of ArrayList?
List communicates that the caller needs the interface operations. It allows implementations to vary without changing that contract. Declare a concrete type when you genuinely need a concrete-specific operation, not merely because you used its constructor.
Does List<Integer> forbid null?
No. ArrayList<Integer> permits null, while List.of rejects it. Generic typing and null policy are separate concerns. Document and validate the policy your method needs.
Does final make a list unmodifiable?
No. final stops reassignment of the variable; it does not prevent changes through that reference. A final ArrayList can still accept add and set. Unmodifiable containers impose a separate restriction, and do not make mutable elements deeply immutable.
Are boxing and parsing the same?
No. Boxing adapts an existing primitive value to a wrapper reference. Parsing interprets text, which may be invalid. Integer.parseInt returns int; Integer.valueOf can produce Integer from an int or parse a String.
Do I need wildcards now?
No. First become comfortable reading List<String> and List<Integer>, using interface contracts, and recognizing null and mutation policies. The later generic classes and methods lesson introduces bounds and wildcard relationships.
Type the group, then define its rules
A generic type protects element compatibility. The implementation controls storage and supported operations. Wrappers bridge primitive values and reference-based containers, but null and equality still need attention. A complete method contract also says whether data may be missing and whether the caller's list will change.
Before moving on, explain why List<int> fails, where the repair unboxed null, and why summarize works with List.of without editing it. Next: List, ArrayList, and LinkedList operations.
Can you read a typed collection safely?
Answer six questions and read the feedback. Quiz answers are temporary and do not save course progress.
References and related learning
This original SovranCode lesson uses the official Java documentation to verify collection and conversion behavior.
- Java 21 List API: ordering and unmodifiable factories
- Java 21 Integer API
- Java language specification: boxing and unboxing conversions
Review arrays · Previous lesson: I/O streams · Java course roadmap.
Last updated: