Arrays have a fixed length. Once you create new int[10], it holds exactly ten slots — forever. Most real programs do not know in advance how many things they will hold: a shopping cart grows as the shopper browses, a leaderboard shrinks as scores are checked, a log fills until the day ends. For all of that, Java ships the Collections framework — a set of ready-made, resizable containers. The two you will reach for constantly are List, an ordered sequence you can grow and shrink, and Map, a key-to-value lookup. Together they replace almost every hand-rolled data structure.
List, ArrayList, and generics
A List keeps items in the order you added them and lets you fetch any one by position. ArrayList is the everyday implementation — it grows automatically as you add. The angle brackets declare what it holds: List<String> reads as a list of strings. That bracketed type is a generic, and it is not decoration. Try fruits.add(42) on a List<String> and the compiler refuses before the program ever runs, sparing you a broken cast later. Generics move a whole family of bug from runtime into the build.
flowchart LR COL["Collections"] --> L["List (ordered, by index)"] COL --> M["Map (key to value)"] L --> LA["ArrayList"] M --> HM["HashMap"] style COL fill:#c2410c,color:#fff style L fill:#c2410c,color:#fff
Why generics exist: life without them
Before generics arrived, a List held Object — anything at all. You could add a String and an Integer to the same list, and the compiler would not object. The price came at read time: to use an item you had to cast it back, as in String s = (String) list.get(0), and a wrong cast blew up at runtime with a ClassCastException. Generics let the compiler prove the cast is safe at build time, so the running program simply never contains that failure. They trade a little typing for an entire class of crash removed.
flowchart TD L["List<String> fruits"] -->|"add(apple)"| OK["accepted"] L -->|"add(42)"| ERR["compile error<br/>before the program runs"] style OK fill:#3776ab,color:#fff style ERR fill:#b45309,color:#fff
Reading and changing a List
A handful of methods cover most of what you do. add(x) appends to the end; get(i) returns the element at position i; size() is the count; remove(i) deletes the element at i and closes the gap; contains(x) reports membership. Positions are zero-based: the first element is get(0), so a three-item list has valid indices 0, 1, 2. Asking for get(3) on that list throws IndexOutOfBoundsException — the single most common collections mistake for newcomers.
flowchart LR L["List: apple, banana, cherry"] --> I0["get(0) = apple"] L --> I1["get(1) = banana"] L --> I2["get(2) = cherry"] L -.->|"get(3)"| OOB["IndexOutOfBounds"] style L fill:#c2410c,color:#fff style OOB fill:#b45309,color:#fff
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("cherry");
for (String f : fruits) {
System.out.println("- " + f);
}
System.out.println("Total items: " + fruits.size());
System.out.println("First item: " + fruits.get(0));
}
}
Walking a collection: for-each
To visit every item without managing an index, use the for-each loop: for (String f : fruits) { ... }. Read the colon as in — for each String f in fruits. The loop runs the body once per element, handing you each in turn, and it works identically over a List, an array, or a Set. It is the safe default: because you never touch an index, you cannot write an off-by-one error or accidentally read past the end of the collection.
Map: lookups by key
A Map pairs each key with one value, like a dictionary where you look up a word to get its definition. prices.put("apple", 2) stores 2 under the key "apple"; prices.get("apple") fetches it back. Put the same key twice and the second value overwrites the first. Ask for a key that was never stored and get returns null — not an error — which is why containsKey exists when you need to tell absent apart from legitimately stored as null.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
stock.put("drill", 3);
stock.put("hammer", 5);
System.out.println("drills: " + stock.get("drill"));
System.out.println("has saw: " + stock.containsKey("saw"));
System.out.println("saw count: " + stock.get("saw"));
stock.put("drill", 4); // overwrites the old value
System.out.println("drills now: " + stock.get("drill"));
}
}
Choosing between them
Reach for a List when position and order matter — a queue of tasks, a sequence of steps, the top ten scores. You reach items by where they sit. Reach for a Map when you need to look something up by name — a product by its SKU, a user by their id, a country by its code. You reach items by what key they live under. If you ever catch yourself scanning a list to find the one matching entry, you almost certainly wanted a Map instead.
What does this program print? Read it carefully and type the exact output (five lines).
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("cherry");
for (String f : fruits) {
System.out.println("- " + f);
}
Map<String, Integer> prices = new HashMap<>();
prices.put("apple", 2);
prices.put("banana", 3);
prices.put("cherry", 5);
System.out.println("Total items: " + fruits.size());
System.out.println("Banana price: " + prices.get("banana"));
}
}
An
ArrayListkeeps insertion order, so the fruits print as added: apple, banana, cherry.fruits.size()is the count of items — three.prices.get("banana")looks up the value stored under that key —3.
A list holds [apple, banana, cherry]. What does get(2) return, and what does get(3) do?
List indices are zero-based, so index 2 is the third item (cherry). A three-item list has valid indices 0, 1, 2 — index 3 is past the end and throws IndexOutOfBoundsException.
List operations. Trace the add, get, size, remove, and contains calls and predict all four lines.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> todo = new ArrayList<>();
todo.add("write");
todo.add("test");
todo.add("ship");
System.out.println("first: " + todo.get(0));
System.out.println("count: " + todo.size());
todo.remove(1);
System.out.println("after remove: " + todo.get(1));
System.out.println("has write: " + todo.contains("write"));
}
}
get(0)is the first item,write;size()is3.remove(1)deletes the item at index 1 (test), leaving[write, ship].After that removal,
get(1)is nowship, andcontains("write")istrue.
Map lookups. A key that was never stored and a key that is overwritten — predict all four lines.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
stock.put("drill", 3);
stock.put("hammer", 5);
System.out.println("drills: " + stock.get("drill"));
System.out.println("has saw: " + stock.containsKey("saw"));
System.out.println("saw count: " + stock.get("saw"));
stock.put("drill", 4);
System.out.println("drills now: " + stock.get("drill"));
}
}
get("drill")returns the stored value3."saw"was never put in, socontainsKeyisfalseandgetreturnsnull.Putting
drillagain overwrites3with4, so the last line reads4.
What is the point of writing List<String> instead of just List?
The <String> is a generic: it tells the compiler the list holds only strings. Try to add an int and you get a compile error — long before the program runs.
Recap
Listis an ordered, resizable sequence;ArrayListis the common implementation. Useadd,get,size,remove,contains.- Indices are zero-based; the last valid index is
size() - 1, and going past it throws. Mapstores key-to-value pairs;getreturnsnullfor a missing key,containsKeytests membership, and putting an existing key overwrites it.- Declare variables as the interface (
List,Map) and only name the concrete class atnew. - A
HashMap's iteration order is undefined — never depend on it.
Next you will control which code runs and how often, with conditionals and loops.