Jagged Arrays in Java — NPE from Null Rows
NullPointerException in adjacency list on node 4: row null.
- Jagged arrays allow each row to have a different length.
- Java 2D arrays are arrays of arrays — each row is a separate object.
- Allocate rows individually:
new int[rows][]then fill each row. - Never use
array[0].lengthfor all rows — usearray[i].length. - Empty rows: prefer
new int[0]over null to avoid NPE. - Best for: adjacency lists, Pascal's triangle, per-user data.
Picture a cinema with different numbers of seats on each row — the front row might have 4 seats, the middle rows 10 each, and the back row only 6. A regular 2D array forces every row to have the same number of seats, like a perfect rectangle. A jagged array is that cinema: each row decides its own length. That's it — it's just an array of arrays where the inner arrays can be different sizes.
Most real-world data isn't rectangular. A student might have three exam scores while another has five. A graph node might connect to two neighbours while another connects to twenty. When you force unequal data into a rectangular 2D array, you waste memory filling the empty slots with zeros or nulls — and worse, you lie about your data's shape in a way that confuses everyone who reads your code later.
Jagged arrays — also called ragged arrays — solve this by letting you declare an outer array of a fixed size and then independently allocate each inner array to exactly the length it needs. Java supports this natively because of how it models multidimensional arrays: a 2D array in Java is literally an array whose elements are references to other arrays. That means you can swap those inner arrays for ones of any size you like, with no hacks required.
By the end of this article you'll know exactly when to reach for a jagged array over a rectangular one, how to declare, initialise, and iterate them safely, and the two runtime traps that catch intermediate developers off guard. You'll also have a mental model solid enough to answer the interview questions that come up whenever 2D arrays are on the table.
Why Java's 2D Arrays Are Already Jagged Under the Hood
Java doesn't have true multidimensional arrays the way C or Fortran do. When you write int[][] grid = new int[3][4], Java actually allocates one array of three elements, where each element is a reference pointing to a separate int array of length four. The JVM stores those three inner arrays at whatever memory addresses it likes — they're not contiguous.
This is the key insight: because each row is an independent object on the heap, you can replace any of those row references with an array of a completely different length. The outer array just holds references — it doesn't care what's on the other end.
Rectangular 2D arrays are really just the special case where you happened to give every row the same length at initialisation time. Jagged arrays take that flexibility and make it explicit and intentional.
Understanding this reference model also explains why grid[0] returns an int[] — you can call .length on it, pass it to a method expecting an array, or reassign it entirely. Each row has a full identity as its own array.
int[][] scores as a filing cabinet (outer array) full of folders (inner arrays). Each folder can hold as many pages as you want. The cabinet just holds the folders — it doesn't dictate page count.Declaring and Initialising Jagged Arrays the Right Way
There are two patterns for creating jagged arrays in Java: allocate-then-fill, and inline initialisation. Each suits a different scenario.
The allocate-then-fill pattern is what you'll use when the row sizes are computed at runtime — reading from a file, responding to user input, or building a graph from a database. You declare the outer array with a fixed size, then loop over it and allocate each inner array individually.
Inline initialisation works when you know the data at compile time. It's terser but less flexible. Both are idiomatic Java — pick based on whether your sizes are known ahead of time.
One thing to be deliberate about: always allocate the inner arrays before you try to write to them. Forgetting this is the single most common cause of NullPointerException with jagged arrays, and it's an easy trap because the outer array exists and has a valid .length even when all its slots are null.
int[][] data = new int[5][], every data[i] is null. Calling data[0].length before assigning a row throws a NullPointerException. Always allocate rows before accessing them.new int[n][] gives you null rows, not empty rows.Iterating Jagged Arrays Safely — and a Real-World Use Case
The cardinal rule of iterating a jagged array: never use the first row's length as the column count for all rows. That assumption is exactly what makes code brittle when row sizes differ.
The safe pattern is to ask each row for its own .length on every iteration. This works whether rows are uniform or wildly different in size. The enhanced for-each loop naturally enforces this because it drives off the actual elements in each row.
The example below builds a simplified adjacency list — one of the most common real-world uses for jagged arrays. In graph theory, each node has a different number of neighbours. Storing that in a rectangular array wastes enormous amounts of space and makes the code actively misleading. A jagged array maps directly to the data's natural shape.
{} is far better than null for a row with no elements. It means your loops still work without null checks — length is just 0. Assign new int[0] rather than leaving a row as null when a node has no neighbours.adjacencyList[0].length for all rows caused a production outage for a graph service.row.length (or adjacencyList[i].length) in inner loops.Jagged Arrays vs Rectangular Arrays — Choosing the Right Tool
Neither structure is universally better — the question is whether your data is inherently rectangular or inherently ragged. Using the wrong one adds either wasted memory (rectangular for ragged data) or unnecessary complexity (jagged for naturally rectangular data).
Rectangular arrays have one real advantage: predictable access patterns are cache-friendly at the hardware level, and the code is simpler to reason about when all rows genuinely do have the same length. Image pixels, game boards, and mathematical matrices are genuinely rectangular — use int[rows][cols] for those.
Jagged arrays win when row sizes vary by design: adjacency lists, Pascal's triangle, per-user permission sets, time-series data where each sensor has a different sample count. The code more honestly reflects the data, and you never waste memory on padding.
The comparison table below summarises the key practical differences so you can make the call quickly.
Performance Characteristics and When Jagged Arrays Hurt
Jagged arrays save memory when row sizes vary, but they introduce non-contiguous memory access. Each row is a separate heap object, so iterating across all elements can cause poor cache locality compared to a flat 1D array. In Java, even rectangular 2D arrays have non-contiguous rows (they are arrays of arrays), so jagged arrays don't make cache behaviour worse than rectangular ones—but they can be worse than a single flat 1D array. If your algorithm frequently accesses elements across different rows, consider storing data in a 1D array with manual index calculation for better cache performance. The real performance danger is the overhead of many small array objects: each int[] adds object header overhead (typically 16–24 bytes per row). For thousands of rows with very few elements each, this overhead can dominate memory usage. A flat 1D array avoids both the heap fragmentation and the per-row overhead. But for most applications, the memory savings from not storing zeros far outweigh the overhead. Only optimise to flat arrays when profiling shows that jagged array access is a bottleneck.
int[1000] uses only the data and one object header. For high-performance numeric code, flat arrays usually win.NullPointerException in Graph Processing Pipeline
new int[n][] allocates all rows with empty arrays, so the adjacency list was used without null checks. They didn't account that rows for nodes with no connections were left as null.int[][] graph = new int[n][] and only rows for nodes with connections were assigned graph[node] = new int[...]. Nodes with zero connections remained null. When iterating graph[node].length on that node, it threw NPE.new int[0]. Or, during construction, always assign graph[node] = new int[0] initially and then replace if connections exist.- Never leave a jagged array row as null when it logically represents an empty list.
- Always initialize all rows, even those that are empty, to avoid silent production failures.
- Use
new int[0]for empty rows — it's a valid array that iterates cleanly.
Arrays.toString() or print data[i] to see if null.data[0].length instead of data[row].length. Verify inner loop bound.new int[0] or allocate it properly: if (data[row] == null) data[row] = new int[0];Key takeaways
array[i] returns a full int[] object you can call .length on.new int[n][] gives you an outer array full of null referencesrow.length or jaggedArray[i].lengthjaggedArray[0].length. That assumption is a hidden time bomb when row sizes differ.new int[0]) over null for rows with no data. Zero-length arrays iterate cleanly without null checks, keeping your loop logic simple and safe.Common mistakes to avoid
3 patternsAccessing a row before allocating it
data[i] = new int[size]; before any read/write.Using the first row's length as the column count for all rows
jaggedArray[row].length in the inner loop, not jaggedArray[0].length.Leaving a row as null instead of an empty array
new int[0] (or new String[0]) for empty rows, so loops handle them gracefully.Interview Questions on This Topic
What is a jagged array in Java, and how does Java's memory model make it possible to have rows of different lengths in a 2D array?
int[][] arr = new int[3][], you get an outer array with three null slots. You then assign each slot an independently sized int[]. The outer array stores references to those arrays, so each row can be a different length. This is fundamentally different from languages like C where a 2D array is a contiguous block of memory. In Java, even a 'rectangular' array like new int[3][4] is really an array of three references to three separate int arrays of length 4.Frequently Asked Questions
That's Arrays. Mark it forged?
4 min read · try the examples if you haven't