Mục lục bài viết

## Thủ Thuật về Java list of array to 2d array Mới Nhất

Update: 2022-03-10 15:58:11,Bạn Cần tương hỗ về Java list of array to 2d array. Bạn trọn vẹn có thể lại Thảo luận ở phía dưới để Mình đc lý giải rõ ràng hơn.

Two-dimensional arrays were introduced in Subsection 3.8.5,

but we haven’t done much with them since then. A 2D array has a type such as

int[][] or String[][], with two pairs of square brackets. The

elements of a 2D array are arranged in rows and columns, and the new operator

for 2D arrays specifies both the number of rows and the number of columns. For example,

int[][] A;

A = new int[3][4];

This creates a 2D array of int that has 12 elements arranged in 3 rows

and 4 columns. Although I haven’t mentioned it, there are initializers for 2D arrays.

For example, this statement creates the 3-by-4 array that is shown in the picture below:

int[][] A = 1, 0, 12, -1 ,

7, -3, 2, 5 ,

-5, -2, 2, -9

;

An array initializer for a 2D array contains the rows of A, separated by commas

and enclosed between braces. Each row, in turn, is a list of values separated by commas

and enclosed between braces. There are also 2D array literals with a similar syntax

that can be used anywhere, not just in declarations. For example,

A = new int[][] 1, 0, 12, -1 ,

7, -3, 2, 5 ,

-5, -2, 2, -9

;

All of this extends naturally to three-dimensional, four-dimensional, and even higher-dimensional

arrays, but they are not used very often in practice.

But before we go any farther, there is a little surprise. Java does not actually have

two-dimensional arrays. In a true 2D array, all the elements of the array occupy a

continuous block of memory, but that’s not true in Java. The syntax for array

types is a clue: For any type BaseType, we should be able to form the type BaseType[], meaning “array of BaseType.”

If we use int[] as the base type, the type that we get is “int[][] meaning “array of int[]” or “array of array

of int.” And in fact, that’s what happens. The elements in a 2D array of type int[][] are variables of type int[].

And remember that a variable of type int[] can only hold a pointer

to an array of int. So, a 2D array is really an array of pointers,

where each pointer can refer to a one-dimensional array. Those one-dimensional

arrays are the rows of the 2D array. A picture will help to explain this.

Consider the 3-by-4 array A defined above.

For the most part, you can ignore the reality and keep the picture of a grid

in mind. Sometimes, though, you will need to remember that each row in the grid

is really an array in itself. These arrays can be referred to as A[0],

A[1], and A[2]. Each row is in fact a value of type

int[]. It could, for example, be passed to a subroutine that asks for

a parameter of type int[].

Some of the consequences of this structure are a little subtle. For example,

thinking of a 2D array, A, as an array of arrays, we see that

A.length makes sense and is equal to the number of rows of A.

If A has the usual shape for a 2D array, then the number of columns in

A would be the same as the number of elements in the first row, that

is, A[0].length. But there is no rule that says that all of the rows

of A must have the same length (although an array created with

new BaseType[rows][columns] will always have that form).

Each row in a 2D array is a separate one-dimensional

array, and each of those arrays can have a different length. In fact, it’s even possible

for a row to be null. For example, the statement

A = new int[3][];

with no number in the second set of brackets, creates an array of 3 elements where

all the elements are null. There are places for three rows, but no actual rows

have been created. You can then create the rows A[0], A[1], and

A[2] individually.

As an example, consider a symmetric matrix. A symmetric matrix, M, is

a two-dimensional array in which the number of rows is equal to the number of columns

and satisfying M[i][j] equals M[j][i] for all i and j. Because of this equality, we only really need to store

M[i][j] for i >= j. We can store the data

in a “triangular matrix”:

It’s easy enough to make a triangular array, if we create each row separately. To create

a 7-by-7 triangular array of double, we can use the code segment

double[][] matrix = new double[7][]; // rows have not yet been created!

for (int i = 0; i < 7; i++)

matrix[i] = new double[i+1]; // Create row i with i + 1 elements.

We just have to remember that if we want to know the value of the matrix at (i,j),

and if i < j, then we actually have to get the value of matrix[j][i]

in the triangular matrix. And similarly for setting values. It’s easy to write a class to

represent symmetric matrices:

/**

* Represents symmetric n-by-n matrices of real numbers.

*/

