Collections & Generics
Arrays have a fixed size, and real data rarely does: items go into a shopping cart and come out again, new students join a class, a shop sells more items every hour. The Java Collections Framework in java.util gives you ready-made, growable data structures. The three you will use every day are List, Set and Map.
Collections are built on generics: the <String> in List<String> tells Java what type of element the collection holds, so the compiler can catch mistakes. In this lesson you will use the main collections, then write generic classes and methods of your own.
List: an ordered, growable sequence
A list keeps elements in order, allows duplicates and gives each element an index, like an array that can grow. ArrayList is the standard implementation. Declare the variable with the interface type List and create an ArrayList; the empty <> (called the diamond) lets Java infer the element type.
import java.util.ArrayList;
import java.util.List;
public class Lists {
public static void main(String[] args) {
List<String> cart = new ArrayList<>();
cart.add("Rice 5kg");
cart.add("Toor dal");
cart.add("Ghee");
cart.add(1, "Atta 10kg"); // insert at index 1
System.out.println(cart);
System.out.println("Items: " + cart.size());
System.out.println("Second item: " + cart.get(1));
System.out.println("Has ghee? " + cart.contains("Ghee"));
cart.set(0, "Basmati rice 5kg"); // replace
cart.remove("Toor dal"); // remove by value
System.out.println(cart);
System.out.println("First: " + cart.getFirst() + ", last: " + cart.getLast());
System.out.println("Reversed: " + cart.reversed());
for (String item : cart) {
System.out.println("- " + item);
}
List<Integer> marks = new ArrayList<>(List.of(67, 92, 45, 88));
marks.removeIf(m -> m < 50);
marks.sort(null); // natural order
System.out.println(marks);
List<String> fixed = List.of("Mon", "Tue");
try {
fixed.add("Wed");
} catch (UnsupportedOperationException e) {
System.out.println("List.of gives an unmodifiable list");
}
}
}[Rice 5kg, Atta 10kg, Toor dal, Ghee] Items: 4 Second item: Atta 10kg Has ghee? true [Basmati rice 5kg, Atta 10kg, Ghee] First: Basmati rice 5kg, last: Ghee Reversed: [Ghee, Atta 10kg, Basmati rice 5kg] - Basmati rice 5kg - Atta 10kg - Ghee [67, 88, 92] List.of gives an unmodifiable list
Unlike arrays, collections print nicely by themselves. A few highlights:
getFirst(),getLast()andreversed()come from sequenced collections, added in Java 21. Older code writescart.get(cart.size() - 1)for the last element.removeIf(m -> m < 50)removes every element matching a condition. Them -> m < 50part is a lambda, a short inline function; you will learn lambdas properly in Lambdas & Functional Interfaces.List.of(...)creates an unmodifiable list. It is perfect for fixed data, but adding to it throwsUnsupportedOperationException. Wrap it innew ArrayList<>(...)when you need to change it.
Collections can only hold objects, so a list of numbers is List<Integer>, not List<int> (that gives error: unexpected type ... required: reference, found: int). Java converts between int and Integer automatically, which is called autoboxing.
Set: unique elements
A set never holds duplicates; adding an element that is already present does nothing and returns false. Choose the implementation by the order you need: HashSet (fastest, no predictable order), LinkedHashSet (keeps insertion order) or TreeSet (keeps elements sorted).
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
public class Sets {
public static void main(String[] args) {
String[] visitors = {"Pune", "Kochi", "Jaipur", "Pune", "Agra", "Kochi"};
Set<String> hash = new HashSet<>();
Set<String> linked = new LinkedHashSet<>();
Set<String> tree = new TreeSet<>();
for (String city : visitors) {
hash.add(city);
linked.add(city);
tree.add(city);
}
System.out.println("Unique cities: " + linked.size());
System.out.println("LinkedHashSet: " + linked);
System.out.println("TreeSet: " + tree);
System.out.println("add Pune again? " + linked.add("Pune"));
Set<String> team = new TreeSet<>(Set.of("Aarav", "Isha", "Rohan"));
Set<String> chess = new TreeSet<>(Set.of("Isha", "Meera", "Rohan"));
Set<String> both = new TreeSet<>(team);
both.retainAll(chess);
System.out.println("In both clubs: " + both);
}
}Unique cities: 4 LinkedHashSet: [Pune, Kochi, Jaipur, Agra] TreeSet: [Agra, Jaipur, Kochi, Pune] add Pune again? false In both clubs: [Isha, Rohan]
Sets are ideal for "have I seen this before?" questions, and contains on a HashSet is very fast even with millions of elements. retainAll keeps only elements that are also in another collection (an intersection); addAll gives a union and removeAll a difference. Hash-based collections rely on equals and hashCode, which is why you must override both together in your own classes (or use records).
Map: keys and values
A map stores key-value pairs: a student's roll number and name, an item and its stock. Each key appears once; putting a value for an existing key replaces the old value. HashMap is the everyday choice, LinkedHashMap keeps insertion order and TreeMap keeps keys sorted.
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
public class Maps {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
stock.put("Samosa", 40);
stock.put("Kachori", 25);
stock.put("Jalebi", 60);
stock.put("Samosa", 35); // same key: value replaced
System.out.println("Samosas left: " + stock.get("Samosa"));
System.out.println("Dhokla: " + stock.get("Dhokla"));
System.out.println("Dhokla or 0: " + stock.getOrDefault("Dhokla", 0));
System.out.println("Has jalebi? " + stock.containsKey("Jalebi"));
String[] sales = {"Samosa", "Jalebi", "Samosa", "Kachori", "Samosa"};
Map<String, Integer> sold = new TreeMap<>();
for (String item : sales) {
sold.merge(item, 1, Integer::sum); // add 1, starting from 1
}
System.out.println(sold);
for (Map.Entry<String, Integer> e : sold.entrySet()) {
System.out.println(e.getKey() + " sold " + e.getValue());
}
}
}Samosas left: 35
Dhokla: null
Dhokla or 0: 0
Has jalebi? true
{Jalebi=1, Kachori=1, Samosa=3}
Jalebi sold 1
Kachori sold 1
Samosa sold 3get returns null for a missing key, so prefer getOrDefault when a sensible default exists. merge(item, 1, Integer::sum) is the standard counting trick: if the key is new, store 1; otherwise add 1 to the old value (Integer::sum is a method reference, also explained in the lambdas lesson). To loop over a map, iterate its entrySet(), keySet() or values().
Generics: type-safe code for any type
Generics let you write a class or method once and use it with many types, while the compiler still checks every use. The letter in angle brackets, such as T (type) or E (element), is a type parameter: a placeholder filled in when the class is used.
import java.util.List;
public class Generic {
record Pair<A, B>(A first, B second) {}
static class Box<T> {
private T item;
void put(T item) { this.item = item; }
T get() { return item; }
boolean isEmpty() { return item == null; }
}
static <T> T firstOr(List<T> list, T fallback) {
return list.isEmpty() ? fallback : list.get(0);
}
static <T extends Comparable<T>> T largest(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
static double total(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) {
sum += n.doubleValue();
}
return sum;
}
public static void main(String[] args) {
Box<String> lunch = new Box<>();
lunch.put("Rajma chawal");
String food = lunch.get(); // no cast needed
System.out.println(food + ", empty? " + lunch.isEmpty());
Pair<String, Integer> score = new Pair<>("Ananya", 97);
System.out.println(score.first() + " scored " + score.second());
System.out.println(firstOr(List.of(), "nobody"));
System.out.println(largest(List.of(45, 92, 78)));
System.out.println(largest(List.of("Kochi", "Agra", "Pune")));
System.out.println(total(List.of(10, 20.5, 3L)));
}
}Rajma chawal, empty? false Ananya scored 97 nobody 92 Pune 33.5
Box<T>andPair<A, B>are generic types;Box<String>can only hold strings, andget()returns aStringwith no cast.<T> T firstOr(...)is a generic method: the<T>before the return type declares its own type parameter.<T extends Comparable<T>>is a bounded type parameter:Tmust be comparable, socompareTois allowed. It works for both numbers and strings.List<? extends Number>is a wildcard: a list of any kind of number (Integer,Double,Long), read-only in practice.
The payoff is safety: with List<String> names, writing names.add(42) fails at compile time with error: incompatible types: int cannot be converted to String, instead of crashing later.
Which collection should I use?
| You need | Use |
|---|---|
| An ordered list, duplicates allowed | ArrayList |
| Unique items, fast lookup | HashSet (or LinkedHashSet to keep order) |
| Unique items, always sorted | TreeSet |
| Look up values by a key | HashMap (or LinkedHashMap, TreeMap) |
| A fixed list or map that never changes | List.of(...), Set.of(...), Map.of(...) |
Common mistakes
List<int>. Use the wrapper type:List<Integer>.- The
removetrap on integer lists.list.remove(2)removes the element at index 2; to remove the value 2, writelist.remove(Integer.valueOf(2)). - Changing a list while looping over it with for-each. This usually throws
ConcurrentModificationException. UseremoveIfinstead. - Adding to
List.of(...). It is unmodifiable; copy it into anArrayListfirst. - Forgetting that
map.getcan returnnull. UsegetOrDefaultorcontainsKey.
Key points
List(ArrayList) is ordered and allows duplicates;Set(HashSet,LinkedHashSet,TreeSet) keeps unique elements;Map(HashMap,TreeMap) stores key-value pairs.- Declare variables with interface types:
List<String> cart = new ArrayList<>();. - Java 21 sequenced collections add
getFirst,getLastandreversed. List.of,Set.ofandMap.ofcreate unmodifiable collections.- Generics (
<T>) give compile-time type safety; bounds (T extends Comparable<T>) and wildcards (? extends Number) control which types are allowed.
Exercise
A school canteen recorded orders as "Aarav:Samosa", "Isha:Dosa", "Aarav:Tea", "Meera:Dosa", "Isha:Tea", "Aarav:Samosa". Write a program that builds a TreeMap<String, List<String>> of each student's items, a TreeSet<String> of all different items sold, and a TreeMap<String, Integer> counting each item. Print all three.
Show solution
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
public class Canteen {
public static void main(String[] args) {
String[] orders = {"Aarav:Samosa", "Isha:Dosa", "Aarav:Tea",
"Meera:Dosa", "Isha:Tea", "Aarav:Samosa"};
Map<String, List<String>> byStudent = new TreeMap<>();
Set<String> items = new TreeSet<>();
Map<String, Integer> counts = new TreeMap<>();
for (String order : orders) {
String[] parts = order.split(":");
String student = parts[0];
String item = parts[1];
byStudent.computeIfAbsent(student, k -> new ArrayList<>()).add(item);
items.add(item);
counts.merge(item, 1, Integer::sum);
}
System.out.println(byStudent);
System.out.println(items);
System.out.println(counts);
}
}{Aarav=[Samosa, Tea, Samosa], Isha=[Dosa, Tea], Meera=[Dosa]}
[Dosa, Samosa, Tea]
{Dosa=2, Samosa=2, Tea=2}computeIfAbsent creates an empty list the first time a student appears and returns the existing list after that, so .add(item) always works. A TreeMap and TreeSet keep the output sorted.
Quick Quiz
1. Which collection automatically removes duplicates?
A Set holds each element at most once.
2. What does map.put("Samosa", 35) do if "Samosa" is already a key?
Keys are unique; putting an existing key replaces its value.
3. Why does List<int> not compile?
Type arguments must be classes; Java autoboxes int values into Integer objects.
4. What does <T extends Comparable<T>> mean in a method?
It is a bounded type parameter: only comparable types are allowed, so compareTo can be called.
Learn Java with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
