Kiểu dữ liệu trong Java

1. Kiểu dữ liệu

Để khai báo một biết trong Java đồi hỏi cần phải chỉ cho nó có kiểu dữ liệu nào để giới hạn nhóm giá trị. Java cần biết kiểu dữ liệu để có cách phân bổ vùng nhớ tốt hơn cho chương trình. Người dùng thì cần biết nó để biết nó dùng để làm gì và sử dụng nó trong giới hạn hợp lý.

Hình 1 – Kiểu dữ liệu nguyên thủy trong Java. Nguồn tử Internet.

Kiểu dữ liệu trong Java được phân làm 2 nhóm chính: Kiểu dữ liệu Nguyên thủy (Primitive Data Type) và Kiểu dữ liệu Tham chiếu (Reference Data Type).

2. Primitive Data Type – Kiểu nguyên thủy

Kiểu dữ liệu nguyên thủy (primitive data type) là kiểu dữ liệu cơ sở trong Java. Kiểu này chỉ lưu giá trị của nó chứ không lưu gì thêm tại một thời điểm. Nó được chứa trong bộ nhớ stack khi được khởi tạo. Trong hình Hình 1 ta thấy Java có 8 kiểu dữ liệu nguyên thủy được chi thành 4 nhóm chính: logic, ký tự, số nguyên và số thực. Kích thước nhỏ nhất là kiểu byte (1 bit) và lớn nhất là long và double (64 bit).

Kiểu dữ liệu Kích thước (bits) Phạm vi giá trị Ví dụ
byte 8 -128
đến 127
byte byteNumber = 100;
Giá trị mặc định: 0
short 16 -32,768
đến 32,767
short shortNumber = 10000;
Giá trị mặc định: 0
int 32 -2,147,483,648
đến 2,147,483,647
int intNumber = 100000;
Giá trị mặc định: 0
long 64 -9,223,372,036,854,775,808
đến 9,223,372,036,854,775,807
long longNumber = 100000l;
Giá trị mặc định:0L
float 32 1.4E-45 đến 3.4028235E38 float floatNumber = 9.08f;
Giá trị mặc định: 0.0F
double 64 4.9E-34 đến 1.7976931348623157E308 double doubleNumber = 9.08;
Giá trị mặc định: 0.0D
boolean 1 true/false boolean boolValue = true;
Giá trị mặc định: false
char 16 0
đến 65,535
char charValue = ‘a’;
Giá trị nhỏ nhất: “\u0000”
Giá trị lớn nhất: “\uffff”
Giá trị mặc định: “\u0000” nghĩa là null.

Bảng 1 – Kiểu dữ liệu nguyên thủy trong Java.

Kiểu dữ liệu nguyên thủy không đủ các hàm hỗ trợ cho lập trình viên nên Java cũng tạo ra kiểu wapper (gói). Wapper cũng là đối tượng nhưng nó chứ hoàn toàn kiểu nguyên thủy. Nó “gói” kiểu nguyên thủy và thêm các hàm khác để tiện dụng kiểu dữ liệu đó hơn. Tương ứng với mỗi kiểu dữ liệu nguyên thủy thì ta có một lớp wapper. Java có 8 kiểu dữ liệu Nguyên thủy thì có 8 kiểu dữ liệu wapper tương ứng.

Primitive data Wrapper class
boolean Boolean
char Character
byte Byte
short Short
int Integer
long Long
float Float
double Double

Bảng 2 – Bảng tương ứng lớp wapper tương ứng với kiểu dữ liệu nguyên thủy.

2.1. Kiểu Boolean

Đây là kiểu dữ liệu có độ lớn nhỏ nhất 1 bit. Nó chỉ có 2 giá trị là 1 hoặc 0 tương ứng với true và false. Nó giống như hành động tắc/ mở công tắc ở bóng đèn nhà bạn.

Trong Java boolean được xem là kiểu dữ liệu nguyên thủy còn Boolean được xem là lớp wapper của nó. Nguyên thủy chỉ chứa 2 giá trị true/false nhưng wapper Boolean lạ chứa 3 giá trị true/fall/null. Bên cạnh đó Boolean còn cung cấp thêm các hàm phụ trợ để so sánh bằng (equal), sao chép (clone), hay kiểm tra đúng sai (isTrue) …