public class SymmetricMatrix

private double[][] matrix; // A triangular matrix to hold the data.

/**

* Creates an n-by-n symmetric matrix in which all entries are 0.

*/

public SymmetricMatrix(int n)

matrix = new double[n][];

for (int i = 0; i < n; i++)

matrix[i] = new double[i+1];

/**

* Returns the matrix entry at position (row,col). (If row

return matrix[row][col];

else

return matrix[col][row];

/**

* Sets the value of the matrix entry at (row,col). (If row

matrix[row][col] = value;

else

matrix[col][row] = value;

/**

* Returns the number of rows and columns in the matrix.

*/

public int size()

return matrix.length; // The size is the number of rows.

// end class SymmetricMatrix

This class is in the file SymmetricMatrix.java,

and a small program to test it can be found in TestSymmetricMatrix.java.

By the way, the standard function Arrays.copyOf() can’t make a full copy

of a 2D array in a single step. To do that, you need to copy each row separately. To make

a copy of a two-dimensional array of int, for example:

int[][] B = new int[A.length][]; // B has as many rows as A.

for (int i = 0; i < A.length; i++)

B[i] = Arrays.copyOf(A[i], A[i].length)); // Copy row i.

As an example of more typical 2D array processing, let’s look at a very well-known example:

John Conway’s Game of Life, invented by mathematician John Horton Conway in 1970.

This Game of Life is not really a trò chơi (although sometimes it’s

referred to as a “zero-person trò chơi” that plays itself). It’s a “two-dimensional cellular

automaton.” This just means that it’s a grid of cells whose content changes over time

according to definite, deterministic rules. In Life, a cell can only have two possible

contents: It can be “alive” or “dead.” We will use

a 2D array to represent the grid, with each element of the array representing the content

of one cell in the grid. In the trò chơi, an initial grid is set up in which each cell is marked as either alive or dead. After that, the trò chơi “plays itself.” The grid

evolves through a series of time steps. The contents of the grid at each time step

are completely determined by the contents at the previous time step, according to simple rules: Each cell in the grid looks at its eight neighbors (horizontal, vertical,

and diagonal) and counts how many of its neighbors are alive. Then the state of the

cell in the next step is determined by the rules:

- If the cell is alive in the current time step: If the cell has 2 or 3 living neighbors,

then the cell remains alive in the next time step; otherwise, it dies. (A living cell dies of

loneliness if it has 0 or 1 living neighbor, and of overcrowding if it has more than 3 living neighbors.) - If the cell is dead in the current time step: If the cell has 3 living neighbors,

then the cell becomes alive in the next time step; otherwise, it remains dead. (Three living cells

give birth to a new living cell.)

Here’s a picture of part of a Life board, showing the same board before and after the

rules have been applied. The rules are applied to every cell in the grid. The picture shows

how they apply to four of the cells:

The Game of Life is interesting because it gives rise to many interesting and surprising

patterns. (Look it up on Wikipedia.) Here, we are just interested in writing a program to

simulate the trò chơi. The complete program can be found in the file Life.java.

In the program, the life grid is shown as a grid of squares in which dead squares are black and

living squares are white. (The program uses MosaicCanvas.java from

Section 4.7 to represent the grid, so you will also need that file to compile

and run the program.) In the program, you can fill the life board randomly with dead and alive cells, or you can use the mouse to set up the trò chơi board. There is a “Step” button

that will compute one time-step of the trò chơi, and a “Start” button that will run time steps

as an animation.

We’ll look at some of the array processing involved in implementing the Game of Life for this program.

Since a cell can only be alive or dead, it is natural to use a two-dimensional array

of boolean[][] to represent the states of all the cells. The array is named

alive, and alive[r][c] is true when the cell in row r,

column c is alive. The number of rows and the number of columns are equal and are given

by a constant, GRID_SIZE. So, for example, to fill the Life grid with random

values, the program uses simple nested for loops:

for (int r = 0; r < GRID_SIZE; r++)

for (int c = 0; c < GRID_SIZE; c++)

// Use a 25% probability that the cell is alive.

alive[r][c] = (Math.random() < 0.25);

Note that the expression (Math.random() < 0.25) is a true/false

value that can be assigned to a boolean array element. The array is also

used to set the color of the cells on the screen. Since the grid of cells is displayed on screen as a

