Chuyển tới nội dung chính

Mảng trong Java

Mảng là một đối tượng chứa dãy phần tử cùng kiểu, truy cập qua chỉ số nguyên bắt đầu từ 0. Kích thước mảng cố định ngay khi tạo và không đổi về sau; trường length cho biết số phần tử. Biến mảng là một tham chiếu trỏ tới khối phần tử liên tiếp trên vùng nhớ động; phần tử chưa gán nhận giá trị mặc định, ví dụ 0 cho kiểu số và null cho kiểu tham chiếu.

Vì sao cần mảng? Hãy tưởng tượng ta phải lưu điểm của ba mươi sinh viên. Khai báo ba mươi biến riêng lẻ là điều không tưởng, và cũng không thể viết một vòng lặp chạy qua chúng. Mảng gom cả dãy giá trị cùng kiểu vào một biến duy nhất, cho ta đánh số từng phần tử và duyệt bằng vòng lặp. Đây là cấu trúc dữ liệu nền tảng nhất, và nhiều cấu trúc phức tạp hơn như ArrayList hay bảng băm đều dựng trên mảng ở tầng dưới.

Có một điểm cần nhớ ngay: chỉ số hợp lệ chạy từ 0 tới length - 1. Mảng năm phần tử có chỉ số từ 0 tới 4, không có phần tử ở chỉ số 5. Nhầm chỗ này là nguồn gốc của lỗi vượt chỉ số mà ta sẽ bàn ở cuối bài.

Mảng là một đối tượng

Người mới hay hình dung mảng như một kiểu dữ liệu đặc biệt nằm ngoài hệ thống kiểu. Không phải. Trong Java, mảng là một đối tượng thật, sống trên vùng nhớ động giống mọi đối tượng khác, và điều đó giải thích gần hết những hành vi ta sắp gặp.

Hệ quả thứ nhất: int[] a không phải là chỗ chứa năm số nguyên, nó chỉ là một biến tham chiếu, và khi chưa gán gì thì nó mang giá trị null. Phải gọi new int[5] thì máy ảo mới cấp phát một khối nhớ thật rồi trả về tham chiếu cho biến giữ. Vì thế hai biến mảng gán cho nhau sẽ cùng trỏ tới một khối duy nhất chứ không phải hai bản sao.

int[] a = {1, 2, 3};
int[] b = a; // copies the reference, not the elements
b[0] = 99;
System.out.println(a[0]); // prints 99: a and b point to the same array

int[] c = a.clone(); // a real copy of the contents
c[1] = 50;
System.out.println(a[1]); // still 2: c is an independent array

Hệ quả thứ hai: length là một trường của đối tượng mảng, không phải phương thức. Ta viết a.length không có cặp ngoặc tròn. Đây là chỗ rất dễ lẫn vì chuỗi thì lại dùng phương thức s.length() có ngoặc, còn tập hợp lại dùng ds.size(). Ba thứ này, ba cách viết khác nhau, và trình biên dịch sẽ báo lỗi nếu ta viết a.length(). Ngoài ra length là trường chỉ đọc: mảng cố định kích thước nên không thể gán a.length = 10 để nới nó ra.

Hệ quả thứ ba: khi cấp phát bằng new, mọi ô được đặt về giá trị mặc định của kiểu phần tử, chứ không phải rác. Đây là bảo đảm của ngôn ngữ, ta dùng được ngay mà không cần khởi tạo thủ công.

Kiểu phần tửGiá trị mặc định sau new
int, long, short, byte0
double, float0.0
charký tự có mã 0
booleanfalse
mọi kiểu tham chiếunull

Cần phân biệt rõ: new int[3] cho ta ba số 0 dùng được ngay, còn new String[3] cho ta ba tham chiếu null, tức là ba ô rỗng chưa trỏ tới đối tượng nào. Đây chính là gốc rễ của lỗi tham chiếu rỗng khi làm việc với mảng đối tượng, ta sẽ quay lại ngay sau đây.

Mảng một chiều