2.2. Kiểu Byte

Kiểu dữ liệu này chứa 8bit hay còn gọi là 1 byte. Giá trị
giao động từ -128 đến 127 (độ phủ rộng của nó là 256). Điều này dễ hiểu thôi. Nếu 1 bit có số 0/1 mà chúng ta đang có 8 vị trí tổng số có thể xãy ra là 2^8 = 256. Ví dụ bên dưới ta thấy có 8 số 0 tương ứng với 8 bit. Và mỗi bit sẽ là 0/1.

0000 0000                 1100
0011                 0101 1100

2.3. Kiểu Short

Kiểu short có độ lớn 16 bit. Sẽ có khoảng rộng là 2^16 = 65,536. Nó nằm trong khoảng giá trị -32,768 ÷ 32,767. Do đó trong quá trình lập trình nếu ta đã biết dữ liệu chính xác không thể lớn hơn giới hạn đó thì mạnh dạng dùng short để Java cấp bộ nhớ hợp lý cho ứng dụng.

2.4. Kiểu Char

Kiểu char có độ lớn 16 bit. Tương tự với kiểu short nó sẽ có độ rộng 65,536. Nhưng nó lại bắt đầu từ số 0. Do đó khoảng giá trị là 0 ÷ 65,535. Trong kiểu char 0 được thể hiện bằng mã \u0000 (u thể hiện unicode) và \uffff là 65,535.

Khi khai báo biến có kiểu dữ liệu là char mà không gán giá trị cho nó thì nó sẽ tự gán cho mình một giá trị nhỏ nhất đó là \u0000 (chính là giá trị 0).

2.5. Kiểu Int

Kiểu int có độ lớn 32 bit. Vậy độ rộng của nó là 2^32 ≈ 4.3 tỉ. Nó có khoảng các nằm trong dãy âm dương (nghĩa là số 0 sẽ nằm giữa). Kiểu int sẽ giao động – 2.15 tỉ ÷ 2.15 tỉ. Hiện tổng dân số thế giới là 7.7 tỉ người (11/2018) gần như gấp 4 lần kiểu int. Do đó không thể dùng kiểu int để thể hiện ID cho một người dân.

Khi khai báo một biến mà không có gán giá trị cho nó. Nó sẽ tự động tạo giá trị mặc định cho mình với giá trị là 0. Ta thấy nó làm vậy khi khai báo thuộc tính của một lớp với giá trị kiểu int.

2.6. Kiểu Float

Kiểu float có độ lớn 32 bit. Tương tự kiểu int, nó sẽ có độ lớn gần 4.3 tỉ. Kiểu float được dùng để biểu diễn số thực. Nó nằm trong khoảng giá trị -3.4 × 1038 ÷ 3.4 × 1038. Trong toán học số thực được ký hiệu là ℝ được sử dụng để thể hiện chia nhỏ số nguyên ra một dãy số rất lớn. Ví dụ số nguyên ta có 0 ÷ 1 là 2 số. Nhưng trong số thực sẽ có rất nhiều số tồn tại trong dãy từ 0 ÷1 ấy. Như 0.1 – 0.2 – 0.25 – 0.257 …

Mặc dù khoảng của số thực kéo dài âm-dương nhưng số mặc định là 0.0F nếu ta khai báo biến số thực mà không gán cho cho nó một giá trị. Kiểu float thường dùng thể hiện các kiểu dữ liệu dạng phần trăm, tỉ lệ… trong các con số nhỏ cần độ chính xác cao.

Hình 2 – Mối tương quan giữa số tự nhiên ℕ, số nguyên ℤ,
số hữu tỉ ℚ, số vô tỉ ℝ/ ℚ và số thực ℝ.

2.7. Kiểu Long

Kiểu long được dùng để thể hiện số nguyên có kích thước lên đến 64 bit (8 byte). Độ rộng của nó sẽ là 264 ≈ 18.466 tỉ-tỉ. Số này khá lớn và được xem là kiểu dữ liệu lớn nhất của Java ở hiện tại. Nó là dãy số âm-dương (độ lớn chia đôi ở giữa là số 0). Điều đó cho thấy kiểu long có độ lớn gấp 1032 lầ so với kiểu int.