MosaicCanvas, setting the colors is done using the MosaicCanvas API.

Note that the actual drawing is done in the MosaicCanvas class

(which has its own 2D array of type Color[][] to keep

track of the colors of each cell). The Life program just has to set the colors in the mosaic,

using the MosaicCanvas API.

This is done in the program in a method named showBoard() that is

called each time the board changes. Again, simple nested for

loops are used to set the color of each square in the grid:

for (int r = 0; r < GRID_SIZE; r++)

for (int c = 0; c < GRID_SIZE; c++)

if (alive[r][c])

display.setColor(r,c,Color.WHITE);

else

display.setColor(r,c,null); // Shows the background color, black.

Of course, the most interesting part of the program is computing the new state

of the board by applying the rules to the current state. The rules apply to each individual cell, so again we can use nested for loops to work through all

the cells on the board, but this time the processing is more complicated. Note first

that we can’t make changes to the values in the array as we work through it,

since we will need to know the old state of a cell when processing its neighboring cells.

In fact, the program uses a second array to hold the new board as it is being created.

When the new board is finished, it can be substituted for the old board. The algorithm

goes like this in pseudocode:

let newboard be a new boolean[][] array

for each row r:

for each column c:

Let N be the number of neighbors of cell (r,c) in the alive array

if ((N is 3) or (N is 2 and alive[r][c]))

newboard[r][c] = true;

else

newboard[r][c] = false;

alive = newboard

Note that at the end of the process, alive is pointing to a

new array. This doesn’t matter as long as the contents of the array represent the

new state of the trò chơi. The old array will be garbage collected. The test for

whether newboard[r][c] should be true or false

might not be obvious, but it implements the rules correctly. We still need to

work on counting the neighbors. Consider the cell in row r and

column c. If it’s not at an edge of the board, then it’s clear where

its neighbors are:

The row above row number r is row number r-1, and the row

below is r+1. Similarly for the columns. We just have to look at the

values of alive[r-1][c-1], alive[r-1][c], alive[r-1][c+1], alive[r][c-1], alive[r][c+1], alive[r+1][c-1], alive[r+1][c], and alive[r+1][c+1], and count the number that

are true. (You should make sure that you understand how the array indexing works

here.)

But there is a problem when the cell is along one of the edges of the grid. In that

case, some of the array elements in the list don’t exist, and an attempt to use them

will cause an exception. To avoid the exception, we have to give special consideration to

cells along the edges. One idea is that before referencing any array element, check that

the array element actually exists. In that case, the code for neighbor counting becomes

if (r-1 >= 0 && c-1 >= 0 && alive[r-1][c-1])

N++; // A cell at position (r-1,c-1) exists and is alive.

if (r-1 >= 0 && alive[r-1][c])

N++; // A cell at position (r-1,c) exists and is alive.

if (r-1 >= 0 && c+1 <= GRID_SIZE && alive[r-1][c+1])

N++; // A cell at position (r-1,c+1) exists and is alive.

// and so on…

All the possible exceptions are avoided. But in my program, I actually do something

that is common in 2D computer games—I pretend that the left edge of the board is

attached to the right edge and the top edge to the bottom edge. For example, for a

cell in row 0, we say that the row “above” is actually the bottom row, row

number GRID_SIZE-1. I use variables to represent the positions above, below, left,

and right of a given cell. The code turns out to be simpler than the code shown above.

Here is the complete method for computing the new board:

private void doFrame() // Compute the new state of the Life board. boolean[][] newboard = new boolean[GRID_SIZE][GRID_SIZE]; for ( int r = 0; r 0 ? r-1 : GRID_SIZE-1; // (for “?:” see Subsection 2.5.5)

below = r < GRID_SIZE-1 ? r+1 : 0;

