An array in Java has a fixed size: create it for 10 elements and you can't fit more. In real code the size is usually unknown in advance: there might be three users, or there might be three thousand. That's what collections are for — a set of ready-made data structures that grow, search and store elements on their own. Let's go through the three main kinds and figure out when to use each.
Three collections receive the same three additions. The List stores everything in a row and remembers the order; the Set spots the repeat and silently drops it; the Map replaces the value under the same key. That is why the final sizes differ: 3, 2 and 2.
Why collections are needed
Almost any program accumulates something: a list of orders, a set of unique tags, a mapping "login → user". Writing such structures by hand is slow and error-prone. The Java standard library already has them, battle-tested over the years and fast.
All collections live in the java.util package. At their core are three interfaces: List, Set and Map. An interface describes what a collection can do, while concrete classes (ArrayList, HashMap, etc.) describe how it's done internally.
Short formula: an ordered sequence with possible duplicates — List; a set of unique values — Set; "key → value" pairs — Map.
List — an ordered list
List stores elements in the order you added them and allows duplicates. Any element can be accessed by index (as in an array), starting from zero.
live example
import java.util.ArrayList;
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
List<String> cities = new ArrayList<>();
cities.add("Moscow");
cities.add("Kazan");
cities.add("Moscow"); // duplicates are allowed
System.out.println(cities.get(0)); // Moscow
System.out.println(cities.size()); // 3
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Here List<String> is "a list of strings". The angle brackets are a generic type; they fix that the elements inside are exactly String — more on that in a separate article about generics.
The two main implementations:
- ArrayList — a regular array inside that expands on its own. Use it by default: fast access by index and fast appending to the end. In 95% of cases it's
ArrayList. - LinkedList — a linked list: each element knows its neighbours. Inserting at the front is cheap, but the familiar "at least it inserts into the middle faster" is a myth. To insert into the middle you first have to walk there along the chain of references, and that is slow.
ArrayListshifts the tail of its array with a single machine copy operation and in practice wins almost always. Reach forLinkedListonly when you need a queue open at both ends — and even thenArrayDequeis usually the better choice.
Short formula: when in doubt, use ArrayList.
Set — a set of unique values
Set stores only unique elements: adding the same value again changes nothing. This is handy when you need to remove duplicates or check "is this one already there?".
live example
import java.util.HashSet;
import java.util.Set;
public class SetDemo {
public static void main(String[] args) {
Set<String> tags = new HashSet<>();
tags.add("java");
tags.add("backend");
tags.add("java"); // the repeat is ignored
System.out.println(tags.size()); // 2
System.out.println(tags.contains("java")); // true
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Implementations:
- HashSet — the fastest, but the order of elements is not guaranteed. Use it by default.
- TreeSet — keeps elements sorted (strings alphabetically, numbers in ascending order). Needed when the iteration order matters.
- LinkedHashSet — preserves insertion order. An in-between option.
Map — a "key → value" dictionary
Map stores pairs: a value is fetched instantly by its key. It's like a phone book: you find a number by a name. Keys are unique, values may repeat.
live example
import java.util.HashMap;
import java.util.Map;
public class MapDemo {
public static void main(String[] args) {
Map<String, Integer> ages = new HashMap<>();
ages.put("Anna", 30);
ages.put("Ivan", 25);
ages.put("Anna", 31); // same key — the value is overwritten
System.out.println(ages.get("Anna")); // 31
System.out.println(ages.getOrDefault("Peter", 0)); // 0 — no such key
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Implementations:
- HashMap — fast access by key, order not guaranteed. The default choice.
- TreeMap — keys are kept sorted.
- LinkedHashMap — preserves the insertion order of keys.
getOrDefault is handy to avoid getting null when a key is absent — there are separate articles about handling missing values and Optional.
How to iterate over collections
The most common way is the for-each loop: it reads simply and needs no indices.
live example
import java.util.List;
public class ForEachDemo {
public static void main(String[] args) {
List<String> cities = List.of("Moscow", "Kazan", "Sochi");
for (String city : cities) { // "for each city in cities"
System.out.println(city);
}
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
For a Map, you iterate over the pairs via entrySet():
live example
import java.util.Map;
public class EntriesDemo {
public static void main(String[] args) {
Map<String, Integer> ages = Map.of("Anna", 30, "Ivan", 25);
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Under the hood, for-each uses an iterator (Iterator) — an object that "walks" through the collection element by element. You need it directly in one important case: when you have to remove elements right during iteration. Removing via cities.remove(...) inside a for-each is not allowed — it will throw a ConcurrentModificationException. The correct way is:
live example
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class IteratorDemo {
public static void main(String[] args) {
List<String> cities = new ArrayList<>(List.of("Moscow", "Kazan", "Sochi"));
Iterator<String> it = cities.iterator();
while (it.hasNext()) {
String city = it.next();
if (city.startsWith("K")) {
it.remove(); // safe removal through the iterator
}
}
System.out.println(cities); // [Moscow, Sochi]
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Why equals and hashCode matter
HashSet and HashMap determine the "sameness" of elements and keys not by their reference in memory, but through two methods: equals (whether two objects are equal in meaning) and hashCode (a "fingerprint" number by which the collection quickly finds the right slot). For String and numbers they're already implemented correctly, which is why the examples above work.
But your own class without these methods will behave unexpectedly in a Set/Map:
live example
import java.util.HashSet;
import java.util.Set;
public class PointDemo {
record Point(int x, int y) {} // a record generates equals and hashCode itself
public static void main(String[] args) {
Set<Point> points = new HashSet<>();
points.add(new Point(1, 2));
System.out.println(points.contains(new Point(1, 2))); // true
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
If Point were a regular class without overridden equals/hashCode, the result would be false: two different objects with the same coordinates would be considered different. Rule: if an object is put into a Set or becomes a key of a Map, it must have consistent equals and hashCode. The easiest way to get them for free is to make the type a record; more on that in the articles about OOP and modern Java features.
EnumMap and EnumSet: collections for enum keys
When the key is an enum, a regular HashMap is overkill: there is nothing to hash, and the set of keys is known in advance. EnumMap keeps values in a plain array indexed by the constant's ordinal — faster than HashMap, smaller in memory, and iteration always follows the declaration order of the constants. EnumSet is even more frugal: a set of enum values is packed into a bit mask. The rule is simple: if the key or the element is an enum, take EnumMap/EnumSet, not HashMap/HashSet.
Immutable collections
Sometimes a collection needs to be protected from changes — for example, returned from a method so that the calling code can't spoil it. For this there are the factory methods List.of, Set.of, Map.of (since Java 9):
live example
import java.util.List;
public class ImmutableDemo {
public static void main(String[] args) {
List<String> roles = List.of("admin", "user"); // an immutable list
try {
roles.add("guest");
} catch (UnsupportedOperationException e) {
System.out.println("UnsupportedOperationException: cannot be changed");
}
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Such collections are immutable: an attempt to add or remove anything will throw an exception. This is safer and more expressive — it's immediately clear that the data isn't meant to be changed. If you later need a mutable copy, wrap it: new ArrayList<>(List.of(...)).
In short
- Three base interfaces:
List(order + duplicates),Set(unique values),Map("key → value" pairs). - By default use
ArrayList,HashSet,HashMap— they're fast and cover most tasks. LinkedListis almost never needed:ArrayListis faster even where it should lose in theory.TreeSet/TreeMap— when you need sorting.- Iterate via for-each; remove during iteration only through
Iterator.remove(). - Elements of a
Setand keys of aMapneed consistentequalsandhashCode; the simplest way is arecord. List.of,Set.of,Map.ofcreate immutable collections — handy for protecting data.
What to read next
- OOP in Java — classes, objects, equals/hashCode in more detail.
- How HashMap works inside — buckets, collisions and why hashCode is needed.
- Generics — what
<String>means and why it's needed. - Lambdas and the Stream API — modern collection processing without manual loops.