Arrays in Java are fundamental building blocks for data storage, but they also have a notable limitation: fixed size. Once declared, you cannot expand or shrink them on the fly. In practical scenarios, we often need a container that can grow or shrink as items are inserted or removed—like storing file lines, dynamic lists of game entities, or user inputs of unpredictable length.
The ArrayList class provides precisely this flexibility. It is part of Java’s java.util package and serves as one of the most widely used data structures for in-memory collections of objects. In this lesson, we will explore:
An ArrayList<T> is a List implementation designed to store objects of type T. You create it by specifying the element type in angle brackets, for example, ArrayList<String>. This approach enforces compile-time type checks, preventing accidental mixing of types.
Null Items vs. Null ArrayList: Although names.add(null) is valid (though uncommon). If the names object itself is null, any method call triggers a NullPointerException.
In addition to appending with add(...), ArrayList also supports positional inserts, as well as multiple ways to remove elements. We’ll organize them below:
Object Removal Ambiguities: When a list contains duplicates, remove("test") only takes out the first match. You can loop or employ alternative methods to remove all occurrences if needed.
Shifting Elements: Insertions or removals in the middle of an ArrayList can be inefficient for large lists because the rest of the elements must shift. For large-scale data scenarios, a linked structure might outperform ArrayList at random inserts. However, for small to medium lists, ArrayList usually performs adequately.
Concurrent Modification: A "concurrent modification" happens when you try to change a list at the same time you’re reading through it. For example, if you use a for-each loop to go through a list and try to remove items during that loop, Java will throw a ConcurrentModificationException. This is because the for-each loop needs the list to stay unchanged while it’s iterating - removing items partway through would make Java lose track of where it is in the list. (We’ll learn safe ways to remove items using iterators in later sections.)
Using "==" Instead of equals: When dealing with objects, contains depends on equals. If you haven’t overridden equals in your custom class, the default behavior might be unexpected.
ArrayList exclusively stores **objects**. But what if you need a list of primitive int? Java’s type system forces you to use a wrapper class such as Integer to achieve this.
Fortunately, Java seamlessly converts between int and Integer whenever you invoke list.add(5) or retrieve an element. This mechanism is called auto-boxing and unboxing.
auto-boxing: The process of wrapping (or "boxing up") a primitive value inside its corresponding wrapper object - like putting an int value into an Integer "box" or a double into a Double "box". Java does this automatically when needed.
auto-unboxing: The reverse process: taking the primitive value back out of its wrapper object "box" (e.g., extracting the int from an Integer). Java handles this automatically too.
Performance & Pitfalls: Creating wrapper objects and boxing/unboxing values requires extra memory and processing time. In tight loops or with large datasets, these small costs can add up significantly. For better performance with large amounts of numeric data, consider using IntStream or arrays of primitives instead.
Beyond fundamental operations (add, remove, get, contains), ArrayList provides several other helpful utilities. We’ll group them here as the "miscellaneous yet important" category.
subList(from, to): Returns a view of a specified portion of the list (from index from up to to - 1). Changes in the sublist will reflect in the original list, so caution is advised.
sort(Comparator): In Java 8 or later, you can call list.sort(Comparator.naturalOrder()) or provide a custom comparator for advanced sorting needs. (Alternatively, use Collections.sort(list).)
Sublist’s Link: fruits.subList(...) creates a "window" into the original list - it doesn’t create a new independent list. Think of it like looking through a window into a room - if you rearrange furniture (modify elements) through the window, the room itself changes. Any modifications made through the sublist (adding, removing, or changing elements) will directly affect the original list at those positions. If you need a separate, independent copy that can be modified without affecting the original, use new ArrayList<>(subList) to create a new list with the same elements.
Sorting: Sorting means arranging elements in a specific order (like alphabetical order for text, or numerical order for numbers). When you call list.sort(null), Java will try to sort the elements in their "natural" order - for example, text will be sorted alphabetically, and numbers from smallest to largest. However, for this to work, the elements must know how to be compared to each other. Built-in types like String and Integer already know how to be compared, but for custom objects you’ll need to teach Java how to compare them. We’ll learn more about this when we cover the Comparable interface in a later chapter.
In this lesson, we examined the fundamentals of ArrayList, starting with creation and basic additions, moving on to iteration, searching, and specialized methods. This knowledge will be invaluable in any project that needs dynamic lists of objects. Keep these key takeaways in mind:
Flexibility vs. Performance: ArrayList is a reliable default, but be mindful that frequent insertions or removals in the middle can be costly. For smaller lists, it’s usually fine.
Mastering ArrayList paves the way for exploring more advanced collections in Java—such as LinkedList, HashMap, or TreeSet. If you keep in mind how to handle insertions/removals, iteration, and various edge cases, you’ll find ArrayList an indispensable data structure for day-to-day development in Java.
You have ArrayList<String> tasks = new ArrayList<>() with elements {"Wash dishes", "Go running", "Buy milk"}. If you call tasks.add(1, "Pay bills"), then tasks.remove("Buy milk"), what is the final content of tasks?
Not quite—remember we remove "Buy milk" after adding "Pay bills".
{"Pay bills", "Wash dishes", "Go running"}
No. Inserting at index 1 puts "Pay bills" between "Wash dishes" and "Go running."
{"Wash dishes", "Pay bills", "Go running"}
Correct! After insertion at index 1, the list is {"Wash dishes", "Pay bills", "Go running", "Buy milk"}. Removing "Buy milk" leaves {"Wash dishes", "Pay bills", "Go running"}.
{"Wash dishes", "Go running"}
No. That would happen if we removed "Pay bills" instead of "Buy milk."
Consider you have ArrayList<String> fruits = new ArrayList<>(List.of("Orange", "Apple", "Banana")); and you call fruits.sort(null);. Which statement is true regarding the result?
Why might removing elements from an ArrayList inside a for-each loop lead to a ConcurrentModificationException, and what is one strategy to remove items safely in such situations?
In terms of performance, when might a linked-based collection (like LinkedList) be more appropriate than ArrayList? Give a scenario based on what you learned.