for ( int c = 0; c 0 ? c-1 : GRID_SIZE-1;

right = c < GRID_SIZE-1 ? c+1 : 0;

int n = 0; // number of alive cells in the 8 neighboring cells

if (alive[above][left])

n++;

if (alive[above][c])

n++;

if (alive[above][right])

n++;

if (alive[r][left])

n++;

if (alive[r][right])

n++;

if (alive[below][left])

n++;

if (alive[below][c])

n++;

if (alive[below][right])

n++;

if (n == 3

alive = newboard;

Again, I urge you to check out the source code, Life.java, and

try the program. Don’t forget that you will also need MosaicCanvas.java.

As a final example for this chapter, we’ll look at a more substantial example of using a 2D array. This is the longest program that we have encountered so far, with 745 lines of

code. The program lets two users play checkers against each other.

The checkers trò chơi is played on an eight-by-eight board, which is based on an

example from Subsection 6.5.1. The players are called “red” and “black,”

after the color of their checkers. I’m not going to explain the rules of checkers here;

possibly you can learn them by trying out the program.

In the program, a player

moves by clicking on the piece that they want to move, and then clicking on the empty square to

which it is to be moved. As an aid to the players,

any square that the current player can legally click at a given time

is highlighted with a brightly colored border. The square containing a piece that has been selected to be moved, if any, is surrounded by a yellow border. Other pieces that can legally be moved are surrounded by a

cyan-colored border. If a piece has already been selected to be moved, each empty square that it

can legally move to is highlighted with a green border. The trò chơi enforces the rule

that if the current player can jump one of the opponent’s pieces, then the

player must jump. When a player’s piece becomes a king, by reaching the

opposite end of the board, a big white “K” is drawn on the piece.

Here is a picture of the program early in a trò chơi. It is black’s turn

to move. Black has selected the piece in the yellow-outlined square to be moved.

Black can click one of the squares outlined in green to complete the move, or

can click one of the squares outlined in cyan to select a different piece to be moved.

I will only cover a part of the programming for this example. I encourage you

to read the complete source code, Checkers.java. It’s long and complex,

but with some study, you should understand all the techniques that it uses. The program is

a good example of state-based, sự kiện-driven, object-oriented programming.

The data about the pieces on the board are stored in a two-dimensional

array. Because of the complexity of the program, I wanted to divide it into

several classes. In addition to the main class, there are several nested classes.

One of these classes is CheckersData, which handles

the data for the board. It is not directly responsible for any part of the graphics or

sự kiện-handling, but it provides methods that can be called by other

classes that handle graphics and events. It is mainly this class that I want to talk about.

The CheckersData class has an instance variable named

board of type int[][]. The value of board is set to “new int[8][8]“, an 8-by-8 grid of integers. The values stored in the grid are

defined as constants representing the possible contents of a square on a

checkerboard:

static final int

EMPTY = 0, // Value representing an empty square.

RED = 1, // A regular red piece.

RED_KING = 2, // A red king.

BLACK = 3, // A regular black piece.

BLACK_KING = 4; // A black king.

The constants RED and BLACK are also used in my program

(or, perhaps, misused) to represent the two players in the trò chơi. When a trò chơi is

started, the values in the array are set to represent the

initial state of the board. The grid of values looks like

A regular black piece can only move “down” the grid. That is, the row number of the

square it moves to must be greater than the row number of the square it comes

from. A regular red piece can only move up the grid. Kings of either color

can move in both directions.

One function of the CheckersData class is to take care of changes

to the data structures that need to be made when one of the users moves a checker.

An instance method named

makeMove() is provided to do this. When a player moves a piece from

one square to another, the values of two elements in the array are

changed. But that’s not all. If the move is a jump, then the piece that was

jumped is removed from the board. (The method checks whether the move is a jump

by checking if the square to which the piece is moving is two rows away from

the square where it starts.) Furthermore, a RED piece that moves to

row 0 or a BLACK piece that moves to row 7 becomes a king. Putting all

that into a subroutine is

good programming: the rest of the program doesn’t have to worry about any of

these details. It just calls this makeMove() method:

/**

* Make the move from (fromRow,fromCol) to (toRow,toCol). It is

* ASSUMED that this move is legal! If the move is a jump, the

* jumped piece is removed from the board. If a piece moves

* to the last row on the opponent’s side of the board, the

* piece becomes a king.

*/

void makeMove(int fromRow, int fromCol, int toRow, int toCol) fromRow – toRow == -2)

// The move is a jump. Remove the jumped piece from the board.

int jumpRow = (fromRow + toRow) / 2; // Row of the jumped piece.

int jumpCol = (fromCol + toCol) / 2; // Column of the jumped piece.

board[jumpRow][jumpCol] = EMPTY;

if (toRow == 0 && board[toRow][toCol] == RED)

board[toRow][toCol] = RED_KING; // Red piece becomes a king.

if (toRow == 7 && board[toRow][toCol] == BLACK)

board[toRow][toCol] = BLACK_KING; // Black piece becomes a king.

// end makeMove()

An even more important function of the CheckersData class is to

find legal moves on the board. In my program, a move in a Checkers trò chơi is

represented by an object belonging to the following class:

/**

* A CheckersMove object represents a move in the trò chơi of

* Checkers. It holds the row and column of the piece that is

* to be moved and the row and column of the square to which

* it is to be moved. (This class makes no guarantee that

* the move is legal.)

*/

private static class CheckersMove

int fromRow, fromCol; // Position of piece to be moved.

int toRow, toCol; // Square it is to move to.

CheckersMove(int r1, int c1, int r2, int c2)

// Constructor. Set the values of the instance variables.

fromRow = r1;

fromCol = c1;

toRow = r2;

toCol = c2;

boolean isJump()

// end class CheckersMove.

The CheckersData class has an instance method which finds all the

legal moves that are currently available for a specified player. This method is

a function that returns an array of type CheckersMove[]. The array

contains all the legal moves, represented as CheckersMove objects. The

specification for this method reads

/**

* Return an array containing all the legal CheckersMoves

* for the specified player on the current board. If the player

* has no legal moves, null is returned. The value of player

* should be one of the constants RED or BLACK; if not, null

* is returned. If the returned value is non-null, it consists

* entirely of jump moves or entirely of regular moves, since

* if the player can jump, only jumps are legal moves.

*/

CheckersMove[] getLegalMoves(int player)

A brief pseudocode algorithm for the method is

Start with an empty list of moves

Find any legal jumps and add them to the list

if there are no jumps:

Find any other legal moves and add them to the list

if the list is empty:

return null

else:

return the list

Now, what is this “list”? We have to return the legal moves in an array. But

since an array has a fixed size, we can’t create the array until we know how

many moves there are, and we don’t know that until near the end of the method,

after we’ve already made the list! A neat solution is to use an

ArrayList instead of an array to hold the moves as we find them. In fact, I use an object defined by the parameterized type

ArrayList so that the list is

restricted to holding objects of type CheckersMove. As we

add moves to the list, it will grow just as large as necessary. At the end of

the method, we can create the array that we really want and copy the data into

it:

Let “moves” be an empty ArrayList

Find any legal jumps and add them to moves

if moves.size() is 0: // There are no legal jumps!

Find any other legal moves and add them to moves

if moves.size() is 0: // There are no legal moves at all!

return null

else:

Let moveArray be an array of CheckersMoves of length moves.size()

Copy the contents of moves into moveArray

return moveArray

Now, how do we find the legal jumps or the legal moves? The information we

need is in the board array, but it takes some work to extract it. We

have to look through all the positions in the array and find the pieces that

belong to the current player. For each piece, we have to check each square that

it could conceivably move to, and check whether that would be a legal move.

If we are looking for legal jumps, we want to look at squares that

are two rows and two columns away from the piece. There are four squares to consider.

Thus, the line in the

algorithm that says “Find any legal jumps and add them to moves” expands

to:

For each row of the board:

For each column of the board:

if one of the player’s pieces is at this location:

if it is legal to jump to row + 2, column + 2

add this move to moves

if it is legal to jump to row – 2, column + 2

add this move to moves

if it is legal to jump to row + 2, column – 2

add this move to moves

if it is legal to jump to row – 2, column – 2

add this move to moves

The line that says “Find any other legal moves and add them to moves”

expands to something similar, except that we have to look at the four squares

that are one column and one row away from the piece. Testing whether a player

can legally move from one given square to another given square is itself

non-trivial. The square the player is moving to must actually be on the board,

and it must be empty. Furthermore, regular red and black pieces can only move

in one direction. I wrote the following utility method to check whether a

player can make a given non-jump move:

/**

* This is called by the getLegalMoves() method to determine

* whether the player can legally move from (r1,c1) to (r2,c2).

* It is ASSUMED that (r1,c1) contains one of the player’s

* pieces and that (r2,c2) is a neighboring square.

*/

private boolean canMove(int player, int r1, int c1, int r2, int c2) r2 >= 8 // end canMove()

This method is called by my getLegalMoves() method to check whether

one of the possible moves that it has found is actually legal. I have a similar

method that is called to check whether a jump is legal. In this case, I pass to

the method the square containing the player’s piece, the square that the player

might move to, and the square between those two, which the player would be

jumping over. The square that is being jumped must contain one of the

opponent’s pieces. This method has the specification:

/**

* This is called by other methods to check whether

* the player can legally jump from (r1,c1) to (r3,c3).

* It is assumed that the player has a piece at (r1,c1), that

* (r3,c3) is a position that is 2 rows and 2 columns distant

* from (r1,c1) and that (r2,c2) is the square between (r1,c1)

* and (r3,c3).

*/

private boolean canJump(int player, int r1, int c1,

int r2, int c2, int r3, int c3) { . . .

Given all this, you should be in a position to understand the complete

getLegalMoves() method. It’s a nice way to finish off this chapter,

since it combines several topics that we’ve looked at: one-dimensional arrays,

ArrayLists, and two-dimensional arrays:

CheckersMove[] getLegalMoves(int player)

if (player != RED && player != BLACK)

return null; // (This will not happen in a correct program.)

int playerKing; // The constant for a King belonging to the player.

if (player == RED)

playerKing = RED_KING;

else

playerKing = BLACK_KING;

ArrayList moves = new ArrayList();

// Moves will be stored in this list.

/* First, check for any possible jumps. Look at each square on

the board. If that square contains one of the player’s pieces,

look at a possible jump in each of the four directions from that

square. If there is a legal jump in that direction, put it in

the moves ArrayList.

*/

for (int row = 0; row < 8; row++)

for (int col = 0; col < 8; col++) board[row][col] == playerKing)

if (canJump(player, row, col, row+1, col+1, row+2, col+2))

moves.add(new CheckersMove(row, col, row+2, col+2));

if (canJump(player, row, col, row-1, col+1, row-2, col+2))

moves.add(new CheckersMove(row, col, row-2, col+2));

if (canJump(player, row, col, row+1, col-1, row+2, col-2))

moves.add(new CheckersMove(row, col, row+2, col-2));

if (canJump(player, row, col, row-1, col-1, row-2, col-2))

moves.add(new CheckersMove(row, col, row-2, col-2));

/* If any jump moves were found, then the user must jump, so we

don't add any regular moves. However, if no jumps were found,

check for any legal regular moves. Look at each square on

the board. If that square contains one of the player's pieces,

look at a possible move in each of the four directions from

that square. If there is a legal move in that direction,

put it in the moves ArrayList.

*/

if (moves.size() == 0)

for (int row = 0; row < 8; row++)

for (int col = 0; col < 8; col++) board[row][col] == playerKing)