Khai báo bằng cặp ngoặc vuông, cấp phát bằng new. Mỗi biến mảng là một tham chiếu trỏ tới khối phần tử liên tiếp trên vùng nhớ động. Vòng lặp for-each duyệt từng phần tử mà không cần chỉ số.

int[] diem = new int[5]; // five ints, default 0
diem[0] = 8; diem[1] = 6; // assign by index
int[] soChan = {2, 4, 6, 8}; // array literal

int tong = 0;
for (int x : soChan) { // for-each loop, read each element
tong += x;
}

Có ba cách khai báo và cấp phát thường gặp. Cách thứ nhất tách khai báo và cấp phát: int[] a; rồi a = new int[5];, khi này năm phần tử nhận giá trị mặc định 0. Cách thứ hai gộp cả hai vào một dòng như int[] diem = new int[5];. Cách thứ ba dùng cú pháp rút gọn với danh sách giá trị: int[] soChan = {2, 4, 6, 8};, khi này độ dài mảng suy ra từ số phần tử liệt kê. Đặt cặp ngoặc vuông cạnh kiểu như int[] a được ưa hơn cách đặt cạnh tên biến như int a[], vì nó nói rõ đây là kiểu mảng số nguyên.

Duyệt bằng for và for-each

Có hai kiểu duyệt. Vòng for dùng chỉ số cho ta biết vị trí phần tử và cho phép sửa giá trị tại chỗ. Vòng for-each gọn hơn khi ta chỉ cần đọc từng phần tử mà không quan tâm chỉ số.

int[] diem = {8, 6, 9, 7};

for (int i = 0; i < diem.length; i++) { // index loop, can modify
diem[i] = diem[i] + 1; // bump every score by one
}
for (int x : diem) { // for-each, read only
System.out.println(x);
}

Cần nhớ vòng for-each chỉ đọc một bản sao giá trị của phần tử với kiểu nguyên thủy, nên gán lại x bên trong không đổi được mảng gốc. Muốn sửa mảng thì phải dùng vòng for với chỉ số.

Cơ chế đằng sau khá đơn giản: mỗi vòng lặp, for-each lấy giá trị ở ô hiện tại rồi chép vào biến x. Với int, cái được chép là con số, nên x là một bản sao rời và ghi vào nó không chạm tới mảng. Với mảng đối tượng, cái được chép là tham chiếu, nên x trỏ tới đúng đối tượng trong mảng; gán x = new SinhVien("D") vẫn không đổi được ô của mảng, nhưng gọi x.ten = "D" thì lại sửa được chính đối tượng đó. Phân biệt hai điều này giúp tránh nhiều lần ngạc nhiên.

Vậy chọn kiểu nào? Dùng for-each khi chỉ cần đọc lần lượt từng phần tử, vì nó ngắn và không thể viết sai điều kiện dừng, tức là loại bỏ luôn nguy cơ vượt chỉ số. Dùng for có chỉ số khi cần ghi đè phần tử, cần biết vị trí, cần duyệt ngược, hoặc cần bước nhảy khác 1.

Mảng đối tượng

Phần tử mảng không nhất thiết là kiểu số. Mảng có thể chứa các tham chiếu đối tượng. Khi vừa cấp phát, mỗi ô của mảng đối tượng mang giá trị null, nghĩa là chưa trỏ tới đâu; ta phải khởi tạo từng đối tượng trước khi dùng, nếu không sẽ gặp lỗi truy cập qua tham chiếu rỗng.

class SinhVien {
String ten;
SinhVien(String ten) { this.ten = ten; }
}

public class Demo {
public static void main(String[] args) {
SinhVien[] lop = new SinhVien[3]; // three null references
lop[0] = new SinhVien("An"); // fill each slot before use
lop[1] = new SinhVien("Binh");
lop[2] = new SinhVien("Cuong");
for (SinhVien sv : lop) {
System.out.println(sv.ten); // safe: every slot is set
}
}
}

