Mảng một chiều
Xem thửTình huống mở đầu
Cô Lan nhập điểm kiểm tra của 30 học viên vào một mảng scores. Trước khi xếp hạng, cô viết một dòng int[] backup = scores; để giữ lại thứ tự nhập ban đầu. Xếp hạng xong, cô mở backup ra xem: 30 điểm cũng đã bị xếp theo thứ tự mới, thứ tự nhập mất sạch. Không có dòng chữ đỏ nào hiện ra.
Cô chưa hề sao chép điểm nào cả, vậy dòng int[] backup = scores; đã làm gì? Giữ câu hỏi này trong đầu, đến giữa bài bạn sẽ tự trả lời được, rồi cuối bài tự sửa nó.
Ôn nhanh bài trước: khi bạn truyền một biến int vào phương thức, phương thức nhận chính biến đó hay một bản sao của giá trị?
Đáp án: một bản sao của giá trị, đó là pass-by-value. Hãy nhớ câu này, vì với mảng, "giá trị" được sao chép lại là một thứ khá đặc biệt.
Mục tiêu bài học
Sau bài này, bạn có thể:
- Tạo được mảng, nói đúng giá trị mặc định của từng ô và chỉ số nào hợp lệ.
- Viết được vòng lặp
forhoặcfor-eachđể tính tổng, tìm số lớn nhất và đếm phần tử. - Giải thích được điều gì xảy ra trong bộ nhớ khi gán
b = agiữa hai biến mảng. - Tạo được bản sao độc lập của mảng và đọc đúng thông báo lỗi chỉ số.
Bài này đo được bằng năm câu trong phần Tự kiểm tra, mỗi câu ghi rõ nó kiểm tra mục tiêu nào.
Hãy xem video đầu tiên để có cái nhìn tổng quát về mảng.
Video thứ hai nhìn lại cùng chủ đề từ một cách giảng khác, bạn xem thêm để chắc hơn.
Bức tranh lớn (Big Picture)
Trước khi vào chi tiết, hãy xem một mảng nằm trong bộ nhớ ra sao.
Cách đọc hình: ô trắng ở trên là biến scores, ô này chỉ ghi địa chỉ của mảng. Mũi tên "address of the array" đi xuống khối năm ô nằm ngang bên dưới: Tủ 0 đến Tủ 4. Ba tủ màu vàng đã có điểm (8, 6, 9), hai tủ màu xám bằng 0 vì chưa ai ghi vào. Màu chỉ là tín hiệu phụ, mỗi ô đều có nhãn chữ.
Có hai thứ tách biệt: biến và mảng. Biến nhỏ, chỉ giữ địa chỉ. Mảng là một khối liền nhau, có kích thước cố định, tủ nào cũng có số thứ tự bắt đầu từ 0.
Xuyên suốt bài, mình dùng một hình ảnh: mảng là một dãy tủ khoá đánh số ở phòng gym, còn biến là mảnh giấy ghi "dãy tủ này nằm ở đâu". Dự án xuyên suốt là bảng điểm của lớp học cô Lan: mỗi tủ giữ điểm của một học viên. Có chỗ hình ảnh này không còn đúng: dãy tủ thật có thể lắp thêm tủ, còn mảng thì không, và mình sẽ chỉ rõ chỗ đó khi tới.
Khái niệm cốt lõi
Bài này có bốn khái niệm. Mỗi khái niệm đi kèm một hình.
Khái niệm 1: mảng có kích thước cố định, chỉ số từ 0
Mảng (array) là một dãy ô liền nhau, cùng kiểu, và số ô cố định ngay lúc tạo. Trong phòng gym, đó là dãy tủ khoá: đã dựng bốn tủ thì có bốn tủ. Chỉ số (index) là số thứ tự của tủ, bắt đầu từ 0. length là số tủ của cả dãy, và nó là một thuộc tính nên không có dấu ngoặc: scores.length.
Mỗi ô của mảng mới tạo đã có sẵn giá trị mặc định: số là 0, boolean là false, còn kiểu đối tượng như String là null.
Cách đọc hình: ô trắng trên cùng là new int[4], bốn mũi tên đi tới bốn tủ màu vàng từ Tủ 0 đến Tủ 3, tủ nào cũng ghi "0 by default". Hai ô màu đỏ ở dưới là data[-1] và data[4], nối bằng nét đứt vì chúng chỉ tới những tủ không có thật. Chỉ số hợp lệ luôn từ 0 đến length - 1.
Chỗ hình ảnh không còn đúng: ở phòng gym bạn xin thêm được tủ, còn mảng tạo xong thì không thể thêm hay bớt ô. Muốn nhiều ô hơn, bạn phải tạo một mảng mới (bạn sẽ làm điều đó ở phần sao chép).
Khái niệm 2: duyệt mảng bằng for và for-each
Duyệt (traverse) là đi qua từng ô. Có hai cách. Vòng for theo chỉ số cho bạn biết ô đang đứng ở vị trí nào và cho phép ghi vào ô. Vòng for-each (for (int x : data)) gọn hơn, nhưng biến x chỉ là một bản sao của giá trị trong ô.
Cách đọc hình: hàng trên: Tủ 1 giữ 6, giá trị này được chép ra biến score của for-each, và gán score = 0 chỉ đổi bản sao (ô màu đỏ). Hàng dưới: câu scores[1] = 0 đi thẳng vào Tủ 1 nên tủ đó thật sự thành 0 (hai ô màu lục).
Quy tắc dễ nhớ: chỉ đọc thì dùng for-each, cần ghi hoặc cần biết vị trí thì dùng for với chỉ số.
Khái niệm 3: biến mảng chỉ giữ địa chỉ, nên b = a không sao chép mảng
Đây là khái niệm quan trọng nhất của bài, và là câu trả lời cho tình huống của cô Lan. Biến mảng giữ địa chỉ (tham chiếu) của mảng, nên b = a chỉ chép địa chỉ. Kết quả là hai biến cùng trỏ vào một mảng: mảnh giấy thứ hai ghi cùng một địa chỉ dãy tủ.
Bạn có thể chạy thử đoạn nhỏ sau:
AliasDemo.java
import java.util.Arrays;
public class AliasDemo {
public static void main(String[] args) {
int[] a = {1, 2, 3};
// Chỉ chép địa chỉ, không tạo mảng mới: a và b là hai tên của một mảng
int[] b = a;
// Ghi qua b, nhưng a cũng thấy vì cùng một mảng
b[0] = 99;
System.out.println("a " + Arrays.toString(a) + ", same " + (a == b));
// clone tạo mảng thứ hai độc lập có cùng nội dung
int[] c = a.clone();
c[1] = 77;
System.out.println("a " + Arrays.toString(a) + ", c " + Arrays.toString(c));
}
}
Kết quả khi chạy [Đã chạy, JDK 17.0.15]:
a [99, 2, 3], same true
a [99, 2, 3], c [99, 77, 3]
Đọc code từng dòng:
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
int[] a = {1, 2, 3}; | Tạo một mảng ba ô và cho a giữ địa chỉ của nó | Cú pháp {...} vừa tạo mảng vừa điền giá trị, độ dài tự bằng 3 |
int[] b = a; | Chép địa chỉ trong a sang b, không tạo mảng mới | Đây là dòng gây ra lỗi của cô Lan: có hai tên nhưng chỉ một mảng |
b[0] = 99; | Đi theo địa chỉ trong b và ghi 99 vào tủ 0 | Mảng bị đổi là mảng chung, nên a cũng thấy |
(a == b) | So sánh hai địa chỉ | Cho true vì hai biến giữ cùng một địa chỉ |
int[] c = a.clone(); | Tạo mảng mới cùng độ dài rồi chép từng giá trị sang | Đây mới là bản sao thật, c giữ một địa chỉ khác |
c[1] = 77; | Ghi 77 vào tủ 1 của mảng mới | Chỉ đổi mảng của c, a giữ nguyên |
Hãy xem từng bước máy thực hiện:
Đang tải Animation…
Bản chữ tương đương của animation:
| Bước | Dòng chạy | Mảng thứ nhất | Mảng thứ hai | Điều xảy ra |
|---|---|---|---|---|
| 1 | int[] a = {1, 2, 3} | [1, 2, 3] | chưa có | a trỏ tới mảng thứ nhất |
| 2 | int[] b = a | [1, 2, 3] | chưa có | b cùng trỏ tới mảng thứ nhất |
| 3 | b[0] = 99 | [99, 2, 3] | chưa có | Ghi qua b, a thấy ngay |
| 4 | int[] c = a.clone() | [99, 2, 3] | [99, 2, 3] | c trỏ tới mảng mới |
| 5 | c[1] = 77 | [99, 2, 3] | [99, 77, 3] | Chỉ mảng thứ hai đổi |
Concept Check 1. Cho int[] x = {5, 6}; int[] y = x; y = new int[]{7, 8};. Giá trị x[0] là gì? Hãy chọn trước khi đọc tiếp.
- A. 7, vì x và y trỏ chung một mảng nên đổi y là đổi x.
- B. 5, vì dòng cuối cho y trỏ sang mảng mới còn x vẫn trỏ mảng cũ.
- C. Lỗi biên dịch, vì y đã nhận một giá trị trước đó rồi.
- D. 0, vì mảng mới ghi đè lên mảng cũ ở cùng địa chỉ.
Đáp án: B [Đã chạy: x[0] là 5 và y[0] là 7]. A nhầm gán lại biến y với ghi vào mảng: y = new int[]{...} đổi mảnh giấy của y chứ không đụng tới dãy tủ cũ, chỉ y[0] = ... mới ghi vào mảng. C sai vì biến không phải final thì gán lại tuỳ ý. D sai vì new luôn tạo một mảng ở chỗ khác.
Khái niệm 4: sao chép thật bằng clone, Arrays.copyOf và System.arraycopy
Sao chép (copy) nghĩa là tạo một mảng thứ hai rồi chép từng giá trị sang. Có ba công cụ có sẵn. a.clone() cho mảng cùng độ dài. Arrays.copyOf(a, n) cho mảng độ dài n mà bạn chọn: nếu n lớn hơn thì các ô thêm vào mang giá trị mặc định, nhỏ hơn thì cắt bớt phần đuôi. System.arraycopy(...) chép vào một mảng đã có sẵn. Đây cũng là cách duy nhất để "đổi kích thước" mảng: tạo mảng mới dài hơn rồi chép sang.
Cách đọc hình: ô trắng trên cùng là mảng nguồn [4, 5, 7]. Ba mũi tên đi tới ba ô lục: a.clone() cho mảng mới [4, 5, 7], Arrays.copyOf(a, 5) cho mảng mới [4, 5, 7, 0, 0], còn System.arraycopy chép vào một mảng đã tồn tại. Cả ba đều không để hai biến dùng chung một mảng, trừ khi bạn tự gán b = a.
Ví dụ đầu tiên (Worked Example) và "Xem từng bước"
Đây là chương trình đầu tiên của dự án: bảng điểm lớp cô Lan có năm chỗ nhưng mới nhập ba điểm.
ScoreBoard.java
import java.util.Arrays;
public class ScoreBoard {
public static void main(String[] args) {
// Tạo mảng 5 ô, mỗi ô tự mang giá trị mặc định 0
int[] scores = new int[5];
scores[0] = 8;
scores[1] = 6;
scores[2] = 9;
System.out.println("length " + scores.length);
System.out.println(Arrays.toString(scores));
int total = 0;
// Lấy ô đầu làm ứng viên điểm cao nhất để có cái mà so sánh
int best = scores[0];
// i chạy từ 0 đến length - 1, dùng dấu nhỏ hơn chứ không dùng nhỏ hơn hoặc bằng
for (int i = 0; i < scores.length; i++) {
total += scores[i];
// Dấu lớn hơn nên khi hai điểm bằng nhau, điểm gặp trước được giữ
if (scores[i] > best) {
best = scores[i];
}
}
System.out.println("total " + total + ", best " + best);
}
}
Predict: hãy dừng lại, lấy giấy nháp và đoán: chương trình in ra những dòng nào, và hai tủ chưa ghi có giá trị gì? Đoán xong rồi hãy đọc tiếp.
Kết quả mong đợi [Đã chạy, JDK 17.0.15]:
length 5
[8, 6, 9, 0, 0]
total 23, best 9
Đọc code từng dòng:
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
import java.util.Arrays; | Báo cho Java biết bạn dùng lớp Arrays | Arrays nằm trong gói java.util, không tự có sẵn |
int[] scores = new int[5]; | Tạo mảng năm ô toàn số 0, cho scores giữ địa chỉ | int[] là kiểu mảng int, số 5 cố định độ dài từ đây |
scores[0] = 8; đến scores[2] = 9; | Ghi giá trị vào tủ 0, 1, 2 | Gán theo chỉ số, hai tủ cuối giữ nguyên giá trị mặc định 0 |
scores.length | Đọc số tủ của mảng, cho 5 | Là thuộc tính, không có dấu ngoặc |
Arrays.toString(scores) | Biến mảng thành chuỗi dạng [8, 6, 9, 0, 0] | In thẳng scores chỉ ra tên kiểu và mã băm, không ra các phần tử |
int total = 0; và int best = scores[0]; | Chuẩn bị hai biến cho vòng lặp | best lấy tủ đầu để có cái mà so sánh, không lấy 0 vì điểm có thể âm trong bài khác |
for (int i = 0; i < scores.length; i++) | Cho i chạy từ 0 đến 4 | Dùng < chứ không dùng <=, vì tủ cuối là length - 1 |
total += scores[i]; | Cộng giá trị tủ i vào tổng | Mỗi vòng đọc đúng một tủ |
if (scores[i] > best) | Nếu tủ này cao hơn best thì cập nhật | Dùng dấu lớn hơn, nên khi có hai điểm bằng nhau thì giữ điểm gặp trước |
Bây giờ xem từng bước:
Đang tải Animation…
Bảng trace (bản chữ tương đương của animation, [Suy luận] bằng cách đi từng dòng và khớp với kết quả chạy thật):
| Dòng chạy | Tủ 0 | Tủ 1 | Tủ 2 | Tủ 3 | Tủ 4 | total | best |
|---|---|---|---|---|---|---|---|
new int[5] | 0 | 0 | 0 | 0 | 0 | chưa có | chưa có |
| ba dòng gán | 8 | 6 | 9 | 0 | 0 | chưa có | chưa có |
total = 0, best = scores[0] | 8 | 6 | 9 | 0 | 0 | 0 | 8 |
| vòng i bằng 0 | 8 | 6 | 9 | 0 | 0 | 8 | 8 |
| vòng i bằng 1 | 8 | 6 | 9 | 0 | 0 | 14 | 8 |
| vòng i bằng 2 | 8 | 6 | 9 | 0 | 0 | 23 | 9 |
| vòng i bằng 3 và 4 | 8 | 6 | 9 | 0 | 0 | 23 | 9 |
Bạn vừa làm được: đọc một chương trình mảng và nói đúng tủ nào chứa gì, hai biến tổng và lớn nhất đổi ra sao sau từng vòng.
Concept Check xen kẽ
Chọn đáp án của bạn trước, rồi đọc phần đáp án ngay bên dưới mỗi câu.
Concept Check 2. Cho int[] a = new int[4]; rồi vòng for (int i = 0; i <= 4; i++) { a[i] = i; }. Điều gì xảy ra?
- A. Chạy tốt và mảng tự giãn thành
[0, 1, 2, 3, 4]. - B. Lỗi biên dịch, vì trình biên dịch thấy ngay chỉ số 4 vượt quá.
- C. Bốn tủ đầu được ghi, rồi ném
ArrayIndexOutOfBoundsExceptionở vòng thứ năm. - D. Không có lỗi nào, lần ghi ra ngoài cuối cùng bị bỏ qua im lặng.
Đáp án: C [Đã chạy: mảng là [0, 1, 2, 3], thông báo Index 4 out of bounds for length 4]. A sai vì mảng không tự giãn. B sai vì giá trị i chỉ biết lúc chạy nên biên dịch vẫn qua. D sai vì Java luôn kiểm tra chỉ số, ghi ra ngoài là lỗi chứ không bị bỏ qua.
Concept Check 3. Cho int[] d = {1, 2, 3}; rồi for (int v : d) { v = v * 2; }. Mảng d sau đó là gì?
- A.
[1, 2, 3], vìvchỉ là bản sao nên gán vào nó không chạm tới mảng. - B.
[2, 4, 6], vì mỗi ô được nhân đôi khivđổi. - C. Lỗi biên dịch, vì không được gán lại biến của vòng for-each.
- D.
[2, 2, 3], vì chỉ lần lặp đầu tiên có hiệu lực với mảng.
Đáp án: A [Đã chạy]. B là hiểu lầm hay gặp nhất: biến v không phải là ô của mảng mà là bản sao giá trị của ô. C sai vì gán biến vòng lặp vẫn hợp lệ, chỉ là vô ích. D không có cơ chế nào giải thích được kết quả này.
Làm cùng mình (Guided Practice)
Bạn sẽ dựng dần class ClassScores qua năm bước nhỏ. Mỗi bước thêm vào bên trong main của bước trước, hỗ trợ giảm dần: bước 1 gần như làm sẵn, bước 5 chỉ còn đề. Gõ vào file ClassScores.java rồi chạy bằng java ClassScores.java.
Bước 1: đọc đầu, cuối và độ dài.
import java.util.Arrays;
public class ClassScores {
public static void main(String[] args) {
int[] scores = {8, 6, 9, 7, 10};
// TODO 1: thay ba số 0 bằng length, phần tử đầu và phần tử cuối
System.out.println("step 1: " + 0 + " " + 0 + " " + 0);
}
}
Chạy xong bạn sẽ thấy: step 1: 5 8 10. Gợi ý: phần tử cuối nằm ở chỉ số scores.length - 1.
Bước 2: tổng và điểm trung bình. Ngay dưới dòng in của bước 1, thêm:
int total = 0;
// TODO 2: dùng vòng for cộng mọi điểm vào total
double average = 0;
System.out.println("step 2: total " + total + " average " + average);
Chạy xong bạn sẽ thấy: step 2: total 40 average 8.0. Gợi ý: average là total chia length, nhớ ép một vế sang double kẻo phép chia nguyên cắt phần lẻ.
Bước 3: giữ bản gốc trước khi sắp xếp.
int[] backup = scores;
// TODO 3: sửa dòng trên để backup là một bản sao thật
Arrays.sort(scores);
System.out.println("step 3: scores " + Arrays.toString(scores) + " backup " + Arrays.toString(backup));
Chạy xong bạn sẽ thấy: step 3: scores [6, 7, 8, 9, 10] backup [8, 6, 9, 7, 10].
Vẽ ra giấy: vẽ hai biến scores và backup, rồi vẽ các mảng mà chúng trỏ tới, trước và sau khi sửa dòng đầu. Đối chiếu: nếu để backup = scores thì chỉ có một mảng, hai mũi tên cùng chỉ vào đó, nên sắp xếp làm cả hai thay đổi. Sau khi sửa phải có hai mảng riêng. Gợi ý: công cụ nằm ở khái niệm 4.
Bước 4: đếm điểm giỏi. Bạn tự quyết cách viết.
int high = 0;
// TODO 4: đếm số điểm từ 8 trở lên bằng for-each
System.out.println("step 4: " + high);
Chạy xong bạn sẽ thấy: step 4: 3. Gợi ý: mỗi lần gặp điểm đủ điều kiện thì tăng high; for-each phù hợp vì bước này chỉ đọc.
Bước 5: nới chỗ cho hai học viên mới.
// TODO 5: tạo mảng bigger dài 7, chứa các điểm của scores, hai ô cuối bằng 0
int[] bigger = scores;
System.out.println("step 5: " + Arrays.toString(bigger));
Chạy xong bạn sẽ thấy: step 5: [6, 7, 8, 9, 10, 0, 0]. Gợi ý: mảng không giãn được, nên phải tạo mảng mới. Lời giải đầy đủ nằm ở cuối bài.
Bạn vừa làm được: tự dựng một chương trình mảng qua năm bước, và tự phân biệt được chỗ nào dùng chung mảng, chỗ nào sao chép thật. Toàn bộ mã nguồn của bài cũng nằm trong thư mục 04-mang/01-mang-mot-chieu của kho GitHub, nhưng bạn không cần mở nó để học.
Thử phá nó đi (Lỗi thường gặp)
Bốn lỗi dưới đây là những lỗi người mới gặp nhiều nhất với mảng. Mỗi dòng đã được chạy thật trên mảng {8, 6, 9, 7, 10} hoặc mảng tương tự [Đã chạy, JDK 17.0.15; thông báo lỗi có thể khác chữ ở phiên bản Java khác].
| Code lỗi | Thông báo lỗi | Nguyên nhân bằng lời thường | Cách sửa |
|---|---|---|---|
scores[5] khi mảng có 5 phần tử | ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5 | Tủ đánh số từ 0 nên mảng năm tủ chỉ có tủ 0 đến 4 | Dùng chỉ số từ 0 đến scores.length - 1, vòng lặp viết i < scores.length |
int[] backup = scores; rồi sắp xếp scores | Không báo lỗi, backup cũng thành [6, 7, 8, 9, 10] | Hai biến cùng giữ một địa chỉ nên chỉ có một mảng | int[] backup = Arrays.copyOf(scores, scores.length); |
names[1].length() với String[] names = new String[3] | NullPointerException: Cannot invoke "String.length()" because "<local3>[1]" is null | Mảng chuỗi mới tạo toàn null, gọi phương thức trên null là lỗi | Gán chuỗi vào ô trước, hoặc kiểm tra names[1] != null |
int n = scores.length(); | error: cannot find symbol kèm symbol: method length() | Với mảng, length là thuộc tính, không phải phương thức | Viết scores.length không có ngoặc |
Ở dòng ba, phần tên biến trong thông báo có thể là <local3> thay vì names khi chương trình chạy bằng java Ten.java, vì tên biến cục bộ không được lưu lại. Lỗi thứ hai là lỗi nguy hiểm nhất vì nó im lặng, và hình dưới đây gom hai mô hình hiểu về nó vào một chỗ.
Cách đọc hình: hàng trên màu đỏ nhạt là mô hình sai: nhiều người tưởng backup = scores tạo mảng thứ hai giữ thứ tự cũ, nên dự đoán backup vẫn là [8, 6, 9]. Hàng dưới màu lục là mô hình đúng: backup chỉ là tên thứ hai của cùng một mảng, nên sau khi sắp xếp, backup in ra [6, 8, 9].
Mô hình đúng này giải thích luôn tình huống ở đầu bài: cô Lan có một mảng với hai tên, không phải hai mảng.
Bạn vừa làm được: đọc bốn thông báo lỗi phổ biến của mảng và nói được nguyên nhân bằng lời thường.
Tự tay làm (Bài tập)
Làm lần lượt từng mức. Mỗi bài có đề, dữ liệu vào, kết quả mong đợi, tiêu chí nghiệm thu và gợi ý. Lời giải nằm riêng ở cuối bài, hãy thử trước rồi mới so.
Mức Cơ bản
Bài 1. Vẽ trạng thái. Dữ liệu vào là đoạn code sau, chạy theo từng dòng:
int[] a = {4, 1, 7};
int[] b = a;
b[1] = 5;
// Dòng này mới tạo mảng thứ hai
int[] c = Arrays.copyOf(a, 4);
c[3] = a[1] + b[2];
Điền bảng ba cột a, b, c sau mỗi dòng, và vẽ số mảng thật sự tồn tại. Kết quả mong đợi: một bảng năm dòng. Tiêu chí nghiệm thu: bạn nói được có bao nhiêu mảng sau dòng 2 và sau dòng 4, và ba mảng cuối cùng trùng với kết quả chạy thật. Gợi ý: chỉ new, clone và copyOf mới tạo mảng mới.
Bài 2. Tìm vị trí điểm cao nhất. Dữ liệu vào: mảng {3, 9, 2, 9}. Viết phương thức int maxIndex(int[] a) trả về chỉ số của phần tử lớn nhất, nếu trùng thì lấy chỉ số đầu tiên. Kết quả mong đợi: 1. Tiêu chí nghiệm thu: dùng vòng for theo chỉ số và không sắp xếp mảng. Gợi ý: giữ lại chỉ số tốt nhất, không giữ giá trị.
Bài 3. Đảo ngược tại chỗ. Dữ liệu vào: mảng {1, 2, 3, 4, 5}. Viết void reverse(int[] a) đảo mảng ngay trong mảng đó, không tạo mảng mới. Kết quả mong đợi: [5, 4, 3, 2, 1]. Tiêu chí nghiệm thu: chỉ dùng một biến tạm và duyệt tối đa nửa mảng. Gợi ý: đổi chỗ tủ đầu với tủ cuối, rồi tiến vào giữa.
Mức Vận dụng
Bài 4. Báo cáo điểm cho lớp cô Lan. Dữ liệu vào: mảng điểm {7, 9, 5, 10, 8, 6}. Viết phương thức String report(int[] s) trả về một chuỗi gồm điểm trung bình, điểm cao nhất, điểm thấp nhất và số học viên có điểm trên trung bình. Kết quả mong đợi:
average 7.5, highest 10, lowest 5, above average 3
Tiêu chí nghiệm thu: in ra khớp từng ký tự, không sắp xếp mảng, và phương thức không làm đổi mảng truyền vào. Gợi ý: cần hai lần duyệt, vì chỉ biết trung bình sau khi đã cộng hết.
Mức Thử thách (tuỳ chọn)
Bài 5. Phổ điểm. Dữ liệu vào: mười hai điểm {8, 6, 9, 8, 10, 7, 8, 6, 9, 10, 8, 5}, mỗi điểm từ 0 đến 10. Viết phương thức trả về mảng 11 phần tử, trong đó ô k là số học viên có điểm đúng bằng k. Kết quả mong đợi: [0, 0, 0, 0, 0, 1, 2, 1, 4, 2, 2]. Tiêu chí nghiệm thu: chỉ duyệt mảng điểm một lần và không dùng if hay switch cho từng mức điểm. Gợi ý: chính giá trị điểm có thể dùng làm chỉ số của ô cần tăng.
Tự kiểm tra (Retrieval Practice)
Trả lời bằng trí nhớ, không xem lại bài. Mỗi câu ghi rõ nó kiểm tra mục tiêu nào trong bốn mục tiêu ở đầu bài.
Câu 1 (nhớ lại, mục tiêu 1). Nêu giá trị mặc định của các ô trong new int[3], new boolean[2] và new String[2], rồi cho biết mảng có 6 ô thì chỉ số nào hợp lệ.
Câu 2 (dự đoán kết quả code, mục tiêu 2). Đoạn sau in ra gì?
int[] a = {3, 8, 2, 8, 5};
int best = 0;
for (int i = 1; i < a.length; i++) {
// So với a[best], không phải so với best
if (a[i] > a[best]) {
best = i;
}
}
System.out.println(best);
Câu 3 (dự đoán trạng thái sau nhiều bước, mục tiêu 3 và 4). Cho int[] p = {1, 2, 3}; int[] q = p; int[] r = Arrays.copyOf(p, 3);. Chạy tiếp q[0] = 10; r[1] = 20; p[2] = 30;. Viết nội dung ba mảng p, q, r.
Câu 4 (tìm lỗi, mục tiêu 4). Vòng for (int i = 0; i <= a.length; i++) { sum += a[i]; } trên mảng a có 5 phần tử ném ra lỗi gì? Thông báo ghi chỉ số nào, độ dài nào, và sửa ở đâu?
Câu 5 (giải thích cho bạn chưa học, mục tiêu 3). Bạn của bạn hỏi: "Mình viết int[] backup = scores; rồi mà sao backup vẫn bị đổi theo?" Hãy giải thích bằng hình ảnh dãy tủ và mảnh giấy ghi địa chỉ, rồi nói cách sửa.
Điều cần nhớ
- Mảng có số ô cố định, chỉ số từ 0 đến
length - 1, và mỗi ô mới tạo đã có giá trị mặc định (mục tiêu 1). - Dùng
fortheo chỉ số khi cần ghi hoặc biết vị trí, dùngfor-eachkhi chỉ đọc, vì biến của for-each là bản sao (mục tiêu 2). - Biến mảng chỉ giữ địa chỉ, nên
b = achỉ thêm một tên cho cùng một mảng (mục tiêu 3). - Muốn mảng độc lập dùng
clonehoặcArrays.copyOf, và lỗi chỉ số cho thông báoIndex N out of bounds for length M(mục tiêu 4).
Cheat sheet một trang.
| Việc cần làm | Cú pháp | Ghi chú |
|---|---|---|
| Tạo mảng rỗng | new int[5] | Năm ô, mỗi ô bằng 0 |
| Tạo mảng có giá trị | int[] a = {4, 1, 7}; | Độ dài tự bằng 3 |
| Đọc hoặc ghi ô i | a[i], a[i] = x | i từ 0 đến a.length - 1 |
| Số ô | a.length | Thuộc tính, không có ngoặc |
| Duyệt chỉ đọc | for (int x : a) | x là bản sao |
| Duyệt có chỉ số | for (int i = 0; i < a.length; i++) | Dùng dấu nhỏ hơn |
| In mảng | Arrays.toString(a) | In a trực tiếp ra [I@... |
| Bản sao cùng độ dài | a.clone() | Mảng mới, độc lập |
| Bản sao độ dài n | Arrays.copyOf(a, n) | Dài hơn thì thêm 0, ngắn hơn thì cắt |
Cách đọc hình: mỗi hàng là một câu hỏi ở bên trái và công cụ nên chọn ở bên phải. Đọc theo hàng ngang: cần số ô thì length, chỉ đọc thì for-each, ghi hoặc cần vị trí thì vòng for theo chỉ số, cần giữ bản gốc thì Arrays.copyOf.
Câu hỏi để dành cho ngày mai: nếu mỗi ô của mảng lại là một mảng khác, ví dụ bảng điểm nhiều môn của nhiều học viên, thì địa chỉ nằm ở đâu và a[1][2] đi qua mấy lần địa chỉ? Hãy nghĩ trước khi sang bài sau.
Đào sâu
Phần này dành cho bạn tò mò. Bỏ qua cũng không thiếu gì ở trên.
Cơ chế bên trong. Biến mảng giữ một tham chiếu tới đối tượng mảng nằm trong vùng nhớ heap. Nhờ vậy truyền một mảng 1 triệu phần tử vào phương thức chỉ chép một địa chỉ, rất rẻ, nhưng phương thức có thể làm đổi mảng của người gọi. Đây chính là điều bạn ôn ở đầu bài: pass-by-value vẫn đúng, chỉ là "giá trị" được chép là địa chỉ. Khi in thẳng một mảng bằng System.out.println(a), bạn nhận chuỗi dạng [I@..., gồm tên kiểu và một mã băm [Có thể khác nhau giữa các lần chạy], không phải các phần tử.
So với lựa chọn khác. Theo mã nguồn OpenJDK, Arrays.copyOf tạo mảng mới rồi dùng System.arraycopy để chép bên dưới, nên chọn cách nào là chuyện dễ đọc và tiện dùng. Khi cần một dãy co giãn được, đó là lúc dùng ArrayList ở chương Collections: nó cũng giữ một mảng bên trong và tự tạo mảng lớn hơn khi đầy.
Hiểu lầm nâng cao. clone và copyOf chỉ sao chép nông: với mảng đối tượng, chúng chép địa chỉ của các đối tượng chứ không chép chính đối tượng. Thử với hai StringBuilder [Đã chạy, JDK 17.0.15]: sau khi nối thêm dấu chấm than vào phần tử đầu của bản sao, mảng gốc cũng thấy An! và hai phần tử đầu là cùng một đối tượng. Nhưng gán một đối tượng mới vào ô của bản sao thì mảng gốc không đổi. Với mảng int không có vấn đề này vì ô chứa luôn giá trị, không chứa địa chỉ.
Giới hạn của bài. Bài này chưa nói về mảng hai chiều (bài sau), về lớp Arrays đầy đủ với sort, fill, equals, binarySearch (bài sau nữa), và về mảng đối tượng tuỳ chỉnh.
Đọc thêm (tài liệu chính thống, Java SE 17):
Bài tiếp theo
Bài sau là mảng đa chiều: mảng mà mỗi ô là một mảng khác, đúng với hình bảng điểm nhiều môn. Bạn sẽ thấy nó chỉ là khái niệm 3 của bài này được lặp lại hai tầng.
Mini-dự án nối tiếp: ở bài sau, bảng điểm của lớp cô Lan có thêm một chiều là môn học: bạn sẽ tính điểm trung bình theo từng học viên và theo từng môn. Hãy giữ lại phương thức report bạn vừa viết làm điểm xuất phát.
Lời giải và đáp án
Hãy thử xong rồi mới đọc phần này.
Lời giải Làm cùng mình. Thay các dòng // TODO bằng những dòng dưới đây, ghép lại chạy ra đúng năm dòng step ở trên [Đã chạy, JDK 17.0.15]:
System.out.println("step 1: " + scores.length + " " + scores[0] + " " + scores[scores.length - 1]);
for (int i = 0; i < scores.length; i++) {
total += scores[i];
}
// Ép sang double trước khi chia, nếu không phép chia nguyên cắt phần lẻ
double average = (double) total / scores.length;
// Bản sao thật: sắp xếp scores sẽ không đụng tới backup
int[] backup = Arrays.copyOf(scores, scores.length);
// Chỉ đọc nên for-each là đủ
for (int score : scores) {
if (score >= 8) {
high++;
}
}
// Mảng không giãn được, nên tạo mảng mới dài 7, hai ô cuối bằng 0
int[] bigger = Arrays.copyOf(scores, 7);
| Dòng code | Máy làm gì | Lý do chọn cách này |
|---|---|---|
scores[scores.length - 1] | Đọc tủ cuối | length là 5 nhưng tủ cuối mang số 4 |
vòng for với total += scores[i] | Cộng từng tủ vào tổng | Cần duyệt cả mảng, mỗi tủ đúng một lần |
(double) total / scores.length | Ép total thành double trước khi chia | Chia hai số nguyên sẽ cắt phần lẻ và cho 8 thay vì 8.0 |
Arrays.copyOf(scores, scores.length) | Tạo mảng mới cùng độ dài và chép giá trị | Làm backup độc lập, sắp xếp scores không ảnh hưởng nó |
vòng for-each với high++ | Đếm điểm từ 8 trở lên | Chỉ đọc nên for-each là đủ và gọn |
Arrays.copyOf(scores, 7) | Tạo mảng dài 7, chép năm điểm, hai ô cuối bằng 0 | Mảng không giãn được, phải tạo mảng mới dài hơn |
Lời giải Bài 1. [Suy luận, khớp với kết quả chạy thật a [4, 5, 7], b [4, 5, 7], c [4, 5, 7, 12]]
| Dòng vừa chạy | a | b | c |
|---|---|---|---|
int[] a = {4, 1, 7}; | [4, 1, 7] | chưa có | chưa có |
int[] b = a; | [4, 1, 7] | cùng mảng với a | chưa có |
b[1] = 5; | [4, 5, 7] | cùng mảng với a | chưa có |
int[] c = Arrays.copyOf(a, 4); | [4, 5, 7] | cùng mảng với a | [4, 5, 7, 0] |
c[3] = a[1] + b[2]; | [4, 5, 7] | cùng mảng với a | [4, 5, 7, 12] |
Sau dòng 2 chỉ có một mảng, sau dòng 4 có hai mảng. Số 12 là 5 cộng 7.
Lời giải Bài 2. [Đã chạy, cho 1]
static int maxIndex(int[] a) {
// best giữ chỉ số, không giữ giá trị
int best = 0;
for (int i = 1; i < a.length; i++) {
if (a[i] > a[best]) {
best = i;
}
}
return best;
}
Biến best giữ chỉ số, không giữ giá trị, nên mỗi lần so sánh dùng a[best]. Vòng lặp bắt đầu từ 1 vì tủ 0 đã là ứng viên. Dùng dấu lớn hơn nên khi gặp số 9 thứ hai, chỉ số vẫn giữ là 1.
Lời giải Bài 3. [Đã chạy, cho [5, 4, 3, 2, 1]]
static void reverse(int[] a) {
// i và j đi từ hai đầu vào giữa, dừng khi gặp nhau
for (int i = 0, j = a.length - 1; i < j; i++, j--) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
Hai biến i và j đi từ hai đầu vào giữa và dừng khi gặp nhau, nên chỉ đổi chỗ nửa mảng. Biến tmp giữ giá trị tủ i để nó không bị ghi mất. Phương thức không trả về gì vì nó sửa chính mảng của người gọi, đúng điều bạn học ở khái niệm 3.
Lời giải Bài 4. [Đã chạy, khớp với kết quả mong đợi]
double avg = (double) total / s.length;
// Lần duyệt thứ hai, vì chỉ biết trung bình sau khi đã cộng hết
int above = 0;
for (int x : s) {
if (x > avg) {
above++;
}
}
Lần duyệt đầu cộng tổng và tìm lớn nhất, nhỏ nhất, giống ScoreBoard. Lần duyệt thứ hai (đoạn trên) đếm số điểm lớn hơn avg và chỉ chạy được sau khi biết avg. Ghép lại với dòng return "average " + avg + ", highest " + high + ", lowest " + low + ", above average " + above;.
Lời giải Bài 5. [Đã chạy, khớp với kết quả mong đợi]
int[] counts = new int[11];
for (int x : s) {
// Chính giá trị điểm làm chỉ số của ô cần tăng
counts[x]++;
}
return counts;
Điểm x chính là chỉ số của ô cần tăng, nên counts[x]++ thay cho cả một dãy if. Mảng có 11 ô vì điểm từ 0 đến 10 là 11 giá trị.
Đáp án Tự kiểm tra.
new int[3]toàn0,new boolean[2]toànfalse,new String[2]toànnull. Mảng 6 ô có chỉ số hợp lệ từ 0 đến 5. Kiểm tra mục tiêu 1.- In ra
1[Đã chạy]. Số 8 ở chỉ số 1 thắng 3, rồi 8 ở chỉ số 3 không lớn hơn chính nó nênbestgiữ 1. Kiểm tra mục tiêu 2. plà[10, 2, 30],qlà[10, 2, 30]vìqcùng mảng vớip, vàrlà[1, 20, 3]vìrlà bản sao riêng [Đã chạy]. Kiểm tra mục tiêu 3 và 4.- Ném
ArrayIndexOutOfBoundsExceptionvới thông báoIndex 5 out of bounds for length 5[Đã chạy]. Vòng cuối cóibằng 5 nhưng chỉ số hợp lệ cuối là 4. Sửa điều kiện thànhi < a.length. Kiểm tra mục tiêu 4. - Dãy tủ chỉ có một.
scoresvàbackuplà hai mảnh giấy cùng ghi địa chỉ của dãy tủ đó, nên ai đổi thứ tự tủ thì mảnh giấy nào cũng thấy. Muốn có bản gốc phải dựng một dãy tủ thứ hai và chép giá trị sang, ví dụ bằngArrays.copyOf(scores, scores.length). Kiểm tra mục tiêu 3.