Jagged Arrays in Java — NPE from Null Rows
NullPointerException in adjacency list on node 4: row null.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- 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.
A jagged array in Java is an array of arrays where each sub-array can have a different length. Unlike languages with true multidimensional arrays (like C# or Fortran), Java's 2D arrays are always jagged under the hood — int[][] is literally an array of references to int[] objects.
This means you can have rows of varying sizes, which is useful for triangular matrices, sparse data structures, or any dataset where row lengths aren't uniform. The catch: each sub-array must be explicitly initialized, or you'll get a NullPointerException when trying to access a null row.
This is a common pitfall — declaring int[][] arr = new int[5][]; gives you five null references, not five empty arrays. You must then initialize each row individually, e.g., arr[i] = new int[i+1];. Jagged arrays shine when memory efficiency matters and row sizes vary significantly, but they hurt performance in tight loops due to pointer chasing and poor cache locality compared to a flat, rectangular array.
For uniform data, a single int[] with manual index calculation is almost always faster and simpler.
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.
What Jagged Arrays Actually Are in Java
A jagged array in Java is an array of arrays where each sub-array can have a different length. Unlike C# or C++, Java's 2D arrays are not rectangular by default — they are arrays of references to arrays. This means int[][] matrix = new int[3][]; creates three null rows. You must explicitly initialize each row: matrix[0] = new int[5]; matrix[1] = new int[10];. The JVM stores the outer array as a contiguous block of references; each inner array is a separate heap object with its own length field. Accessing matrix[i][j] involves two pointer dereferences and two bounds checks — one for the outer array, one for the inner. This structure is not cache-friendly for rectangular data because rows are scattered in memory. Use jagged arrays when rows have inherently different sizes — for example, storing a triangular matrix, adjacency lists in graph algorithms, or variable-length records in a memory-constrained system. They save space compared to a rectangular array padded with zeros, but the access pattern can hurt performance in tight loops.
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.
package io.thecodeforge.jaggedarrays; public class ArrayMemoryModel { public static void main(String[] args) { // A standard rectangular 2D array — looks uniform, but each row // is a separate int[] object allocated on the heap. int[][] rectangle = new int[3][4]; // We can prove rows are independent objects by checking each row's length. System.out.println("Rectangular array — all rows report the same length:"); for (int rowIndex = 0; rowIndex < rectangle.length; rowIndex++) { System.out.println(" rectangle[" + rowIndex + "].length = " + rectangle[rowIndex].length); } // Now replace row 1 entirely with a shorter array. // This is legal because rectangle[1] is just a reference. rectangle[1] = new int[2]; // row 1 now has only 2 columns System.out.println("\nAfter replacing row 1 with a 2-element array:"); for (int rowIndex = 0; rowIndex < rectangle.length; rowIndex++) { System.out.println(" rectangle[" + rowIndex + "].length = " + rectangle[rowIndex].length); } // This confirms: 2D arrays in Java are arrays of array references, // not a single flat memory block. } }
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.
package io.thecodeforge.jaggedarrays; public class JaggedArrayInit { public static void main(String[] args) { // ── Pattern 1: Allocate-then-fill ───────────────────────────────── // Useful when row sizes come from data you read at runtime. // Imagine storing the number of goals scored by each player // across a variable number of matches they participated in. int numberOfPlayers = 4; int[][] playerGoals = new int[numberOfPlayers][]; // outer array only — rows are null! // Each player played a different number of matches. int[] matchesPlayed = {3, 5, 2, 4}; for (int player = 0; player < numberOfPlayers; player++) { // Allocate each row to exactly the size this player needs. playerGoals[player] = new int[matchesPlayed[player]]; } // Populate with sample goal data. playerGoals[0] = new int[]{1, 0, 2}; // Player 0: 3 matches playerGoals[1] = new int[]{0, 1, 1, 0, 3}; // Player 1: 5 matches playerGoals[2] = new int[]{2, 1}; // Player 2: 2 matches playerGoals[3] = new int[]{0, 0, 1, 2}; // Player 3: 4 matches System.out.println("── Player Goal Records (Allocate-then-fill) ──"); for (int player = 0; player < playerGoals.length; player++) { System.out.print("Player " + player + " (" + playerGoals[player].length + " matches): "); for (int goals : playerGoals[player]) { System.out.print(goals + " "); } System.out.println(); } // ── Pattern 2: Inline initialisation ───────────────────────────── // Clean and readable when data is known at compile time. // Great for things like Pascal's triangle rows or lookup tables. String[][] weeklySchedule = { {"Standup", "Code Review"}, // Monday {"Standup"}, // Tuesday — short day {"Standup", "Architecture Meeting", "1-on-1"}, // Wednesday {"Standup", "Demo Prep"}, // Thursday {"Standup", "Sprint Review", "Retrospective"} // Friday }; String[] dayNames = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"}; System.out.println("\n── Weekly Meeting Schedule (Inline init) ──"); for (int day = 0; day < weeklySchedule.length; day++) { System.out.println(dayNames[day] + " (" + weeklySchedule[day].length + " meetings):"); for (String meeting : weeklySchedule[day]) { System.out.println(" - " + meeting); } } } }
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.
package io.thecodeforge.jaggedarrays; public class GraphAdjacencyList { // Computes the total number of directed connections across all nodes. static int countTotalEdges(int[][] adjacencyList) { int totalEdges = 0; // Use adjacencyList[node].length — NOT a fixed column count. for (int node = 0; node < adjacencyList.length; node++) { totalEdges += adjacencyList[node].length; } return totalEdges; } // Checks whether a direct connection exists from sourceNode to targetNode. static boolean hasEdge(int[][] adjacencyList, int sourceNode, int targetNode) { // Guard against asking about nodes that don't exist. if (sourceNode >= adjacencyList.length) return false; for (int neighbour : adjacencyList[sourceNode]) { // safe: iterates actual row if (neighbour == targetNode) return true; } return false; } public static void main(String[] args) { // 5 nodes (0–4). Each node connects to a different number of neighbours. // Node 0 → connects to 1, 2 // Node 1 → connects to 3 // Node 2 → connects to 0, 3, 4 // Node 3 → connects to 4 // Node 4 → no outgoing connections int[][] cityConnections = { {1, 2}, // node 0 has 2 neighbours {3}, // node 1 has 1 neighbour {0, 3, 4}, // node 2 has 3 neighbours {4}, // node 3 has 1 neighbour {} // node 4 has 0 neighbours (empty, not null!) }; System.out.println("── Graph Adjacency List ──"); for (int node = 0; node < cityConnections.length; node++) { System.out.print("Node " + node + " → "); if (cityConnections[node].length == 0) { System.out.print("(no outgoing connections)"); } else { for (int neighbour : cityConnections[node]) { System.out.print(neighbour + " "); } } System.out.println(); } System.out.println("\nTotal directed edges: " + countTotalEdges(cityConnections)); // Check specific connections System.out.println("\nEdge 2 → 4 exists? " + hasEdge(cityConnections, 2, 4)); System.out.println("Edge 0 → 4 exists? " + hasEdge(cityConnections, 0, 4)); System.out.println("Edge 4 → 0 exists? " + hasEdge(cityConnections, 4, 0)); } }
{} 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.
package io.thecodeforge.jaggedarrays; public class PascalsTriangle { // Pascal's triangle is the textbook jagged array use case: // row 0 has 1 element, row 1 has 2, row N has N+1. // A rectangular array would waste roughly half its allocated space. static int[][] buildPascalsTriangle(int numberOfRows) { int[][] triangle = new int[numberOfRows][]; for (int row = 0; row < numberOfRows; row++) { triangle[row] = new int[row + 1]; // row index + 1 gives exact column count needed triangle[row][0] = 1; // first element of every row is always 1 triangle[row][row] = 1; // last element of every row is always 1 // Fill middle elements: each is the sum of the two elements above it. for (int col = 1; col < row; col++) { triangle[row][col] = triangle[row - 1][col - 1] + triangle[row - 1][col]; } } return triangle; } public static void main(String[] args) { int[][] pascal = buildPascalsTriangle(6); System.out.println("── Pascal's Triangle (6 rows) ──"); for (int row = 0; row < pascal.length; row++) { // Print leading spaces to visually centre each row. String indent = " ".repeat((pascal.length - row - 1) * 2); System.out.print(indent); for (int value : pascal[row]) { System.out.printf("%-4d", value); // left-align each number in a 4-char field } System.out.println(); } // Demonstrate memory efficiency: a rectangular alternative would need // 6 rows × 6 cols = 36 cells. Our jagged version uses 1+2+3+4+5+6 = 21 cells. int jaggedCells = 0; for (int[] row : pascal) jaggedCells += row.length; System.out.println("\nJagged cells used: " + jaggedCells); System.out.println("Rectangular would need: " + (pascal.length * pascal.length)); System.out.printf("Memory saving: %.0f%%%n", (1.0 - (double) jaggedCells / (pascal.length * pascal.length)) * 100); } }
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.
package io.thecodeforge.jaggedarrays; public class PerformanceComparison { public static void main(String[] args) { final int ROWS = 10000; final int MAX_COLS = 10; // Create a jagged array where each row has a random length between 1 and 10. int[][] jagged = new int[ROWS][]; for (int i = 0; i < ROWS; i++) { jagged[i] = new int[(int)(Math.random() * MAX_COLS) + 1]; } // Flat array alternative: same total elements. int totalElements = 0; for (int[] row : jagged) totalElements += row.length; int[] flat = new int[totalElements]; // (Index calculation would require an offset array, omitted for brevity) // Measure iteration speed on jagged. long start = System.nanoTime(); long sum = 0; for (int i = 0; i < ROWS; i++) { for (int j = 0; j < jagged[i].length; j++) { sum += jagged[i][j]; } } long jaggedTime = System.nanoTime() - start; // Flat iteration (assumes we can access via offset array) start = System.nanoTime(); for (int i = 0; i < flat.length; i++) { sum += flat[i]; } long flatTime = System.nanoTime() - start; System.out.println("Jagged iteration (ns): " + jaggedTime); System.out.println("Flat iteration (ns): " + flatTime); System.out.println("Flat was " + (jaggedTime / flatTime) + "x faster in this run"); } }
int[1000] uses only the data and one object header. For high-performance numeric code, flat arrays usually win.Memory Layout — Why Your Jagged Array Might Be Fragmenting the Heap
Before you write another jagged array, understand what the JVM actually does. Every row is a separate object on the heap. That means each new int[n] call allocates a new array object with its own header (12-16 bytes on 64-bit JVMs), length field, and reference from the parent array. For a small jagged array with 5 rows averaging 3 columns each, you're paying for 6 objects instead of 1. That overhead kills L1 cache locality and pressures the garbage collector. The real problem surfaces when you iterate: each row access is a pointer chase. In a rectangular array, the JVM knows the stride and can prefetch. In a jagged array, every row boundary is a branch prediction gamble. If you're doing hot-path work, measure before you choose jagged over rectangular. I've seen production systems where switching from jagged to rectangular cut GC pause times by 40%.
// io.thecodeforge public class MemoryFootprintDemo { public static void main(String[] args) { // Jagged: 6 objects on heap int[][] jagged = new int[5][]; for (int i = 0; i < 5; i++) { jagged[i] = new int[3]; } // Rectangular: 1 object, contiguous int[][] rectangular = new int[5][3]; // Use Instrumentation or JOL to verify System.out.println("Rectangular: single allocation"); System.out.println("Jagged: at least " + (1 + jagged.length) + " allocations"); } }
Copying Jagged Arrays — The Shallow Clone Trap That Corrupts Data
Here's the incident: junior copies a jagged array with Arrays.copyOf() or clone(), patches a row, and the original array changes too. Sound familiar? clone() on a jagged array is shallow — it copies the row references, not the row contents. So int[][] copy = gives you a new outer array, but original.clone();copy[0] == original[0] is true. Same goes for System.arraycopy(). The fix is a deep copy: iterate each row and do copy[i] = original[i].clone(). But even that fails if you have rows of different lengths — you need Arrays.copyOf(original[i], original[i].length) for safety. In Spring Boot apps handling time-series events where rows are different daily batches, I always write a utility method: deepCopyJagged(int[][]). It's defensive code that stops production bugs before they start. Never trust your downstream code to not mutate a supposedly copied jagged array.
// io.thecodeforge public class DeepCopyExample { public static int[][] deepCopyJagged(int[][] original) { if (original == null) return null; int[][] copy = new int[original.length][]; for (int i = 0; i < original.length; i++) { if (original[i] != null) { copy[i] = Arrays.copyOf(original[i], original[i].length); } } return copy; } public static void main(String[] args) { int[][] batchData = {{1,2}, {3,4,5}}; int[][] safeCopy = deepCopyJagged(batchData); safeCopy[0][0] = 99; System.out.println(batchData[0][0]); // Still 1 } }
clone() on jagged arrays is always shallow. If any downstream code mutates rows, your original array silently corrupts. Always deep-copy jagged arrays in public APIs.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.`for (int i=0; i<data.length; i++) System.out.println(data[i] == null ? "null row" : "row "+i+" length="+data[i].length);``java -ea YourClass` to enable assertions, then add `assert data[i] != null;` in loop.new int[0] or allocate it properly: if (data[row] == null) data[row] = new int[0];Add debug print: `System.out.println("Row "+i+" len="+data[i].length);` inside loop.Use enhanced for-each: `for (int[] row : data) for (int val : row) { ... }` to avoid index errors.for (int j = 0; j < data[i].length; j++) using i not 0.`int total = 0; for (int[] row : data) total += row.length; System.out.println("Total elements: "+total);`Add logging to print each row: `for (int i=0; i<data.length; i++) System.out.println("Row "+i+": "+Arrays.toString(data[i]));`| Feature / Aspect | Rectangular 2D Array | Jagged Array |
|---|---|---|
| Declaration | new int[rows][cols] | new int[rows][] then allocate each row |
| Row lengths | All identical — enforced at allocation | Each row independently sized |
| Memory usage | rows × cols regardless of actual data | Exactly what the data needs — no waste |
| Code complexity | Simpler — single col count | Slightly more verbose — per-row .length required |
| Best for | Matrices, game boards, image pixels | Graphs, Pascal's triangle, per-user data |
| Cache behaviour | Inner arrays can still be non-contiguous in Java | Same — Java never guarantees contiguity |
| Null row risk | None — all rows allocated upfront | Real risk if rows not explicitly allocated |
| Iterating columns | array[0].length safe (all rows equal) | Must use array[i].length per row — never array[0].length |
| File | Command / Code | Purpose |
|---|---|---|
| ArrayMemoryModel.java | public class ArrayMemoryModel { | Why Java's 2D Arrays Are Already Jagged Under the Hood |
| JaggedArrayInit.java | public class JaggedArrayInit { | Declaring and Initialising Jagged Arrays the Right Way |
| GraphAdjacencyList.java | public class GraphAdjacencyList { | Iterating Jagged Arrays Safely |
| PascalsTriangle.java | public class PascalsTriangle { | Jagged Arrays vs Rectangular Arrays |
| PerformanceComparison.java | public class PerformanceComparison { | Performance Characteristics and When Jagged Arrays Hurt |
| MemoryFootprintDemo.java | public class MemoryFootprintDemo { | Memory Layout |
| DeepCopyExample.java | public class DeepCopyExample { | Copying Jagged Arrays |
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.Given a jagged array representing an adjacency list, write a method that returns true if a direct edge exists between two given nodes — and explain any edge cases you'd guard against.
from index is valid. (2) Guard against null rows — if the adjacency list was built without allocating empty rows for nodes with no edges, graph[from] could be null. (3) The to node may not exist in any adjacency list, but we only check outgoing edges; we don't validate to exists — that's caller's responsibility. (4) Self-loops: if from == to and the edge exists, it works.If you allocate `int[][] table = new int[4][]` and immediately print `table[0].length`, what happens and why? How would you fix it?
new int[4][] only allocates the outer array. The inner arrays are all null. table[0] is null, so calling .length on it dereferences null. The fix is to allocate each row before accessing it: table[0] = new int[5]; or use inline initialisation if the data is known at compile time.Compare jagged arrays with ArrayList
int[][]) have a fixed number of rows; you can't add or remove rows after creation. ArrayList<int[]> allows dynamic addition of rows. In terms of memory, jagged arrays have less overhead because they are a pure array of references, while ArrayList has an internal array that may be larger than needed (capacity > size). For iteration, both incur similar overhead: each row is an object. However, ArrayList adds the overhead of the ArrayList object per row (if each row is a separate ArrayList), but typically you'd have one ArrayList holding int[] elements, so the per-row overhead is just the int[] object, same as jagged. For performance-critical code with long-lived data, jagged arrays are more cache-friendly because they avoid the extra indirection through ArrayList's internal array. For most applications, the flexibility of ArrayList outweighs the minor performance difference. Profile if in doubt.Frequently Asked Questions
A jagged array (also called a ragged array) is a multidimensional array where each inner array can have a different length. In Java this works naturally because a 2D array is actually an array of references to independent inner arrays, each of which you allocate separately and can size however you need.
Use each row's own .length property in your inner loop — never hardcode a column count or borrow the length from another row. The safest pattern is the enhanced for-each: for (int[] row : jaggedArray) { for (int value : row) { ... } }, because it drives off the actual elements in each row automatically.
Reach for a jagged array whenever your data is naturally uneven: adjacency lists for graphs, Pascal's triangle, per-user permissions, or any collection where different records have a different number of sub-items. Use a rectangular 2D array when every row genuinely has the same number of columns — matrices, game boards, image buffers — because the code is simpler and the intent is clearer.
ArrayList<int[]> allows dynamic addition of rows, while jagged arrays have a fixed number of rows. If you need to add rows at runtime, use ArrayList. Jagged arrays are more memory-efficient if the row count is fixed and each row's length is known on creation. Iteration overhead is similar.
No. A jagged array's base type is fixed at declaration (e.g., int[][]). Every inner array must be int[]. If you need mixed types, use Object[] where each element can be a different array type, but you lose type safety.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Arrays. Mark it forged?
5 min read · try the examples if you haven't