Command Palette

Search for a command to run...

[Java Basics] Mảng một chiều trong Java: khai báo, khởi tạo và duyệt mảng

Một array trong Java là một object có độ dài cố định và đồng nhất về type: mọi slot đều giữ cùng một type, và số slot được chốt ngay lúc object được tạo ra. Sau đó không gì thay đổi được nữa. Chỉ một câu đó thôi đã giải thích gần như toàn bộ phần còn lại của bài, và nó cũng là lý do ArrayList xuất hiện ở phần sau của series.

Array cũng là chỗ đầu tiên mà mô hình reference của các bài trước thôi là lý thuyết. Một array variable không chứa các phần tử, nó chứa địa chỉ của một object trên heap — nên int[] b = a; cho bạn hai cái tên trỏ vào một array, chứ không phải hai array.

Năm ô có index nằm trong cặp ngoặc vuông với length cố định là 5

Mọi dòng output, lỗi compile và stack trace bên dưới đều lấy từ việc compile và chạy code thật trên OpenJDK 21.0.6.

Array trong Java là gì

Array là một object. Nó nằm trên heap như mọi object khác, và variable bạn khai báo giữ reference tới nó. Điểm đặc biệt nằm ở hình dạng của object đó: một field length không bao giờ ghi được, theo sau là length slot cùng một type, nằm liền nhau và truy cập được bằng index.

Array variable trong stack frame trỏ tới heap object có field length và các slot có index

Ba hệ quả xuất hiện ngay lập tức, và nên nói trước khi nói tới cú pháp:

Tính chấtNghĩa là gì
Độ dài cố địnhLength chọn lúc tạo và không đổi. Không thể thêm phần tử.
Đồng nhất typeint[] chỉ giữ int, không giữ gì khác. Compiler bắt buộc điều đó.
Reference typeVariable giữ địa chỉ. Gán nó là copy địa chỉ, không copy phần tử.

Chuyện length là final không phải quy ước phong cách, chính javac chặn:

int[] a = new int[3];
a.length = 5;
LengthAssign.java:4: error: cannot assign a value to final variable length
        a.length = 5;
         ^
1 error

Khai báo và khởi tạo mảng trong Java

Khai báo là đặt tên cho một variable có array type. Khởi tạo là việc khác: cấp phát object. Hai việc này thường viết chung một dòng nhưng không phải là một.

new int[5] điền số 0, dạng khởi tạo bằng ngoặc nhọn, và array của object có slot null

Có ba cách tạo ra object array, cả ba đều gặp trong code thật. Dòng thứ tư dùng cú pháp khai báo kiểu C, thứ mà một phần bên dưới khuyên không nên viết:

public class ArrayBasics {
    public static void main(String[] args) {
        int[] a = new int[5];
        int[] b = {1, 2, 3};
        int[] c = new int[]{10, 20, 30};
        int d[] = {7, 8};

        System.out.println("a.length = " + a.length);
        System.out.println("b.length = " + b.length);
        System.out.println("c.length = " + c.length);
        System.out.println("d.length = " + d.length);
        System.out.println("a[0] = " + a[0] + ", b[0] = " + b[0] + ", c[2] = " + c[2] + ", d[1] = " + d[1]);
    }
}
a.length = 5
b.length = 3
c.length = 3
d.length = 2
a[0] = 0, b[0] = 1, c[2] = 30, d[1] = 8

new int[5] xin năm slot rồi để JVM điền giá trị mặc định vào. Dạng ngoặc nhọn {1, 2, 3} thì bạn đưa thẳng giá trị và length suy ra từ số phần tử bạn viết — bạn không phải ghi số 3 ở đâu cả.

Length trong new int[n] không nhất thiết phải là hằng số. Mọi expression trả về int đều được, đó là cách array lấy kích thước từ input của người dùng hoặc từ một file:

int n = 3 + 2;
int[] a = new int[n];

Length âm thì compile vẫn qua, chỉ nổ lúc runtime:

Exception in thread "main" java.lang.NegativeArraySizeException: -1
	at NegSize.main(NegSize.java:4)

