Command Palette

Search for a command to run...

[Java Basics] Mảng nhiều chiều trong Java

Java không có mảng hai chiều thật sự. Thứ mà int[][] khai báo là một array mà các phần tử của nó lại là reference tới các object int[] khác — một array của array. Mọi điều gây bất ngờ ở mảng hai chiều trong Java đều đi ra từ đúng một sự thật đó.

Đó là lý do các row có thể dài ngắn khác nhau, lý do không tồn tại một "số cột" đáng tin cậy, lý do Arrays.toString in ra rác khi gặp grid, và lý do copy một mảng hai chiều theo cách hiển nhiên nhất lại không hề copy dữ liệu. Hiểu cấu trúc một lần thì tất cả những chỗ đó hết bất ngờ.

Một outer array giữ reference tới các row object riêng biệt

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

Java không có mảng hai chiều thật sự

Trong C, int a[3][4] giữ một khối liên tục gồm mười hai ô int và tính địa chỉ từ hai index. Java làm khác: new int[3][4] cấp phát bốn object — một outer array chứa ba reference, cộng với ba object int[4] riêng biệt nằm trên heap.

Variable grid, outer array chứa reference, và ba row object riêng trên heap

Chính tên type nói ra điều đó. In runtime class của grid và của một row:

int[][] grid = new int[3][4];
System.out.println(grid.getClass().getName());
System.out.println(grid[0].getClass().getName());
[[I
[I

[I nghĩa là "array of int". [[I nghĩa là "array of array of int". Outer object không chứa một int nào cả — nó chứa reference. Bài trước đã cho thấy một array variable giữ reference tới object trên heap chứ không giữ các phần tử; mảng hai chiều chỉ là áp dụng đúng quy tắc đó hai lần, và mọi thứ bên dưới đều là hệ quả.

Các row đúng là những object độc lập, và có thể chứng minh bằng ==:

int[] row = new int[3];
int[][] shared = {row, row, row};
shared[0][0] = 5;
System.out.println("shared = " + java.util.Arrays.deepToString(shared));
System.out.println("shared[0] == shared[1] ? " + (shared[0] == shared[1]));

int[][] proper = new int[3][3];
proper[0][0] = 5;
System.out.println("proper = " + java.util.Arrays.deepToString(proper));
System.out.println("proper[0] == proper[1] ? " + (proper[0] == proper[1]));
shared = [[5, 0, 0], [5, 0, 0], [5, 0, 0]]
shared[0] == shared[1] ? true
proper = [[5, 0, 0], [0, 0, 0], [0, 0, 0]]
proper[0] == proper[1] ? false

Grid thứ nhất chỉ có một row object được tham chiếu ba lần, nên một lệnh ghi hiện ra ở cả ba "row". new int[3][3] tạo ba object khác nhau, nên cùng lệnh ghi đó chỉ rơi vào một chỗ. Không có gì được xử lý đặc biệt cho mảng hai chiều ở đây — đó chỉ là mô hình reference thông thường.

Khai báo và tạo mảng hai chiều trong Java

Có ba cách tạo mảng hai chiều, và chúng cấp phát những thứ khác nhau.

Cú phápCấp phát ra gì
new int[3][4]outer array và cả 3 row, mọi phần tử bằng 0
{{1, 2}, {3, 4}}outer array và một row cho mỗi nhóm ngoặc lồng
new int[3][]chỉ outer array — cả 3 reference row đều là null

Bản thân phần khai báo không mang kích thước. int[][] grid; là khai báo hợp lệ; int[3][4] grid; là lỗi parse, vì cặp ngoặc vuông trong khai báo nói "đây là một array type", chứ không nói "nó to bằng này":

int[3][4] grid;
DeclDim.java:3: error: ']' expected
        int[3][4] grid;
            ^
DeclDim.java:3: error: not a statement
        int[3][4] grid;
           ^
DeclDim.java:3: error: not a statement
        int[3][4] grid;
                  ^
3 errors

Bạn được phép bỏ các chiều phía sau trong new, nhưng không bao giờ được bỏ chiều đầu: các kích thước bạn đưa vào phải là phần đầu liên tiếp của danh sách chiều, nên new int[3][] hợp lệ còn new int[][4] thì không parse nổi:

int[][] grid = new int[][4];
MissingDim.java:3: error: ']' expected
        int[][] grid = new int[][4];
                                 ^
1 error

Cú pháp khởi tạo lồng nhau chỉ dùng được ngay tại chỗ khai báo, và số row cùng nội dung đều lấy từ dấu ngoặc:

int[][] table = {{1, 2}, {3, 4}, {5, 6}};
System.out.println(java.util.Arrays.deepToString(table));
[[1, 2], [3, 4], [5, 6]]

Cả int[][] grid lẫn int grid[][] đều compile được. Hãy dùng cách thứ nhất; cách thứ hai là cú pháp cũ kiểu C mà không codebase Java hiện đại nào còn viết.

grid.length là số row, grid[0].length là độ dài một row

Đây là chỗ gây nhầm nhiều nhất, nên cần nói thật chính xác. grid.length là độ dài của outer array, tức số row. grid[0].length là độ dài của object nằm ở slot 0 — tức row đầu tiên, không hơn.

int[][] grid = new int[3][4];
System.out.println("grid.length    = " + grid.length);
System.out.println("grid[0].length = " + grid[0].length);
System.out.println("grid[2].length = " + grid[2].length);
grid.length    = 3
grid[0].length = 4
grid[2].length = 4

Với grid tạo bằng new int[3][4] thì các row tình cờ bằng nhau, nên grid[0].length trông y như số cột. Nó không phải. Java không lưu con số đó ở đâu cả, và không có gì ngăn bạn thay một row bằng row dài ngắn khác sau đó:

int[][] grid = new int[3][4];
System.out.println("before : " + Arrays.deepToString(grid));
grid[1] = new int[] {1, 2, 3, 4, 5, 6, 7};
System.out.println("after  : " + Arrays.deepToString(grid));
System.out.println("grid[0].length = " + grid[0].length + ", grid[1].length = " + grid[1].length);
before : [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
after  : [[0, 0, 0, 0], [1, 2, 3, 4, 5, 6, 7], [0, 0, 0, 0]]
grid[0].length = 4, grid[1].length = 7

Một row chỉ là một slot chứa reference. Gán một int[] khác vào đó là phép gán reference bình thường, và hình chữ nhật kia chưa từng được ai bảo đảm. Quy tắc cần thuộc là: có một số row và N độ dài row, không bao giờ có số cột.

Jagged array: các row dài ngắn khác nhau

Array có các row dài ngắn khác nhau gọi là jagged array. Nó không phải tính năng riêng — nó chỉ là thứ bạn nhận được khi thôi tạo các row bằng nhau.

Ba row dài 1, 2 và 3 trên cùng một outer array, bên cạnh new int[3][] còn để null

Cách tường minh là new int[3][], câu lệnh này cấp phát outer array và để mỗi reference row là null, sau đó gán từng row:

import java.util.Arrays;

public class Jagged {
    public static void main(String[] args) {
        int[][] tri = new int[3][];
        tri[0] = new int[1];
        tri[1] = new int[2];
        tri[2] = new int[3];

        int n = 1;
        for (int i = 0; i < tri.length; i++) {
            for (int j = 0; j < tri[i].length; j++) {
                tri[i][j] = n++;
            }
        }

        System.out.println("tri          = " + Arrays.deepToString(tri));
        System.out.println("tri.length   = " + tri.length);
        for (int i = 0; i < tri.length; i++) {
            System.out.println("tri[" + i + "].length = " + tri[i].length);
        }

        int[][] literal = {{1}, {2, 3}, {4, 5, 6}};
        System.out.println("literal      = " + Arrays.deepToString(literal));
    }
}
tri          = [[1], [2, 3], [4, 5, 6]]
tri.length   = 3
tri[0].length = 1
tri[1].length = 2
tri[2].length = 3
literal      = [[1], [2, 3], [4, 5, 6]]

Cú pháp khởi tạo lồng nhau tạo ra đúng hình dạng đó mà không cần new nào — các nhóm ngoặc không bắt buộc phải bằng nhau.

new int[3][] để các row là null

Bước hay bị bỏ qua là phép gán. new int[3][] cho bạn ba slot reference, và một slot reference chưa chứa gì thì là null:

int[][] rows = new int[3][];
System.out.println("rows    = " + Arrays.deepToString(rows));
System.out.println("rows[0] = " + rows[0]);
rows    = [null, null, null]
rows[0] = null

Dùng một row như vậy trước khi gán sẽ ném exception, và Java 21 nói rõ hỏng ở đâu:

public class JaggedNpe {
    public static void main(String[] args) {
        int[][] rows = new int[3][];
        rows[0][0] = 1;
    }
}
Exception in thread "main" java.lang.NullPointerException: Cannot store to int array because "rows[0]" is null
	at JaggedNpe.main(JaggedNpe.java:4)

Đọc độ dài của một row còn thiếu thì ra message khác, cũng cụ thể như vậy:

Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "rows[0]" is null
	at JaggedNpe2.main(JaggedNpe2.java:4)

⚠️ Message chỉ gọi tên rows[0] khi class file có mang tên local variable. Compile bằng javac trơn, cùng chương trình đó in ra because "<local1>[0]" is null; javac -g trả lại cái tên. IDE và build tool mặc định truyền -g, nên thường bạn thấy dạng dễ đọc.

Để ý sự bất đối xứng so với cấp phát chữ nhật: new int[3][4] tạo sẵn các row cho bạn nên không có null nào. new int[3][] cố tình không làm thế, vì nó không biết bạn muốn mỗi row dài bao nhiêu.

Duyệt mảng hai chiều

Hai vòng lặp, vòng ngoài chạy qua các row và vòng trong chạy qua các phần tử của row hiện tại. Thứ tự row-major — hết row 0, rồi hết row 1, và cứ thế.

Thứ tự thăm của vòng lặp lồng đánh số trên từng ô, với bound trong là grid[i].length

public class Traverse {
    public static void main(String[] args) {
        int[][] grid = {{1, 2, 3}, {4, 5}, {6, 7, 8, 9}};

        System.out.println("-- indexed --");
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[i].length; j++) {
                System.out.print(grid[i][j] + " ");
            }
            System.out.println();
        }

        System.out.println("-- for-each --");
        for (int[] row : grid) {
            for (int v : row) {
                System.out.print(v + " ");
            }
            System.out.println();
        }
    }
}
-- indexed --
1 2 3 
4 5 
6 7 8 9 
-- for-each --
1 2 3 
4 5 
6 7 8 9 

Hai dạng cho cùng một thứ tự. Dạng indexed cho bạn ij, cần đến khi vị trí có ý nghĩa — transpose, ghi sang array khác, in tọa độ. Dạng for-each ngắn hơn và không thể vượt biên, nên khi chỉ cần giá trị thì dùng nó. Chú ý type của variable ở vòng ngoài: int[] row, không phải int, vì phần tử của mảng hai chiều chính là array.

Chi tiết quan trọng nhất ở bản indexed là bound của vòng trong. Nó là grid[i].length, độ dài của row đang được duyệt — không phải grid[0].length. Trên grid jagged ở trên, bound sai vẫn compile ngon lành và chết ở row thứ hai:

for (int i = 0; i < grid.length; i++) {
    for (int j = 0; j < grid[0].length; j++) {   // sai
        System.out.print(grid[i][j] + " ");
    }
}
1 2 3 4 5 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
	at BadBound.main(BadBound.java:6)

Nó in hết row đầu và hai phần tử đầu của row 1, rồi đòi grid[1][2] trên một row dài 2. Viết grid[i].length ngay từ đầu không tốn gì trên grid chữ nhật và là bản duy nhất sống sót trên grid jagged, nên hãy để nó thành thói quen chứ đừng để thành bản vá.

In mảng hai chiều: Arrays.deepToString

Arrays.toString gọi String.valueOf trên từng phần tử. Với mảng hai chiều thì mỗi phần tử là một object int[], nên thứ bạn nhận được là Object.toString mặc định của từng row — mã type, @, rồi identity hash:

import java.util.Arrays;

public class PrintGrid {
    public static void main(String[] args) {
        int[][] grid = {{1, 2}, {3, 4}};
        System.out.println("println      : " + grid);
        System.out.println("toString     : " + Arrays.toString(grid));
        System.out.println("deepToString : " + Arrays.deepToString(grid));
    }
}
println      : [[I@2a139a55
toString     : [[I@14ae5a5, [I@7f31245a]
deepToString : [[1, 2], [3, 4]]

Arrays.deepToString đi đệ quy vào mọi array lồng bên trong và là câu trả lời cho mọi thứ nhiều hơn một chiều. Nó chạy được ở mọi độ sâu và với cả reference type:

int[][][] cube = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}};
System.out.println(Arrays.deepToString(cube));

String[][] names = {{"Hoang", "Lan"}, {"Minh"}};
System.out.println(Arrays.deepToString(names));
[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
[[Hoang, Lan], [Minh]]

Mấy chữ số hex sau dấu @ là identity hash và khác nhau ở mỗi lần chạy, thêm một lý do đừng đưa chúng ra trước mặt người dùng. Khi debug một grid, Arrays.deepToString là bản vá một dòng.

Arrays.equals so với Arrays.deepEquals

Cùng một sự phân đôi như vậy xuất hiện ở phép so sánh. Arrays.equals so từng phần tử bằng equals, mà với row thì đó là Object.equals — tức so identity của reference. Hai grid giống hệt nhau về cấu trúc vì thế không bằng nhau theo method đó:

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

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("Arrays.deepEquals(a, b): " + Arrays.deepEquals(a, b));
a == b                 : false
a.equals(b)            : false
Arrays.equals(a, b)    : false
Arrays.deepEquals(a, b): true

Arrays.equals trả về false vì nó so a[0] với b[0] như hai object, mà đó là hai object int[] khác nhau tuy cùng nội dung. Arrays.deepEquals đi đệ quy và so các con số. Cặp tương ứng cũng có ở phần hash:

System.out.println("hashCode     : " + (Arrays.hashCode(a) == Arrays.hashCode(b)));
System.out.println("deepHashCode : " + (Arrays.deepHashCode(a) == Arrays.deepHashCode(b)));
hashCode     : false
deepHashCode : true
Độ sâuInSo sánhHash
Array 1 chiềuArrays.toStringArrays.equalsArrays.hashCode
Từ 2 chiều trở lênArrays.deepToStringArrays.deepEqualsArrays.deepHashCode

Copy mảng hai chiều: shallow và deep

Arrays.copyOf copy các phần tử của array bạn đưa vào. Với mảng hai chiều thì các phần tử đó là reference của row, nên bản copy nhận một outer array mới trỏ tới đúng những row cũ. Ghi qua bản copy sẽ thấy được ở bản gốc:

import java.util.Arrays;

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

        int[][] shallow = Arrays.copyOf(original, original.length);
        shallow[0][0] = 99;

        System.out.println("original : " + Arrays.deepToString(original));
        System.out.println("shallow  : " + Arrays.deepToString(shallow));
        System.out.println("same outer array? " + (original == shallow));
        System.out.println("same row 0?       " + (original[0] == shallow[0]));
    }
}
original : [[99, 2], [3, 4]]
shallow  : [[99, 2], [3, 4]]
same outer array? false
same row 0?       true

Đọc hai dòng cuối cùng lúc: outer array đúng là hai object khác nhau, còn row 0 là một object dùng chung cho cả hai. Đó chính là ý nghĩa của chữ "shallow". original.clone()System.arraycopy hành xử y hệt, nên không cách viết một dòng nào cho bạn một grid độc lập.

Muốn deep copy thì phải cấp phát một row mới cho từng row:

int[][] source = {{1, 2}, {3, 4}};
int[][] deep = new int[source.length][];
for (int i = 0; i < source.length; i++) {
    deep[i] = Arrays.copyOf(source[i], source[i].length);
}
deep[0][0] = 99;

System.out.println("source : " + Arrays.deepToString(source));
System.out.println("deep   : " + Arrays.deepToString(deep));
System.out.println("same row 0? " + (source[0] == deep[0]));
source : [[1, 2], [3, 4]]
deep   : [[99, 2], [3, 4]]
same row 0? false

Để ý đích đến được khai báo là new int[source.length][] — chỉ outer array, vì mỗi row sắp bị thay bằng một bản copy. Viết new int[rows][cols] cũng chạy, nhưng sẽ tạo ra các row rồi vứt đi ngay, và sẽ hỏng lặng lẽ khi nguồn là jagged. Copy theo source[i].length cho từng row mới là thứ giữ nguyên được hình dạng.

Ba chiều và hơn nữa

Quy tắc lặp lại đệ quy. int[][][] là array của int[][], mà mỗi cái lại là array của int[]:

int[][][] cube = new int[2][3][4];
cube[1][2][3] = 42;

System.out.println("cube.length       = " + cube.length);
System.out.println("cube[0].length    = " + cube[0].length);
System.out.println("cube[0][0].length = " + cube[0][0].length);
System.out.println("cube[0] type      = " + cube[0].getClass().getName());
System.out.println("cube[0][0] type   = " + cube[0][0].getClass().getName());
System.out.println("cube[1][2][3]     = " + cube[1][2][3]);

int[][][] partial = new int[2][3][];
System.out.println("partial[0][0]     = " + partial[0][0]);
cube.length       = 2
cube[0].length    = 3
cube[0][0].length = 4
cube[0] type      = [[I
cube[0][0] type   = [I
cube[1][2][3]     = 42
partial[0][0]     = null

Mọi quy tắc từ trường hợp hai chiều chuyển sang nguyên vẹn: các chiều phía sau có thể bỏ và để lại null, sub-array ở bất kỳ tầng nào cũng có thể dài ngắn khác nhau, và Arrays.deepToString vẫn in được toàn bộ.

Ba chiều có những chỗ dùng chính đáng — một chồng ảnh, một grid theo thời gian, một khối voxel nhỏ. Vượt qua đó thì bắt đầu loạn index: stock[warehouse][product][month][variant] compile được, nhưng người đọc không biết index nào là gì, và không có gì ngăn bạn đảo nhầm hai cái. Một class với field có tên, hoặc một Map khóa bằng một record nhỏ, mang được cái ý nghĩa mà cặp ngoặc thứ tư đã vứt đi.

Ví dụ thực tế: một matrix nhỏ

Điền một matrix 3x4, in thẳng cột, tính tổng từng row, rồi transpose sang array mới. Toàn bộ ở đây là vòng lặp lồng thông thường; điểm cần nhìn là cách xử lý hình dạng, không phải thuật toán.

public class Matrix {
    public static void main(String[] args) {
        int[][] m = new int[3][4];

        for (int i = 0; i < m.length; i++) {
            for (int j = 0; j < m[i].length; j++) {
                m[i][j] = (i + 1) * (j + 1) * 7;
            }
        }

        System.out.println("matrix " + m.length + "x" + m[0].length);
        print(m);

        System.out.println("row sums");
        for (int i = 0; i < m.length; i++) {
            int sum = 0;
            for (int j = 0; j < m[i].length; j++) {
                sum += m[i][j];
            }
            System.out.printf("  row %d -> %d%n", i, sum);
        }

        int[][] t = transpose(m);
        System.out.println("transposed " + t.length + "x" + t[0].length);
        print(t);
    }

    static int[][] transpose(int[][] src) {
        int[][] out = new int[src[0].length][src.length];
        for (int i = 0; i < src.length; i++) {
            for (int j = 0; j < src[i].length; j++) {
                out[j][i] = src[i][j];
            }
        }
        return out;
    }

    static void print(int[][] m) {
        for (int[] row : m) {
            for (int v : row) {
                System.out.printf("%5d", v);
            }
            System.out.println();
        }
    }
}
matrix 3x4
    7   14   21   28
   14   28   42   56
   21   42   63   84
row sums
  row 0 -> 70
  row 1 -> 140
  row 2 -> 210
transposed 4x3
    7   14   21
   14   28   42
   21   42   63
   28   56   84

Ba chi tiết đáng gọi tên. printf("%5d", v) căn phải mỗi số trong một ô rộng năm ký tự, đó là thứ làm các cột thẳng hàng — nối chuỗi thường không làm được, vì độ rộng của một con số thay đổi theo giá trị. Phần transpose cấp phát new int[src[0].length][src.length], hai chiều bị đảo có chủ đích, rồi ghi out[j][i] = src[i][j]. Và transpose đọc src[0].length làm số row mới, nghĩa là nó giả định đầu vào là hình chữ nhật: gặp jagged array thì nó sẽ mất phần tử hoặc ném exception. Giả định như vậy không sao, miễn là bạn cố ý.

Hình dạng bộ nhớ: các row trong Java không nằm liền một khối

Trong C, int[3][4] là mười hai giá trị int nằm nối đuôi nhau, và a[i][j] là một phép tính địa chỉ. Trong Java, mười hai giá trị đó nằm trong ba object heap riêng biệt, mỗi cái có header riêng và field length riêng, và được với tới qua outer array chứa reference.

Không có chỗ nào trong specification nói ba row object đó nằm cạnh nhau trong bộ nhớ. Chúng được cấp phát như ba object độc lập, và một garbage collector có di dời object có thể chuyển từng cái đi nơi khác trong suốt vòng đời chương trình, nên vị trí tương đối của chúng cũng không cố định.

Hệ quả thực tế gói gọn một dòng: bên trong một row thì các phần tử nằm liền nhau và quét rất tốt, nhưng đi từ row này sang row kế tiếp là một lần gián tiếp qua outer array tới một địa chỉ có thể nằm bất cứ đâu trên heap. Code duyệt một grid lớn theo từng phần tử theo thứ tự row-major vì thế có cache locality kém hơn so với array tương đương trong C, còn duyệt theo cột thì còn tệ hơn. Khi điều đó thật sự có ảnh hưởng, cách chuẩn là dùng một int[rows * cols] phẳng và index bằng data[i * cols + j], đổi lại bạn phải tự làm phép tính index. Với grid thông thường trong code thông thường thì nó không quan trọng, và bản dễ đọc thắng.

Hệ quả thứ hai là số lượng object. new int[1000][1000] tạo ra 1001 object — một outer array chứa reference và một nghìn row — chứ không phải một khối một triệu phần tử. Mỗi row trong số đó đều mang phần overhead mà mọi object Java đều mang.

Những lỗi thường gặp với mảng hai chiều

Mặc định hình dạng là chữ nhật. Không có gì bảo đảm điều đó. Một method nhận int[][] có thể bị đưa vào một jagged array, hoặc một array có row null, bởi bất kỳ caller nào. Nếu thuật toán của bạn cần hình chữ nhật, hãy kiểm tra ngay đầu method thay vì phát hiện ra qua một exception ở giữa ba vòng lặp.

Dùng grid[0].length làm bound cho mọi row. Nó chạy được cho tới lúc không chạy được, và lỗi là ArrayIndexOutOfBoundsException ở giữa quá trình xử lý chứ không phải lỗi compile. Hãy dùng grid[i].length.

Đảo nhầm index row và cột. grid[i][j]grid[j][i] đều compile và đều trông có lý, nên compiler không giúp được gì. Trên grid vuông bạn nhận kết quả sai trong im lặng; trên grid không vuông thường sẽ là lỗi vượt biên với những con số không khớp vòng lặp bạn đang đọc:

int[][] m = new int[2][5];
System.out.println(m[4][1]);
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 2
	at SwapIndex.main(SwapIndex.java:4)

"Index 4 out of bounds for length 2" là outer array đang than phiền. Con số 5 không xuất hiện ở đâu trong message, vì index thứ hai chưa từng được chạm tới. Đọc chữ length trong message đó là số row thì lỗi hiện ra ngay.

new int[3][] rồi dùng luôn. Các row là null cho tới khi bạn gán. Đây là lỗi tạo ra NullPointerException từ đoạn code không hề có chữ null nào.

Gán sai type vào một row. Slot row chứa một int[], không phải một int, và compiler nói thẳng:

int[][] grid = new int[2][];
grid[0] = 5;
RowType.java:4: error: incompatible types: int cannot be converted to int[]
        grid[0] = 5;
                  ^
1 error

Đọc grid[0].length trên grid không có row nào. Array có 0 row là hợp lệ, và grid[0] trên đó ném exception trước khi .length kịp được tính. Hãy kiểm tra grid.length > 0 trước khi chạm vào grid[0] nếu số row không nằm trong tay bạn.

FAQ

Java có hỗ trợ mảng hai chiều không?

Không theo nghĩa một khối chữ nhật liền mạch như C. Java hỗ trợ array của array, và int[][] là cú pháp cho nó. Với dữ liệu hình chữ nhật thì nó hành xử đúng như bạn mong đợi ở một mảng hai chiều, nhưng cấu trúc bên dưới là một outer array chứa reference tới các row object độc lập, và điều đó lộ ra mỗi khi bạn copy, so sánh, in, hoặc cho các row dài ngắn khác nhau.

Lấy số cột của mảng hai chiều trong Java thế nào?

Không có số cột. grid[0].length cho độ dài của row đầu tiên, đó là thứ gần nhất có sẵn và chỉ đúng khi mọi row tình cờ dài bằng nhau. Trong vòng lặp hãy luôn dùng grid[i].length, độ dài của row đang duyệt, và kiểm tra grid.length > 0 trước khi chạm vào grid[0].

Vì sao in mảng hai chiều lại ra thứ như [[I@2a139a55?

Vì array không override toString, nên bạn nhận Object.toString mặc định: mã type của JVM, dấu @, và một identity hash dạng hex. [[I nghĩa là "array of array of int". Dùng Arrays.deepToString(grid) để in giá trị; Arrays.toString(grid) không đủ, vì nó chỉ bóc một tầng và in mỗi row thành [I@....

Các row trong mảng hai chiều của Java có thể dài ngắn khác nhau không?

Có, và đó gọi là jagged array. Tạo riêng outer array bằng new int[3][] rồi gán từng row, hoặc viết khởi tạo lồng nhau với các nhóm ngoặc khác kích thước như {{1}, {2, 3}, {4, 5, 6}}. Ngay cả grid tạo bằng new int[3][4] cũng có thể bị thay một row bằng array dài ngắn khác, vì slot row chỉ là một reference.

Copy mảng hai chiều trong Java sao cho đúng?

Lặp và copy từng row. Arrays.copyOf, clone()System.arraycopy đều copy reference của row, nên bản copy dùng chung row với bản gốc và ghi qua bên này thì bên kia thấy. Bản deep là int[][] deep = new int[src.length][]; rồi deep[i] = Arrays.copyOf(src[i], src[i].length); cho mọi i, cách này cũng giữ nguyên hình dạng jagged.

Mảng hai chiều trong Java có nằm liền nhau trong bộ nhớ không?

Không. Mỗi row là một object heap riêng với header và length của nó, và không có gì bảo đảm hai row nằm cạnh nhau — một garbage collector có di dời object có thể chuyển từng cái đi riêng. Các phần tử trong cùng một row thì nằm liền nhau nên quét rất tốt, nhưng đi qua row là một lần gián tiếp qua outer array. Nếu benchmark cho thấy chuyện đó có ảnh hưởng, hãy làm phẳng thành một int[rows * cols] và index bằng data[i * cols + j].

Kết luận

Chỉ một sự thật gánh cả bài này: int[][] là một array chứa reference tới các object int[], không phải một hình chữ nhật. Từ đó suy ra số row không phải số cột, các row có thể dài ngắn khác nhau, các row nullnew int[3][] để lại, bản shallow copy dùng chung row, và lý do Arrays.toString in ra địa chỉ còn Arrays.deepToString in ra giá trị.

Những thói quen đáng giữ rất ngắn. Đặt bound của vòng trong là grid[i].length. In và so sánh bằng các bản deep. Copy từng row khi cần độc lập. Với tới một class thay vì thêm chiều thứ tư.

Bài tiếp theo trong series: các thuật toán cơ bản với array — tìm giá trị lớn nhất và nhỏ nhất, tìm kiếm, và sắp xếp một array.

Bài viết liên quan

[Java Basics] Đọc và ghi file text trong Java

Đọc và ghi file text trong Java: FileReader và FileWriter, vì sao cần BufferedReader và BufferedWriter, try-with-resources, API hiện đại Files và Path, đường dẫn tương đối, các exception thật khi file không tồn tại, và character encoding quyết định dữ liệu tiếng Việt có sống sót hay không.

[Java Basics] Từ khóa this, static và final trong Java

Ba từ khóa this, static và final trong Java trên JDK 21 — this trỏ vào đâu, vì sao static field dùng chung cho mọi object, thứ tự static initialiser, constant inlining soi bằng javap, và vì sao final không hề làm object immutable.

[Java Basics] Field, method và constructor trong Java

Field, instance method và constructor trong Java: giá trị default của field, field initialiser, overloading constructor và chaining bằng this(...), thứ tự khởi tạo chứng minh bằng lệnh in, cùng mọi error message thật của javac từ việc viết void trước constructor tới việc đặt this(...) không phải đầu tiên.

[Java Basics] Vòng lặp lồng nhau, break và continue trong Java

Vòng lặp lồng nhau trong Java và hai keyword cắt ngang vòng lặp: số lần thân vòng lặp trong chạy, break chỉ thoát vòng lặp gần nhất, continue bỏ qua update trong while, labeled break và continue, cùng bẫy switch nằm trong loop.