<< Home

Chapter 10: Arrays and Collections

Programs often need to manage groups of related values rather than single variables. Whether storing a list of numbers, a set of objects, or a mapping between keys and values, Java provides structured ways to organize and manipulate data efficiently. These are the arrays and collections of the Java language and standard library.

Arrays are the simplest form of data grouping in Java. They hold a fixed number of elements of the same type and provide fast, direct access to each element by index. Arrays are ideal for predictable, compact storage when the size of the data is known in advance. However, because they cannot grow or shrink dynamically, more flexible tools are often needed for real-world applications.

To handle dynamic data structures, Java introduces the Collections Framework, a unified system of interfaces and classes for storing and manipulating groups of objects. It includes familiar structures such as ArrayList, HashSet, and HashMap, each designed for specific use cases like ordered lists, unique sets, or key–value pairs. Collections bring together performance, consistency, and ease of use under a common API that works seamlessly with modern Java features like generics, streams, and lambda expressions.

💡 The Collections Framework is built on a small set of core interfaces (such as List, Set, and Map) that define how groups of data behave. Once you learn these interfaces, you can work fluently with any concrete collection type in Java.

This chapter introduces arrays and the core collection types, showing how to create, populate, and iterate over them. You will learn when to use each structure, how to choose between them for efficiency, and how generics make type-safe collections possible without repetitive code. Together, arrays and collections form the backbone of most Java programs, allowing you to organize information clearly and process it at scale.

⚠️ Arrays and collections behave differently when it comes to mutability, resizing, and type safety. Understanding these distinctions early will prevent common errors, such as index out-of-bounds exceptions or unintended data sharing between references.

Array creation and initialization

An array in Java is a fixed-size sequence of elements, all of which share the same type. Arrays provide indexed access to their contents, beginning at index 0 and extending to length - 1. Because their size is fixed when created, arrays are efficient for storing known quantities of data but cannot grow or shrink dynamically after initialization.

You declare an array by specifying the type of its elements followed by square brackets, and then create it using the new keyword:

int[] numbers = new int[5];
String[] names = new String[3];

These examples create arrays capable of holding five integers and three strings respectively. When an array is created, all elements are automatically initialized to default values based on their type: 0 for numeric types, false for booleans, and null for object references.

System.out.println(numbers[0]); // 0
System.out.println(names[0]);   // null

Individual elements are accessed using their index, and can be assigned values just like ordinary variables:

numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;

System.out.println(numbers[1]); // 20

Alternatively, arrays can be initialized with predefined values using an array initializer. This syntax both declares and fills the array in one step:

int[] primes = {2, 3, 5, 7, 11};
String[] colors = {"red", "green", "blue"};

When you use an initializer, Java automatically determines the array’s length from the number of provided elements. You can also separate the declaration and initialization if preferred:

int[] scores;
scores = new int[] {90, 80, 70, 60};
💡 You can obtain the number of elements in any array using its length property, not a method call. For example, numbers.length returns the array size, while String.length() is a method that returns the number of characters in a string.

Attempting to access an element outside the valid index range will cause a java.lang.ArrayIndexOutOfBoundsException. This is a runtime error that immediately stops program execution, so index checks are important when iterating over arrays.

int[] data = {1, 2, 3};
System.out.println(data[3]); // Error: index out of bounds
⚠️ Array indices always start at 0. The last valid index is array.length - 1. Off-by-one errors are among the most common bugs when working with arrays.

Multidimensional arrays

Java supports arrays of arrays, known as multidimensional arrays. These allow you to represent structured data such as matrices, grids, or tables. Although they are sometimes called “two-dimensional arrays,” in Java they are actually arrays whose elements are themselves arrays, which provides flexibility but also means that rows can have different lengths if needed.

💡 Multidimensional arrrays in Java are actually arrays of references, not a single block of memory. This means that each row can be created, replaced, or resized independently, offering flexibility not found in lower-level languages.

The most common form is the two-dimensional array, which you can declare and create like this:

int[][] matrix = new int[3][4];

This creates an array of three rows and four columns. All elements are initialized to 0. You can assign or access values using two indices, the first for the row and the second for the column:

matrix[0][0] = 10;
matrix[1][2] = 25;

System.out.println(matrix[1][2]);  // 25

You can also initialize multidimensional arrays directly with nested braces:

int[][] numbers = {
  {1, 2, 3},
  {4, 5, 6},
  {7, 8, 9}
};

Each inner brace defines a row. The outer array contains three elements (rows), each of which is an array of three integers. You can mix different lengths for inner arrays to create a jagged array:

int[][] jagged = {
  {1, 2, 3},
  {4, 5},
  {6, 7, 8, 9}
};

In this case, jagged[0].length is 3, jagged[1].length is 2, and so on. This flexibility can be useful for representing irregular data sets.

Iterating over multidimensional arrays

You can use nested loops to iterate through all elements of a multidimensional array. The outer loop handles rows, and the inner loop handles columns:

for (int i = 0; i < matrix.length; i++) {
  for (int j = 0; j < matrix[i].length; j++) {
    System.out.print(matrix[i][j] + " ");
  }
  System.out.println();
}

Alternatively, the enhanced for-each loop provides a simpler and more readable form when you do not need explicit indices:

for (int[] row : numbers) {
  for (int value : row) {
    System.out.print(value + " ");
  }
  System.out.println();
}
⚠️ When iterating over multidimensional arrays, always use the actual length of each subarray (array[i].length) rather than assuming all rows have the same number of elements. Failing to do so can cause ArrayIndexOutOfBoundsException.

Collections Framework overview

The Java Collections Framework is a unified architecture for storing and manipulating groups of objects. It provides a set of interfaces and classes that make it easy to manage dynamic data, such as lists of items, sets of unique values, queues, and mappings between keys and values. Unlike arrays, collections can grow and shrink automatically as elements are added or removed, offering flexibility without manual resizing or index management.

At the heart of the framework are a few core interfaces that define common operations, and a number of classes that implement them in different ways for performance and behavior. The main interfaces include:

These interfaces form a hierarchy under java.util, which all concrete collection classes follow. This design allows you to switch between implementations (such as from ArrayList to LinkedList) without rewriting surrounding code, since both share the same interface.

import java.util.*;

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");

for (String name : names) {
  System.out.println(name);
}

This example works the same way if you change ArrayList to LinkedList, because both implement List<E>. The uniform API makes it easy to focus on logic rather than data structure details.

💡 The Collections Framework uses generics to ensure type safety. By declaring a collection as List<String> rather than a raw List, you prevent accidental insertion of incompatible types and eliminate the need for casting.

Common collection implementations

ClassImplementsCharacteristics
ArrayList<E>List<E>Resizable array, fast random access, slower insertions and deletions in the middle.
LinkedList<E>List<E>, Deque<E>Efficient insertions and deletions; slower random access.
HashSet<E>Set<E>Unordered, unique elements; fast add, remove, and lookup.
LinkedHashSet<E>Set<E>Preserves insertion order while maintaining uniqueness.
TreeSet<E>Set<E>, SortedSet<E>Automatically sorts elements; slower operations due to tree structure.
HashMap<K,V>Map<K,V>Unordered key–value mapping; fast lookups and updates.
LinkedHashMap<K,V>Map<K,V>Preserves insertion order of keys.
TreeMap<K,V>SortedMap<K,V>Maintains keys in sorted order.

Key benefits of the Collections Framework

⚠️ The Collection interfaces only work with objects, not primitive types. To store primitives efficiently, use their wrapper classes (for example, Integer instead of int) or specialized collections from libraries such as java.util.stream or third-party frameworks.

Lists, Sets, and Maps

The three most widely used types of collections in Java are lists, sets, and maps. Each serves a distinct purpose and offers a different way to organize data. Understanding their differences is essential for choosing the right structure for your needs.

Lists

A List<E> is an ordered collection that allows duplicate elements. Each element has a defined position (index) starting at 0, and the order of elements is preserved as they are added. Lists are ideal when you need sequential access or when duplicates matte. For example, for storing items in the order they were entered or maintaining a history of actions.

The two most common implementations are ArrayList and LinkedList:

import java.util.*;

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
names.add("Alice"); // duplicates allowed

System.out.println(names.get(1)); // Bob

for (String name : names) {
  System.out.println(name);
}

ArrayList uses an internal array, offering fast random access (get()) but slower insertions or deletions in the middle of the list. LinkedList uses linked nodes, making it efficient for frequent insertions and removals but slower for indexed access.

💡 Use ArrayList for most cases where read speed is more important than insertion speed. Use LinkedList when you expect to insert or remove many elements in the middle of the list.

Sets

A Set<E> is a collection that does not allow duplicate elements. Sets are useful when you only care about whether an item exists, not how many times it appears. They also ignore insertion order unless you use an implementation that preserves it.

The main set implementations are HashSet, LinkedHashSet, and TreeSet:

import java.util.*;

Set<String> colors = new HashSet<>();
colors.add("red");
colors.add("green");
colors.add("blue");
colors.add("red"); // ignored, already present

System.out.println(colors.size()); // 3
System.out.println(colors.contains("green")); // true
💡 Use TreeSet when you need sorted, unique data. Otherwise, HashSet is faster for general-purpose use.

Maps

A Map<K,V> associates unique keys with corresponding values. Maps are not technically part of the Collection hierarchy but are a fundamental part of the Collections Framework. You can think of them as dictionaries or lookup tables, where each key maps to exactly one value.

import java.util.*;

Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Charlie", 35);

System.out.println(ages.get("Bob")); // 25
System.out.println(ages.containsKey("Alice")); // true
💡 Use a Map when you need to associate identifiers or keys with related data, such as usernames and passwords or product IDs and prices. The key lookup pattern makes access both clear and efficient.

Keys in a map must be unique. If you insert a new value using an existing key, the previous value is replaced. The three main map implementations are HashMap, LinkedHashMap, and TreeMap:

for (String key : ages.keySet()) {
  System.out.println(key + " is " + ages.get(key));
}
⚠️ Unlike lists or sets, maps use put() and get() instead of add(). Forgetting this distinction is a common source of confusion when switching between collection types.

Iterators and enhanced for loops

Java provides several ways to step through the elements of a collection. The most flexible is the Iterator interface, which works with any type that implements Collection<E>. For simpler, read-only traversal, the enhanced for loop (also called the for-each loop) offers a cleaner and more concise syntax. Understanding both approaches will help you navigate and manipulate collections effectively.

Using an Iterator

An Iterator provides sequential access to the elements of a collection without exposing its internal structure. It supports three key methods:

import java.util.*;

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");

Iterator<String> it = names.iterator();

while (it.hasNext()) {
  String name = it.next();
  System.out.println(name);
}

The iterator automatically handles the details of collection traversal. When the end is reached, hasNext() returns false and the loop exits. Calling next() after that point will throw a NoSuchElementException.

💡 Iterators are especially useful when you need to remove elements during iteration, since doing so directly from the collection (for example, with remove() in a for loop) will cause a ConcurrentModificationException.
Iterator<String> it = names.iterator();
while (it.hasNext()) {
  String name = it.next();
  if (name.startsWith("B")) {
    it.remove();  // safe removal during iteration
  }
}

Enhanced for loops

The enhanced for loop offers a simpler, more readable way to iterate through arrays and collections when you do not need to modify their structure. It automatically retrieves each element in order and works with any class that implements the Iterable<E> interface.

List<Integer> numbers = List.of(10, 20, 30, 40);

for (int n : numbers) {
  System.out.println(n);
}

This form eliminates the need to call iterator() or use index variables, reducing the risk of off-by-one errors and improving readability. The enhanced for loop can also be used with arrays:

int[] scores = {90, 80, 70};

for (int s : scores) {
  System.out.println(s);
}
💡 Use the enhanced for loop for read-only traversal. If you need to add, remove, or replace elements, use an explicit iterator instead.

Iterating over maps

Maps do not directly implement Iterable because they store key–value pairs rather than single elements. However, you can iterate over their entries, keys, or values through specific views returned by methods like entrySet(), keySet(), and values().

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 92);

// Iterate through keys
for (String name : scores.keySet()) {
  System.out.println(name);
}

// Iterate through values
for (int score : scores.values()) {
  System.out.println(score);
}

// Iterate through key–value pairs
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
  System.out.println(entry.getKey() + ": " + entry.getValue());
}
⚠️ Avoid modifying a collection while iterating over it with the enhanced for loop. To safely remove or alter elements during iteration, always use an Iterator instead.