Khi nào bắt buộc dùng dạng anonymous

Dạng rút gọn {1, 2, 3} chỉ hợp lệ trong một khai báo. Dùng ở chỗ khác là parser bỏ cuộc trước cả khi kịp kiểm tra type:

int[] a;
a = {1, 2, 3};
ReassignInit.java:4: error: illegal start of expression
        a = {1, 2, 3};
            ^
ReassignInit.java:4: error: not a statement
        a = {1, 2, 3};
             ^
ReassignInit.java:4: error: ';' expected
        a = {1, 2, 3};
              ^
3 errors

new int[]{1, 2, 3} — array anonymous — là dạng dùng được ở mọi nơi: gán sau khi đã khai báo, truyền làm argument, và trả về từ một method.

public class Anonymous {
    static int sum(int[] xs) {
        int total = 0;
        for (int x : xs) total += x;
        return total;
    }

    public static void main(String[] args) {
        int[] a;
        a = new int[]{1, 2, 3};
        System.out.println("reassigned: " + a.length);
        System.out.println("sum = " + sum(new int[]{4, 5, 6}));
    }
}
reassigned: 3
sum = 15

Vì sao int[] a đọc tốt hơn int a[]

int a[] là Java hợp lệ, thừa hưởng từ C. Nó compile được, nó chạy được, và bạn vẫn không nên viết như vậy, vì cặp ngoặc thuộc về type chứ không thuộc về variable. Đặt ngoặc cạnh tên rồi khai báo nhiều tên một dòng thì ý nghĩa lệch hẳn so với vẻ ngoài:

int[] a, b;          // both are int[]
int c[], d;          // c is int[], d is a plain int

Dòng thứ hai khai báo một array và một int. Không có cảnh báo nào; sai lầm chỉ lộ ra khi bạn dùng tới d:

CStyleBad.java:4: error: incompatible types: int[] cannot be converted to int
        d = new int[2];
            ^
1 error

Viết type trước. int[] a đọc là "a là array của int", đúng thứ bạn định nói.

Giá trị mặc định của một array vừa tạo

new luôn tạo ra một object đã được khởi tạo đầy đủ. Mỗi slot nhận giá trị mặc định của element type — cùng bộ mặc định mà field nhận được, đã nói ở bài về variable và data type. Khác với local variable, một slot của array không bao giờ ở trạng thái "chưa được gán".

public class Defaults {
    public static void main(String[] args) {
        int[] ints = new int[3];
        double[] doubles = new double[3];
        boolean[] flags = new boolean[3];
        char[] chars = new char[3];
        String[] names = new String[3];

        System.out.println("int[]     " + ints[0]);
        System.out.println("double[]  " + doubles[0]);
        System.out.println("boolean[] " + flags[0]);
        System.out.println("char[]    code " + (int) chars[0]);
        System.out.println("String[]  " + names[0]);
    }
}
int[]     0
double[]  0.0
boolean[] false
char[]    code 0
String[]  null
Element typeGiá trị mặc định
byte, short, int, long0
float, double0.0
booleanfalse
char'\u0000', code point 0
Mọi reference typenull

Ở trên char được in ra dưới dạng code point là có chủ ý. Đó là ký tự '\u0000', không có hình dạng nào để hiển thị — in thẳng nó ra chỉ đẩy một byte vô hình vào terminal chứ không cho bạn thấy gì.

Dòng cuối bảng mới là dòng gây đau. String[] là array của reference, nên new String[3] cấp ba slot reference rỗng, không phải ba chuỗi rỗng.

length là field, không phải method

a.length không có cặp ngoặc tròn. Đó là một field trên array object, và nó final. Điều này không nhất quán với String.length() và với List.size(), và nó là lỗi cú pháp phổ biến nhất của người mới khi làm việc với array.

int[] a = new int[5];
System.out.println(a.length());
LengthCall.java:4: error: cannot find symbol
        System.out.println(a.length());
                            ^
  symbol:   method length()
  location: variable a of type int[]