Kiểu long được sử dụng khi số lượng lớn hơn 2.15 tỷ. Và cũng là sự lựa chọn cuối cùng vì nó là kiểu dữ liệu lớn nhất hiện tại trong Java. Nó được sử dụng làm ID cho các giao dịch là phổ biến. Vì số lượng các giao dịch rất nhiều mỗi ngày.

Giá trị mặc định của các kiểu dữ liệu thường là số dương nhỏ nhất. Long không phải là ngoại lệ. Do đó giá trị mặc định của nó là 0.0L.

2.8. Kiểu Double

Giống kiểu long về độ lớn 64 bit (8 byte) nhưng nó thuộc nhánh số thực. Khoảng giới hạn của nó là -1.798 × 10308 ÷ 1.798 × 10308. Kiểu double lớn hơn kiểu float 10270 lần. Nó thường được sử dụng trong một hệ qui chiếu cực nhỏ chẳng hạn như độ lớn nguyên tử, phân tử… hay sử dụng trong các hệ số cực lớn như không gian, vũ trụ… hay dùng trong các số cần có độ chính xác cự kỳ cao.

Để chọn kiểu dữ liệu nào phù hợp ta cần phải phỏng đoán độ lớn của biến đó sẽ lớn nhất có thể bao nhiêu dựa trên giới hạn thời gian mà ứng dụng có thể tồn tại. Nhưng nếu ta quá tham lam chọn số quá lớn thì bộ nhớ giành cho nó quá lãng phí. Vì khi khai báo biến có kiểu dữ liệu nào thì JVM cần phải cấp phát vùng nhớ tương ứng để chuẩn bị không gian cho nó. Không ai được chiếm không gian của nó mặc dù nó không được sửa dụng.

Điều này cực kỳ quan trọng đối với các hệ thống máy tính thời đầu. Bộ nhớ của RAM kém nên cần tối ưu. Nhưng hiện nay thì không còn quan trọng nhiều. Nhưng bạn sẽ thấy nó ảnh hưởng đến các hệ thống nhỏ như thiết bị Mobile hay IoT (các con cảm biến…). Do đó nếu ta xây dựng tư duy tốt về việc lựa chọn kiểu dữ liệu là ta đang góp phần vào tiết kiệm tài nguyên không cần thiết do các sản phẩm của ta gây ra.

3. Reference Data Type – Kiểu Dữ Liệu Tham Chiếu

Kiểu dữ liệu tham chiếu (Reference Data Type) đôi khi còn được gọi là Không nguyên thủy (Non-Primitive Data Type). Nó là các đối tượng được lưu giá trị trong bộ nhớ heap theo cơ chế hoạt động dạng con trỏ. Kiểu tham chiếu được xem là bảng nâng cấp lên từ kiểu nguyên thủy. Vì nó được hình thành dựa trên các kiểu dữ liệu nguyên thủy. Các hàm hỗ trợ trong kiểu tham chiếu sẽ giúp ta dễ dàng thao tác với kiểu dữ liệu nguyên thủy hơn.

Hãy tưởng tượng rằng khi ta gọi một biến có kiểu dữ liệu tham chiếu để lấy giá trị của nó. Thực chất ta không gọi giá trị của nó mà ta đang bảo nó đi đến vùng nhớ heap lấy giá trị hiện tại của biến đó đi. Nếu biến đó được dùng vùng nhớ với biến khác thì biến khác đã thay đổi vùng nhớ đó thì biến hiện ta đang sử dụng cũng bị thay đổi đổi theo.


Hình 3 – Biến tham chiếu đến vùng nhớ từ biến a, b.

Trong hình ta thấy biến a, b đang dùng chung vùng nhớ. Vậy khi biến a hoặc biến b thay đổi giá trị vùng nhớ thì cả 2 điều bị ảnh hưởng. Vùng nhớ sẽ được giải phóng bởi Gabage Collection (GC) trong Java nếu nó không được ai trỏ đến nó.

Cuối cùng máy tính cũng chỉ hoạt động trên kiểu nguyên thủy mà kiểu tham chiếu như là một người thông dịch. Kiểu tham chiếu sẽ được thể hiện ở 4 dạng: kiểu class, kiểu interface, kiểu enum, kiểu array.


