CodeGym /Các khóa học /JAVA 25 SELF /trường transient, serialVersionUID

trường transient, serialVersionUID

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Chi tiết hơn về transient

Trong Java, từ khóa transient — là cách để nói với bộ tuần tự: “Vui lòng đừng đụng vào trường này, hãy bỏ qua nó khi lưu đối tượng!”. Nếu bạn khai báo một trường là transient, nó sẽ không đi vào luồng byte đã tuần tự hóa. Điều này đặc biệt hữu ích cho dữ liệu nhạy cảm (ví dụ: mật khẩu) hoặc các tính toán tạm thời không cần lưu.

Ví dụ: tại sao cần transient?

Giả sử chúng ta có một lớp người dùng:

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private transient String password; // Không muốn lưu mật khẩu!

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // Ở đây có getter và setter
}

Nếu chúng ta tuần tự hóa đối tượng của lớp này, trường password sẽ không vào tệp (hoặc luồng khác). Nghĩa là khi giải tuần tự, mật khẩu sẽ nhận giá trị mặc định — với đối tượng là null, với số là 0, với booleanfalse.

Hoạt động thế nào trong thực tế?

Hãy làm một thử nghiệm nhỏ. Trước hết, tuần tự hóa người dùng:

import java.io.*;

public class TransientDemo {
    public static void main(String[] args) throws Exception {
        User user = new User("vasya", "qwerty123");

        // Ghi đối tượng vào tệp
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.ser"));
        out.writeObject(user);
        out.close();

        // Bây giờ đọc đối tượng trở lại
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.ser"));
        User restored = (User) in.readObject();
        in.close();

        System.out.println("Username: " + restored.username);
        System.out.println("Password: " + restored.password);
    }
}

Kết quả:

Username: vasya
Password: null

Như bạn thấy, trường password không được khôi phục — nó là transient, nên bộ tuần tự đã bỏ qua.

Dùng transient ở đâu và để làm gì?

  • Mật khẩu và token. Không bao giờ tuần tự hóa chúng!
  • Dữ liệu cache hoặc tạm thời. Ví dụ, nếu bạn có một trường có thể tính lại “ngay lập tức”.
  • Các đối tượng không thể hoặc không cần tuần tự hóa. Ví dụ: tham chiếu đến kết nối DB, luồng, socket.

Đặc điểm hành vi của các trường transient

Khi đối tượng được giải tuần tự, tất cả các trường được đánh dấu transient sẽ nhận giá trị mặc định. Nếu cần khôi phục ý nghĩa của chúng, bạn có thể dùng phương thức readObject và tự điền lại (tính lại cache, hỏi mật khẩu từ người dùng, v.v.).

2. serialVersionUID: định danh phiên bản duy nhất của lớp

serialVersionUID là một trường tĩnh đặc biệt kiểu long, xác định “phiên bản” của lớp có thể tuần tự hóa. Khi tuần tự hóa, giá trị serialVersionUID được ghi lại; khi giải tuần tự, JVM so khớp nó với giá trị trong lớp hiện tại. Nếu chúng không trùng khớp — sẽ ném ngoại lệ và đối tượng không được khôi phục.

Khai báo serialVersionUID như thế nào?

Rất đơn giản:

private static final long serialVersionUID = 1L;

Thường khai báo trực tiếp trong lớp implements Serializable:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // ... các trường và phương thức khác
}

Tại sao cần serialVersionUID?

Hãy tưởng tượng bạn đã lưu một đối tượng của lớp vào tệp, sau đó thay đổi cấu trúc lớp (thêm một trường, đổi tên gì đó, v.v.). Nếu serialVersionUID khác nhau, JVM sẽ coi lớp không tương thích với phiên bản cũ và không cho bạn giải tuần tự đối tượng. Điều này ngăn các lỗi bất ngờ.

Điều gì xảy ra nếu không khai báo serialVersionUID?

Nếu bạn không khai báo serialVersionUID tường minh, trình biên dịch sẽ tự sinh nó — dựa trên cấu trúc lớp. Nhưng chỉ một thay đổi nhỏ (ví dụ, thêm hoặc xóa trường) cũng sẽ dẫn tới thay đổi serialVersionUID. Kết quả là bạn không thể giải tuần tự các đối tượng đã lưu bằng phiên bản cũ của lớp.

Vì vậy, khuyến nghị luôn đặt serialVersionUID một cách tường minh!

Minh họa: serialVersionUID không khớp

1) Trước tiên tạo lớp và tuần tự hóa đối tượng:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;

    public User(String username) {
        this.username = username;
    }
}

