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, byte | 0 |
double, float | 0.0 |
char | ký tự có mã 0 |
boolean | false |
| mọi kiểu tham chiếu | null |
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.
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]
}
}
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)
và 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 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í.
length và giá trị mặc địnhViế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
- 1Với mảng int[] a, cách lấy số phần tử đúng là gì?
- 2Sau khi chạy new SinhVien[3], ba ô của mảng chứa gì?
- 3Sau int[] b = a; rồi b[0] = 99; thì a[0] bằng bao nhiêu?
- 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.
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.