Hình 4 – Kiểu dữ liệu tham chiếu trong Java.

3.1. Kiểu Class

Một đối tượng được sinh ra từ định nghĩa của một lớp bằng phương thức new. Phương thức này sẽ gọi phương thức khởi tạo có tên constructor trong lớp đó. Đối tượng này và các thuộc tính của nó được lưu trong vùng nhớ heap.

Trong lớp chứa các thuộc tính, phương thức và phương thức khỏi tạo (constructor) của lớp. Để sử dụng ta cần phải tạo ra một đối tượng sau đó mới gọi các thuộc tính của nó. Đối tượng ấy sẽ tồn tại từ lúc khởi tạo cho đến khi không còn thám chiếu trỏ đến nó. Ví dụ: một khối mã tạo sau đó thực hiện hành động nào đó. Sau đó thoát ra khỏi khối mã đó. Bộ dọn rác (Gabage Collection) sẽ tự động xóa nó.

public class Student {

    public static void main(String[] args) {
        Student student = new Student("Tien Nguyen", 32);
        System.out.println("Name: " + student.getName());
        System.err.println("Age: " + student.getAge());
        System.out.println(student);
    }

    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" + "name=" + name + ", age=" + age + '}';
    }
}

Trên là ví dụ một lớp Student và phương thức main để chạy khối mã khởi tạo đối tượng và in ra màn hình giá trị các thuộc tính của đối tượng. Đối tượng sẽ tồn tại trong bộ nhớ heap từ khi new cho đến khi kết thúc phương thức main.

3.2. Kiểu Inteface

Interface không giống một lớp. Nó được sử dụng trong thiết kế nhiều hơn. Lập trình viên thường sử dụng nó để tạo nên một sự ràng buộc tránh tự do nhằm giảm đi lỗi khi hệ thống quá lớn không còn kiểm soát được được.

Interface được xem là một khuôn mẫu để một lớp thực thi theo khuôn mẫu đó. Do đó các phương thức trong interface không có thân mà thân sẽ được lớp thực thi nó thực hiện. Các phương thức này các các phương thức trừu tượng mặc dù nó có khai báo public abstract hay không.

Nếu cần khai báo một thuộc tính trong một lớp interface thì thuộc tính đó luôn là public static final mặc dù ta có khai báo dòng mã này hay không. Điều đó cho thấy thuộc tính đó không thuộc đối tượng mà nó chỉ mượn interface làm tham chiếu đến nó thôi. Thuộc tính tồn tại như là một đối tượng riêng.

Như đã nói ở trên, interface không tạo nên một đối tượng thực sự trong vùng nhớ. Do đó nó không cần phương thức khởi tạo. Khi sử dụng các thuộc tính hay phương thức trong interface chỉ cần gọi trực tiếp. Ví dụ: Person.go() trong đó Person là một interfacego là một phương thức trong interface được thực thi bởi các lớp như Student, Teacher.

// interface not have constructor method.
public interface Person {
    void go();
}

// class Student implement interface Person.
public class Student implements Person {
    @Override
    public void go() {
        System.err.println("Student is going!");
    }
}

// class Teacher implement interface Person.
public class Teacher implements Person {
    @Override
    public void go() {
        System.err.println("Teacher is going!");
    }
}

3.3. Kiểu Enum

Enum là kiểu dữ liệu mang tính chất đại diện cho giá trị của nó. Giá trị của nó vẫn được lưu trữ trong vùng nhớ heap. Mục tiêu sử dụng enum là giúp dễ hiểu, giảm lỗi trong quá trình viết mã.

Giả sử ta cần sử dụng các ngày trong tuần. Giá trị lần lược là 1, 2, 3, 4, 5, 6, 7 tương ứng với thứ 2, thứ 3, thứ 4, …, chủ nhật. Nếu ta có một phương thức cần truyền thứ trong tuần thì ta truyền một trong các số nguyên từ một số trong 7 số trên. Nếu khác không đọc quy ước số này thì sẽ khó mà hiểu được tham số đã truyền vào là thứ mấy. Enum sẽ giúp ta có cách gọi khác thay vì số 1 thì chương trình sẽ sử dụng MONDAY thực chất là số 1. Ta có thể gọi enum là định danh (alias).