1 error
Thứ cần đoCách hỏi kích thước
Arraya.length
Strings.length()
List, Set, Mapc.size()

Không có quy tắc nào để suy ra ở đây, chỉ có một sự thật phải nhớ: array dùng field, mọi thứ còn lại dùng method.

Index chạy từ 0 tới length - 1

Index bắt đầu từ 0, nên index hợp lệ cuối cùng của một array độ dài nn - 1, và a[a.length - 1] là cách viết chuẩn để lấy phần tử cuối. Mọi lần truy cập — đọc lẫn ghi — đều được JVM kiểm tra bounds.

Khoảng index hợp lệ 0 tới length-1 với index 3 và index -1 bước ra ngoài

int[] a = {10, 20, 30};
System.out.println("first = " + a[0] + ", last = " + a[a.length - 1]);
System.out.println(a[3]);
first = 10, last = 30
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at Bounds.main(Bounds.java:5)

Index âm cũng ném đúng exception đó, kèm luôn giá trị gây lỗi trong message:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3
	at BoundsNeg.main(BoundsNeg.java:4)

Định dạng message rất đáng đọc kỹ: Index <cái bạn xin> out of bounds for length <cái đang có>. Cả hai con số đều nằm sẵn đó, nên hiếm khi bạn cần tới debugger — lỗi lệch một đơn vị thường tự khai báo bằng Index 3 out of bounds for length 3, đúng thứ mà i <= a.length sinh ra.

⚠️ Việc kiểm tra bounds không miễn phí, nhưng cũng không phải tùy chọn. Chính nó chặn một index sai âm thầm đọc vùng bộ nhớ của thứ khác — loại lỗi đã làm nên danh tiếng của C.

Duyệt mảng trong Java

Java cho bạn hai loại for và chúng không thay thế được cho nhau. Cú pháp của for thuộc về bài viết riêng về loop; ở đây chỉ cần biết khi nào chọn cái nào.

Vòng for có index

Dùng khi bạn cần vị trí — để in ra, để so sánh hai phần tử cạnh nhau, hoặc để ghi vào array.

String[] fruits = {"apple", "banana", "cherry"};

for (int i = 0; i < fruits.length; i++) {
    System.out.println(i + " -> " + fruits[i]);
}
0 -> apple
1 -> banana
2 -> cherry

Điều kiện là i < fruits.length, không bao giờ là i <= fruits.length. Cũng lưu ý fruits.length được đọc lại ở mỗi vòng, và điều đó vừa đúng vừa không tốn gì: length không thể đổi sau lưng bạn.

Vòng for-each

Dùng khi bạn chỉ cần giá trị. Nó đọc dễ hơn và không thể vượt bounds, đơn giản vì không có index nào để viết sai.

for (String fruit : fruits) {
    System.out.println(fruit.toUpperCase());
}
APPLE
BANANA
CHERRY

Variable của vòng lặp là một bản sao của slot, không phải chính slot đó. Gán vào nó thì không có gì thay đổi:

int[] a = {1, 2, 3};
for (int x : a) {
    x = x * 10;
}
System.out.println("after for-each: " + java.util.Arrays.toString(a));
for (int i = 0; i < a.length; i++) {
    a[i] = a[i] * 10;
}
System.out.println("after indexed : " + java.util.Arrays.toString(a));
after for-each: [1, 2, 3]
after indexed : [10, 20, 30]

Quy tắc gói trong một dòng: đọc bằng for-each, ghi bằng index.

Duyệt ngược

Duyệt ngược nghĩa là bắt đầu ở length - 1 và đi lùi về 0. Không có dạng for-each ngược.

for (int i = fruits.length - 1; i >= 0; i--) {
    System.out.println("reverse " + i + " -> " + fruits[i]);
}
reverse 2 -> cherry
reverse 1 -> banana
reverse 0 -> apple

Điều kiện là i >= 0, không phải i > 0 — index 0 là một phần tử thật.

