Series: Java Core for Beginners Lecture: 07 of 12 Topics: Arrays · Multi-dimensional arrays · String immutability · String methods · StringBuilder · String.format & printf
Arrays — Fixed-Size Sequences
An array is a data structure that stores a fixed number of elements of the same type in a contiguous block of memory. It is the most fundamental collection in Java — and in programming generally.
Arrays are the right tool when:
- You know the number of elements upfront and it will not change
- You need fast, index-based access to elements (
O(1)time) - You are working with primitives and want to avoid the overhead of wrapper objects
Their key limitation is that the size is fixed at creation time. You cannot add or remove elements from an array. When you need a resizable sequence, use ArrayList (Lecture 8).
Every array in Java is a reference type — even an array of primitives. The array variable holds a reference to the array object on the heap. This has the same implications you saw with other reference types: assignment copies the reference, == compares addresses, and null means no array has been assigned.
Declaring, Creating, and Initializing Arrays
Declaration
int[] scores; // preferred style — type with brackets
String[] names;
double[] prices;
int scores[]; // also valid — C-style, but not recommended in Java
The brackets in int[] signal that the variable holds an array of int. The preferred Java style puts the brackets next to the type, not the variable name.
Creation with new
int[] scores = new int[5]; // array of 5 ints, all initialized to 0
String[] names = new String[3]; // array of 3 Strings, all initialized to null
double[] prices = new double[10]; // array of 10 doubles, all initialized to 0.0
boolean[] flags = new boolean[4]; // array of 4 booleans, all initialized to false
When you create an array with new, Java automatically initializes every element to the type’s default value: 0 for numeric types, false for boolean, null for reference types.
The size is specified as a non-negative integer and is stored in the array’s length field (not a method — no parentheses):
int[] scores = new int[5];
System.out.println(scores.length); // 5 — always use .length, never hardcode
Array initializer (literal syntax)
When you know the values at compile time, use an array initializer:
int[] scores = {95, 87, 72, 100, 88};
String[] days = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"};
double[] rates = {0.05, 0.08, 0.12};
The size is inferred from the number of elements. This syntax is only valid at the point of declaration — you cannot use it for reassignment.
Accessing elements
Array elements are accessed by zero-based index — the first element is at index 0, the last at index length - 1:
int[] scores = {95, 87, 72, 100, 88};
System.out.println(scores[0]); // 95 — first element
System.out.println(scores[4]); // 88 — last element
System.out.println(scores[2]); // 72
scores[2] = 80; // modify an element
System.out.println(scores[2]); // 80
Accessing an index outside the valid range throws ArrayIndexOutOfBoundsException at runtime:
System.out.println(scores[5]); // EXCEPTION: index 5 is out of bounds for length 5
System.out.println(scores[-1]); // EXCEPTION: negative index
This is one of the most common runtime errors in Java. Always ensure your indices are in the range [0, array.length - 1].
Arrays are objects
Because arrays are reference types, assignment copies the reference:
int[] original = {1, 2, 3, 4, 5};
int[] alias = original; // both point to the SAME array
alias[0] = 99;
System.out.println(original[0]); // 99 — change is visible through original too
To create an independent copy, use Arrays.copyOf() or System.arraycopy() (covered in section 4).
Iterating Over Arrays
Standard for loop
The classic approach gives you full control over the index:
int[] scores = {95, 87, 72, 100, 88};
for (int i = 0; i < scores.length; i++) {
System.out.println("scores[" + i + "] = " + scores[i]);
}
Use the standard for loop when you need the index value itself — for example, to print "Element 2: 72" or to modify elements based on their position.
Enhanced for loop (for-each)
When you only need the values and not the indices, the enhanced for loop is cleaner:
int[] scores = {95, 87, 72, 100, 88};
for (int score : scores) {
System.out.println(score);
}
Read this as: “for each score in scores“. The loop variable score takes on each element in turn — it is a copy of the element for primitive arrays. Modifying score inside the loop does not affect the array:
for (int score : scores) {
score = 0; // modifies the local copy only — array is unchanged
}
To modify elements, use the standard for loop with index-based assignment.
Iterating backwards
for (int i = scores.length - 1; i >= 0; i--) {
System.out.print(scores[i] + " ");
}
// 88 100 72 87 95
Common Array Operations
Java provides the java.util.Arrays class with a rich set of utility methods. Always import java.util.Arrays to use them.
Printing an array
The default toString() on an array prints its memory address — not useful. Use Arrays.toString():
int[] scores = {95, 87, 72, 100, 88};
System.out.println(scores); // [I@1b6d3586 — useless
System.out.println(Arrays.toString(scores)); // [95, 87, 72, 100, 88]
Sorting
int[] scores = {95, 87, 72, 100, 88};
Arrays.sort(scores);
System.out.println(Arrays.toString(scores)); // [72, 87, 88, 95, 100]
String[] names = {"Charlie", "Alice", "Bob"};
Arrays.sort(names);
System.out.println(Arrays.toString(names)); // [Alice, Bob, Charlie]
Arrays.sort() sorts in ascending order in-place using a highly optimized dual-pivot quicksort for primitives and merge sort for objects.
Binary search
After sorting, you can search efficiently with Arrays.binarySearch():
int[] scores = {72, 87, 88, 95, 100}; // must be sorted first
int index = Arrays.binarySearch(scores, 88);
System.out.println(index); // 2
int missing = Arrays.binarySearch(scores, 90);
System.out.println(missing); // negative value — not found
binarySearch returns the index if found, or a negative value if not. Never use it on an unsorted array — the result is undefined.
Copying
int[] original = {1, 2, 3, 4, 5};
// Copy with same length
int[] copy1 = Arrays.copyOf(original, original.length);
// Copy with different length (truncates or pads with defaults)
int[] shorter = Arrays.copyOf(original, 3); // [1, 2, 3]
int[] longer = Arrays.copyOf(original, 7); // [1, 2, 3, 4, 5, 0, 0]
// Copy a range [fromIndex, toIndex)
int[] middle = Arrays.copyOfRange(original, 1, 4); // [2, 3, 4]
Filling
int[] zeros = new int[5];
Arrays.fill(zeros, 7);
System.out.println(Arrays.toString(zeros)); // [7, 7, 7, 7, 7]
Comparing
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
int[] c = {1, 2, 4};
System.out.println(a == b); // false — different objects
System.out.println(Arrays.equals(a, b)); // true — same content
System.out.println(Arrays.equals(a, c)); // false
Passing arrays to methods
Arrays are passed by reference. The method receives a reference to the same array in memory — any modifications inside the method affect the original:
public static void doubleAll(int[] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i] *= 2;
}
}
int[] numbers = {1, 2, 3, 4, 5};
doubleAll(numbers);
System.out.println(Arrays.toString(numbers)); // [2, 4, 6, 8, 10]
Returning arrays from methods
A method can return an array the same way it returns any other object:
public static int[] range(int start, int end) {
int[] result = new int[end - start];
for (int i = 0; i < result.length; i++) {
result[i] = start + i;
}
return result;
}
int[] r = range(1, 6);
System.out.println(Arrays.toString(r)); // [1, 2, 3, 4, 5]
Multi-Dimensional Arrays
Java supports arrays of arrays, commonly used to represent grids, matrices, and tables.
Two-dimensional arrays
// Declaration and creation
int[][] matrix = new int[3][4]; // 3 rows, 4 columns, all zeros
// Initializer syntax
int[][] grid = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// Accessing elements: [row][column]
System.out.println(grid[0][0]); // 1 — top-left
System.out.println(grid[1][2]); // 6 — row 1, column 2
System.out.println(grid[2][2]); // 9 — bottom-right
Iterating over a 2D array
int[][] grid = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
for (int row = 0; row < grid.length; row++) {
for (int col = 0; col < grid[row].length; col++) {
System.out.printf("%3d", grid[row][col]);
}
System.out.println();
}
Output:
1 2 3
4 5 6
7 8 9
Or with for-each:
for (int[] row : grid) {
for (int value : row) {
System.out.printf("%3d", value);
}
System.out.println();
}
Printing a 2D array
System.out.println(Arrays.deepToString(grid));
// [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Use Arrays.deepToString() for multi-dimensional arrays. Arrays.toString() only prints one level.
Jagged arrays
In Java, a 2D array is actually an array of arrays. Each row can have a different length — this is called a jagged array:
int[][] jagged = new int[3][];
jagged[0] = new int[]{1};
jagged[1] = new int[]{2, 3};
jagged[2] = new int[]{4, 5, 6};
System.out.println(Arrays.deepToString(jagged));
// [[1], [2, 3], [4, 5, 6]]
Jagged arrays are useful for representing triangular data or variable-length rows, like Pascal’s triangle.
Common 2D array operations
// Matrix addition
public static int[][] add(int[][] a, int[][] b) {
int rows = a.length;
int cols = a[0].length;
int[][] result = new int[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
result[i][j] = a[i][j] + b[i][j];
}
}
return result;
}
// Transpose
public static int[][] transpose(int[][] matrix) {
int rows = matrix.length;
int cols = matrix[0].length;
int[][] result = new int[cols][rows];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
result[j][i] = matrix[i][j];
}
}
return result;
}
String Immutability — Why It Matters
You have used String throughout this series. Now it is time to understand its most important characteristic: immutability.
A String object in Java cannot be modified after it is created. Every method that appears to change a string actually creates and returns a new String object:
String s = "hello";
s.toUpperCase(); // returns "HELLO" — s is unchanged
System.out.println(s); // "hello"
String upper = s.toUpperCase(); // capture the new String
System.out.println(upper); // "HELLO"
System.out.println(s); // "hello" — still unchanged
This is a common source of bugs for beginners — they call a method on a string and forget to use the return value.
Why Java strings are immutable
Safety. Immutable strings can be safely shared across threads without synchronization. Multiple variables can hold references to the same String object with no risk of one thread modifying the value another thread is reading.
String pool. Java maintains a string pool (also called the string intern pool) — a cache of string literals. When you write "hello" in two places, Java reuses the same object rather than creating two:
String a = "hello";
String b = "hello";
System.out.println(a == b); // true — same object from the pool
System.out.println(a.equals(b)); // true
This optimization is only possible because strings are immutable — if one variable could change its string’s value, it would silently affect all variables pointing to the same pooled object.
Security. Strings are used extensively for sensitive data: file paths, database connection strings, network addresses. Immutability ensures that once a String is passed to a method, the method cannot secretly alter it.
The performance implication
Because each “modification” creates a new object, concatenating strings inside a loop is expensive:
String result = "";
for (int i = 0; i < 10_000; i++) {
result += i; // creates a NEW String object on every iteration!
}
This loop creates 10,000 intermediate String objects. For string building in loops, always use StringBuilder (section 8).
The String API — Essential Methods
String has a rich API. Here are the methods you will use most frequently, organized by category.
Length and character access
String s = "Hello, World!";
s.length() // 13 — number of characters
s.charAt(0) // 'H' — character at index 0
s.charAt(7) // 'W'
s.isEmpty() // false — true only if length() == 0
s.isBlank() // false — true if empty or contains only whitespace (Java 11+)
Searching
String s = "Hello, World!";
s.contains("World") // true
s.startsWith("Hello") // true
s.endsWith("!") // true
s.indexOf("o") // 4 — index of first 'o'
s.lastIndexOf("o") // 8 — index of last 'o'
s.indexOf("o", 5) // 8 — search from index 5 onwards
s.indexOf("xyz") // -1 — not found
Extracting substrings
String s = "Hello, World!";
s.substring(7) // "World!" — from index 7 to end
s.substring(7, 12) // "World" — from index 7 up to (not including) 12
s.substring(0, 5) // "Hello"
Indices in substring work like a half-open interval [start, end) — the character at start is included, the character at end is excluded.
Transforming
String s = " Hello, World! ";
s.toLowerCase() // " hello, world! "
s.toUpperCase() // " HELLO, WORLD! "
s.trim() // "Hello, World!" — removes leading/trailing whitespace
s.strip() // "Hello, World!" — Unicode-aware trim (Java 11+)
s.stripLeading() // "Hello, World! "
s.stripTrailing() // " Hello, World!"
s.replace("World", "Java") // " Hello, Java! "
s.replace('l', 'r') // " Herro, Worrd! " — replaces all occurrences
s.replaceAll("\\s+", "_") // replaces whitespace runs with underscore (regex)
s.replaceFirst("\\s+", "_") // replaces only the first match
Splitting and joining
// Splitting
String csv = "Alice,30,Engineer";
String[] parts = csv.split(",");
System.out.println(parts[0]); // "Alice"
System.out.println(parts[1]); // "30"
System.out.println(parts[2]); // "Engineer"
// Split with limit — stop after N pieces
String[] twoparts = csv.split(",", 2);
System.out.println(twoparts[0]); // "Alice"
System.out.println(twoparts[1]); // "30,Engineer"
// Joining
String joined = String.join(", ", "Alice", "Bob", "Charlie");
System.out.println(joined); // "Alice, Bob, Charlie"
String[] names = {"Alice", "Bob", "Charlie"};
String result = String.join(" | ", names);
System.out.println(result); // "Alice | Bob | Charlie"
Comparing
String a = "Hello";
String b = "hello";
a.equals(b) // false — case-sensitive
a.equalsIgnoreCase(b) // true
a.compareTo(b) // negative — 'H' (72) < 'h' (104) in Unicode
a.compareToIgnoreCase(b) // 0 — equal ignoring case
compareTo() returns:
- Negative if
acomes beforeblexicographically - Zero if they are equal
- Positive if
acomes afterb
This is the method used internally by Arrays.sort() and Collections.sort() to sort strings alphabetically.
Converting other types to String
String.valueOf(42) // "42"
String.valueOf(3.14) // "3.14"
String.valueOf(true) // "true"
String.valueOf('A') // "A"
Integer.toString(42) // "42"
Double.toString(3.14) // "3.14"
Converting String to other types
int n = Integer.parseInt("42"); // 42
double d = Double.parseDouble("3.14"); // 3.14
long l = Long.parseLong("9876543210"); // 9876543210L
boolean b = Boolean.parseBoolean("true"); // true
// parseInt with radix (number base)
int hex = Integer.parseInt("FF", 16); // 255
int bin = Integer.parseInt("1010", 2); // 10
parseInt, parseDouble, and similar methods throw NumberFormatException if the string is not a valid number — always handle this when parsing user input (covered in Lecture 10).
String.chars() and character processing
String s = "Hello, World!";
// Count occurrences of a character
long count = s.chars()
.filter(c -> c == 'l')
.count();
System.out.println(count); // 3
// Convert to char array
char[] chars = s.toCharArray();
for (char c : chars) {
System.out.print(c + " ");
}
StringBuilder and StringBuffer
When you need to build a string piece by piece — especially in a loop — use StringBuilder. Unlike String, StringBuilder is mutable: it holds a resizable character buffer that can be modified in place without creating new objects.
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("World");
sb.append("!");
String result = sb.toString();
System.out.println(result); // "Hello, World!"
StringBuilder vs String concatenation in a loop
// SLOW — creates 10,000 String objects
String slow = "";
for (int i = 0; i < 10_000; i++) {
slow += i;
}
// FAST — single StringBuilder, modifies in place
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
sb.append(i);
}
String fast = sb.toString();
The difference in performance is dramatic for large loops. The StringBuilder approach runs in O(n) time; string concatenation in a loop runs in O(n²) because each += copies all previous characters into a new object.
Key StringBuilder methods
StringBuilder sb = new StringBuilder("Hello, World!");
// Inspection
sb.length() // 13
sb.charAt(0) // 'H'
sb.indexOf("World") // 7
// Modification — all return the StringBuilder itself (fluent interface)
sb.append(" Goodbye!"); // "Hello, World! Goodbye!"
sb.insert(5, " Beautiful") // inserts at index 5
sb.delete(5, 15) // removes characters [5, 15)
sb.deleteCharAt(0) // removes character at index 0
sb.replace(7, 12, "Java") // replaces [7, 12) with "Java"
sb.reverse() // reverses the entire sequence
sb.setCharAt(0, 'h') // sets character at index 0
// Chaining — fluent style
String result = new StringBuilder()
.append("Java")
.append(" ")
.append("Core")
.append(" ")
.append("2024")
.toString();
// "Java Core 2024"
StringBuilder with initial capacity
If you know approximately how long the final string will be, pass an initial capacity to avoid internal buffer resizing:
StringBuilder sb = new StringBuilder(1024); // pre-allocate 1024 chars
The default initial capacity is 16 characters. The buffer automatically doubles when it runs out of space, but pre-allocating avoids intermediate resizing for large strings.
StringBuffer
StringBuffer is the thread-safe version of StringBuilder — all its methods are synchronized. Use it when multiple threads need to modify the same buffer concurrently. In single-threaded code (the vast majority of cases), use StringBuilder — it is faster because it has no synchronization overhead.
Use StringBuilder — single-threaded code (99% of the time)
Use StringBuffer — multi-threaded shared buffer (rare, and usually better handled differently)
Formatting Output — String.format and printf
Raw string concatenation produces correct output but is hard to read when mixing many variables. Java provides String.format() and System.out.printf() for precise, template-based formatting.
String.format()
String.format() returns a formatted string without printing it. It uses the same format specifiers as C’s printf:
String name = "Alice";
int age = 30;
double gpa = 3.857;
boolean honor = true;
String report = String.format("Name: %-10s | Age: %3d | GPA: %.2f | Honor: %b",
name, age, gpa, honor);
System.out.println(report);
// "Name: Alice | Age: 30 | GPA: 3.86 | Honor: true"
Common format specifiers
| Specifier | Type | Example | Output |
|---|---|---|---|
%s |
String | format("%s", "hi") |
hi |
%d |
Integer (int, long) |
format("%d", 42) |
42 |
%f |
Floating-point | format("%f", 3.14) |
3.140000 |
%.2f |
Float, 2 decimal places | format("%.2f", 3.14159) |
3.14 |
%e |
Scientific notation | format("%e", 12345.0) |
1.234500e+04 |
%c |
Character | format("%c", 'A') |
A |
%b |
Boolean | format("%b", true) |
true |
%n |
Newline (platform-safe) | format("A%nB") |
A then B |
%% |
Literal % |
format("100%%") |
100% |
Width and alignment flags
// %Nd — integer with minimum width N (right-aligned by default)
String.format("%5d", 42) // " 42"
// %-Nd — left-aligned
String.format("%-5d", 42) // "42 "
// %0Nd — zero-padded
String.format("%05d", 42) // "00042"
// %N.Mf — float with width N and M decimal places
String.format("%8.2f", 3.14) // " 3.14"
// %-Ns — left-aligned string in field of width N
String.format("%-10s|", "hi") // "hi |"
// %+d — always show sign
String.format("%+d", 42) // "+42"
String.format("%+d", -42) // "-42"
printf — format and print in one call
System.out.printf() is equivalent to System.out.print(String.format(...)):
System.out.printf("%-15s %5d %8.2f%n", "Alice", 30, 75000.50);
System.out.printf("%-15s %5d %8.2f%n", "Bob", 25, 62500.00);
System.out.printf("%-15s %5d %8.2f%n", "Charlie", 35, 90000.75);
Output:
Alice 30 75000.50
Bob 25 62500.00
Charlie 35 90000.75
Note the %n at the end — it outputs the platform-appropriate line separator (\r\n on Windows, \n on Unix). Prefer %n over \n in printf for portability.
Formatted strings for tables
A common pattern for printing tabular data:
String[] headers = {"Name", "Score", "Grade"};
Object[][] rows = {
{"Alice", 95, "A"},
{"Bob", 82, "B"},
{"Charlie", 67, "D"},
};
// Header
System.out.printf("%-10s %6s %6s%n", (Object[]) headers);
System.out.println("-".repeat(24));
// Rows
for (Object[] row : rows) {
System.out.printf("%-10s %6d %6s%n", row);
}
Output:
Name Score Grade
------------------------
Alice 95 A
Bob 82 B
Charlie 67 D
Text blocks (Java 15+)
For multiline strings — SQL queries, JSON, HTML templates — text blocks eliminate escape sequences and visual noise:
String json = """
{
"name": "Alice",
"age": 30,
"scores": [95, 87, 100]
}
""";
String sql = """
SELECT u.name, u.email, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.total > 100
ORDER BY o.total DESC
""";
The leading whitespace common to all lines is stripped automatically based on the indentation of the closing """. Text blocks are pure String values — you can call any String method on them and pass them to String.format().
Summary
- An array is a fixed-size, zero-indexed sequence of elements of the same type. Size is set at creation and stored in
.length. Default values are applied automatically. - Arrays are reference types — assignment copies the reference. Use
Arrays.copyOf()for independent copies. java.util.ArraysprovidestoString(),sort(),binarySearch(),copyOf(),fill(), andequals()— prefer these over manual implementations.- Multi-dimensional arrays are arrays of arrays. Access elements with
[row][col]. UseArrays.deepToString()to print them. Rows can have different lengths (jagged arrays). Stringis immutable — every “modification” returns a new object. Reassign the variable to capture the result. Never test string equality with==; use.equals().- Java’s string pool reuses string literals.
new String("...")bypasses the pool — avoid it. - The
StringAPI covers searching (indexOf,contains,startsWith), extracting (substring,charAt), transforming (replace,trim,toLowerCase), splitting (split), and comparing (equals,compareTo). StringBuilderis mutable and should be used for string construction in loops. It is significantly faster than repeated+=concatenation.StringBufferis the thread-safe alternative.String.format()andprintfenable precise, template-based output formatting. Master the common specifiers:%s,%d,%f,%.2f,%n,%-Ns.- Text blocks (Java 15+) provide readable multiline string literals, ideal for SQL, JSON, and HTML.
Exercises
Exercise 1 — Array statistics Write a program that takes an array of double values {4.5, 7.2, 1.8, 9.0, 3.3, 6.6, 2.1, 8.4} and computes and prints: the minimum, maximum, sum, average, and the index of the largest element. Do not use Arrays.sort() — implement the search manually with a loop.
Exercise 2 — Array rotation Write a method rotateLeft(int[] arr, int k) that rotates an array to the left by k positions. For example, {1, 2, 3, 4, 5} rotated left by 2 becomes {3, 4, 5, 1, 2}. Handle the case where k is larger than the array length using the modulus operator. Return a new array without modifying the original.
Exercise 3 — 2D array — grade book Create a 5×4 two-dimensional int array representing grades for 5 students across 4 subjects. Fill it with reasonable values. Write methods to:
- Print the grid formatted with
printf(students as rows, subjects as columns) - Compute and print each student’s average grade
- Find and print the subject (column) with the highest overall average
Exercise 4 — String analysis Write a method analyzeString(String s) that prints:
- Total character count
- Count of vowels (a, e, i, o, u — case-insensitive)
- Count of consonants
- Count of digits
- Count of spaces
- Whether the string is a palindrome (reads the same forwards and backwards, ignoring case and spaces)
- The string reversed
Test it with "A man a plan a canal Panama".
Exercise 5 — StringBuilder word processor Write a program using StringBuilder that starts with the string "the quick brown fox jumps over the lazy dog" and:
- Capitalizes the first letter of every word
- Replaces
"fox"with"cat" - Inserts
"very "before"lazy" - Appends
" — a classic pangram" - Prints the final result and its length
Do not split into an array — work directly with StringBuilder methods (indexOf, insert, replace, delete, etc.).
Exercise 6 — Formatted table You have the following data:
Product | Unit Price | Quantity | Subtotal
Java Book | 49.99 | 3 | 149.97
IntelliJ IDEA | 249.00 | 1 | 249.00
USB Hub | 19.99 | 2 | 39.98
Write a program that stores this data in parallel arrays (or a 2D array), computes the subtotals, and prints the table formatted exactly as shown above using printf. Add a totals row at the bottom. Use String.format() to build each line rather than concatenation.
Up next: Lecture 8 — Collections Framework — where you will go beyond fixed-size arrays and discover Java’s powerful, flexible collection types: ArrayList, LinkedList, HashSet, TreeSet, HashMap, and TreeMap.