public enum Day {
    MONDAY(1),
    TUESDAY(2),
    WEDNESDAY(3),
    THURDAY(4),
    FRIDAY(5),
    SATURDAY(6),
    SUNDAY(7);

    private int code;

    Day(int code) {
        this.code = code;
    }

    @Override
    Public String toString() {
        return “Thứ “ + code;
    }
}

Lớp Day được xây dựng để định nghĩa các ngày trong tuần. Từ thứ 2 đến chủ nhật bằng chữ để thể hiện ý nghĩa của nó.

Chúng ta có thể sử dụng toán tử == trong kiểu enum vì nó chỉ là một cách gọi định danh. Nó không hoàn toàn như classinterface. Chúng ta cần phải sử dụng phương thức equal khi so sánh 2 biến với nhau.

Phương thức toString là phương thức mặc định trong các đối tượng. Enum cũng thế. Chúng ta có thể @Override phương thức toString để trả về một chuỗi cần in ra màn hình hay cần sử lý nào đó dưới dạng kiểu chuỗi. Trong đoạn mã ở trên ta cũng có @Override phương thức toString này.

Enum có thể tồn tại một phương thức trừu tượng trong nó. Khi khai báo các phần tử định danh cần phải thực khi phương thức trừu tượng ấy. Các này được ít sử dụng vì gần như các giá trị trong enum là giá trị cố định.

3.4. Kiểu Array

Mảng (Array) là một tập hợp các phần tử có chung một kiểu dữ liệu được phân bỗ trên các địa chỉ liên tiếp nhau trên vùng nhớ với độ dài cố định mà không thể thay đổi kích thức của nó sau khai báo. Phần tử của mảng có thể là kiểu nguyên thủy hay tham chiếu nhưng chúng phải cùng kiểu cho tất cả. Các phần tử trong mảng được định danh bằng vị trí của nó trong mảng bắt đầu từ 0 đến chiều dài của mảng – 1. Do đó để gọi một phần tử trong mảng cần gọi bằng chỉ số phần tử đó.

int[] arNumber = {1, 2, 3, 4}   // array have 4 elements.
int value = arNumber[1]; // get value of element with index is 1.

Mảng có thể có 1 đến n chiều. Với mảng một chiều ta xem như là một dãy các phần tử. Với 2 chiều ta xem nó như là một bảng. Tương tự với 3 chiều nó sẽ hình khối được thể hiện với 3 trục tọa x, y, z.

int[][] ar2Number = new int[10][2]; 
int[][] ar2Number2 = new int[10][]; 
int[][][] ar3Number = new int[10][][];

Đối với mảng 2 chiều nó được xem giống như mảng của mảng do đó nó không cần khai báo độ rộng của chiều cột mà chỉ cần khai báo độ rộng của chiều hàng. Khi ta thực hiện gọi lệnh int[0][] ta sẽ có được giá trị là một mảng 1-2-3-4-5-6-7-8-9-10. Tương ứng là dòng thứ nhất trong bảng.


Hình 6 – Biểu diễn mảng 2 chiều trong bảng.


Hình 5 – Hình mảng của mảng là mảng 2 chiều.

Ta sử dụng mảng để chứa các phần tử cùng kiểu dữ liệu. Do đó để đẩy phần tử vào, lấy ra và duyệt qua các phần tử của mảng là hành động thường xảy ra.

int[] arNumber = new int[3];
arNumber[0] = 1;        // set 1 to array of int with position 0.
arNumber[1] = 2;
arNumber[2] = 3;
System.out.println(arNumber[0]);    // print element’s value 0.
// for each elements
for(int el: arNumber) {     // loop element in array with for-each.
    System.out.println(el);
}

3.5. Kiểu lớp String

Trong bảng trên ta không thấy kiểu String. Ta thường dùng kiểu String trong lập trình. Vậy thì nó đang ở đâu?

String thực chất là một đối tượng biểu diễn một chuỗi các giá trị của kiểu char. Một String tương được một mảng kiểu char. Đoạn code bên dưới sẽ cho thấy mối liên hệ giữa kiểu char và kiểu String. Thực chất kiểu String được hiểu như mảng của char.

