1. Mảng kiểu String
Và ngắn gọn, chúng ta hãy nói về mảng kiểu String.
Như đã nói, mảng có thể có bất kỳ kiểu nào. Vì vậy, ta có thể tạo mảng kiểu String. Mã sẽ trông như thế nào nếu chúng ta cần viết chương trình “nhập từ bàn phím 10 dòng và in chúng ra màn hình theo thứ tự ngược lại”.
String[] array = new String[10]; // Tạo đối tượng-mảng với 10 phần tử
for (int i = 0; i < 10; i++) // Vòng lặp từ 0 đến 9
{
array[i] = console.nextLine(); // Đọc một dòng từ bàn phím và lưu vào ô mảng
}
for (int i = 9; i >= 0; i--) // Vòng lặp từ 9 xuống 0
{
System.out.println(array[i]); // In ra màn hình phần tử tiếp theo của mảng
}
Mã hầu như không thay đổi! Chỉ cần thay kiểu int bằng String khi tạo mảng. Còn lại thì hoàn toàn giống nhau!
Làm quen với null
Đố bạn — các ô của mỗi mảng dưới đây chứa gì ngay sau khi được tạo?
int[] numbers = new int[10];
String[] strings = new String[10];
User[] users = new User[10];
Nếu suy nghĩ, nhiều khả năng các ô numbers chứa 0. Đúng vậy.
Theo cùng logic đó, các ô strings phải được điền bằng chuỗi rỗng — "". Tạm cho là thế.
Thế còn các ô của mảng users chứa gì? Những User rỗng ư?
Ở đây mới là điều tinh quái: không phải kiểu dữ liệu nào cũng có “giá trị mặc định”. Vì vậy, các tác giả Java đã tạo ra một hằng đặc biệt — null. null là một tham chiếu rỗng. Khi một biến cho kiểu đối tượng được tạo, giá trị khởi đầu của nó là null: tham chiếu không trỏ đến đâu, tức là không có tham chiếu.
String name; // name chứa null
name = "Alex"; // name chứa tham chiếu đến đối tượng/chuỗi "Alex"
name = null; // name chứa null
Làm việc với null
Không thể gọi phương thức trên một đối tượng nếu tham chiếu của nó null — vì chẳng có đối tượng nào cả! Với đoạn mã sau, chương trình sẽ kết thúc với lỗi:
String name; // name chứa null
String upperName = name.toUpperCase(); // Sẽ xảy ra lỗi NullPointerException: name == null
Còn về “câu đố” của chúng ta, câu trả lời là:
int[] numbers = new int[10]; // {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
String[] strings = new String[10]; // {null, null, null, null, null, null, null, null, null, null}
User[] users = new User[10]; // {null, null, null, null, null, null, null, null, null, null}
Chỉ các kiểu nguyên thủy mới có “giá trị mặc định”. Tất cả các kiểu còn lại mặc định có giá trị null. Giá trị khởi đầu của String không phải là chuỗi rỗng. Cuộc đời là vậy.
2. Mảng kiểu String trong bộ nhớ
Hình 1. Cách đối tượng String được bố trí trong bộ nhớ:

Xin lưu ý rằng văn bản của chuỗi không được lưu trực tiếp trong biến: một khối bộ nhớ riêng được cấp phát cho nó. Còn trong biến kiểu String lưu địa chỉ (tham chiếu) tới đối tượng chứa văn bản.
Hình 2. Cách mảng số nguyên được bố trí trong bộ nhớ:

Bạn cũng đã thấy hình này.
Hình 3. Cách mảng chuỗi được bố trí trong bộ nhớ:

Bên trái ta thấy biến-mảng kiểu String[] (lưu địa chỉ của đối tượng-mảng).
Ở giữa — đối tượng-mảng kiểu String.
Bên phải — các đối tượng chuỗi, chứa những văn bản nào đó.
Trong các ô của đối tượng-mảng kiểu String không lưu trực tiếp các chuỗi (văn bản), mà là địa chỉ (tham chiếu) của chúng. Giống hệt như trong các biến kiểu String lưu địa chỉ của chuỗi (văn bản).
3. Khởi tạo nhanh mảng trong Java
Mảng là một cấu trúc rất hữu ích, vì vậy các nhà phát triển Java đã cố gắng làm cho việc làm việc với chúng thuận tiện nhất có thể. Và điều đầu tiên họ làm — đơn giản hóa việc khởi tạo mảng, đưa các giá trị ban đầu vào nó.
Bởi vì rất thường xuyên, ngoài dữ liệu chương trình đọc từ đâu đó, nó còn cần dữ liệu nội bộ của chính mình. Ví dụ, chúng ta cần lưu độ dài (số ngày) của tất cả các tháng trong một mảng. Mã có thể trông như sau:
int[] months = new int[12];
months[0] = 31; // tháng Một
months[1] = 28; // tháng Hai
months[2] = 31; // tháng Ba
months[3] = 30; // tháng Tư
months[4] = 31; // tháng Năm
months[5] = 30; // tháng Sáu
months[6] = 31; // tháng Bảy
months[7] = 31; // tháng Tám
months[8] = 30; // tháng Chín
months[9] = 31; // tháng Mười
months[10] = 30; // tháng Mười một
months[11] = 31; // tháng Mười hai
Nhưng có cách viết ngắn gọn hơn — cảm ơn các tác giả của Java:
// độ dài (số ngày) của các tháng trong năm
int[] months = new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
Chỉ cần liệt kê tất cả các giá trị của mảng, cách nhau bởi dấu phẩy!
Thì ra trình biên dịch có thể suy ra kiểu của vùng chứa (đối tượng-mảng) dựa trên kiểu của biến mảng. Còn để xác định độ dài mảng — đơn giản đếm số phần tử được viết trong ngoặc nhọn.
Vì vậy đoạn mã này còn có thể viết ngắn hơn nữa:
// độ dài (số ngày) của các tháng trong năm
int[] months = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
Chẳng phải rất đẹp sao? 🙂 Cách viết này gọi là “khởi tạo nhanh mảng”. Nhân tiện, nó hoạt động không chỉ với kiểu int...
// tên các tháng trong năm
String[] months = { "Tháng Một", "Tháng Hai", "Tháng Ba", "Tháng Tư", "Tháng Năm", "Tháng Sáu", "Tháng Bảy", "Tháng Tám", "Tháng Chín", "Tháng Mười", "Tháng Mười một", "Tháng Mười hai" };
4. Vòng lặp for-each
Duyệt qua tất cả phần tử của mảng là thao tác xuất hiện thường xuyên đến mức người ta đã nghĩ ra một vòng lặp chuyên dụng — for-each. Nó được viết như sau:
for (int score : scores)
{
System.out.println("Điểm: " + score);
}
Ở đây vòng lặp tự duyệt các phần tử — không có chỉ số thủ công. Nhưng không thể thay đổi phần tử trực tiếp (biến của vòng lặp chỉ giữ bản sao của giá trị), bù lại rất tiện để chỉ “đi qua” mảng.
Trình biên dịch sẽ chuyển mã vòng lặp for-each thành mã dưới đây:
for (int i = 0; i < scores.length; i++)
{
int score = scores[i];
System.out.println("Điểm: " + score);
}
Vậy nên chẳng có phép màu nào cả. Và giờ bạn hiểu vì sao không thể thay đổi phần tử mảng thông qua biến score.
Hãy viết thêm ví dụ: tìm tổng tất cả điểm.
int sum = 0;
for (int score : scores)
{
sum += score;
}
System.out.println("Tổng tất cả điểm: " + sum);
Khi nào dùng vòng lặp nào?
- Nếu cần chỉ số hoặc cần thay đổi phần tử — dùng vòng lặp for.
- Nếu chỉ cần duyệt qua các giá trị — for-each nhanh và gọn hơn.
5. Thay đổi các phần tử của mảng
Nếu chúng ta cần tăng mỗi điểm lên 1 điểm (ví dụ vì hạnh kiểm tốt) thì sao? Lúc này cần vòng lặp for cổ điển với chỉ số, vì chỉ như vậy ta mới có thể thay đổi giá trị trong mảng.
Ví dụ: tăng mỗi phần tử lên 1
for (int i = 0; i < grades.length; i++) {
grades[i] = grades[i] + 1;
}
Sau khi chạy đoạn mã này, mảng grades sẽ có các giá trị mới.
Vì sao không thể làm điều này bằng for-each?
for (int grade : grades) {
grade = grade + 1; // Không có tác dụng!
}
Đoạn mã này sẽ không thay đổi mảng, vì grade — là bản sao giá trị từ mảng, chứ không phải tham chiếu tới chính phần tử.
6. Tính toán dựa trên mảng
Mảng thường được dùng cho tính toán: tổng, trung bình, lớn nhất, nhỏ nhất, v.v.
Ví dụ: tổng của tất cả phần tử mảng
int sum = 0;
for (int i = 0; i < grades.length; i++) {
sum += grades[i]; // tương đương với sum = sum + grades[i];
}
System.out.println("Tổng điểm: " + sum);
Ví dụ: tìm giá trị lớn nhất
int max = grades[0]; // bắt đầu từ phần tử đầu tiên
for (int i = 1; i < grades.length; i++) {
if (grades[i] > max) {
max = grades[i];
}
}
System.out.println("Điểm cao nhất: " + max);
Ví dụ: tìm giá trị nhỏ nhất
int min = grades[0]; // bắt đầu từ phần tử đầu tiên
for (int i = 1; i < grades.length; i++) {
if (grades[i] < min) {
min = grades[i];
}
}
System.out.println("Điểm thấp nhất: " + min);
Ví dụ: tính giá trị trung bình
int sum = 0;
for (int i = 0; i < grades.length; i++) {
sum += grades[i];
}
double average = (double) sum / grades.length; // nhất thiết phải ép sang double!
System.out.println("Điểm trung bình: " + average);
Lưu ý: để nhận được số thực thay vì số nguyên, cần ép tổng sang kiểu double trước khi chia.
7. Bài tập thực hành
Nhập mảng từ bàn phím
Thường xuyên cần điền mảng bằng các giá trị người dùng nhập. Ta dùng lớp Scanner và vòng lặp.
Scanner console = new Scanner(System.in);
int n = 5; // kích thước mảng
int[] numbers = new int[n];
System.out.println("Nhập " + n + " số:");
for (int i = 0; i < n; i++) {
numbers[i] = console.nextInt();
}
System.out.println("Bạn đã nhập:");
for (int i = 0; i < n; i++) {
System.out.println(numbers[i]);
}
In mảng theo thứ tự ngược
Đôi khi cần in các phần tử mảng từ cuối lên (ví dụ để biết ai là người vào phòng cuối cùng).
for (int i = grades.length - 1; i >= 0; i--) {
System.out.println("Điểm thứ " + (i + 1) + ": " + grades[i]);
}
8. Những lỗi thường gặp khi làm việc với mảng một chiều
Lỗi số 1: vượt quá giới hạn mảng
Vấn đề phổ biến nhất — cố truy cập phần tử không tồn tại. Trong Java điều này dẫn tới ngoại lệ ArrayIndexOutOfBoundsException. Hãy nhớ: chỉ số hợp lệ cuối cùng — arr.length - 1.
int[] arr = new int[5];
System.out.println(arr[5]); // Lỗi! Chỉ số từ 0 đến 4.
Lỗi số 2: quên khởi tạo mảng
Khai báo biến nhưng không tạo mảng:
int[] arr;
arr[0] = 5; // Lỗi! Mảng chưa được tạo.
Cần phải tạo mảng bằng new:
arr = new int[10];
Lỗi số 3: cố gắng thay đổi phần tử mảng bằng for-each
Trong vòng lặp for-each, biến là bản sao của giá trị, không phải tham chiếu tới phần tử:
for (int x : arr) {
x = 100; // Không làm thay đổi mảng!
}
Để thay đổi giá trị, hãy dùng for thường với chỉ số.
Lỗi số 4: dùng sai độ dài mảng
Đôi khi nhầm lẫn giữa độ dài mảng và chỉ số cuối cùng:
for (int i = 0; i <= arr.length; i++) { // Lỗi! Phải là i < arr.length
// ...
}
Đoạn mã như vậy sẽ dẫn tới vượt quá giới hạn.
Lỗi số 5: chuyển đổi kiểu ngầm không hợp lệ
Nếu mảng có kiểu int thì không thể gán trực tiếp một giá trị kiểu double:
int[] arr = new int[3];
arr[0] = 3.14; // Lỗi! 3.14 là double.
GO TO FULL VERSION