if (canMove(player,row,col,row+1,col+1))

moves.add(new CheckersMove(row,col,row+1,col+1));

if (canMove(player,row,col,row-1,col+1))

moves.add(new CheckersMove(row,col,row-1,col+1));

if (canMove(player,row,col,row+1,col-1))

moves.add(new CheckersMove(row,col,row+1,col-1));

if (canMove(player,row,col,row-1,col-1))

moves.add(new CheckersMove(row,col,row-1,col-1));

/* If no legal moves have been found, return null. Otherwise, create

an array just big enough to hold all the legal moves, copy the

legal moves from the ArrayList into the array, and return the array.

*/

if (moves.size() == 0)

return null;

else

CheckersMove[] moveArray = new CheckersMove[moves.size()];

for (int i = 0; i < moves.size(); i++)

moveArray[i] = moves.get(i);

return moveArray;

// end getLegalMoves

The checkers program is complex, and you can be sure that it didn’t just fall together.

It took a good giảm giá of design work to decide what classes and objects would be used,

what methods should be written, and what algorithms the methods should use.

The complete source code is in the file Checkers.java.

Take a look!

End of Chapter 7

Reply

5

0

Chia sẻ

### Review Chia Sẻ Link Cập nhật Java list of array to 2d array ?

– Một số Keywords tìm kiếm nhiều : ” **Video full hướng dẫn Java list of array to 2d array tiên tiến và phát triển nhất ** , * Share Link Download Java list of array to 2d array * “.

#### Hỏi đáp vướng mắc về Java list of array to 2d array

Bạn trọn vẹn có thể để lại phản hồi nếu gặp yếu tố chưa hiểu nhé.

#Java #list #array #array Java list of array to 2d array

## Bình luận gần đây