Array Basics
An array is a fixed-size, ordered container that holds elements of a single type, stored in one contiguous block of memory and accessed by numeric index. Arrays are the most basic data structure in Java: ArrayList, String, HashMap, and many other classes are built on arrays internally. This lesson looks closely at how arrays are declared, created, stored, and copied, building on the introduction in the Java Arrays lesson.
How an Array Is Stored
int[] scores = {72, 85, 90, 64};
scores (reference, on the stack)
│
▼ heap
┌────────┬────┬────┬────┬────┐
│length=4│ 72 │ 85 │ 90 │ 64 │
└────────┴────┴────┴────┴────┘
header [0] [1] [2] [3]
- The array is an object on the heap. The variable
scoresholds a reference to it. - Elements sit next to each other in memory. To find
scores[i], the JVM computes "start of the data + i × element size". That is why access by index takes the same time for any position, and why arrays are very fast. - The length is stored with the array and never changes.
Declaring Array Variables
int[] numbers; // preferred: the type is "int array"
String[] names;
double[][] grid; // array of arrays
int legacy[]; // C-style: legal, but splits the type across the line
int[] a, b; // both a and b are int[]
int c[], d; // c is int[], d is a plain int: a classic trap
Declaring a variable creates no array. Until it is assigned, a local array variable cannot be used, and an array field is null.
Creating Arrays
With new and a size
int[] counts = new int[5]; // 5 ints
String[] labels = new String[3]; // 3 String references
boolean[] seen = new boolean[100];
The size can be any int expression evaluated at runtime:
int n = Integer.parseInt(args[0]);
double[] samples = new double[n];
A negative size throws NegativeArraySizeException. A size of zero is legal and produces an empty array.
With an initializer
int[] primes = {2, 3, 5, 7, 11};
String[] days = {"Mon", "Tue", "Wed"};
The size is inferred from the number of values. The short brace form is allowed only in a declaration. Elsewhere, use new Type[]{...}:
int[] values;
values = new int[]{1, 2, 3}; // correct
printAll(new String[]{"a", "b"}); // passing an array literal to a method
You cannot give both a size and values: new int[3]{1, 2, 3} is a compile error.
Default Values
Every element of an array created with new starts with the default value of its type:
| Element type | Default |
|---|---|
byte, short, int, long | 0 |
float, double | 0.0 |
char | '\u0000' (the null character) |
boolean | false |
| Any reference type | null |
This differs from local variables, which have no defaults. An array of objects holds references, and they all start as null:
String[] names = new String[3];
System.out.println(names[0]); // null
System.out.println(names[0].length()); // NullPointerException
StringBuilder[] buffers = new StringBuilder[2];
buffers[0] = new StringBuilder(); // each element must be created separately
buffers[1] = new StringBuilder();
new StringBuilder[2] creates an array with room for two references. It does not create any StringBuilder objects.
Reading and Writing Elements
double[] temps = new double[7];
temps[0] = 31.5;
temps[6] = 29.0;
double first = temps[0];
temps[3] += 1.5; // compound assignment works on elements
temps[4]++; // so does increment
Valid indexes run from 0 to length - 1. Any other index throws ArrayIndexOutOfBoundsException, whose message names both the bad index and the length:
java.lang.ArrayIndexOutOfBoundsException: Index 7 out of bounds for length 7
The index must be an int (or a type that widens to int: byte, short, char). A long index requires a cast.
Filling and Generating Arrays
import java.util.Arrays;
int[] zeros = new int[5];
Arrays.fill(zeros, -1); // [-1, -1, -1, -1, -1]
int[] squares = new int[6];
Arrays.setAll(squares, i -> i * i); // [0, 1, 4, 9, 16, 25]
String[] seats = new String[4];
Arrays.setAll(seats, i -> "A" + (i + 1)); // [A1, A2, A3, A4]
Arrays.fill puts one value in every slot. Arrays.setAll computes each element from its index using a lambda.
Note that Arrays.fill with an object puts the same reference in every slot:
List<String>[] buckets = new List[3];
Arrays.fill(buckets, new ArrayList<>()); // all three slots share ONE list
buckets[0].add("x");
System.out.println(buckets[2]); // [x]
Arrays.setAll(buckets, i -> new ArrayList<>()); // a separate list per slot
(Arrays of generic types such as List<String>[] produce an unchecked warning; prefer List<List<String>> in real code.)
Arrays Are Objects
Every array is an object, so it has a class, can be stored in an Object variable, and inherits Object's methods:
int[] a = {1, 2, 3};
Object o = a;
System.out.println(a.getClass().getSimpleName()); // int[]
Array types are not written as classes; they are created by the JVM for each element type (int[], String[], int[][]).
Assignment copies the reference
int[] original = {1, 2, 3};
int[] alias = original; // both variables refer to one array
alias[0] = 100;
System.out.println(original[0]); // 100
Copying the contents
int[] copy1 = original.clone(); // full copy
int[] copy2 = Arrays.copyOf(original, original.length); // full copy
int[] bigger = Arrays.copyOf(original, 5); // [100, 2, 3, 0, 0]
int[] middle = Arrays.copyOfRange(original, 1, 3); // [2, 3]
int[] target = new int[5];
System.arraycopy(original, 0, target, 2, 3); // [0, 0, 100, 2, 3]
System.arraycopy(src, srcPos, dest, destPos, length) copies into an existing array and is what most copy methods use internally.
All of these are shallow copies. For an array of objects, the new array holds the same references:
StringBuilder[] src = { new StringBuilder("a") };
StringBuilder[] cp = src.clone();
cp[0].append("!");
System.out.println(src[0]); // a! (same StringBuilder object)
Comparing and printing
== and equals on arrays compare identity, not contents. Arrays provides content-based versions:
int[] x = {1, 2, 3};
int[] y = {1, 2, 3};
System.out.println(x == y); // false
System.out.println(x.equals(y)); // false (Object.equals)
System.out.println(Arrays.equals(x, y)); // true
System.out.println(Arrays.toString(x)); // [1, 2, 3]
System.out.println(x); // [I@1b6d3586 (type code + hash)
System.out.println(Arrays.hashCode(x)); // content-based hash
For nested arrays, use Arrays.deepEquals and Arrays.deepToString.
Array Covariance and ArrayStoreException
Because a String is an Object, Java allows a String[] to be assigned to an Object[] variable. This is called array covariance:
Object[] things = new String[2]; // compiles
things[0] = "text"; // fine
things[1] = 42; // compiles, but throws ArrayStoreException at runtime
The array remembers that it was created as a String[] and checks every store. This runtime check is one reason generic collections like List<String>, which are checked at compile time, are generally safer than arrays of objects.
Arrays Versus ArrayList
| Array | ArrayList | |
|---|---|---|
| Size | Fixed at creation | Grows and shrinks automatically |
| Primitives | Stored directly (int[]) | Must be boxed (List<Integer>) |
| Memory | Minimal overhead | Extra per object; boxed numbers cost much more |
| Type safety | Runtime store check for object arrays | Compile-time generics |
| Syntax | a[i], a.length | list.get(i), list.size() |
| Utility methods | Static methods in Arrays | Instance methods: add, remove, contains, ... |
Use arrays for fixed-size data, numeric computation, performance-critical code, and APIs that require them (main(String[] args), String.split, byte buffers). Use ArrayList for most collections whose size changes. Converting between them:
String[] arr = {"b", "a", "c"};
List<String> list = new ArrayList<>(Arrays.asList(arr)); // array → modifiable list
String[] back = list.toArray(new String[0]); // list → array
Practical Example: Exam Results
import java.util.Arrays;
public class ExamResults {
public static void main(String[] args) {
String[] students = {"Ada", "Linus", "Grace", "Alan", "Margaret"};
int[] marks = new int[students.length]; // one slot per student, all 0
int[] entered = {78, 92, 45, 66, 88};
System.arraycopy(entered, 0, marks, 0, entered.length);
char[] grades = new char[marks.length];
for (int i = 0; i < marks.length; i++) {
grades[i] = marks[i] >= 85 ? 'A' : marks[i] >= 70 ? 'B' : marks[i] >= 50 ? 'C' : 'F';
}
int[] sorted = marks.clone(); // keep the original order intact
Arrays.sort(sorted);
int median = sorted[sorted.length / 2];
System.out.println("Marks: " + Arrays.toString(marks));
System.out.println("Grades: " + Arrays.toString(grades));
System.out.println("Sorted: " + Arrays.toString(sorted));
System.out.println("Median: " + median);
System.out.println("Top: " + students[indexOfMax(marks)]);
}
static int indexOfMax(int[] values) {
int best = 0;
for (int i = 1; i < values.length; i++) {
if (values[i] > values[best]) best = i;
}
return best;
}
}
Output:
Marks: [78, 92, 45, 66, 88]
Grades: [B, A, F, C, A]
Sorted: [45, 66, 78, 88, 92]
Median: 78
Top: Linus
marks is sized from students.length, so the two arrays always match. System.arraycopy loads the entered values into it. The grades go into a char[], whose elements start at '\u0000' until assigned. The program sorts a clone, because Arrays.sort rearranges the array in place; sorting marks directly would break the link between each mark and its student. indexOfMax returns a position rather than a value, so the same index can look up the student's name in the parallel array.