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 for hoặc for-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 = a giữ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

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]:

text
a [99, 2, 3], same true
a [99, 2, 3], c [99, 77, 3]

Đọc code từng dòng:

Dòng codeMá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ủ 0Mả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ớiChỉ đổ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ướcDòng chạyMảng thứ nhấtMảng thứ haiĐiều xảy ra
1int[] a = {1, 2, 3}[1, 2, 3]chưa cóa trỏ tới mảng thứ nhất
2int[] b = a[1, 2, 3]chưa cób cùng trỏ tới mảng thứ nhất
3b[0] = 99[99, 2, 3]chưa cóGhi qua b, a thấy ngay
4int[] c = a.clone()[99, 2, 3][99, 2, 3]c trỏ tới mảng mới
5c[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.

Hình: ba cách có một bản sao thật và kết quả của từng cách

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

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]:

text
length 5
[8, 6, 9, 0, 0]
total 23, best 9

Đọc code từng dòng:

Dòng codeMá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 ArraysArrays 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, 2Gá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 5Là 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ặpbest 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 4Dù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ổngMỗ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ậtDù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ạyTủ 0Tủ 1Tủ 2Tủ 3Tủ 4totalbest
new int[5]00000chưa cóchưa có
ba dòng gán86900chưa cóchưa có
total = 0, best = scores[0]8690008
vòng i bằng 08690088
vòng i bằng 186900148
vòng i bằng 286900239
vòng i bằng 3 và 486900239

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ì v chỉ 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 khi v đổ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.

java
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:

java
        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.

java
        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.

java
        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.

java
        // 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ỗiThông báo lỗiNguyên nhân bằng lời thườngCách sửa
scores[5] khi mảng có 5 phần tửArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5Tủ đánh số từ 0 nên mảng năm tủ chỉ có tủ 0 đến 4Dù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 scoresKhô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ảngint[] 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 nullMảng chuỗi mới tạo toàn null, gọi phương thức trên null là lỗiGá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ứcViế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:

java
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:

text
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ì?

java
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 for theo chỉ số khi cần ghi hoặc biết vị trí, dùng for-each khi 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 = a chỉ 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 clone hoặc Arrays.copyOf, và lỗi chỉ số cho thông báo Index N out of bounds for length M (mục tiêu 4).

Cheat sheet một trang.

Việc cần làmCú phápGhi chú
Tạo mảng rỗngnew 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 ô ia[i], a[i] = xi từ 0 đến a.length - 1
Số ôa.lengthThuộc tính, không có ngoặc
Duyệt chỉ đọcfor (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ảngArrays.toString(a)In a trực tiếp ra [I@...
Bản sao cùng độ dàia.clone()Mảng mới, độc lập
Bản sao độ dài nArrays.copyOf(a, n)Dài hơn thì thêm 0, ngắn hơn thì cắt
Hình: tóm tắt chọn công cụ nào cho việc nào với mảng

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]:

java
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 codeMáy làm gìLý do chọn cách này
scores[scores.length - 1]Đọc tủ cuốilength 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ổngCầ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 chiaChia 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ênChỉ đọ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 0Mả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ạyabc
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 achưa có
b[1] = 5;[4, 5, 7]cùng mảng với achư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]

java
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]]

java
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]

java
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]

java
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.

  1. new int[3] toàn 0, new boolean[2] toàn false, new String[2] toàn null. Mảng 6 ô có chỉ số hợp lệ từ 0 đến 5. Kiểm tra mục tiêu 1.
  2. 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ên best giữ 1. Kiểm tra mục tiêu 2.
  3. p là [10, 2, 30], q là [10, 2, 30] vì q cùng mảng với p, và r là [1, 20, 3] vì r là bản sao riêng [Đã chạy]. Kiểm tra mục tiêu 3 và 4.
  4. Ném ArrayIndexOutOfBoundsException với thông báo Index 5 out of bounds for length 5 [Đã chạy]. Vòng cuối có i bằng 5 nhưng chỉ số hợp lệ cuối là 4. Sửa điều kiện thành i < a.length. Kiểm tra mục tiêu 4.
  5. Dãy tủ chỉ có một. scores và backup là 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ằng Arrays.copyOf(scores, scores.length). Kiểm tra mục tiêu 3.