Điều then chốt là mảng đối tượng không chứa đối tượng, nó chứa tham chiếu tới đối tượng. Hình dung lop là một dãy ba ô, mỗi ô giữ một mũi tên. Lúc new SinhVien[3], cả ba mũi tên đều rỗng, tức là null. Chỉ khi gán lop[0] = new SinhVien("An") thì máy ảo mới tạo một đối tượng thật rồi cho mũi tên ô số 0 chỉ vào đó. Nếu quên bước gán mà đã gọi lop[0].ten, chương trình ném lỗi tham chiếu rỗng, vì ta đang đòi lấy trường của một thứ không tồn tại.

Vì các ô chỉ giữ mũi tên, hai ô khác nhau có thể cùng chỉ vào một đối tượng. Khi đó sửa qua ô này thì nhìn qua ô kia cũng thấy đổi, bởi cả hai nói về cùng một thực thể. Đây không phải lỗi của mảng mà là bản chất của kiểu tham chiếu, nhưng nó hay làm người mới bối rối nên cần thấy tận mắt.

class SinhVien {
String ten;
SinhVien(String ten) { this.ten = ten; }
}

public class Demo {
public static void main(String[] args) {
SinhVien[] lop = new SinhVien[2];
SinhVien an = new SinhVien("An");
lop[0] = an;
lop[1] = an; // both slots point at the SAME object
lop[0].ten = "An B"; // edit through slot 0
System.out.println(lop[1].ten); // prints "An B", not "An"
}
}

Mảng đa chiều

Java cài mảng đa chiều bằng mảng của mảng. Một mảng hai chiều thực chất là mảng các tham chiếu, mỗi tham chiếu trỏ tới một mảng con; nhờ đó các hàng có thể có độ dài khác nhau, gọi là mảng răng cưa. Ta duyệt bằng hai vòng lặp lồng nhau.

int[][] bang = new int[3][4]; // 3 rows, 4 columns
for (int i = 0; i < bang.length; i++) { // outer: rows
for (int j = 0; j < bang[i].length; j++) { // inner: columns
bang[i][j] = i * 4 + j;
}
}
int[][] rangCua = { {1}, {2, 3}, {4, 5, 6} }; // jagged: rows differ in length

Java không có mảng hai chiều thật sự theo nghĩa một khối chữ nhật liền mạch như vài ngôn ngữ khác. Cái ta gọi là mảng hai chiều chỉ là một mảng mà mỗi phần tử lại là một tham chiếu tới một mảng một chiều khác. Vì thế bang.length cho số hàng, còn bang[i].length cho số cột của riêng hàng i. Hai con số này độc lập nhau, và đó là lý do vòng lặp trong ví dụ phải hỏi bang[i].length chứ không dùng chung một hằng số cột.

Chính cấu trúc mảng của mảng cho ta mảng răng cưa: mỗi hàng có thể dài ngắn khác nhau, thậm chí một hàng có thể còn null nếu chưa cấp phát. Cách viết new int[3][] chỉ tạo ba ô rỗng để chờ, ta gán từng hàng sau, mỗi hàng một độ dài tuỳ ý. Điều này rất hợp với dữ liệu tự nhiên lởm chởm, ví dụ mỗi lớp có số sinh viên khác nhau, và nó tiết kiệm bộ nhớ hơn hẳn so với ép mọi hàng về cùng độ dài.

public class Demo {
public static void main(String[] args) {
int[][] lop = new int[3][]; // three row slots, all null for now
lop[0] = new int[]{8}; // row 0 has one element
lop[1] = new int[]{7, 9}; // row 1 has two
lop[2] = new int[]{6, 5, 10}; // row 2 has three

for (int i = 0; i < lop.length; i++) { // rows
for (int j = 0; j < lop[i].length; j++) { // this row's own length
System.out.print(lop[i][j] + " ");
}
System.out.println();
}
}
}

Lớp tiện ích Arrays

Lớp java.util.Arrays gom các thao tác thông dụng trên mảng: sắp xếp, tìm kiếm nhị phân, so sánh, sao chép, điền giá trị và in ra chuỗi. Dùng các hàm này gọn và ít lỗi hơn tự viết tay.

