Array Data Structure — Why 33MB Allocations Crashed Prod
A 4K pixel buffer allocates 33MB instantly.
20+ years shipping performance-critical code where algorithms decide the bill. Everything here is grounded in real deployments.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- Array = contiguous memory block holding elements of same type. Address of element i = base_address + (i * element_size) — this is why access is O(1)
- Key components: contiguous storage (memory locality), zero-based indexing (offset from start), fixed size (determined at creation)
- Performance insight: CPU loads 64-byte cache lines — array iteration touches 16 ints per cache miss, unlike pointer-chasing linked structures
- Production trap: Unbounded array allocation from user input — new int[userInput] can cause OutOfMemoryError and DoS the service
- Biggest mistake: Off-by-one in loops (i <= length instead of i < length) — crashes production after processing millions of records
An array is a contiguous block of memory where each element is the same size and type, accessed via an index offset from the base address. That contiguous layout is why array lookups are O(1) — the CPU calculates base_address + (index * element_size) in a single instruction.
The zero-based indexing isn't arbitrary; it's a direct consequence of that formula: the first element is at offset 0. When you allocate an array of 10 million 32-bit integers, you're asking the OS for exactly 40MB of contiguous virtual memory. If that allocation fails — or fragments the heap — your production JVM or Node process crashes with an OutOfMemoryError, not because you ran out of total memory, but because no single free block was large enough.
Arrays are the foundation of nearly every data structure (strings, hash tables, heaps, ring buffers) and the default choice when you need cache-friendly iteration or fixed-size collections. But they're also the sharpest edge in systems programming: no bounds checking in C/C++ means buffer overflows; in managed languages like Java or C#, bounds checks add overhead but prevent memory corruption.
The tradeoff is that resizing an array requires allocating a new block and copying all elements — O(n) work that kills latency if done on the hot path. That's why ArrayList (or std::vector) amortizes resizing by doubling capacity, but still pays the copy cost when it grows.
In the JVM specifically, primitive arrays (int[], byte[]) store values directly in contiguous memory — a new int[10_000_000] is exactly 40MB plus a small header. Object arrays (String[], Integer[]) store references (pointers), not the objects themselves, so the memory is contiguous only for the references; the actual objects are scattered across the heap.
This distinction matters when you're doing 33MB allocations: a primitive array of that size is a single contiguous block, while an object array of the same logical size might require far more memory and cause GC pressure. The crash you hit was likely a primitive array allocation failing because the JVM's heap was fragmented — the allocator couldn't find 33MB of contiguous free space, even though total free memory was higher.
Picture a row of numbered lockers in a school hallway. Each locker has a number on the door starting from zero, and each one holds exactly one item. An array is exactly that — a row of numbered slots in your computer's memory, where each slot holds one piece of data and you can jump directly to any slot by its number. That numbering-from-zero thing trips people up at first, but it'll make perfect sense in a moment.
Every app you've ever used — your music player, your Instagram feed, the leaderboard in your favourite game — is secretly managing lists of data behind the scenes. Someone's playlist is a list of songs. A leaderboard is a list of scores. A shopping cart is a list of items. The moment you need to store and work with a collection of related things in code, you need a data structure — and the array is the simplest, fastest, and most fundamental one that exists. Learning arrays isn't just a box to tick; it's learning the raw material that almost every other data structure is built on top of.
Before arrays existed as a concept, programmers had to create a separate variable for every single item they wanted to store. Want to track scores for ten players? That's ten separate variables. Want to loop over them? You can't — you'd have to write the same code ten times by hand. Arrays solve this by letting you group related items under a single name and access any one of them instantly using a position number. One name, many values, instant access.
By the end you'll know exactly what an array is and why it's stored contiguously in memory, how to declare, populate, and iterate over an array in Java, the rules and limits of arrays and why those rules exist, the most common mistakes that crash production (the off-by-one error), and how to answer array questions confidently in a technical interview.
What an Array Actually Is — Memory, Indexes, and the Zero Rule
Your computer's RAM is basically a very long street of tiny numbered houses — billions of them. Each house stores exactly one byte of data. When your program needs to store something, the operating system hands it a chunk of consecutive houses on that street. An array is simply a reserved block of those consecutive memory slots, all the same size, all sitting right next to each other.
Because the slots are contiguous (back-to-back), the computer can do something brilliant: if it knows where the first slot starts and how big each slot is, it can calculate the address of ANY slot instantly using simple arithmetic. Want slot number 7? Start address + (7 × slot size). Done. No searching, no scanning. This is called O(1) random access — constant time, regardless of how large the array is.
Now, why does indexing start at zero and not one? Because the index isn't really a 'position number' — it's an offset from the start. The first element is zero steps away from the start, so its index is 0. The second is one step away, so its index is 1. Once that clicks, zero-based indexing feels completely natural.
In Java, every array has a fixed size decided at the moment it's created. You're essentially reserving those memory slots in advance. You can't grow or shrink it later — that constraint is the price you pay for the blazing-fast access speed.
package io.thecodeforge.ds; public class ArrayMemoryDemo { public static void main(String[] args) { // Declare and create an array of 5 student scores. // Java reserves 5 consecutive int-sized slots in memory right now. // Each int in Java takes 4 bytes, so this reserves 20 bytes in a row. int[] studentScores = new int[5]; // Assign values to each slot using the index (offset from start). // Index 0 = first slot (0 steps from the start) // Index 4 = last slot (4 steps from the start) studentScores[0] = 91; // first student studentScores[1] = 78; // second student studentScores[2] = 85; // third student studentScores[3] = 62; // fourth student studentScores[4] = 99; // fifth student // Access any element instantly by its index — no looping needed. // The JVM computes: base_address + (3 * 4 bytes) = value at index 3 System.out.println("Score at index 3: " + studentScores[3]); // .length gives us the number of slots (declared size), not the last index. // Last valid index is always length - 1. System.out.println("Total slots reserved: " + studentScores.length); System.out.println("Last valid index : " + (studentScores.length - 1)); // Shorthand: declare and fill in one line using an array literal. // Java counts the values and sets the size automatically (5 here). int[] dailySteps = {4200, 8100, 6750, 9300, 7050}; System.out.println("\nSteps on day 1 (index 0): " + dailySteps[0]); System.out.println("Steps on day 5 (index 4): " + dailySteps[4]); } }
- Index 0: first element. 0 steps from the start address.
- Index length-1: last element. The maximum valid index.
- Index length: DOES NOT EXIST. Accessing it throws ArrayIndexOutOfBoundsException.
- Index -1: DOES NOT EXIST. Negative indexes are invalid in Java (unlike Python).
- Loop condition: always i < array.length. Never i <= array.length.
Worked Example — Array Operations Step by Step
Let arr = [10, 20, 30, 40, 50] (0-indexed, length 5).
- Access arr[2] → read memory at base + 2*sizeof(int) → returns 30. O(1).
- Update arr[2] = 35 → write 35 at that address → arr = [10,20,35,40,50]. O(1).
- Insert 25 at index 2 → shift elements 2..4 one position right → arr = [10,20,25,35,40,50]. O(n) because we moved 3 elements.
- Delete arr[2] (value 25) → shift elements 3..5 one position left → arr = [10,20,35,40,50]. O(n).
- Linear search for 40 → check index 0,1,2,3 → found at index 3. O(n) worst case.
- Binary search for 40 in sorted arr → mid=2 (35), 40>35 → search right half → mid=3 (40) → found. O(log n).
Key insight: arrays pay O(1) for random access but O(n) for insertion/deletion in the middle because elements must be shifted to maintain contiguity.
package io.thecodeforge.ds; import java.util.Arrays; public class ArrayOperationsDemo { public static void main(String[] args) { int[] arr = {10, 20, 30, 40, 50}; // 1. Access by index — O(1) System.out.println("Access arr[2]: " + arr[2]); // 30 // 2. Update by index — O(1) arr[2] = 35; System.out.println("After update : " + Arrays.toString(arr)); // 3. Insert at index 2 — O(n) shift int[] expanded = new int[arr.length + 1]; // Copy elements before insertion point System.arraycopy(arr, 0, expanded, 0, 2); // Place new value expanded[2] = 25; // Shift remaining elements right System.arraycopy(arr, 2, expanded, 3, arr.length - 2); System.out.println("After insert : " + Arrays.toString(expanded)); // 4. Delete at index 2 — O(n) shift int[] contracted = new int[expanded.length - 1]; System.arraycopy(expanded, 0, contracted, 0, 2); System.arraycopy(expanded, 3, contracted, 2, expanded.length - 3); System.out.println("After delete : " + Arrays.toString(contracted)); // 5. Linear search — O(n) int target = 40; int foundIndex = -1; for (int i = 0; i < contracted.length; i++) { if (contracted[i] == target) { foundIndex = i; break; } } System.out.println("Linear search for " + target + ": index " + foundIndex); // 6. Binary search — O(log n) requires sorted array Arrays.sort(contracted); int bsResult = Arrays.binarySearch(contracted, 40); System.out.println("Binary search for 40: index " + bsResult); } }
- O(1) access: address = base + index * element_size. Single CPU instruction.
- O(n) insert: shift all elements right from insertion point. Worst case: insert at index 0.
- O(n) delete: shift all elements left from deletion point. Worst case: delete at index 0.
- O(1) append: only when spare capacity exists. Dynamic arrays amortize this to O(1).
- Binary search: O(log n) but requires sorted array. Sorting is O(n log n) one-time cost.
Reading, Writing, and Looping — Putting Arrays to Work
Storing data in an array is only half the story. The real power shows up when you loop over the array to process every element — calculating a total, finding the maximum, printing a list, or transforming values one by one.
Java gives you two clean ways to loop over an array. The classic for loop gives you the index as well as the value, which is essential when you need to know WHERE in the array you are (for example, to print 'Student 3 scored 85'). The enhanced for-each loop is cleaner when you only care about the values themselves and don't need the index.
Knowing when to use each one is a small but important judgment call. If you need the index, use the classic for loop. If you just need to read every value, use for-each. Never use for-each when you need to write back to the array — it gives you a copy of each value, not direct access to the slot, so any changes you make won't stick.
We'll also look at two classic array problems every developer solves in their first week: finding the sum of all elements and finding the largest value. These two patterns — the accumulator and the running champion — come up constantly.
package io.thecodeforge.ds; public class ArrayIterationPatterns { public static void main(String[] args) { int[] monthlyRainfall = {45, 30, 55, 80, 120, 95, 110, 105, 70, 40, 25, 50}; // ── PATTERN 1: Classic for loop (use when you need the index) ────────── System.out.println("=== Monthly Rainfall Report ==="); String[] monthNames = {"Jan","Feb","Mar","Apr","May","Jun", "Jul","Aug","Sep","Oct","Nov","Dec"}; for (int i = 0; i < monthlyRainfall.length; i++) { System.out.println(monthNames[i] + ": " + monthlyRainfall[i] + " mm"); } // ── PATTERN 2: Enhanced for-each (use when you only need values) ─────── int totalRainfall = 0; for (int rainfall : monthlyRainfall) { totalRainfall += rainfall; } System.out.println("\nTotal annual rainfall: " + totalRainfall + " mm"); System.out.printf("Monthly average : %.1f mm%n", (double) totalRainfall / monthlyRainfall.length); // ── PATTERN 3: Finding the maximum (running champion pattern) ─────────── int peakRainfall = monthlyRainfall[0]; int peakMonth = 0; for (int i = 1; i < monthlyRainfall.length; i++) { if (monthlyRainfall[i] > peakRainfall) { peakRainfall = monthlyRainfall[i]; peakMonth = i; } } System.out.println("\nWettest month: " + monthNames[peakMonth] + " with " + peakRainfall + " mm"); } }
for (int score : scores) { score = score 2; } expecting to double every score in the array, nothing will change. The variable score is a copy, not a reference to the slot. Use a classic indexed for loop if you need to modify elements: for (int i = 0; i < scores.length; i++) { scores[i] = scores[i] 2; }Arrays vs ArrayList — Knowing Which Tool to Reach For
Once you're comfortable with arrays, you'll quickly hit their biggest limitation: the size is fixed. You decide how many elements it holds when you create it, and that's it forever. In real applications — a chat app adding new messages, a shopping cart where items come and go — you often don't know the size in advance.
Java's ArrayList solves this. It's a resizable array under the hood (it literally uses an array internally, and silently creates a new bigger array when it gets full). You trade a tiny bit of performance for the flexibility of a dynamic size.
So when should you pick which one? Use a raw array when you know the exact size upfront and you're optimising for speed and memory — think storing the 12 months of the year, or the RGB values of every pixel in an image (millions of values where memory matters). Use an ArrayList when the size will change at runtime — a list of search results, a queue of orders, user-entered data.
For interview purposes, you'll mostly work with raw arrays because problems are designed to test your logic, not your knowledge of library classes. Understanding how arrays work is the foundation — ArrayList is just a convenient wrapper on top of that same concept.
package io.thecodeforge.ds; import java.util.ArrayList; public class ArrayVsArrayList { public static void main(String[] args) { // ── RAW ARRAY: Fixed size, fast, lightweight ───────────────────────── double[] weeklyTemperatures = {18.5, 21.0, 19.8, 23.4, 25.1, 22.7, 20.3}; System.out.println("=== Fixed-Size Array (Weekly Temperatures) ==="); for (int day = 0; day < weeklyTemperatures.length; day++) { System.out.printf("Day %d: %.1f°C%n", day + 1, weeklyTemperatures[day]); } // ── ARRAYLIST: Dynamic size, flexible ──────────────────────────────── ArrayList<String> shoppingCart = new ArrayList<>(); shoppingCart.add("Apples"); shoppingCart.add("Bread"); shoppingCart.add("Orange Juice"); System.out.println("\n=== Dynamic ArrayList (Shopping Cart) ==="); System.out.println("Cart after adding 3 items: " + shoppingCart); shoppingCart.remove("Bread"); System.out.println("Cart after removing Bread: " + shoppingCart); System.out.println("Number of items in cart : " + shoppingCart.size()); System.out.println("First item in cart: " + shoppingCart.get(0)); } }
- array.length: property. No parentheses. Returns the declared size.
- list.size(): method. With parentheses. Returns the current element count.
- ArrayList<Integer> autoboxes int -> Integer. 4 bytes -> ~16 bytes per element.
- For memory-critical code with millions of elements, use raw int[] not ArrayList<Integer>.
- ArrayList doubles capacity when full. This causes a one-time O(n) copy. Amortized O(1) per add.
Array Memory Layout in the JVM — Primitive vs Object Arrays
Understanding how the JVM stores arrays in memory is the key to understanding their performance characteristics. Every Java array is an object on the heap with a small header (typically 12-16 bytes depending on JVM compressed oops settings), followed by the element data. The .length field is stored in the array header, not as a separate variable.
Primitive arrays (int[], double[], char[]) store the actual values contiguously. Object arrays (String[], Object[]) store references (pointers) to objects elsewhere on the heap. This distinction has massive performance implications: iterating over an int[] touches one contiguous memory region, while iterating over a String[] touches N+1 memory regions (the reference array plus each String object).
An int[1000] allocates one contiguous block: 16 bytes header + 4000 bytes data = 4016 bytes. An Integer[1000] allocates 1001 objects: one array of 1000 references (16 + 8000 = 8016 bytes) plus 1000 Integer objects (~16 bytes each = 16000 bytes). Total: ~24016 bytes. Same logical data, 6x more memory.
package io.thecodeforge.ds; public class ArrayMemoryLayout { public static void main(String[] args) { // Primitive array: values stored directly in the array memory int[] primitiveArray = new int[1_000_000]; // Memory: 16 bytes header + 4 bytes * 1,000,000 = ~4MB contiguous block // Object array: stores REFERENCES to objects, not the objects themselves Integer[] objectArray = new Integer[1_000_000]; for (int i = 0; i < objectArray.length; i++) { objectArray[i] = i; // autoboxing: creates 1M Integer objects on the heap } // Memory: 16 bytes header + 8 bytes * 1,000,000 references = ~8MB for references // PLUS: 1,000,000 * ~16 bytes per Integer object = ~16MB for objects // Total: ~24MB vs ~4MB for primitive array — 6x more memory System.out.println("Primitive array length: " + primitiveArray.length); System.out.println("Object array length : " + objectArray.length); // Demonstrate that object arrays hold references, not values Integer a = 42; Integer[] refs = {a}; a = 99; // reassign the variable System.out.println("\nArray still holds: " + refs[0]); // still 42 — the reference didn't change // Default values differ by type System.out.println("\nDefault values in new arrays:"); System.out.println("int[] default : " + new int[1][0]); // 0 System.out.println("boolean[] default: " + new boolean[1][0]); // false System.out.println("double[] default : " + new double[1][0]); // 0.0 System.out.println("String[] default : " + new String[1][0]); // null } }
- Primitive array: values stored inline. One contiguous memory block. Cache-friendly.
- Object array: references stored inline, objects scattered on heap. Pointer-chasing. Cache-hostile.
- int[1M]: ~4MB. Integer[1M]: ~24MB. 6x memory difference for the same logical data.
- Default values: primitives are 0/false/0.0. Objects are null.
- Array header: 12-16 bytes (JVM-dependent). Contains class pointer, length, and padding.
Array Performance — Why Contiguous Memory Wins (and Burns You)
Arrays get their superpower from one thing: contiguous memory allocation. When you declare int[] metrics = new int[100], the JVM grabs a single block of 400 bytes (assuming 4-byte ints) and hands you the base address. Accessing metrics[47] is a single arithmetic instruction — base + (47 * 4). That's O(1) random access, no chasing pointers, no cache misses.
The flip side? You pay for that speed with rigidity. Inserting or deleting in the middle requires shifting every subsequent element. That's O(n) per operation. In production, I've seen teams naively insert into the front of a 100k-element array and wonder why their API latency spiked. Shift costs are real and measurable.
Cache friendliness is the hidden win. Because elements are adjacent, a CPU cache line fetch pulls in several array elements at once. Looping sequentially over an array can be 10-100x faster than traversing a linked list for the same operation. Your CPU doesn't hate you — but only if you respect memory layout.
// io.thecodeforge — dsa tutorial public class ArrayShiftCost { public static void insertAt(int[] original, int index, int value) { // Shift right from end to index for (int i = original.length - 1; i > index; i--) { original[i] = original[i - 1]; } original[index] = value; } public static void main(String[] args) { int[] readings = new int[6]; for (int i = 0; i < 5; i++) readings[i] = i * 10; insertAt(readings, 2, 999); for (int val : readings) { System.out.print(val + " "); } } }
2D Arrays — When One Dimension Isn't Enough
Real systems don't live on a straight line. Game boards, image pixels, matrix multiplication, seating charts — they all need 2D arrays. In Java, int[][] grid = new int[3][4] creates an array of 3 references, each pointing to an array of 4 ints. That's an array of arrays, not a true rectangular block unless you use a flattened array.
Accessing grid[row][col] is two pointer indirections: first to the row array, then to the element. That's still O(1), but the memory locality isn't as tight as a flat 1D array. For performance-critical code (image processing, scientific computing), flatten to int[] flat = new int[rows cols] and compute index as row cols + col. One memory fetch, one multiplication — cache-friendly and fast.
The gotcha in production: ragged arrays. Java allows each row to have different lengths. That sounds flexible until you ship code that assumes all rows are equal. Your grid algorithm crashes with an ArrayIndexOutOfBoundsException because row 5 has 3 columns and row 6 has 10. Validate your data shape, or use a rectangular 2D array library.
// io.thecodeforge — dsa tutorial public class MatrixFlatten { public static void main(String[] args) { int rows = 3, cols = 4; int[] flatGrid = new int[rows * cols]; // Fill with row-major order for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { flatGrid[r * cols + c] = r * cols + c; } } // Access element at row 2, col 1 int r = 2, c = 1; int value = flatGrid[r * cols + c]; System.out.println("Value at (2,1): " + value); // Print entire flattened matrix for (int i = 0; i < flatGrid.length; i++) { System.out.print(flatGrid[i] + " "); } } }
Declaration – Reserve Your Memory Slot
Declaring an array in Java doesn’t create data—it reserves a name and a type for a future block of contiguous memory. Without initialization, the array variable holds a null reference. Why this matters: you must specify the element type (int, String, etc.) and brackets before the array is usable. The declaration tells the JVM what kind of items will live in that block. A common trap: thinking declaration alone allocates space. It does not. You’ve only named the parking spot; you haven’t built the garage. Always pair declaration with initialization before accessing elements, or you’ll hit NullPointerException at runtime. Keep declarations lean and close to first use for clarity.
// io.thecodeforge — dsa tutorial public class ArrayDeclaration { public static void main(String[] args) { // Declaration only – no memory allocated int[] scores; // Uncommenting next line throws NullPointerException // System.out.println(scores.length); // Declaration + initialization in one step int[] ages = new int[5]; System.out.println(ages.length); // 5 } }
Initialization – Fill the Slots with Values
Initialization is where your array gets its initial values. In Java, you have three clean paths: static initialization (curly braces with values), dynamic initialization (new keyword with size), or anonymous arrays for one-off use. Static initialization sets both size and content in one line—preferred when you know the data upfront. Dynamic initialization fills all slots with default values (0, false, or null) and is best when size is known but values come later. Why choose carefully: static arrays are faster to write but lock in data at compile time; dynamic arrays give runtime flexibility. Always initialize inline to avoid null references and keep intent obvious. Never leave an array half-initialized—it’s a bug waiting to happen.
// io.thecodeforge — dsa tutorial public class ArrayInitialization { public static void main(String[] args) { // Static initialization – size inferred int[] primes = {2, 3, 5, 7, 11}; // Dynamic initialization – all zeros int[] scores = new int[3]; scores[0] = 90; // Anonymous array – no variable name System.out.println(primes[0]); System.out.println(scores[1]); // 0 } }
Basics
An array is a contiguous block of memory that stores multiple values of the same type, accessible by an index. Arrays are fixed-size: once declared, their length cannot change. In Java, every array object knows its length (via the length field) and throws ArrayIndexOutOfBoundsException if you access outside bounds. Arrays start at index 0 because the index represents an offset from the base memory address. For example, arr[0] accesses the first element, arr[1] the second. You declare an array with type and square brackets, like int[] numbers; then allocate space with new int[5]. This reserves 5 contiguous integer slots in memory, each 4 bytes (on most JVMs). Arrays are zero-initialized for primitives (0, 0.0, false) and null for objects. Understanding this foundation is critical because every array operation—reading, writing, resizing—depends on these invariants. Without the basics, advanced patterns like sliding windows or prefix sums will feel shaky.
// io.thecodeforge — dsa tutorial public class ArrayBasics { public static void main(String[] args) { int[] numbers = new int[5]; // allocate: 5 slots numbers[0] = 10; // first slot numbers[1] = 20; // second slot System.out.println("Element at index 0: " + numbers[0]); System.out.println("Element at index 2 (default): " + numbers[2]); System.out.println("Array length: " + numbers.length); // Uncomment next line to see the exception: // System.out.println(numbers[10]); // IndexOutOfBounds } }
index < array.length, not <=.Conclusion 🎯
You now understand arrays from their low-level memory layout to practical looping and comparison with ArrayList. Arrays give you direct, CPU-friendly memory access and predictable performance—but they punish out-of-bounds access and resizing. When you need speed and fixed capacity, choose arrays. When you need dynamic growth, use ArrayList. The zero-index rule isn't arbitrary; it's a memory offset calculation. Work through the examples in your own IDE. Run them. Break them. Fix them. That repetition builds intuition faster than any tutorial. Arrays are your first real taste of how data structures map to hardware. Next topics: linked lists (non-contiguous storage) or hash tables (direct access via keys). For now, practice declaring, initializing, and looping—these patterns appear in algorithms daily.
// io.thecodeforge — dsa tutorial public class ArrayConclusion { public static void main(String[] args) { int[] scores = {85, 92, 78, 95}; int sum = 0; for (int score : scores) { sum += score; } double avg = (double) sum / scores.length; System.out.println("Average score: " + avg); System.out.println("🚀 Happy Coding!"); } }
Image Processing Pipeline Crashes with OutOfMemoryError After Array Allocation Spike
- Array allocation is eager — new int[N] allocates N * element_size bytes immediately and blocks. If N comes from user input, validate it before allocating.
- There is no such thing as a 'lazy array' in Java. The memory is committed the moment you call new. Unbounded user input into array sizes is a denial-of-service vector.
- Buffer pooling (reusable arrays from a pool) caps total memory usage regardless of concurrency. This is how production image processing libraries (ImageIO, TwelveMonkeys) handle large pixel buffers.
- OutOfMemoryError is not always a leak. Massive transient allocations that exceed GC throughput look identical. Check allocation rate, not just retained heap. jstat -gc shows allocation rate.
- Always set a maximum array size for any allocation derived from external input. Make it a named constant, not a magic number.
assert index >= 0 && index < array.length for critical paths.Arrays.toString(). The most common pattern: shift start index or loop direction wrong.jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>jhat -port 7401 /tmp/heap.hprofgrep -n 'ArrayIndexOutOfBounds' /var/log/app/error.log | tail -5awk '/ArrayIndexOutOfBounds/,/^$/' /var/log/app/error.logjstat -gcutil <pid> 1000 10grep -i 'full gc' /var/log/app/gc.log | tail -20jstat -gc to see allocation rate. Pool buffer arrays or stream data in chunks instead of loading entire arrays.grep -n 'System.out.println(Arrays' src/**/*.javaecho 'Arrays.toString(array)' | grep -r --include='*.java'grep -n 'for.*<=' src/**/*.javagrep -n 'for.*>=' src/**/*.javafor (int i = 0; i <= arr.length; i++) is almost always wrong.| Feature / Aspect | Raw Array (int[]) | ArrayList<Integer> | LinkedList |
|---|---|---|---|
| Size | Fixed at creation | Dynamic — grows automatically | Dynamic — grows automatically |
| Access by index | O(1) direct | O(1) via method call | O(n) must traverse nodes |
| Insert at end | O(1) if space, impossible if full | O(1) amortized | O(1) with tail pointer |
| Insert at middle | O(n) shift | O(n) shift | O(1) after reaching position |
| Delete at middle | O(n) shift | O(n) shift | O(1) after reaching position |
| Memory per element (1M elements) | ~4MB (int) | ~24MB (Integer + references) | ~40MB (node + two pointers) |
| Cache locality | Excellent — contiguous | Good — internal array is contiguous | Poor — nodes scattered on heap |
| Can store primitives? | Yes | No — autoboxes to wrapper | No — autoboxes to wrapper |
| Syntax to access | arr[2] | list.get(2) | list.get(2) but O(n) internally |
| Syntax to get size | arr.length | list.size() | list.size() |
| Best use case | Known size, performance-critical, large datasets | Unknown size, general purpose, under 100k elements | Frequent insert/delete at known positions |
| Supported utilities | Arrays.sort(), Arrays.copyOf() | Collections.sort() | Collections.sort() |
| File | Command / Code | Purpose |
|---|---|---|
| io | public class ArrayMemoryDemo { | What an Array Actually Is |
| io | public class ArrayOperationsDemo { | Worked Example |
| io | public class ArrayIterationPatterns { | Reading, Writing, and Looping |
| io | public class ArrayVsArrayList { | Arrays vs ArrayList |
| io | public class ArrayMemoryLayout { | Array Memory Layout in the JVM |
| ArrayShiftCost.java | public class ArrayShiftCost { | Array Performance |
| MatrixFlatten.java | public class MatrixFlatten { | 2D Arrays |
| ArrayDeclaration.java | public class ArrayDeclaration { | Declaration – Reserve Your Memory Slot |
| ArrayInitialization.java | public class ArrayInitialization { | Initialization – Fill the Slots with Values |
| ArrayBasics.java | public class ArrayBasics { | Basics |
| ArrayConclusion.java | public class ArrayConclusion { | Conclusion 🎯 |
Key takeaways
Arrays.equals() compares contents. == compares reference identity. Always use Arrays.equals() for value comparison.Common mistakes to avoid
7 patternsOff-by-one error when looping — i <= array.length instead of i < array.length
length which doesn't exist. The program crashes.i < array.length. The last valid index is always length - 1. Use this pattern every time until it's muscle memory.Confusing array declaration with array creation (null reference)
array[0]. The variable was declared (int[] scores;) but never initialized with new int[size].int[] scores = new int[5]; or use an array literal int[] scores = {90, 85, 78};. Both declare AND create in one step.Trying to modify array elements inside a for-each loop
for (int i = 0; i < scores.length; i++) { scores[i] += 10; }. Verify changes with Arrays.toString(scores) after the loop.Using ArrayList<Integer> for millions of elements when int[] would suffice
Using grid[0].length for all rows in a 2D jagged array
grid[row].length for each individual row in the inner loop. Better: validate uniform row lengths at load time if your data should be rectangular.Allocating arrays from unbounded user input without validation
new int[1_000_000_000] allocates 4GB immediately, exhausting heap.new. Reject or cap sizes that exceed the maximum. Add a metric to monitor array allocation sizes.Assuming array equality compares contents (using == instead of Arrays.equals)
arr1 == arr2 returns false even when both arrays contain the same values. In Java, == compares reference identity, not contents.Arrays.equals(arr1, arr2) for 1D arrays or Arrays.deepEquals(arr1, arr2) for multi-dimensional arrays to compare element values.Practice These on LeetCode
Interview Questions on This Topic
What is the time complexity of accessing an element in an array by index, and why? Can you explain the memory reason behind your answer?
What's the difference between an array and an ArrayList in Java? When would you choose one over the other in a production system?
Given an integer array, how would you find the second-largest element without sorting it? What is the time complexity of your approach?
largest and secondLargest. Iterate through the array once, updating both as you go. For each element, if it's greater than largest, set secondLargest = largest and then largest = current. Else if it's greater than secondLargest and not equal to largest, update secondLargest = current. This is O(n) time and O(1) extra space. Sorting would be O(n log n) and allocate O(n) extra space for the copy, which is worse for this specific question.Why does array insertion in the middle cost O(n) while access costs O(1)? Explain the memory layout trade-off.
What is the difference between a primitive array (int[]) and an object array (Integer[]) in terms of memory layout and performance?
How would you implement a dynamic array (like ArrayList) from scratch using only raw arrays? What is the amortized cost of append?
size variable for the number of elements actually stored. On append, if size == capacity, create a new larger array (commonly 1.5x or 2x capacity), copy all elements using System.arraycopy, then assign the new array. Add the new element and increment size. The amortized cost of append is O(1): if you double capacity on each resize, you copy 1 + 2 + 4 + ... + N/2 + N elements over N appends, which sums to < 2N operations, so average cost per append is O(1). The worst-case cost of a single append (when resize happens) is O(n) because of the copy.Frequently Asked Questions
The index in an array represents the offset (distance) from the starting memory address, not a human-friendly position number. The first element is literally zero steps away from the start, so its offset is 0. This design comes from C and assembly language, and Java inherited it. Once you think of 'index' as 'offset', zero-based counting feels completely logical.
Java throws an ArrayIndexOutOfBoundsException at runtime and your program stops. This happens when you try to access a negative index or an index equal to or greater than the array's length. The fix is always to ensure your loop condition uses i < array.length (strictly less than), and that any direct index access is within the range 0 to length-1.
No — a Java array is strongly typed. When you declare int[] scores, every single slot in that array must hold an int. You cannot mix types. If you need to store mixed types, you could use an Object[] array (since all Java classes extend Object) or, more practically, create a class that wraps the different pieces of data you need and store objects of that class in an array.
Access is O(1) because you just compute an address. Insertion at position i requires shifting all elements from i to the end one slot right to make room — that's O(n) moves in the worst case. Only appending at the end (when there is spare capacity) is O(1).
A static array has a fixed size set at creation. A dynamic array (like Python's list or Java's ArrayList) doubles its capacity when full, copying all elements to a new larger buffer. This copy is O(n) but happens so rarely that the amortized cost per append is still O(1).
Arrays are stored in contiguous memory. The address of index i is simply base_address + i * element_size. This is a single arithmetic instruction (base + offset) that the CPU executes in constant time, regardless of array size. The array doesn't need to be searched — the index tells the CPU exactly where in memory the element lives.
Use arrays when you need frequent random access by index, when memory layout matters for cache efficiency (sequential iteration), or when the size is known upfront. Use linked lists when you need frequent insertions/deletions in the middle without shifting, or when size varies widely at runtime and you can't pre-allocate.
A primitive int[N] uses 16 bytes (header) + 4N bytes (data). For N=1,000,000, that's ~4MB. An Integer[N] uses 16 bytes (header) + 8N bytes (references) + ~16N bytes (Integer objects) = ~24N + 16 bytes total. At N=1,000,000, that is ~24MB — a 6x difference. The exact overhead depends on JVM version and compressed oops settings.
Use Arrays.equals(arr1, arr2) for 1D arrays and Arrays.deepEquals(arr1, arr2) for multi-dimensional arrays. The == operator compares reference identity (whether both variables point to the same object), not contents. Two arrays with identical values but different object references will return false with ==.
20+ years shipping performance-critical code where algorithms decide the bill. Everything here is grounded in real deployments.
That's Arrays & Strings. Mark it forged?
8 min read · try the examples if you haven't