char[] chars = {'N', 'g', 'u', 'y', 'e', 'n', 'V', 'a', 'n', 'T', 'i', 'e', 'n'};
String s = new String(chars);

Kiểu String được sử dụng rất nhiều trong lập trình. Nhưng kiểu String trong Java khá đặc biệt. Nó là một lớp được thực thi từ 3 lớp: Serializable, ComparableCharSequence.


Hình 7 – Lớp String trong Java được thực thi từ 3 lớp Serializable, Comparable, CharSequence.

Nếu làm việc với chuỗi ta cần lưu ý đến 3 lớp String, StringBufferStringBuilder. Chúng có gần như có công dụng gần giống nhau nhưng cần lưu ý:

  1. String là kiểu dữ liệu không thay đổi (immutable). Khi ta thay đổi giá trị trong kiểu String thì một thực thể tham chiếu mới được tạo ra.
  2. String Buffer là kiểu dữ liệu có thể thay đổi (immutable). Thường được dùng trong đa luồng (multible thread). Người ta thường dùng nó để tránh việc tranh chấp giữa các luồng dữ liệu.
  3. StringBuilder là kiểu dữ có thể thay đổi (mutable). Nhưng thường dữ dụng đơn luồng.

Điều đó cho thấy StringBuilder và StringBuffer sẽ chạy tốt với dữ liệu lớn và String thì chạy nhanh ở dữ liệu nhỏ. Khi ta hiểu được cơ chế hoạt động của 3 loại lớp này chúng ta sẽ có cách sử dụng để giảm bộ nhớ trong trường hợp immutable/ mutable và chương trình sẽ đỡ bị lỗi trong trường hợp mutable với đơn và đa luồng.

Kiểu String có tính chất khá đặc biệt. Nó vừa có tính nguyên thủy và tính đối tượng.

Tính nguyên thủy của String được thể hiện qua việc khai báo String dưới dạng string literal. Giá trị của nó được lưu vào stack. Nếu 2 string literal có nội dung giống nhau sẽ chứa trong một stack. Điều này sẽ tiết kiệm vùng nhớ. Các khai báo string literal như sau:

String name = “Tien Nguyen”;

Tính đối tượng của String thể hiện giá trị của nó chứa trong vùng nhớ heap. Nó sẽ yêu cầu quản lý bộ nhớ phức tạp và tốn bộ nhớ hơn. Đối với string là đối tượng, giá trị của nó không nhau nhưng chứa trên 2 vùng nhớ khác nhau.

String name = new String(“Tien Nguyen”);

Bạn có thể tham khảo mã nguồn từ

4. Tóm lược

Java có 2 kiểu dữ liệu chính là nguyên thủy và tham chiếu. Nếu là nguyên thủy thì nó được chứa trong vùng nhớ stack. Nếu là tham chiếu thì nó chứa trong vùng nhớ heap. Nếu so sánh cơ chế hoạt động vùng nhớ thì stack sẽ tiết kiệm bộ nhớ và nhanh hơn heap. Do đó nếu không bắt buộc sử dụng kiểu tham chiếu thì hãy sử dụng kiểu nguyên thủy là tốt nhất.

Java cung cấp 8 kiểu dữ liệu nguyên thủy từ boolean, byte, short, char, int, float, long, double. Thứ tự tăng dần của nó cũng là độ tăng dần của độ lớn. Kh khai báo kiểu dữ liệu thì JVM sẽ chiếm vùng nhớ để giành cho kiểu dữ liệu này mặc dù sử dụng hết hay không đi nữa. Do đó để tiết kiệm vùng nhớ ta cần phải chọn kiểu thích hợp để giảm đi bộ nhớ lãng phí.

Tôi cũng có thể làm một lập trình viên Java trong các ứng dụng PC nếu tôi không rành nhiều về kiểu dữ liệu. Nhưng đối với các thiết bị bộ nhớ kém thì cần ta phải quan tâm để có thể lập trình ứng dụng không bị đứng. Do đó nên học về nó và xem nó là một bước rất cơ bản trên con đường trở thành lập trình viên chuyên nghiệp.

Hình 8 – Tổng quan các kiểu dữ liệ trong Java.
Hình 8 – Tổng quan các kiểu dữ liệ trong Java.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *