Giới thiệu về cấu trúc mảng
Trong lập trình ứng dụng, nhu cầu lưu trữ tập hợp dữ liệu lớn luôn xuất hiện thường xuyên. Ví dụ, bạn cần quản lý danh sách điểm số của hàng trăm học sinh hoặc giá thành của nhiều mặt hàng. Nếu sử dụng từng biến riêng lẻ cho mỗi giá trị, mã nguồn sẽ trở nên cồng kềnh và khó bảo trì. Để giải quyết vấn đề này, ngôn ngữ Java cung cấp cơ chế mảng (array), cho phép gom nhóm nhiều giá trị cùng kiểu vào một đối tượng duy nhất.
Định nghĩa và bản chất
Mảng là một khối bộ nhớ liên tục dùng để lưu trữ nhiều phần tử có cùng kiểu dữ liệu. Tất cả các phần tử trong mảng đều nằm kế cận nhau trên vùng nhớ Heap, giúp tăng hiệu suất truy cập dữ liệu.
Quy trình khởi tạo
Tương tự như các tham chiếu khác, khi khai báo mảng, bạn chưa thể sử dụng ngay nó mà phải thông qua quá trình khởi tạo. Có bốn phương thức chính để tạo mới một mảng:
1. Khai báo định danh trước, gán độ dài sau
Dành cho trường hợp cần tách biệt bước định nghĩa tham chiếu với việc cấp phát bộ nhớ:
int[] dataSet; // Bước 1: Định nghĩa tên mảng
dataSet = new int[5]; // Bước 2: Cấp phát không gian chứa 5 giá trị
2. Gộp khai báo và định hình kích thước
Làm gọn cú pháp bằng cách gộp hai dòng lệnh ở trên thành một:
float[] priceList = new float[10];
- Bắt buộc phải chỉ định số lượng ô trống.
- Giá trị chiều dài phải là số nguyên dương.
3. Khởi tạo dữ liệu tĩnh kèm theo
Phương thức này vừa tạo mảng vừa đưa dữ liệu vào, độ dài mảng được tính toán tự động dựa trên số lượng phần tử:
char[] codes = new char[]{'A', 'B', 'C'};
Lưu ý: Trong dấu ngoặc vuông bên trái không được ghi thêm kích thước cụ thể.
4. Cú pháp rút gọn
Sử dụng khi không cần gọi hàm new rõ ràng:
String[] names = {"User1", "User2", "User3"};
Với cách này, nếu đã có câu lệnh định nghĩa ban đầu (ví dụ String[] n;), thì không thể gán trực tiếp chuỗi giá trị vào biến đó ngay sau dấu phân cách = bằng ngoặc nhọn {} đơn thuần mà phải dùng new String[]{"..."}.
Truy cập và thao tác dữ liệu
Để tương tác với nội dung mảng, người lập trình sử dụng số thứ tự (index). Các quy tắc quan trọng bao gồm:
- Số bắt đầu từ 0 đến tổng kích thước trừ 1.
- Lấy giá trị:
tênMảng[index] - Gán giá trị:
tênMảng[index] = giáTri; - Chiều dài cố định khi tạo: Sử dụng thuộc tính
tênMảng.length.
Nếu số thứ tự nằm ngoài phạm vi cho phép, chương trình sẽ bị ném ra ngoại lệ ArrayIndexOutOfBoundsException. Ví dụ minh họa:
// Tạo mảng chứa 5 phần tử
int[] ids = new int[5];
// Gán dữ liệu
ids[0] = 100;
ids[1] = 200;
// Truy cập lỗi sẽ gây crash
// System.out.println(ids[5]);
// Kiểm tra độ dài
System.out.println("Tổng số phần tử: " + ids.length);
Duyệt và xử lý tập hợp
Công việc lặp qua từng phần tử để thay đổi hay đọc dữ liệu thường sử dụng vòng lặp for.
Tính tổng hoặc tìm giá trị đặc biệt
Dưới đây là ví dụ về việc duyệt toàn bộ mảng để tính tổng các phần tử tìm số lớn nhất:
int[] values = {5, 15, 25, 10, 30};
int total = 0;
int maxVal = values[0];
for (int i = 0; i < values.length; i++) {
total += values[i];
if (values[i] > maxVal) {
maxVal = values[i];
}
}
System.out.println("Tổng: " + total + " | Lớn nhất: " + maxVal);
Truyền mảng vào hàm và nhận lại kết quả
Mảng được truyền vào hàm dưới dạng tham chiếu. Hàm có thể nhận mảng vào và trả về một mảng mới tùy chỉnh.
public static void main(String[] args) {
int[] inputData = {12, 7, 18, 24, 9};
// Gọi hàm xử lý lấy chẵn
int[] result = extractEven(inputData);
// In kết quả
for(int x : result) {
System.out.print(x + ", ");
}
}
public static int[] extractEven(int[] src) {
// Đếm số lượng số chẵn trước để định kích thước mảng mới
int count = 0;
for (int x : src) {
if (x % 2 == 0) count++;
}
// Cấp phát mảng đích
int[] dest = new int[count];
int idx = 0;
// Sao chép vào
for (int x : src) {
if (x % 2 == 0) {
dest[idx++] = x;
}
}
return dest;
}
Hiểu sâu về bộ nhớ và tham chiếu
Mô hình lưu trữ
Mảng là kiểu dữ liệu tham chiếu. Khi bạn khai báo int[] A, hệ thống lưu trữ địa chỉ của mảng ở ngăn xếp (Stack). Tại ngăn xếp Heap, dữ liệu thực sự được lưu giữ.
- Tham chiếu kép: Khi gán
A = B, địa chỉ của B được gán cho A. Cả hai biến đều trỏ về cùng một khối dữ liệu trên Heap. - Fresh Allocation: Mỗi lần gặp từ khóa
new, hệ thống cấp phát một vùng nhớ mới trên Heap.
Mặc dù chưa khởi tạo bởi lập trình viên, các phần tử trong mảng luôn có giá trị mặc định (default value) để đảm bảo an toàn:
| Type | Default Value |
| int, short, byte, long | 0 |
| float, double | 0.0 |
| boolean | false |
| Reference (Class, String, Array) | null |
Mở rộng kích thước mảng
Vì mảng trong Java có kích thước bất biến, để "tăng" kích thước ta phải tạo một mảng mới lớn hơn và sao chép dữ liệu cũ sang.
Thực hiện thủ công với vòng lặp
int[] oldArr = {1, 2, 3};
int newSize = oldArr.length * 2;
int[] newArr = new int[newSize];
// Sao chép từng phần tử
for(int i=0; i
Sử dụng thư viện System
Phương thức System.arraycopy giúp sao chép tối ưu hơn:
int[] buffer = new int[oldArr.length * 2];
System.arraycopy(oldArr, 0, buffer, 0, oldArr.length);
oldArr = buffer;
Sử dụng Arrays.copyOf
Trong gói java.util, phương thức copyOf trả về ngay mảng mới đã được copy đầy đủ:
import java.util.Arrays;
// ...
oldArr = Arrays.copyOf(oldArr, newSize);
Thuật toán sắp xếp dữ liệu
Việc tổ chức dữ liệu theo thứ tự là yêu cầu cốt lõi. Dưới đây là ba phương pháp phổ biến:
1. Sắp xếp nổi bọt (Bubble Sort)
So sánh cặp phần tử liền kề và đổi chỗ nếu sai thứ tự.
int[] nums = {8, 2, 9, 1};
for (int i = 0; i < nums.length - 1; i++) {
for (int j = 0; j < nums.length - 1 - i; j++) {
if (nums[j] > nums[j+1]) {
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
2. Sắp xếp chọn (Selection Sort)
Luôn tìm phần tử nhỏ nhất còn lại trong đoạn chưa sắp xếp và đưa về vị trí đầu tiên.
for (int i = 0; i < nums.length - 1; i++) {
int minIdx = i;
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[minIdx]) {
minIdx = j;
}
}
if (minIdx != i) {
int temp = nums[i];
nums[i] = nums[minIdx];
nums[minIdx] = temp;
}
}
3. Phương thức tích hợp sẵn
Để tiết kiệm thời gian, hãy dùng công cụ có sẵn của JDK:
Arrays.sort(nums); // Mặc định sắp xếp tăng dần