Tự nới một mảng cố định khi nó đầy
Thêm phần tử
Gọi phương thức thêm với giá trị mới cần đưa vào.
Kiểm tra sức chứa
So số phần tử thật với length của mảng hiện tại.
Cấp mảng lớn hơn
Nếu đầy, dùng Arrays.copyOf để cấp mảng gấp đôi và chép sang.
Gán vào ô trống
Đặt giá trị vào vị trí kế tiếp rồi tăng biến đếm.
import java.util.Arrays;

int[] a = {5, 2, 9, 1};
Arrays.sort(a); // sorts in place: {1, 2, 5, 9}
int viTri = Arrays.binarySearch(a, 5); // binary search on a sorted array
int[] b = Arrays.copyOf(a, 6); // copy and grow, padding with 0
System.out.println(Arrays.toString(a));// readable text: [1, 2, 5, 9]

Vài phương thức khác cũng hay dùng. Arrays.fill điền một giá trị cho toàn mảng, tiện khi cần khởi tạo hàng loạt. Arrays.equals so hai mảng theo từng phần tử, khác hẳn phép so == vốn chỉ so hai tham chiếu có trỏ cùng một khối nhớ hay không. Arrays.copyOfRange sao một đoạn con của mảng.

import java.util.Arrays;

int[] a = new int[4];
Arrays.fill(a, 7); // {7, 7, 7, 7}
int[] b = {7, 7, 7, 7};
System.out.println(a == b); // false: different references
System.out.println(Arrays.equals(a, b)); // true: same contents
int[] giua = Arrays.copyOfRange(a, 1, 3); // sub-array of length 2

Hai điểm hay vấp khi dùng Arrays. Thứ nhất, Arrays.toString chỉ in đẹp mảng một chiều; đưa mảng hai chiều vào nó sẽ in ra một dãy mã băm khó hiểu, vì mỗi phần tử của mảng ngoài lại là một đối tượng mảng. Với mảng nhiều chiều phải dùng Arrays.deepToString. Cùng lý do đó, so hai mảng hai chiều phải dùng Arrays.deepEquals chứ Arrays.equals chỉ so từng tham chiếu hàng.

Thứ hai, Arrays.sort sắp tăng dần theo thứ tự tự nhiên, nhưng ta đổi được tiêu chí bằng cách truyền thêm một bộ so sánh. Lưu ý bản nhận bộ so sánh chỉ chạy với mảng đối tượng, nên muốn sắp giảm dần một dãy số ta phải dùng Integer[] thay vì int[].

import java.util.Arrays;
import java.util.Comparator;

public class Demo {
public static void main(String[] args) {
int[][] bang = { {1, 2}, {3, 4} };
System.out.println(Arrays.deepToString(bang)); // [[1, 2], [3, 4]]

Integer[] a = {5, 2, 9}; // boxed, so a Comparator works
Arrays.sort(a, Comparator.reverseOrder()); // descending
System.out.println(Arrays.toString(a)); // [9, 5, 2]
}
}
Ghi nhớ

Arrays.binarySearch chỉ cho kết quả đúng khi mảng đã được sắp xếp trước. Mảng có kích thước cố định; khi cần một tập dữ liệu co giãn theo thời gian chạy, hãy nghĩ tới khung tập hợp sẽ học ở chương 3, ví dụ ArrayList.

Mảng so với ArrayList

Mảng và ArrayList đều lưu một dãy phần tử, nhưng khác nhau ở vài điểm cốt lõi. Mảng có kích thước cố định, ấn định ngay khi tạo và không đổi; ArrayList tự nới khi thêm phần tử. Mảng dùng cú pháp chỉ số a[i] và trường length; ArrayList dùng phương thức get(i), add(x)size(). Mảng chứa được cả kiểu nguyên thủy như int; ArrayList chỉ chứa đối tượng, nên số nguyên phải bọc thành Integer.

import java.util.ArrayList;

int[] mang = {1, 2, 3};
mang[0] = 10; // fixed size, index access
System.out.println(mang.length); // field, no parentheses

ArrayList<Integer> ds = new ArrayList<>();
ds.add(1); ds.add(2); ds.add(3); // grows on demand
ds.set(0, 10); // method call, not index syntax
System.out.println(ds.size()); // method, with parentheses

Quy tắc chọn: khi biết trước số phần tử và cần hiệu năng sát phần cứng, dùng mảng. Khi số phần tử thay đổi theo thời gian chạy hoặc cần thêm bớt linh hoạt, dùng ArrayList.

Nói thêm cho rõ đánh đổi. ArrayList không phải cấu trúc khác hẳn: bên trong nó vẫn là một mảng, chỉ khác ở chỗ khi đầy thì tự cấp mảng lớn hơn rồi chép sang, đúng như bài tập 2 ở cuối bài yêu cầu làm tay. Cái ta trả giá là mỗi phần tử phải là đối tượng, nên ArrayList<Integer> lưu tham chiếu tới các đối tượng Integer chứ không lưu thẳng số: tốn thêm bộ nhớ, thêm một lần nhảy con trỏ mỗi lần đọc, và thêm chi phí bọc hoặc mở bọc số. Với vài nghìn phần tử thì không ai thấy khác biệt, nhưng với hàng chục triệu số trong một vòng lặp nóng thì int[] thắng rõ rệt.

Thực tế nên mặc định chọn ArrayList vì nó an toàn và tiện, rồi chỉ hạ xuống mảng thô khi có lý do đo đạc được về bộ nhớ hoặc tốc độ. Mảng vẫn cần cho những thứ có kích thước cố định tự nhiên, ví dụ tham số String[] args của main.

Lỗi thường gặp

Lỗi vượt chỉ số ArrayIndexOutOfBoundsException xảy ra khi truy cập một chỉ số ngoài khoảng hợp lệ, thường vì vòng lặp viết i <= a.length thay vì i < a.length, khiến nó chạm tới chỉ số bằng length vốn không tồn tại. Với mảng đối tượng, truy cập một ô còn null sẽ ném NullPointerException; phải khởi tạo đối tượng cho ô đó trước khi dùng. Cuối cùng, đừng dùng == để so nội dung hai mảng vì nó chỉ so tham chiếu; hãy dùng Arrays.equals.

int[] a = {1, 2, 3};
for (int i = 0; i <= a.length; i++) { // BUG: i reaches 3, out of bounds
System.out.println(a[i]); // throws at i == 3
}
// fix: use i < a.length

Hãy nhìn kỹ nguyên nhân gốc của lỗi vượt chỉ số. Nó gần như luôn quy về một trong ba chỗ. Một là lệch một đơn vị ở điều kiện dừng, tức viết i <= a.length thay vì i < a.length, vì ta lẫn giữa số lượng phần tử và chỉ số cuối cùng. Số lượng là length, còn chỉ số cuối là length - 1. Hai là trộn chỉ số của hai mảng khác độ dài trong cùng một vòng lặp, ví dụ duyệt theo a.length nhưng lại đọc b[i] trong khi b ngắn hơn. Ba là với mảng hai chiều răng cưa, ta lấy độ dài của hàng đầu rồi áp cho mọi hàng, trong khi mỗi hàng có length riêng.

Cách phòng tốt nhất không phải là cẩn thận hơn mà là né hẳn: chỗ nào chỉ cần đọc thì dùng for-each, vì khi đó không có biến chỉ số nào để viết sai. Chỉ dùng vòng for có chỉ số khi thật sự cần ghi đè hoặc cần biết vị trí.

Ba nhầm lẫn về length và giá trị mặc định

Viết a.length() cho mảng là sai. Mảng dùng trường a.length không ngoặc, chuỗi dùng phương thức s.length() có ngoặc, tập hợp dùng ds.size(). Nguyên nhân gốc: ba thứ này là ba kiểu khác nhau, Java không thống nhất cách hỏi độ dài.

