An array is a fixed-size, indexed block of values that all share one type. It is the simplest
container Java has, it is what ArrayList is built on, and the moment you need it to grow
you should stop using it.
Creating an array
int[] scores = new int[5]; // five slots, all 0
scores[0] = 90;
scores[1] = 85;
int[] filled = {90, 85, 77, 62, 100}; // literal — size inferred
String[] names = {"Ana", "Bo", "Cy"};
System.out.println(filled.length); // 5 — a field, not a method. No parentheses.
System.out.println(filled[0]); // 90 — indexes start at 0
System.out.println(filled[4]); // 100 — the last one is length - 1
Two things to fix in your head now. The size is permanent — an
int[5] holds five values forever; there is no add. And
length has no parentheses, unlike String.length() and
List.size(). Java is inconsistent here and everyone trips on it.
A new array is not empty — it is filled with the default for its type: 0 for numbers,
false for booleans, and null for anything else.
String[] words = new String[3];
System.out.println(words[0]); // null
// System.out.println(words[0].length()); // NullPointerException — the slot exists, the object does not
Reading past the end
int[] numbers = {10, 20, 30};
// numbers[3] -> ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
for (int i = 0; i < numbers.length; i++) { // < not <=
System.out.println(numbers[i]);
}
Java checks every access and throws rather than reading whatever memory happened to be next. That check costs a little speed and buys you an entire class of security vulnerability you will never have. The exception message tells you both the bad index and the real length, which is usually enough to spot the mistake without a debugger.
Iterating
int[] scores = {90, 85, 77};
for (int score : scores) { // prefer this when you do not need the index
System.out.println(score);
}
for (int i = 0; i < scores.length; i++) {
System.out.println((i + 1) + ": " + scores[i]); // use this when you do
}
The Arrays methods worth knowing
The java.util.Arrays class saves you writing loops. Four of these come up constantly;
the rest you can look up when you need them:
int[] numbers = {5, 2, 9, 1};
Arrays.sort(numbers); // sorts IN PLACE — returns nothing
System.out.println(Arrays.toString(numbers)); // [1, 2, 5, 9]
System.out.println(Arrays.binarySearch(numbers, 5)); // 2 — only valid on a SORTED array
int[] copy = Arrays.copyOf(numbers, 6); // longer copy, padded with 0
System.out.println(Arrays.toString(copy)); // [1, 2, 5, 9, 0, 0]
int[] slice = Arrays.copyOfRange(numbers, 1, 3); // [2, 5] — from inclusive, to exclusive
System.out.println(Arrays.equals(numbers, copy)); // false — compares CONTENTS, unlike ==
Arrays.toString() is the one to remember first. Printing an array directly gives you
something like [I@1b6d3586 — the type and a hash code — because arrays do not override
toString(). That output confuses every beginner exactly once.
Arrays.sort mutates the array and returns void, so
int[] sorted = Arrays.sort(numbers); does not compile. And binarySearch
silently returns nonsense on an unsorted array rather than complaining.
Copying, and the trap in it
int[] original = {1, 2, 3};
int[] alias = original; // NOT a copy — two names for one array
alias[0] = 99;
System.out.println(original[0]); // 99
int[] real = Arrays.copyOf(original, original.length); // a genuine copy
real[0] = 1;
System.out.println(original[0]); // 99 — untouched by the copy
Assignment copies the reference, not the contents. This is the same primitives-versus-references distinction from Data Types, and it is the reason a method that takes an array can modify the caller's data.
Two-dimensional arrays
int[][] grid = new int[3][4]; // 3 rows, 4 columns
grid[0][0] = 1;
int[][] table = {
{1, 2, 3},
{4, 5, 6}
};
for (int[] row : table) {
System.out.println(Arrays.toString(row));
}
System.out.println(Arrays.deepToString(table)); // [[1, 2, 3], [4, 5, 6]]
A 2D array is really an array of arrays, which means the rows can have different lengths, and that
Arrays.toString is not enough — it would print the row objects. Use
deepToString for anything nested.
Totals and searches without a loop
Arrays have a stream() bridge, which is the tidiest way to answer the arithmetic
questions you would otherwise loop for:
int[] scores = {90, 85, 77, 62};
System.out.println(Arrays.stream(scores).sum()); // 314
System.out.println(Arrays.stream(scores).max().getAsInt()); // 90
System.out.println(Arrays.stream(scores).average().orElse(0)); // 78.5
System.out.println(Arrays.stream(scores).anyMatch(s -> s < 70)); // true
Arrays.fill(scores, 0); // every slot to 0
System.out.println(Arrays.toString(scores)); // [0, 0, 0, 0]
Note average() returns an OptionalDouble — an empty array has no average,
and the API makes you say what should happen in that case rather than returning a misleading zero.
When to stop using an array
Arrays are the right tool when the size is known and fixed, when you are working with primitives
and care about memory, or when an API hands you one. For everything else, use a
List:
// Awkward: an array that needs to grow
String[] arr = new String[2];
arr = Arrays.copyOf(arr, 3); // manual resize, every time
// Natural
List<String> list = new ArrayList<>();
list.add("Ana");
list.add("Bo");
list.remove("Ana");
System.out.println(list.size()); // 1
Converting between the two comes up constantly, and one direction has a trap:
String[] arr = {"a", "b", "c"};
List<String> fixed = Arrays.asList(arr); // a VIEW — fixed size, add() throws
List<String> mutable = new ArrayList<>(Arrays.asList(arr)); // a real, growable list
List<String> immutable = List.of(arr); // truly immutable
String[] back = mutable.toArray(new String[0]);
System.out.println(back.length); // 3
Arrays.asList returns a fixed-size list backed by the original array. Calling
add on it throws UnsupportedOperationException, and writing to it changes
the array. When you want a list you can modify, wrap it in new ArrayList<>(...).
Next
You have been writing code inside main this whole time.
Methods is next — how to give a block of code a name, parameters and
a result.