Arrays in Java — exercises with reversing, rotating, removing duplicates and merging (and a bug hiding where you least expect it)
When I did my first exercise with matrices, I tested it with a 3×3 square matrix and it worked first time. Delighted. Then I tried it with one of 2 rows and 4 columns and the program crashed: I had used table.length where I needed table[0].length. With a square matrix, both give the same value, so the bug stayed hidden until I tried a different shape. Ever since, I always test with more than one case, and you’ll see that these exercises include several “awkward” cases on purpose.
Here are three solved and commented array exercises, and at the end a “What’s wrong here?” section with three faulty snippets for us to hunt down. If you haven’t yet seen the theory or the practice, it’s worth going through them first.
How to get the most out of it: read the statement, try to solve it yourself in your editor, and only then look at the solution. Even if you don’t get it, the attempt makes the solution ten times easier to understand. And when you’re done, test your code with odd cases: empty array, a single element, repeated values.
Table of Contents
Exercise 1: reverse and rotate an array (without touching the original)
Statement. Write two methods:
reverse(int[] a): returns a new array with the same elements in reverse order.rotate(int[] a, int k): returns a new array with the elements shiftedkpositions to the right. The ones that leave on the right come back in on the left. Ifkis negative, it rotates to the left.
In both cases, the original array must not change.
Example. With {1, 2, 3, 4, 5}: reversed is {5, 4, 3, 2, 1}; rotated 2 positions to the right is {4, 5, 1, 2, 3}.
Commented solution
static int[] reverse(int[] a) {
int[] r = new int[a.length]; // NEW array: the original is left alone
for (int i = 0; i < a.length; i++) {
r[i] = a[a.length - 1 - i]; // the first of r is the last of a, and so on
}
return r;
}
// Rotates k positions to the right (negative k = to the left)
static int[] rotate(int[] a, int k) {
int n = a.length;
int[] r = new int[n];
if (n == 0) {
return r; // avoids dividing by zero with %
}
int shift = ((k % n) + n) % n; // always leaves it between 0 and n-1
for (int i = 0; i < n; i++) {
r[(i + shift) % n] = a[i]; // what leaves on the right comes back in on the left
}
return r;
}
Why it works
In reverse: the key is the mirror position. In an array of length n, the element at position i has its “opposite” at n - 1 - i. With 5 elements: position 0 pairs with 4, 1 with 3, and 2 with itself. And since we write into r, which is a new array, the original stays intact.
In rotate: every element at position i moves to position i + shift. If that sum falls off the end of the array, the % (remainder) operator brings it back to the start: that’s what makes what leaves on the right come in on the left. Let’s try it with {1, 2, 3, 4, 5} and a shift of 2: the 1 (position 0) goes to 2, the 4 (position 3) goes to (3 + 2) % 5 = 0, and the 5 goes to (4 + 2) % 5 = 1.
Test cases
int[] grades = {1, 2, 3, 4, 5};
System.out.println("Original: " + Arrays.toString(grades));
System.out.println("Reversed: " + Arrays.toString(reverse(grades)));
System.out.println("Rotated 2 (right):" + Arrays.toString(rotate(grades, 2)));
System.out.println("Rotated -1 (left):" + Arrays.toString(rotate(grades, -1)));
System.out.println("Rotated 7: " + Arrays.toString(rotate(grades, 7)));
System.out.println("Original: " + Arrays.toString(grades));
System.out.println("Empty reversed: " + Arrays.toString(reverse(new int[0])));
Original: [1, 2, 3, 4, 5] Reversed: [5, 4, 3, 2, 1] Rotated 2 (right):[4, 5, 1, 2, 3] Rotated -1 (left):[2, 3, 4, 5, 1] Rotated 7: [4, 5, 1, 2, 3] Original: [1, 2, 3, 4, 5] Empty reversed: []
Notice three things: rotating 7 gives the same as rotating 2 (because 7 % 5 = 2), rotating -1 brings the first element to the end, and the original comes out the same at the start and at the end.
Questions you might be asking
Why ((k % n) + n) % n and not simply k % n? Because in Java the remainder of a negative number is negative: -1 % 5 gives -1, not 4, and a negative position would throw an ArrayIndexOutOfBoundsException. Adding n and doing % n again always turns it into a value between 0 and n - 1.
Why do we check n == 0 first? Because k % 0 is a division by zero and throws an ArithmeticException. An empty array is an edge case that is always worth covering.
Couldn’t we reverse “in place”, swapping elements? Yes, and it uses less memory, but it modifies the original. For this statement (which asks not to touch it), the copy is the right call. Further down you’ll see what happens when it isn’t done that way.
Writing int[] r = a; thinking it copies the array. It only copies the address: r and a would be the same array, and reversing “in r” would modify a. For a real copy, new int[...] or Arrays.copyOf.
Exercise 2: remove duplicates
Statement. Write withoutDuplicates(int[] a), which returns a new array with the values of a without repetitions, keeping the order of each value’s first appearance.
Example. With {4, 7, 4, 0, 7, 7, 9, 0} it returns {4, 7, 0, 9}.
An array can’t change size, and you don’t know how many distinct values there will be until you’ve gone through it. Think about first creating a big enough “temporary” array and trimming it at the end with Arrays.copyOf.
Commented solution
// Is value among the first 'upTo' positions of a?
static boolean contains(int[] a, int upTo, int value) {
for (int i = 0; i < upTo; i++) {
if (a[i] == value) {
return true;
}
}
return false;
}
static int[] withoutDuplicates(int[] a) {
int[] temp = new int[a.length]; // at most, all of them fit
int count = 0; // how many distinct values so far
for (int n : a) {
if (!contains(temp, count, n)) { // we only look at what's already filled in
temp[count] = n;
count++;
}
}
return Arrays.copyOf(temp, count); // trim to the real size
}
Why it works
We go through a and, for each value, check whether we’ve already stored it in temp. If it isn’t there, we add it in the next free position (count) and advance the counter. When we finish, temp has the distinct values at the start and filler zeros at the end, so Arrays.copyOf(temp, count) keeps only the good part.
The key to the whole exercise is the upTo parameter. Notice that contains doesn’t go through all of temp, only the first count positions. Why? Because the rest of the array hasn’t been filled in yet and holds default zeros.
Test cases
int[] repeated = {4, 7, 4, 0, 7, 7, 9, 0};
System.out.println("With duplicates: " + Arrays.toString(repeated));
System.out.println("Without duplicates: " + Arrays.toString(withoutDuplicates(repeated)));
System.out.println("Without duplicates (zeros): " + Arrays.toString(withoutDuplicates(new int[]{0, 0, 0})));
With duplicates: [4, 7, 4, 0, 7, 7, 9, 0] Without duplicates: [4, 7, 0, 9] Without duplicates (zeros): [0]
Questions you might be asking
What would happen if contains went through all of temp? The filler zeros would be mistaken for values already stored. With {0, 0, 0} the program would think the 0 is already there from the start and return an empty array instead of [0]. That’s why the test case with zeros matters: it’s exactly where the bug shows up.
Why not use Arrays.sort and remove the consecutive ones? You can, and it’s more efficient, but it would lose the order of appearance that the statement asks for (and it would also modify the original unless you work on a copy).
What does this solution cost? For each element we go through the ones already stored, so in the worst case (all distinct) it makes on the order of n² comparisons. For the sizes in these exercises it doesn’t matter, but with millions of values you’d use other structures (you’ll see HashSet later).
This “big temporary array + copyOf at the end” pattern is the standard way to build results whose size you don’t know in advance, until you get to ArrayList.
Exercise 3: merge two sorted arrays
Statement. Write merge(int[] a, int[] b), which receives two arrays already sorted from smallest to largest and returns a new array with all their elements, also sorted. You can’t sort everything again from scratch: you have to take advantage of the fact that they come sorted.
Example. With {1, 4, 6, 9} and {2, 3, 7, 8, 10, 12} it returns {1, 2, 3, 4, 6, 7, 8, 9, 10, 12}.
Picture two piles of sorted cards face up. You always take the smaller of the two on top. To know “which one is on top” in each pile you need a position in each array: i for a and j for b. And what do you do when one pile runs out?
Commented solution
static int[] merge(int[] a, int[] b) {
int[] r = new int[a.length + b.length];
int i = 0; // position in a
int j = 0; // position in b
int k = 0; // position in r
while (i < a.length && j < b.length) { // while both still have elements
if (a[i] <= b[j]) {
r[k] = a[i];
i++;
} else {
r[k] = b[j];
j++;
}
k++;
}
while (i < a.length) { // leftovers of a (if any)
r[k] = a[i];
i++;
k++;
}
while (j < b.length) { // leftovers of b (if any)
r[k] = b[j];
j++;
k++;
}
return r;
}
Why it works
With three positions (i, j, k) we move through the three arrays at once. On each pass we compare the two “top” elements (a[i] and b[j]), copy the smaller one into r and advance only in the array we took it from. When one of the two runs out, the first loop ends; one of the next two loops copies what’s left of the other (and the other does nothing, because its condition is already false).
Why the “leftovers” are copied as they are without comparing: since the remaining array is already sorted and everything in it is greater than or equal to what’s been copied, it can be pasted straight onto the end.
Test cases
int[] a = {1, 4, 6, 9};
int[] b = {2, 3, 7, 8, 10, 12};
System.out.println("Merge: " + Arrays.toString(merge(a, b)));
System.out.println("With empty: " + Arrays.toString(merge(a, new int[0])));
System.out.println("With repeated: " + Arrays.toString(merge(new int[]{1, 3, 3}, new int[]{3, 4})));
Merge: [1, 2, 3, 4, 6, 7, 8, 9, 10, 12] With empty: [1, 4, 6, 9] With repeated: [1, 3, 3, 3, 4]
Questions you might be asking
Why <= and not < when comparing? With <=, if there’s a tie a wins, and the result comes out just as sorted. With < it would also work; the only change is which array a repeated value comes from first. What matters is that the condition covers the equality case on one of the two sides.
What happens if one of the arrays is empty? The first loop doesn’t run even once (because a.length or b.length is 0) and the leftovers loop copies the other one whole. That’s why the merge(a, new int[0]) case works without special code.
Why not concatenate and sort with Arrays.sort? It would give the same result, but the statement asks to take advantage of the fact that they’re already sorted. This merge goes through each array only once, whereas sorting from scratch is costlier. It’s also the heart of a famous algorithm (merge sort) that you’ll see later.
When an algorithm has several “positions” (here i, j, k), give them names that say which array they belong to, or add a comment when declaring them, as in the solution. Almost all the mistakes in this type of exercise come from advancing the wrong position.
What’s wrong here?
Three code snippets with one array bug each. Before reading the explanation, try to find the bug.
Snippet 1: the column sums
static int[] columnSums(int[][] table) {
int[] sums = new int[table.length];
for (int r = 0; r < table.length; r++) {
for (int c = 0; c < table[r].length; c++) {
sums += table[r];
}
}
return sums;
}
With the table {{1,2,3},{4,5,6},{7,8,9}} it prints [12, 15, 18], perfect. But with {{1,2,3,4},{5,6,7,8}} this appears:
java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
What’s wrong? The sums array is created with table.length (the number of rows), but it needs one position per column. In a square matrix they coincide and the bug hides; with 2 rows and 4 columns, sums has 2 positions and it runs off the end at column 2.
Fix: int[] sums = new int[table[0].length];. With that, the second table gives [6, 8, 10, 12]. It’s exactly my anecdote from the beginning.
Snippet 2: the reverse that ruins the original
static int[] reverse(int[] a) {
for (int i = 0; i < a.length / 2; i++) {
int tmp = a[i];
a[i] = a[a.length - 1 - i];
a[a.length - 1 - i] = tmp;
}
return a;
}
// In main:
int[] grades = {1, 2, 3, 4};
int[] rev = reverse(grades);
System.out.println(Arrays.toString(rev) + " " + Arrays.toString(grades));
It prints [4, 3, 2, 1] [4, 3, 2, 1]: both are reversed.
What’s wrong? The method swaps the elements directly in the array it receives (a) and returns it. Since a is the address of the original array, rev and grades are the same array. The method “works”, but it wrecks the data of whoever calls it.
Fix: work on a copy, for example int[] r = Arrays.copyOf(a, a.length);, do the swaps in r and return r. With that, the result is [4, 3, 2, 1] [1, 2, 3, 4].
Snippet 3: the comparison that never gets it right
int[] expected = {1, 2, 3, 4};
int[] obtained = merge(new int[]{1, 3}, new int[]{2, 4});
if (obtained == expected) {
System.out.println("Correct");
} else {
System.out.println("Incorrect");
}
It prints Incorrect, even though obtained holds exactly [1, 2, 3, 4].
What’s wrong? == compares addresses, not contents. expected and obtained are two different arrays in memory, even if they contain the same thing.
Fix: if (Arrays.equals(obtained, expected)). With that it prints Correct.
Typical mistakes in these exercises
- Writing
int[] r = a;believing it copies. It only copies the address. - Using
table.lengthwheretable[0].lengthis needed (or the other way round). In square matrices you don’t notice. - Forgetting the edge cases: empty array, a single element, all values equal, zeros.
- Going through a whole temporary array instead of only the part already filled in.
- Comparing arrays with
==instead ofArrays.equals.
Cheat sheet: arrays in Java
The essentials in 30 seconds
- An array has a fixed size and one type. Indexes go from
0tolength - 1. - Arrays use
lengthwithout parentheses (Stringuseslength()). - An array is an object: the variable holds a reference, not the data.
- Default values on creation:
0(numbers),false(boolean),'\u0000'(char),null(objects).
Create and traverse
| I want to… | Code |
|---|---|
| Create empty | int[] a = new int[5]; |
| Create with values | int[] a = {5, 2, 9}; |
| Size | a.length |
| Last element | a[a.length - 1] |
| Loop with index | for (int i = 0; i < a.length; i++) { … a[i] … } |
| Loop without index | for (int x : a) { … x … } |
| Fill | Arrays.fill(a, 4); → [4, 4, 4] |
i <= a.length. The last valid index is a.length - 1; otherwise you get ArrayIndexOutOfBoundsException.
Copy and compare
| I want to… | Code | Result |
|---|---|---|
| ❌ “Copy” | int[] m = n; | Two names for the same array: m[0] = 1 also changes n[0] |
| ✅ Real copy | Arrays.copyOf(n, n.length) | A new, independent array |
| Longer copy | Arrays.copyOf(n, 5) | [5, 2, 9, 0, 0] (padded with zeros) |
| Copy a range | Arrays.copyOfRange(n, 1, 3) | [2, 9] (the end is not included) |
| ❌ Compare | a == b | Compares references: two equal copies give false |
| ✅ Compare contents | Arrays.equals(a, b) | true if same values in the same order |
Modifying the array a method receives (reverse, sort…) and discovering you changed the original. If you want to return a different one, call Arrays.copyOf first.
Sort and search (import java.util.Arrays;)
| I want to… | Code |
|---|---|
| Sort (modifies the array) | Arrays.sort(a); |
| Search (only if sorted) | Arrays.binarySearch(a, value) |
System.out.println(Arrays.toString(a)); | |
| Print a matrix | Arrays.deepToString(m) |
binarySearchon[1, 2, 9]: searching2→1(the position). Searching5→-3, i.e.-(insertion point) - 1: negative = not found.println(a)on an array prints something like[I@1b6d3586, not the data.
binarySearch on an unsorted array doesn’t throw an error; it gives a meaningless result. Sort first.
2D arrays (matrices)
int[][] t = {{1, 2, 3, 4},
{5, 6, 7, 8}};
t.length // 2 → number of ROWS
t[0].length // 4 → number of COLUMNS (of row 0)
t[1][2] // 7 → row 1, column 2
// full traversal
for (int r = 0; r < t.length; r++) {
for (int c = 0; c < t[r].length; c++) {
System.out.print(t[r] + " ");
}
System.out.println();
}
- A matrix is an array of arrays: rows can have different lengths (
new int[2][], thenir[0] = new int[1]; ir[1] = new int[3];). - To build a matrix the size of another: rows =
t.length, columns =t[0].length.
new int[table.length] when you need one result per column. On a 2×4 table that creates 2 slots, and writing to column 2 throws ArrayIndexOutOfBoundsException. The right one: new int[table[0].length].
Arrays as parameters and return values
static int[] doubled(int[] a) { // receives the REFERENCE
int[] r = new int[a.length]; // new result → original untouched
for (int i = 0; i < a.length; i++) r[i] = a[i] * 2;
return r;
}
- Writing
a[i] = …inside does affect the outside array. Reassigninga = new int[3]inside does not.
Templates for the exercises
Reverse (into another array):
int[] r = new int[a.length]; for (int i = 0; i < a.length; i++) r[i] = a[a.length - 1 - i];
[1,2,3,4,5] → [5,4,3,2,1]
Rotate k positions to the right (k can be negative or larger than n):
int n = a.length; int[] r = new int[n]; int d = ((k % n) + n) % n; // normalizes k to 0..n-1 for (int i = 0; i < n; i++) r[(i + d) % n] = a[i];
rotate(2) → [4,5,1,2,3] · rotate(-1) → [2,3,4,5,1] · rotate(7) = rotate(2)
Remove duplicates (array + counter pattern):
int[] temp = new int[a.length];
int count = 0;
for (int x : a) {
boolean found = false;
for (int j = 0; j < count; j++) // look only up to count, not temp.length
if (temp[j] == x) found = true;
if (!found) temp[count++] = x;
}
int[] r = Arrays.copyOf(temp, count); // trim to the real size
{4,7,4,0,7,7,9,0} → [4,7,0,9] · {0,0,0} → [0]
Scanning temp up to temp.length instead of count. Unused slots hold 0 and “find” a 0 you never added.
Merge two sorted arrays (two pointers):
int[] r = new int[a.length + b.length];
int i = 0, j = 0, k = 0;
while (i < a.length && j < b.length)
r[k++] = (a[i] <= b[j]) ? a[i++] : b[j++];
while (i < a.length) r[k++] = a[i++]; // leftovers from a
while (j < b.length) r[k++] = b[j++]; // leftovers from b
{1,4,6,9} + {2,3,7,8,10,12} → [1,2,3,4,6,7,8,9,10,12]
Most repeated errors
| Symptom | Likely cause |
|---|---|
ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5 | <= instead of <, or an index computed without checking |
| The original changed “by itself” | You did m = n or modified the parameter in place |
a == b is false with identical data | It compares references → Arrays.equals |
[I@… appears on screen | Missing Arrays.toString |
| “Ghost” zeros in the result | Array bigger than needed, not trimmed with copyOf(…, count) |
binarySearch returns nonsense | The array wasn’t sorted |
| Fails only on non-square matrices | Mixing up t.length and t[0].length |
Before you submit, ask yourself
- Does my loop stop at
length - 1and no further? - Am I modifying the original without meaning to?
- What happens with an empty array? And with one element?
- Does it work on a matrix that is not square?
Summary
- To leave the original alone, work with a new array (
neworArrays.copyOf). - The
%operator lets you “wrap around” an array; with negatives, use((k % n) + n) % n. - For results of unknown size: big temporary array, a counter and
Arrays.copyOfat the end. - Two sorted arrays are merged with one position per array and copying the leftovers at the end.
- Always test with different cases: empty, one element, repeated values, non-square matrices.
Checklist: did you manage it?
- Did you try each exercise before looking at the solution?
- Can you explain why
reversereturns a new array? - Do you understand why
containsreceives theupToparameter? - Do you know what happens with the “leftovers” when merging?
- Did you find the three bugs in “What’s wrong here?” without reading the explanation?
With this you close the arrays block. The next step in Java is Strings, which are much more like arrays than they seem. See you there!
Keep goingAll Java (en) articlesDid this article help you? Here is how you can support SergioLearns →