2) Sau đó đổi serialVersionUID:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 2L; // Từ 1L thành 2L!
    private String username;

    public User(String username) {
        this.username = username;
    }
}

Kết quả:

java.io.InvalidClassException: User; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

JVM cảnh báo rõ ràng: “Các phiên bản không tương thích!”

Nên chọn giá trị serialVersionUID nào?

Thường dùng các giá trị đơn giản (1L, 2L, 42L), còn trong các dự án lớn IDE sẽ tạo các giá trị “dài”. Quan trọng nhất — chỉ thay đổi khi cấu trúc lớp thay đổi theo cách không tương thích.

3. Thực hành: các trường transientserialVersionUID trong thực tế

Ví dụ: lớp với trường transient

Hãy sửa một ứng dụng học tập (ví dụ, trình quản lý liên hệ) và thêm vào lớp người dùng một trường để lưu token ủy quyền tạm thời, trường này không được đưa vào tuần tự hóa.

import java.io.Serializable;

public class Contact implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private String phone;
    private transient String sessionToken; // token tạm thời

    public Contact(String name, String phone, String sessionToken) {
        this.name = name;
        this.phone = phone;
        this.sessionToken = sessionToken;
    }

    @Override
    public String toString() {
        return "Contact{" +
               "name='" + name + '\'' +
               ", phone='" + phone + '\'' +
               ", sessionToken='" + sessionToken + '\'' +
               '}';
    }
}

Bây giờ thử tuần tự hóa và giải tuần tự đối tượng:

import java.io.*;

public class TransientAndSUIDDemo {
    public static void main(String[] args) throws Exception {
        Contact c = new Contact("Ivan", "+19990001122", "token-12345");

        // Lưu đối tượng
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("contact.ser"));
        out.writeObject(c);
        out.close();

        // Khôi phục đối tượng
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("contact.ser"));
        Contact restored = (Contact) in.readObject();
        in.close();

        System.out.println("Trước khi tuần tự hóa: " + c);
        System.out.println("Sau khi giải tuần tự: " + restored);
    }
}

Kết quả:

Trước khi tuần tự hóa: Contact{name='Ivan', phone='+19990001122', sessionToken='token-12345'}
Sau khi giải tuần tự: Contact{name='Ivan', phone='+19990001122', sessionToken='null'}

Như bạn thấy, trường sessionToken không được khôi phục — nó là transient.

Ví dụ: thử nghiệm với serialVersionUID

1) Trước tiên tuần tự hóa đối tượng với serialVersionUID = 1L.
2) Sau đó đổi serialVersionUID thành 2L và cố gắng giải tuần tự cùng tệp đó.

Kết quả: bạn sẽ nhận được InvalidClassException, như đã minh họa ở trên.

4. Vì sao nên đặt serialVersionUID một cách tường minh?

  • Tường minh tốt hơn ngầm định. Bạn kiểm soát tính tương thích: nếu cấu trúc lớp không thay đổi ở mức gây vỡ, hãy giữ serialVersionUID cũ và các đối tượng sẽ được giải tuần tự bình thường.
  • Sinh tự động có rủi ro. Bất kỳ thay đổi nào cũng có thể làm thay đổi giá trị được tính toán và “phá vỡ” tính tương thích của dữ liệu đã lưu.
  • IDE sẽ giúp. Hầu hết IDE (ví dụ, IntelliJ IDEA) có thể tự động tạo serialVersionUID.

5. Những lỗi thường gặp khi làm việc với transientserialVersionUID

Lỗi số 1: quên đánh dấu trường nhạy cảm là transient.
Hậu quả là mật khẩu hoặc token vô tình nằm trong các tệp đã tuần tự hóa. Điều này không chỉ bất cẩn mà còn nguy hiểm.

Lỗi số 2: không khai báo serialVersionUID tường minh.
Lớp đã thay đổi và giờ không thể giải tuần tự các đối tượng cũ: JVM coi chúng không tương thích, mặc dù về bản chất cấu trúc có thể chưa thay đổi ở mức nghiêm trọng.

Lỗi số 3: thay đổi serialVersionUID mà không cần thiết.
Nếu bạn chỉ thêm getter hoặc bình luận, không cần thay đổi serialVersionUID — nếu không dữ liệu cũ sẽ không còn giải tuần tự được.

Lỗi số 4: serialVersionUID không static hoặc không final.
Trường phải được khai báo là private static final long serialVersionUID. Nếu không, JVM sẽ không xử lý đúng.

Lỗi số 5: quên khôi phục trường transient sau khi giải tuần tự.
Nếu giá trị đó quan trọng cho hoạt động của đối tượng, hãy khôi phục trong readObject — nếu không đối tượng có thể hoạt động không đúng.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION