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.
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.
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};
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
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.
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();
}
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:
Collection<E>— The root interface for most data containers. It defines methods such asadd(),remove(),contains(), andsize().List<E>— An ordered collection that allows duplicate elements and provides positional access via indexes.Set<E>— A collection that does not allow duplicates and ignores insertion order (unless otherwise specified).Map<K,V>— A structure that stores key–value pairs for efficient lookup and retrieval.Queue<E>andDeque<E>— Interfaces for ordered collections that process elements in specific sequences (FIFO, LIFO, etc.).
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.
List<String> rather than a raw List, you prevent accidental insertion of incompatible types and eliminate the need for casting.
Common collection implementations
| Class | Implements | Characteristics |
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
- Consistency: All collections share common method names and conventions.
- Flexibility: You can change implementations without changing your code’s logic.
- Performance: Each class is optimized for specific use cases, letting you choose the right one for your needs.
- Interoperability: Collections integrate smoothly with streams, iterators, and enhanced for-loops.
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.
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
HashSetstores elements in no particular order and offers fastadd(),remove(), andcontains()operations.LinkedHashSetpreserves the order in which elements were added.TreeSetstores elements in sorted order, according to natural ordering or a provided comparator.
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
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:
HashMapoffers constant-time lookups for most operations but does not preserve order.LinkedHashMapremembers the order of insertion or access, making it suitable for caches or predictable iteration order.TreeMapmaintains its keys in sorted order.
for (String key : ages.keySet()) {
System.out.println(key + " is " + ages.get(key));
}
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:
hasNext()— Returnstrueif there is another element to read.next()— Returns the next element in the sequence.remove()— Removes the last element returned bynext()(optional operation).
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.
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);
}
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());
}
for loop. To safely remove or alter elements during iteration, always use an Iterator instead.