Tưởng new SinhVien[3] đã tạo ra ba sinh viên. Không, nó tạo ba tham chiếu null. Phải gán từng đối tượng vào từng ô trước khi dùng, nếu không sẽ gặp NullPointerException. Nguyên nhân gốc: giá trị mặc định của kiểu tham chiếu là null, khác với kiểu số là 0.

Tưởng gán mảng cho nhau là sao chép. int[] b = a; chỉ chép tham chiếu, hai biến trỏ chung một khối nhớ nên sửa qua b thì a cũng đổi. Muốn bản sao thật hãy dùng a.clone() hoặc Arrays.copyOf.

Bài tập thực hành

Bài tập 1: bảng hai chiều và lớp Arrays

Viết chương trình tạo mảng hai chiều int[3][4] và điền giá trị i*4+j cho mỗi ô. Dùng lớp Arrays để in từng hàng, sau đó sắp xếp một hàng bất kỳ và tìm kiếm nhị phân một giá trị trong hàng đã sắp.

Lời giải chi tiết

Bước 1: hai vòng lặp lồng nhau điền i*4+j. Bước 2: dùng Arrays.toString in từng hàng. Bước 3: Arrays.sort sắp một hàng, rồi Arrays.binarySearch trên hàng đã sắp đó.

import java.util.Arrays;

public class Demo {
public static void main(String[] args) {
int[][] bang = new int[3][4];
for (int i = 0; i < bang.length; i++) { // fill the table
for (int j = 0; j < bang[i].length; j++) {
bang[i][j] = i * 4 + j;
}
}
for (int[] hang : bang) { // print each row
System.out.println(Arrays.toString(hang));
}
Arrays.sort(bang[1]); // sort one row in place
int viTri = Arrays.binarySearch(bang[1], 6); // search the sorted row
System.out.println("Vi tri cua 6: " + viTri);
}
}

Ý chính: lớp Arrays cho ta in, sắp xếp và tìm kiếm gọn gàng; nhớ rằng binarySearch chỉ đúng trên mảng đã sắp.

Bài tập 2: lớp TapSo tự nới mảng

Cài đặt lớp TapSo bao một mảng int bên trong và cung cấp phương thức thêm phần tử (tự nới mảng bằng Arrays.copyOf khi đầy), lấy tổng và lấy lớn nhất. So sánh cách làm này với việc dùng ArrayList sẽ học ở chương 3.

Lời giải chi tiết

Bước 1: giữ một mảng và một biến đếm số phần tử thật. Bước 2: khi đầy thì gọi Arrays.copyOf để cấp mảng lớn gấp đôi rồi chép sang. Bước 3: phương thức lấy tổng và lớn nhất chỉ duyệt trong phạm vi n phần tử thật.

import java.util.Arrays;

class TapSo {
private int[] data = new int[2]; // backing array, grows on demand
private int n = 0; // number of real elements

void them(int x) {
if (n == data.length) { // full: grow first
data = Arrays.copyOf(data, data.length * 2);
}
data[n++] = x;
}
int tong() {
int s = 0;
for (int i = 0; i < n; i++) s += data[i];
return s;
}
int lonNhat() {
if (n == 0) throw new IllegalStateException("tap rong"); // guard: no elements yet
int m = data[0];
for (int i = 1; i < n; i++) if (data[i] > m) m = data[i];
return m;
}
}

public class Demo {
public static void main(String[] args) {
TapSo t = new TapSo();
t.them(5); t.them(2); t.them(9); // triggers a resize at the third add
System.out.println(t.tong()); // prints 16
System.out.println(t.lonNhat()); // prints 9
}
}

Ý chính: Arrays.copyOf cho ta tự nới mảng cố định, nhưng so với ArrayList ở chương 3 thì cách thủ công này dài dòng và dễ sai hơn.

Bài tập 3: bảng điểm răng cưa