Array là object: ký hiệu [I nghĩa là gì

Vì array là object, nó có class, và bạn hỏi được class đó. Tên nhìn như rác cho tới khi bạn biết quy ước:

int[] ints = new int[3];
double[] doubles = new double[3];
String[] names = new String[3];

System.out.println(ints.getClass().getName());
System.out.println(doubles.getClass().getName());
System.out.println(names.getClass().getName());
System.out.println(ints instanceof Object);
System.out.println(ints.getClass().getSuperclass().getName());
[I
[D
[Ljava.lang.String;
true
java.lang.Object

Một dấu [ là một chiều. Chữ cái đứng sau là element type: I cho int, D cho double, J cho long, Z cho boolean. Với element type là reference thì viết L, tên class đầy đủ, rồi một dấu chấm phẩy — nên mới có [Ljava.lang.String;.

Bạn không phải viết ký hiệu này, nhưng bạn sẽ phải đọc nó. Nó xuất hiện trong stack trace, trong output của reflection, và trong message ClassNotFoundException; biết [B nghĩa là "array của byte" giúp bạn đỡ hoang mang rất nhiều về sau. Dòng output cuối là nửa còn lại của ý này: superclass của một array là java.lang.Object, nên array là một Object và truyền được vào mọi chỗ nhận Object.

Copy một array và cái bẫy reference

int[] b = a; không copy gì ngoài một địa chỉ. Sau đó cả hai tên cùng chạm tới một heap object, nên ghi qua tên nào cũng nhìn thấy được từ tên kia.

Hai tên trỏ một heap array so với Arrays.copyOf tạo ra array thứ hai

import java.util.Arrays;

public class AliasVsCopy {
    public static void main(String[] args) {
        int[] a = {1, 2, 3};
        int[] b = a;                 // copies the reference
        System.out.println("a == b -> " + (a == b));
        b[0] = 99;
        System.out.println("a = " + Arrays.toString(a) + "  b = " + Arrays.toString(b));

        int[] x = {1, 2, 3};
        int[] y = Arrays.copyOf(x, x.length);   // copies the elements
        System.out.println("x == y -> " + (x == y));
        y[0] = 99;
        System.out.println("x = " + Arrays.toString(x) + "  y = " + Arrays.toString(y));
    }
}
a == b -> true
a = [99, 2, 3]  b = [99, 2, 3]
x == y -> false
x = [1, 2, 3]  y = [99, 2, 3]

Chuyện tương tự xảy ra khi truyền array vào method. Truyền array là truyền reference, nên method sửa được array của bạn; còn gán lại parameter bên trong method thì không có tác dụng gì, vì parameter là một variable riêng đang giữ bản sao của địa chỉ:

static void zeroFirst(int[] xs) { xs[0] = 0; }
static void reassign(int[] xs)  { xs = new int[]{9, 9, 9}; }
after zeroFirst: [0, 2, 3]
after reassign : [0, 2, 3]

Arrays.copyOf, Arrays.copyOfRange, System.arraycopy và clone

Bốn cách để có một array thứ hai thật sự. Chúng khác nhau ở chỗ bạn kiểm soát được gì, chứ không phải ở chi phí.

import java.util.Arrays;

public class Copies {
    public static void main(String[] args) {
        int[] a = {1, 2, 3, 4, 5};

        int[] same = Arrays.copyOf(a, a.length);
        int[] shorter = Arrays.copyOf(a, 3);
        int[] longer = Arrays.copyOf(a, 7);
        int[] range = Arrays.copyOfRange(a, 1, 4);
        int[] cloned = a.clone();

        int[] target = new int[5];
        System.arraycopy(a, 1, target, 0, 3);

        System.out.println("original    = " + Arrays.toString(a));
        System.out.println("copyOf same = " + Arrays.toString(same));
        System.out.println("copyOf 3    = " + Arrays.toString(shorter));
        System.out.println("copyOf 7    = " + Arrays.toString(longer));
        System.out.println("copyOfRange = " + Arrays.toString(range));
        System.out.println("clone       = " + Arrays.toString(cloned));
        System.out.println("arraycopy   = " + Arrays.toString(target));
        System.out.println("a == cloned -> " + (a == cloned));
    }
}
original    = [1, 2, 3, 4, 5]
copyOf same = [1, 2, 3, 4, 5]
copyOf 3    = [1, 2, 3]
copyOf 7    = [1, 2, 3, 4, 5, 0, 0]
copyOfRange = [2, 3, 4]
clone       = [1, 2, 3, 4, 5]
arraycopy   = [2, 3, 4, 0, 0]
a == cloned -> false

Ba chi tiết mà output làm rõ. Arrays.copyOf với length lớn hơn sẽ đệm bằng giá trị mặc định của element type, nên copyOf 7 kết thúc bằng hai số 0. Arrays.copyOfRange(a, 1, 4) lấy index 1, 2 và 3 — from tính vào, to không tính, đúng quy ước của String.substring. Còn System.arraycopy là cách duy nhất ghi vào một array bạn đã có sẵn, nhận vào nguồn, vị trí nguồn, đích, vị trí đích và số phần tử.

clone() trên array của object là shallow

clone() cho bạn một array mới. Nó không cho bạn các phần tử mới. Với int[] thì không khác gì, vì phần tử là giá trị. Với array của object, hai array cùng trỏ tới đúng những object cũ:

StringBuilder[] a = { new StringBuilder("Java"), new StringBuilder("21") };
StringBuilder[] b = a.clone();

System.out.println("a == b       -> " + (a == b));
System.out.println("a[0] == b[0] -> " + (a[0] == b[0]));

b[0].append(" SE");
b[1] = new StringBuilder("22");

System.out.println("a = " + Arrays.toString(a));
System.out.println("b = " + Arrays.toString(b));
a == b       -> false
a[0] == b[0] -> true
a = [Java SE, 21]
b = [Java SE, 22]

Thay slot 1 của b không đụng gì tới a, vì slot thuộc về array. Còn sửa object ở slot 0 thông qua b thì a nhìn thấy ngay, vì object là đồ dùng chung. Mọi phép copy array trong JDK đều là shallow; deep copy là thứ bạn phải tự viết.

So sánh array: ==, equals và Arrays.equals

Array không override equals. Nó thừa hưởng Object.equals, tức là so sánh identity — đúng y hệt phép ==. Nghĩa là hai trong ba cách so sánh array quen thuộc nhất đều cho bạn câu trả lời sai.

int[] a = {1, 2, 3};
int[] b = {1, 2, 3};

System.out.println("a == b            -> " + (a == b));
System.out.println("a.equals(b)       -> " + a.equals(b));
System.out.println("Arrays.equals(a,b)-> " + Arrays.equals(a, b));
System.out.println("a.equals(a)       -> " + a.equals(a));
a == b            -> false
a.equals(b)       -> false
Arrays.equals(a,b)-> true
a.equals(a)       -> true

Việc a.equals(a) trả về true chính là bằng chứng equals ở đây là identity chứ không phải so sánh nội dung. Với array của object cũng vậy — hai String[] cùng nội dung vẫn không equals nhau:

a == b             -> false
a.equals(b)        -> false
Arrays.equals(a,b) -> true

Arrays.equals so length trước rồi so từng phần tử, dùng equals cho phần tử là object. Trừ khi bạn thật sự muốn hỏi "có phải đúng cùng một array object không", đây là thứ bạn cần.

In một array ra màn hình

System.out.println(a) gọi toString(), mà array cũng không override method đó. Bạn nhận về Object.toString(): tên class như ở phần trước, một dấu @, và identity hash code ở hệ 16.

int[] a = {1, 2, 3};
String[] names = {"Hoang", "Lan"};
char[] letters = {'J', 'a', 'v', 'a'};

System.out.println(a);
System.out.println(Arrays.toString(a));
System.out.println(names);
System.out.println(Arrays.toString(names));
System.out.println(letters);
[I@2a139a55
[1, 2, 3]
[Ljava.lang.String;@15db9742
[Hoang, Lan]
Java

Phần hex phía sau là identity hash code, khác nhau giữa các lần chạy và giữa các máy, nên [I@2a139a55 sẽ không phải con số của bạn. Nó không phải địa chỉ và cũng không phải dữ liệu có ích — thấy nó trong log nghĩa là ai đó đã lỡ nối một array vào chuỗi.

char[] là ngoại lệ duy nhất, và đó là một trường hợp đặc biệt thật sự trong PrintStream: println(char[]) là một overload in ra các ký tự. System.out.println(letters) in ra Java, trong khi cùng lệnh đó với int[] lại in ra dạng tên class kèm hash code.

Arrays.toString(a) mới là thứ bạn cần. Với array một chiều thì nó luôn đúng.

Các method hữu ích trong java.util.Arrays

Một dòng import java.util.Arrays; là có đủ mọi thứ trong bảng dưới. Sort và search sẽ được nói tử tế ở một bài sau — ở đây chỉ cần biết cái gì đã có sẵn để khỏi tự viết lại.

MethodLàm gì
Arrays.toString(a)Dạng [1, 2, 3] dễ đọc để in và ghi log
Arrays.equals(a, b)So sánh từng phần tử, thứ mà ==equals không làm
Arrays.fill(a, v)Ghi v vào mọi slot; fill(a, from, to, v) chỉ ghi một khoảng
Arrays.copyOf(a, n)Array mới độ dài n, cắt bớt hoặc đệm giá trị mặc định
Arrays.copyOfRange(a, from, to)Array mới lấy từ khoảng nửa mở [from, to)
Arrays.sort(a)Sort tại chỗ, tăng dần
Arrays.binarySearch(a, key)Index của key, chỉ đúng khi array đã được sort
Arrays.stream(a)Biến array thành stream để dùng sum(), max(), average()

fillcopyOf là hai method bạn dùng ngay từ ngày đầu:

int[] scores = new int[5];
Arrays.fill(scores, 7);
System.out.println("fill      = " + Arrays.toString(scores));

Arrays.fill(scores, 1, 4, 0);
System.out.println("fill 1..4 = " + Arrays.toString(scores));

int[] grown = Arrays.copyOf(scores, 8);
System.out.println("copyOf 8  = " + Arrays.toString(grown));

String[] names = new String[3];
Arrays.fill(names, "n/a");
System.out.println("fill obj  = " + Arrays.toString(names));

int[] nums = {5, 1, 4};
System.out.println("stream sum = " + Arrays.stream(nums).sum());
fill      = [7, 7, 7, 7, 7]
fill 1..4 = [7, 0, 0, 0, 7]
copyOf 8  = [7, 0, 0, 0, 7, 0, 0, 0]
fill obj  = [n/a, n/a, n/a]
stream sum = 10

Để ý bản có khoảng: fill(scores, 1, 4, 0) ghi vào index 1, 2 và 3 rồi bỏ qua index 4. Lại là khoảng nửa mở.

Array của object bắt đầu bằng null

Đây là chỗ giá trị mặc định null thôi là chuyện vặt. new String[3] tạo ra ba slot reference chưa trỏ vào đâu. Duyệt qua chúng rồi gọi method là nổ, và message của Java 21 chỉ thẳng ra expression nào đang null:

public class ObjectArray {
    public static void main(String[] args) {
        String[] names = new String[3];
        names[0] = "Hoang";

        for (int i = 0; i < names.length; i++) {
            System.out.println(i + " -> " + names[i]);
        }

        for (int i = 0; i < names.length; i++) {
            System.out.println(names[i].toUpperCase());
        }
    }
}
0 -> Hoang
1 -> null
2 -> null
HOANG
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "names[i]" is null
	at ObjectArray.main(ObjectArray.java:11)

Vòng đầu in ra null một cách bình thản, vì phép nối chuỗi biến một reference null thành đoạn chữ null. Vòng thứ hai gọi method lên nó và nổ ngay ở index 1.

Phần because "names[i]" is null cần tên variable nằm trong class file. Compile bằng javac trơn thì cùng lần chạy đó lại báo theo vị trí:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "<local1>[<local2>]" is null
	at ObjectArray.main(ObjectArray.java:11)

Compile bằng javac -g — mặc định của mọi IDE và build tool — thì bạn có bản dễ đọc. Dù kiểu nào thì cách sửa vẫn thế: điền vào các slot trước khi đọc, hoặc kiểm tra null trong vòng lặp.

Không thể làm một array dài ra

Không có add. Length là final, nên "làm dài ra" nghĩa là cấp phát một array lớn hơn rồi copy nội dung cũ sang:

int[] a = {1, 2, 3};
System.out.println("before = " + Arrays.toString(a) + ", length = " + a.length);

a = Arrays.copyOf(a, a.length + 1);
a[a.length - 1] = 4;

System.out.println("after  = " + Arrays.toString(a) + ", length = " + a.length);
before = [1, 2, 3], length = 3
after  = [1, 2, 3, 4], length = 4

Đọc kỹ dòng đó: array ban đầu không hề được resize. Một array mới độ dài 4 được cấp phát, ba giá trị cũ được copy sang, rồi a được trỏ lại vào object mới. Object cũ thành rác.

Làm vậy một lần thì không sao. Làm trong vòng lặp nghĩa là cấp phát và copy ở từng lần thêm phần tử, và đó chính là lý do ArrayList tồn tại — nó giữ một array bên trong, nới ra theo từng bước lớn, và cho bạn add với remove. Dùng array khi kích thước đã biết và cố định; dùng ArrayList khi chưa biết. Một bài sau trong series sẽ nói kỹ về collection.

Command-line arguments là một array

String[] args trong main đã nằm sẵn ở đó từ chương trình đầu tiên của series. Nó là một String[] bình thường, giữ các argument truyền vào sau tên class — khác với C, tên chương trình không nằm trong đó.

public class Args {
    public static void main(String[] args) {
        System.out.println("args.length = " + args.length);
        for (int i = 0; i < args.length; i++) {
            System.out.println("args[" + i + "] = " + args[i]);
        }
        if (args.length < 2) {
            System.out.println("usage: java Args <name> <times>");
            return;
        }
        int times = Integer.parseInt(args[1]);
        for (int i = 0; i < times; i++) {
            System.out.println("Hello, " + args[0]);
        }
    }
}

Chạy không truyền gì, args là một array rỗng — length 0, và không bao giờ là null:

args.length = 0
usage: java Args <name> <times>

Chạy bằng java Args Hoang 3:

args.length = 2
args[0] = Hoang
args[1] = 3
Hello, Hoang
Hello, Hoang
Hello, Hoang

Mọi thứ đi vào đều là String, kể cả số 3, nên mới cần Integer.parseInt(args[1]). Và kiểm tra args.length trước khi chạm vào args[1] không phải là phong cách phòng thủ, đó là khác biệt giữa một dòng hướng dẫn sử dụng và một ArrayIndexOutOfBoundsException ném vào mặt người dùng.

FAQ

length và length() trong Java khác nhau thế nào?

length là một public final field trên array object, viết không có ngoặc tròn: a.length. length() là method của String: s.length(). Collection thì không dùng cái nào trong hai cái đó mà dùng size(). Gọi a.length() trên array sẽ không compile — cannot find symbol: method length() — và đây thuần túy là sự thiếu nhất quán mang tính lịch sử của ngôn ngữ, không có quy tắc nào phía sau.

Có thể thay đổi kích thước của một array trong Java không?

Không. Length được chốt lúc object được tạo và field length là final, nên a.length = 5 là lỗi compile. Cách duy nhất để "nới" một array là cấp phát một array lớn hơn bằng Arrays.copyOf rồi copy phần tử sang, tức là một object mới chứ không phải một object được resize. Nếu kích thước thay đổi lúc runtime, hãy dùng ArrayList.

Vì sao in array ra lại thấy [I@2a139a55?

Vì array không override toString(), nên bạn nhận về Object.toString(): tên class của array, dấu @, và identity hash code ở hệ 16. [I nghĩa là "array của int". Dùng Arrays.toString(a) để in các phần tử. Ngoại lệ là char[], được System.out.println in ra thành chữ nhờ một overload riêng.

Copy một array trong Java thế nào?

Dùng Arrays.copyOf(a, a.length) để copy toàn bộ, Arrays.copyOfRange(a, from, to) để lấy một đoạn, a.clone() cho bản copy toàn bộ viết ngắn hơn, hoặc System.arraycopy khi bạn muốn ghi vào một array đã có. int[] b = a; không phải là copy — nó chỉ làm b thành cái tên thứ hai của cùng một array. Cả bốn cách copy đều là shallow, nên với array của object thì hai array vẫn trỏ tới cùng những object đó.

Vì sao duyệt String array lại bị NullPointerException?

new String[n] điền null vào các slot chứ không điền chuỗi rỗng. Đọc một slot thì không sao và còn in ra null; gọi method lên nó thì nổ. Java 21 nói thẳng expression nào null — Cannot invoke "String.toUpperCase()" because "names[i]" is null — miễn là class file có debug information, thứ mà javac -g và mọi IDE đều tạo ra.

Nên dùng array hay ArrayList?

Dùng array khi số phần tử đã biết và cố định, khi bạn cần primitive mà không muốn boxing, hoặc khi một API trả về array cho bạn. Dùng ArrayList bất cứ khi nào phần tử được thêm vào hay bỏ ra, vì array không nới được và mô phỏng việc đó nghĩa là cấp phát mới cộng copy toàn bộ ở từng lần thêm. ArrayList dùng một array bên trong và trả chi phí đó dần cho bạn.

Kết luận

Một array là một object với length đã được chốt lúc tạo và không bao giờ dịch chuyển. Mọi thứ trong bài này đều đi ra từ đó: length là final field chứ không phải method, index chạy từ 0 tới length - 1 và được kiểm tra ở mọi lần truy cập, new điền giá trị mặc định vào các slot nên array của object bắt đầu bằng toàn null, và assignment copy reference nên Arrays.copyOf mới là thứ tạo ra array thứ hai.

Những thói quen đáng giữ: Arrays.toString để in, Arrays.equals để so sánh, a[a.length - 1] để lấy phần tử cuối, for-each để đọc và index để ghi, và chuyển sang ArrayList ngay khi kích thước thôi cố định.

Bài tiếp theo trong series: mảng nhiều chiều và mảng răng cưa trong Javaint[][], new int[3][4] thực sự cấp phát cái gì, vì sao một hàng bản thân nó cũng là một array object, và cách dựng rồi duyệt một jagged array có các hàng dài ngắn khác nhau.

Bài viết liên quan

[Java Basics] Phương thức trong Java: khai báo và gọi method

Cách khai báo và gọi method trong Java: các thành phần của một declaration, static method so với instance method và lỗi non-static method cannot be referenced from a static context, return statement, call stack và StackOverflowError, cách đọc stack trace, Javadoc, cùng những lỗi compiler thật mà người mới hay gặp.

[Java Basics] Vòng lặp trong Java: for, while và do-while

Vòng lặp trong Java giải thích bằng cách chạy thật: thứ tự thực thi chính xác của header for, while so với do-while, enhanced for và lý do nó không ghi ngược vào array, lỗi off-by-one với length, và ba cách tạo ra vòng lặp vô hạn.

[Java Basics] Tính đa hình trong Java: overriding và overloading

Tính đa hình trong Java trên OpenJDK 21: quy tắc method overriding, @Override, dynamic dispatch chứng minh bằng javap, overriding và overloading, field hiding, static hiding, upcasting và downcasting, cùng cái bẫy trong constructor.

[Java Basics] Tính kế thừa trong Java: extends và super

extends hoạt động thế nào, subclass kế thừa gì và không kế thừa gì, vì sao constructor không bao giờ được kế thừa, super(...) xâu chuỗi constructor lên tới java.lang.Object rồi chạy ngược xuống ra sao, field hiding khác overriding chỗ nào, protected qua package, final class, vấn đề fragile base class, và khi nào composition là lựa chọn đúng.