基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Java transient关键字

知识点图片

概述:Java transient关键字用于在序列化时忽略指定字段,以保护密码等敏感数据或排除临时内容。反序列化时,被忽略的字段将恢复为其类型的默认值(如null或0)。


1. 核心概念:transient 是什么?

transient 是一个Java关键字,用于修饰类的成员变量(字段)。它的核心作用一句话就能概括:

告诉Java虚拟机(JVM),在对该对象进行序列化(Serialization)时,忽略被 transient 修饰的变量。

换句话说,当一个包含 transient 变量的对象被写入到持久化存储(如文件、数据库或网络流)时,这个 transient 变量的值不会被保存。相应地,当从持久化存储中反序列化(Deserialization)该对象时,这个 transient 变量会被赋予其数据类型的默认值(例如,对象类型为 nullint0booleanfalse)。

2. 为什么需要 transient?(使用场景)

你可能会问,既然要忽略某个字段,为什么不干脆不定义它呢?transient 的存在有其非常重要的实际意义,主要用于以下几种场景:

a. 保护敏感数据

这是最常见的用途。某些字段包含敏感信息,如密码、密钥、个人身份信息等。将这些信息直接序列化到文件或通过网络传输会带来安全风险。通过将它们声明为 transient,可以确保这些敏感数据不会被持久化。

java
class User implements java.io.Serializable {
    private String username;
    private transient String password; // 密码不应被序列化

    // ... 构造函数、getter/setter等
}

b. 避免序列化临时或可派生的数据

有些字段的值是临时的,或者可以根据其他字段计算得出。序列化这些字段不仅浪费存储空间,还可能在反序列化后导致数据不一致。

例如,一个人的年龄可以根据其出生日期动态计算。

java
import java.time.LocalDate;
import java.time.Period;

class Person implements java.io.Serializable {
    private String name;
    private LocalDate birthDate;
    private transient int age; // 年龄是可计算的,不需要序列化

    public int getAge() {
        if (age == 0 && birthDate != null) {
            this.age = Period.between(birthDate, LocalDate.now()).getYears();
        }
        return age;
    }
    // ...
}

c. 排除不可序列化的字段

一个类要能被序列化,它必须实现 java.io.Serializable 接口。如果你的类中包含某个成员变量,而该成员变量的类型没有实现 Serializable 接口(例如 ThreadSocketInputStream 等与特定运行时环境绑定的对象),那么在尝试序列化你的类实例时,会抛出 NotSerializableException

使用 transient 关键字可以解决这个问题,让序列化过程跳过这些不可序列化的字段。

java
class SessionManager implements java.io.Serializable {
    private String sessionId;
    private transient java.net.Socket connection; // Socket是不可序列化的

    // ...
}

3. 如何使用 transient?(示例代码)

下面是一个完整的例子,展示了 transient 的效果。

java
import java.io.*;

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

    private String username;
    private transient String password; // 标记为transient
    private transient String sessionToken; // 标记为transient

    public UserAccount(String username, String password) {
        this.username = username;
        this.password = password;
        this.sessionToken = generateSessionToken(); // 模拟生成一个会话令牌
    }

    private String generateSessionToken() {
        return "TOKEN_" + System.currentTimeMillis();
    }

    @Override
    public String toString() {
        return "UserAccount{" +
               "username='" + username + '\'' +
               ", password='" + password + '\'' +
               ", sessionToken='" + sessionToken + '\'' +
               '}';
    }

    public static void main(String[] args) {
        UserAccount user = new UserAccount("admin", "pa$$w0rd");
        System.out.println("序列化前: " + user);

        // 1. 序列化对象
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
            oos.writeObject(user);
            System.out.println("对象已成功序列化到 user.ser");
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("------------------------------------");

        // 2. 反序列化对象
        UserAccount deserializedUser = null;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
            deserializedUser = (UserAccount) ois.readObject();
            System.out.println("对象已成功从 user.ser 反序列化");
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println("反序列化后: " + deserializedUser);
    }
}

运行输出:

plaintext
序列化前: UserAccount{username='admin', password='pa$$w0rd', sessionToken='TOKEN_1678886400000'}
对象已成功序列化到 user.ser
------------------------------------
对象已成功从 user.ser 反序列化
反序列化后: UserAccount{username='admin', password='null', sessionToken='null'}

分析:
从输出可以清晰地看到:

  • 序列化前passwordsessionToken 字段都有值。
  • 反序列化后username 字段被成功恢复,但被 transient 修饰的 passwordsessionToken 字段都变成了它们类型的默认值 null

4. 深入探讨

a. transientstatic 的关系

transient 关键字不能修饰 static 变量。
原因在于:序列化是针对对象实例的状态进行保存,而 static 变量属于,不属于任何对象实例。静态变量本身就不会被标准的序列化机制所包含。因此,对 static 变量使用 transient 是没有意义的,Java语法也不允许。

b. 自定义序列化 (writeObjectreadObject)

如果你希望对 transient 字段进行特殊处理(例如,加密后保存),而不是完全忽略它,你可以通过在类中实现 writeObjectreadObject 方法来自定义序列化和反序列化逻辑。

java
class SecureUser implements Serializable {
    private String username;
    private transient String password;

    // ...

    // 自定义序列化逻辑
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // 先执行默认的序列化,非transient字段会被写入
        // 手动处理transient字段
        String encryptedPassword = encrypt(this.password); // 假设有个加密方法
        out.writeObject(encryptedPassword);
    }

    // 自定义反序列化逻辑
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // 先执行默认的反序列化
        // 手动读取并处理我们自己写入的数据
        String encryptedPassword = (String) in.readObject();
        this.password = decrypt(encryptedPassword); // 假设有个解密方法
    }

    private String encrypt(String plain) { /* ... 加密实现 ... */ return "encrypted_" + plain; }
    private String decrypt(String encrypted) { /* ... 解密实现 ... */ return encrypted.replace("encrypted_", ""); }
}

通过这种方式,password 字段虽然是 transient(不会被默认机制序列化),但我们通过自定义方法控制了它的读写,实现了安全存储。

5. 总结

特性 描述
核心作用 在对象序列化时,忽略被其修饰的成员变量。
应用场景 1. 保护敏感数据(如密码)。
2. 忽略临时或可派生数据(如计算得出的年龄)。
3. 排除不可序列化的字段,避免 NotSerializableException
反序列化结果 transient 修饰的字段会被恢复为其类型的默认值(对象为 null,数字为 0,布尔为 false)。
注意事项 - transient 只能修饰成员变量,不能修饰方法或类。
- transientstatic 变量无效,因为静态变量不参与序列化。
- 如果类实现了 Externalizable 接口,那么 transient 关键字将被完全忽略,所有字段的序列化行为都由 writeExternalreadExternal 方法全权控制。

transient 是Java序列化机制中一个简单但非常实用的工具,掌握它可以让你更灵活、更安全地控制对象的持久化过程。

00:00
00:00