Mỗi lớp có số sinh viên khác nhau. Hãy dùng mảng răng cưa int[][] để lưu điểm của ba lớp, rồi tính điểm trung bình của từng lớp và tìm lớp có trung bình cao nhất. Chú ý mỗi hàng có length riêng.

Lời giải chi tiết

Bước 1: khai báo mảng răng cưa bằng cú pháp liệt kê, mỗi hàng một độ dài. Bước 2: vòng ngoài duyệt các lớp, vòng trong duyệt đúng diem[i].length phần tử của lớp đó, đây là chỗ mấu chốt vì nếu lấy độ dài của hàng đầu áp cho mọi hàng thì sẽ vượt chỉ số. Bước 3: chia tổng cho số phần tử của chính hàng ấy để ra trung bình, rồi so dần để giữ lớp tốt nhất. Ép về double khi chia, nếu không phép chia hai số nguyên sẽ cắt cụt phần thập phân.

public class Demo {
public static void main(String[] args) {
int[][] diem = {
{8, 6, 9}, // lop 0: three students
{7, 10}, // lop 1: two students
{5, 6, 7, 8} // lop 2: four students
};

int lopTotNhat = 0;
double tbTotNhat = -1;

for (int i = 0; i < diem.length; i++) {
int tong = 0;
for (int j = 0; j < diem[i].length; j++) { // this row's own length
tong += diem[i][j];
}
double tb = (double) tong / diem[i].length; // cast to avoid integer division
System.out.println("Lop " + i + " trung binh " + tb);
if (tb > tbTotNhat) {
tbTotNhat = tb;
lopTotNhat = i;
}
}
System.out.println("Lop cao nhat: " + lopTotNhat);
}
}

Ý chính: mảng hai chiều là mảng của mảng, nên mỗi hàng tự mang độ dài của mình. Luôn hỏi diem[i].length thay vì giả định mọi hàng dài bằng nhau.

Câu hỏi tự kiểm

Kiểm tra nhanh: mảng0/4 đúngchưa trả lời
  1. 1Với mảng int[] a, cách lấy số phần tử đúng là gì?
  2. 2Sau khi chạy new SinhVien[3], ba ô của mảng chứa gì?
  3. 3Sau int[] b = a; rồi b[0] = 99; thì a[0] bằng bao nhiêu?
  4. 4Với mảng hai chiều răng cưa int[][] m, biểu thức nào cho số cột của riêng hàng i?

Tóm tắt

Mảng là một đối tượng trên vùng nhớ động, còn biến mảng chỉ là tham chiếu tới nó. Gán mảng cho nhau là chép tham chiếu chứ không chép nội dung, muốn bản sao thật phải dùng clone hoặc Arrays.copyOf.

length là trường, viết không ngoặc, và không sửa được vì mảng cố định kích thước. Sau new, mọi ô nhận giá trị mặc định: 0 cho kiểu số, false cho boolean, null cho kiểu tham chiếu. Mảng đối tượng chỉ chứa tham chiếu, nên phải khởi tạo từng đối tượng trước khi dùng.

Duyệt bằng for-each khi chỉ đọc vì nó không thể sai chỉ số; dùng for có chỉ số khi cần ghi đè hoặc cần biết vị trí. Mảng nhiều chiều là mảng của mảng nên có thể răng cưa, và mỗi hàng có length riêng.

Lớp Arrays gom sắp xếp, tìm kiếm nhị phân, sao chép, so sánh và in; nhớ binarySearch chỉ đúng trên mảng đã sắp, và mảng nhiều chiều cần deepToString với deepEquals. Cần một dãy co giãn theo thời gian chạy thì dùng ArrayList, còn mảng giữ vai trò khi kích thước cố định tự nhiên hoặc khi cần hiệu năng sát phần cứng.

Ghi nhớ

Mảng là kiểu tham chiếu kích thước cố định, truy cập qua chỉ số từ 0 và trường length. Mảng đa chiều là mảng của mảng nên có thể răng cưa. Lớp Arrays gom các thao tác thông dụng; cần co giãn theo thời gian chạy thì dùng ArrayList ở chương 3.