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.
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.
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
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. It wins when there are frequent insertions and removals at the start or middle, but access by index is slow (you have to "walk" to the element). Rarely needed.
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?".
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
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.
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
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.
List<String> cities = List.of("Moscow", "Kazan", "Sochi");
for (String city : cities) { // "for each city in cities"
System.out.println(city);
}
For a Map, you iterate over the pairs via entrySet():
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());
}
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:
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
}
}
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:
record Point(int x, int y) {} // a record generates equals and hashCode itself
Set<Point> points = new HashSet<>();
points.add(new Point(1, 2));
System.out.println(points.contains(new Point(1, 2))); // true
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.
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):
List<String> roles = List.of("admin", "user"); // an immutable list
// roles.add("guest"); // would throw UnsupportedOperationException
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. LinkedList— for frequent insertions in the middle;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.
- Generics — what
<String>means and why it's needed. - Lambdas and the Stream API — modern collection processing without manual loops.