Arrays: a row of boxes, fixed at build time
int[] scores = new int[3]; // three slots, all starting at 0
scores[0] = 88;
scores[1] = 92;
scores[2] = 79;
int[] nums = {88, 92, 79}; // same thing, values supplied up front
String[] names = {"Maya", "Sam"};
System.out.println(nums.length); // 3
System.out.println(nums[0]); // 88
Think of an array as a row of mailboxes bolted to a wall. You decide how many when you install it, and after that the count is fixed forever.
An array's length can never change. Want a fourth score? You can't add one. You have to build a new, bigger array and copy everything across. That sounds like an annoying limitation, and it is — which is why the next section exists.
Indexes start at 0, so a 3-element array has valid indexes 0, 1, and 2. Reaching past the end:
System.out.println(nums[3]);
throws ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3. The error message is unusually good — it tells you the bad index and the actual length, which is normally enough to spot the mistake.
Arrays also fill themselves in with defaults: numeric types get 0, boolean gets false, and anything object-shaped — including String — gets null.
String[] names = new String[2];
System.out.println(names[0]); // null
System.out.println(names[0].length()); // NullPointerException
The first line prints the word null without complaint. The second crashes, because there is no object there to call a method on. If you make a String[] and don't fill every slot, expect a null.
Printing an array
import java.util.Arrays;
int[] nums = {3, 1, 4, 1, 5};
Arrays.sort(nums);
System.out.println(Arrays.toString(nums));
Prints:
[1, 1, 3, 4, 5]
Arrays.sort sorts in place — it rearranges nums itself and returns nothing. Writing int[] sorted = Arrays.sort(nums); won't compile.
Now the part that confuses everyone:
System.out.println(nums);
prints something like [I@1b6d3586. That's not a bug and not an error. Arrays never got taught how to describe themselves, so Java falls back to the default description: a type tag ([I = "array of int") plus an @ plus a hash of the object. It's an identity, not contents.
Always use Arrays.toString() to look at an array.
ArrayList: an array that grows
Fixed length is painful, so most real code uses ArrayList instead.
import java.util.ArrayList;
import java.util.List;
List<String> names = new ArrayList<>();
names.add("Maya");
names.add("Sam");
names.add(0, "Ravi"); // insert at a position
System.out.println(names.get(0)); // Ravi
System.out.println(names.size()); // 3
System.out.println(names.contains("Sam")); // true
The list started empty and grew to three as you added. No capacity to declare, no copying.
Note names.size() — not .length, not .length(). That's the third spelling of the same idea, and there's no principle behind the difference. Arrays use a field, String uses a method called length(), collections use a method called size(). Three different historical decisions. Everyone mixes them up; the compiler always catches it.
Changing and removing:
List<String> names = new ArrayList<>();
names.add("Maya");
names.add("Sam");
names.set(0, "Alex"); // replace what's at index 0
System.out.println(names); // [Alex, Sam]
names.remove("Alex"); // remove by value
System.out.println(names); // [Sam]
Unlike arrays, lists print nicely — System.out.println(names) gives you [Sam] directly, because ArrayList does know how to describe itself.
One sharp edge: remove has two versions. remove("Alex") removes by value. remove(0) removes by index. With a List<String> that's unambiguous, but with a List<Integer> it's genuinely dangerous — list.remove(1) removes position 1, while list.remove(Integer.valueOf(1)) removes the number 1.
And of course:
for (String name : names) {
System.out.println(name);
}
The enhanced for from the last lesson works on lists too.
What the angle brackets mean
In List<String>, the <String> part is a generic. It means "this list holds Strings and nothing else."
That's a promise the compiler enforces:
List<String> names = new ArrayList<>();
names.add(123); // won't compile
You get incompatible types: int cannot be converted to String at compile time, rather than a confusing crash later when something tries to treat 123 as text. It also means values come out already typed — no casting needed on the way back.
new ArrayList<>() leaves the brackets empty on the right side. The compiler already knows from the left side, so it fills in the blank. This is called the diamond operator.
Generics can't hold primitive types. List<int> is a compile error. You need List<Integer>:
List<Integer> ages = new ArrayList<>();
ages.add(18); // int goes in, becomes Integer
int first = ages.get(0); // Integer comes out, becomes int
System.out.println(first); // 18
Integer is the wrapper class for int — an object that carries an int inside it. Java converts between them automatically, which is called autoboxing, and it's why the code above looks like it shouldn't need explaining.
HashMap: look things up by name
Where a list finds things by position, a map finds them by a key. It's Java's version of a Python dictionary.
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> scores = new HashMap<>();
scores.put("Maya", 88);
scores.put("Sam", 95);
System.out.println(scores.get("Maya")); // 88
System.out.println(scores.get("Ravi")); // null
System.out.println(scores.getOrDefault("Ravi", 0)); // 0
System.out.println(scores.containsKey("Sam")); // true
Map<String, Integer> means keys are Strings and values are Integers.
Look hard at scores.get("Ravi"). There is no Ravi, and the map doesn't complain — it returns null. That's a design choice with consequences:
int n = scores.get("Ravi"); // NullPointerException
That line crashes, and the stack trace points here — not at the missing put, which might be in a completely different file. The null travels silently until something tries to use it. Reach for getOrDefault whenever a sensible default exists.
Walking a map:
for (Map.Entry<String, Integer> e : scores.entrySet()) {
System.out.println(e.getKey() + ": " + e.getValue());
}
Prints each pair, one per line. entrySet() hands you the key-value pairs; getKey() and getValue() pull them apart.
One thing to expect: HashMap does not keep insertion order. Iterate it twice and you may get different orderings. If order matters, use LinkedHashMap, which is a drop-in replacement that remembers.
Which one should you use?
- Fixed size, primitives, performance really matters → array
- Almost everything else →
ArrayList - You need to look things up by a name or ID →
HashMap
As a beginner, defaulting to ArrayList and HashMap will not steer you wrong.
Ten-second check
For List<String> list, is it list.length or list.size()?
size(). Arrays use .length (a field), String uses .length() (a method), and collections use .size(). Three spellings, no unifying logic, and mixing them up is a daily occurrence even for experienced developers.