Java transient关键字使用示例

Java transient关键字用于类属性/变量,表示该类的序列化过程在为该类的任何实例创建持久字节流时应该忽略此类变量。

transient变量是不能序列化的变量。根据Java语言规范[jls-8.3.1.30] -“变量可以标记为transient,以表明它们不是对象的持久状态的一部分。”

在这篇文章中,我将讨论有关在serialization上下文中使用transient`关键字的各种概念。

正文目录如下

[TOC]

1. Java transient关键字是什么?

java中的修饰符transient可以应用于类的字段成员,以关闭这些字段成员的序列化。每个标记为transient的字段将不会序列化。使用transient关键字向java虚拟机表明,transient变量不是对象的持久状态的一部分。

让我们写一个非常基本的例子来理解上面的类比到底是什么意思。我将创建一个Employee类并定义3个属性,即firstNamelastNameconfidentialInfo。由于某些原因,我们不想存储/保存confidentialInfo,因此我们将该字段标记为transient

public class Employee implements Serializable {
    private static final long serialVersionUID = 2624368016355021172L;

    private String           firstName;
    private String           lastName;
    private transient String confidentialInfo;

    // Getter and Setter
}

现在让我们序列化一个Employee类的实例

public class TransSerializationTest {
    public static void main(String[] args) {
        try {
            Employee emp = new Employee();
            emp.setFirstName("Chen");
            emp.setLastName("Shuaishuai");
            emp.setConfidentialInfo("password");

            System.out.println("Read before Serialization:");
            System.out.println("firstName: " + emp.getFirstName());
            System.out.println("lastName: " + emp.getLastName());
            System.out.println("confidentialInfo: " + emp.getConfidentialInfo());

            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:/chenss.txt"));
            //Serialize the object
            oos.writeObject(emp);
            oos.close();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

现在我们反序列化到Java对象中,并校验confidentialInfo对象是否被保存下来?

public class TransDeSerializationTest {
    public static void main(String[] args) {
        try {
            ObjectInputStream ooi = new ObjectInputStream(new FileInputStream("E:/chenss.txt"));
            //Read the object back
            Employee readEmpInfo = (Employee) ooi.readObject();
            System.out.println("Read From Serialization:");
            System.out.println("firstName: " + readEmpInfo.getFirstName());
            System.out.println("lastName: " + readEmpInfo.getLastName());
            System.out.println("confidentialInfo: " + readEmpInfo.getConfidentialInfo());
            ooi.close();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

运行结果输出:

Read From Serialization:
Chen
Shuaishuai
null

很明显,confidentialInfo在序列化的时候没有被保存到持久对象,这也正是我们在java中使用transient关键字的原因。

2. 什么时候应该在java中使用transient关键字?

现在我么对于transient关键字非常了解了。让我们通过确定需要使用transient关键字的场景来扩展理解。

  1. 首先,非常符合逻辑的情况是,您可能有从类实例中的其他字段派生/计算的字段。它们应该每次都以编程方式计算,而不是通过序列化来持久化状态。一个例子是基于时间戳的值;例如一个人的年龄或时间戳与当前时间戳之间的持续时间。在这两种情况下,您将根据当前系统时间而不是在序列化实例时计算变量的值。
  2. 第二个逻辑示例可以是不应该以任何形式(数据库或字节流)泄漏到JVM外部的任何安全信息
  3. 另一个例子是JDK或应用程序代码中没有标记为Serializable的字段。不实现可序列化接口且在任何可序列化类中引用的类不能序列化;并将抛出异常java.io.NotSerializableException。在序列化主类之前,这些不可序列化的引用应该标记为transient
  4. 最后,有时序列化某些字段是没有意义的。例如,在任何类中,如果您添加了一个logger引用,那么序列化该logger实例有什么用呢?绝对用不上。逻辑上,你只会序列化表示实例状态的信息。Loggers从不共享实例的状态。它们只是用于编程/调试目的的实用程序。类似的例子可以参照线程类的引用。线程表示进程在任何给定时间点的状态,并且不需要用实例存储线程状态;仅仅因为它们不构成类实例的状态。

以上四个用例是您应该在引用变量中使用transient关键字的时候。如果您有更多可以使用transient的逻辑情况,请与我分享,我会在这里更新列表,让每个人都能从你的知识中受益。

阅读更多:实现可序列化接口的简单指南

3. Transient 和 final

我说的是在final关键字中使用transient,因为它在不同的情况下有不同的行为,而java中的其他关键字通常不是这样。

为了使这个概念更实际,我对Employee类进行了如下修改:

public class Employee implements Serializable {
    private static final long serialVersionUID = 2624368016355021172L;

    private String           firstName;
    private String           lastName;
    //final field 1
    public final transient String confidentialInfo = "password";
    //final field 2
    private final transient Logger logger = Logger.getLogger("demo");

    //Getter and Setter
}

现在当我重新运行序列化(写/读)的时候,会有如下的输出内容:

Read From Serialization:
firstName: Chen
lastName: Shuaishuai
confidentialInfo: password
logger: null

很奇怪。我们已将confidentialInfo标记为transient;字段仍然被序列化了。而对于类似的声明,logger却没有被序列化。为什么?

原因是,无论何时将任何final字段/引用计算为“常量表达式”,JVM都会对其进行序列化,忽略transient关键字的存在。

在上面的例子中,值password是一个常量表达式,logger demo的实例是引用。因此,根据规则,confidentialInfo被持久化,而logger没有被持久化。

您是否在想,如果我从两个字段中删除transient呢?那么,实现可序列化引用的字段将保持不变。因此,如果在上面的代码中删除transientString(实现Serializable)将被持久化;而Logger(不实现Serializable)将不会被持久化,并且将会抛出异常java.io.NotSerializableException

如果希望持久保存不可序列化字段的状态,那么可以使用readObject()writeObject()方法。writeObject()/readObject()通常在内部链接到序列化/反序列化机制中,因此会自动调用。

阅读更多:java中的SerialVersionUID和相关的快速事实

4. 案例研究:HashMap如何使用transient关键字?

到目前为止,我们一直在讨论与transient关键字相关的概念,这些概念基本上都是理论性的。让我们了解一下在HashMap类中逻辑地使用transient的正确用法。它将使您更好地了解java中transient关键字的实际用法。

在理解使用transient创建的解决方案之前,让我们先确定问题本身。

HashMap用于存储键-值对,这一点我们都知道。我们还知道HashMap中键的位置是根据键实例的哈希码计算的。现在,当我们序列化一个HashMap时,这意味着HashMap中的所有键以及与键相关的所有值也将被序列化。序列化之后,当我们反序列化HashMap实例时,所有关键实例也将被反序列化。我们知道在这个序列化/反序列化过程中,可能会丢失信息(用于计算hashcode),最重要的是它本身是一个新实例。

在java中,任何两个实例(甚至是相同类的实例)都不能有相同的hashcode。这是一个大问题,因为应该根据新的hashcode放置键的位置不正确。当检索键的值时,您将在这个新的HashMap中引用错误的索引。

阅读更多:使用java中的hashCode和equals方法

因此,当一个哈希表被序列化时,它意味着哈希索引,和表的顺序不再有效,不应该被保留。这是问题陈述。

现在看看如何在HashMap类中解决这个问题。如果通过HashMap.java的源代码。你会发现下面的声明:

    transient Node<K,V>[] table;
    transient Set<Map.Entry<K,V>> entrySet;
    transient int size;
    transient int modCount;

所有重要字段都标记为transient(所有字段实际上都是在运行时计算/更改的),因此它们不是序列化HashMap实例的一部分。为了再次填充这个重要的信息,HashMap类使用writeObject()readObject()方法,如下所示:

private void writeObject(java.io.ObjectOutputStream s)
    throws IOException {
    int buckets = capacity();
    // Write out the threshold, loadfactor, and any hidden stuff
    s.defaultWriteObject();
    s.writeInt(buckets);
    s.writeInt(size);
    internalWriteEntries(s);
}
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
    Node<K,V>[] tab;
    if (size > 0 && (tab = table) != null) {
        for (Node<K,V> e : tab) {
            for (; e != null; e = e.next) {
                s.writeObject(e.key);
                s.writeObject(e.value);
            }
        }
    }
}
private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        s.defaultReadObject();
        reinitialize();
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        s.readInt();                // Read and ignore number of buckets
        int mappings = s.readInt(); // Read number of mappings (size)
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                                             mappings);
        else if (mappings > 0) { // (if zero, use defaults)
            // Size the table using given load factor only if within
            // range of 0.25...4.0
            float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
            float fc = (float)mappings / lf + 1.0f;
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                       DEFAULT_INITIAL_CAPACITY :
                       (fc >= MAXIMUM_CAPACITY) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                         (int)ft : Integer.MAX_VALUE);

            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;

            // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
        }
    }

使用上面的代码,HashMap仍然允许像通常那样处理非transient字段,但是它们在字节数组的末尾一个接一个地写存储的键-值对。在反序列化时,它允许默认情况下处理的非transient变量,然后逐个读取键-值对。对于每个键,哈希值和索引将被再次计算,并被插入到表中的正确位置,以便再次检索时不会出现任何错误。

上面使用transient关键字就是一个很好的例子。您应该记住它,并在下一次java面试问题中提到它。

相关帖子:HashMap在Java中是如何工作的?

5. 摘要说明

  1. 修饰符transient可以应用于类的字段成员,以关闭这些字段成员的序列化。
  2. 你可以在需要对现有状态字段进行保护或计算的字段的类中使用transient关键字。当序列化那些字段(如日志记录器和线程)毫无意义时,可以使用它。
  3. 序列化不关心访问修饰符,如private;所有非transient字段都被认为是对象持久状态的一部分,并且都符合持久状态的条件。
  4. 无论何时将任何final字段/引用计算为“常量表达式”,JVM都会对其进行序列化,忽略transient关键字的存在。
  5. HashMap类是java中transient关键字的一个很好的用例。

这是我对transient关键字的看法。如果你想在这篇文章中添加一些东西,请通过评论告诉我。我很乐意扩展这个帖子。

参考:
Java transient keyword example

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • 在一个方法内部定义的变量都存储在栈中,当这个函数运行结束后,其对应的栈就会被回收,此时,在其方法体中定义的变量将不...
    Y了个J阅读 4,412评论 1 14
  • Java性能问题一直困扰着广大程序员,由于平台复杂性,要定位问题,找出其根源确实很难。随着10多年Java平台的改...
    程序员技术圈阅读 4,708评论 0 65
  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,834评论 0 24
  • 时间如流水,当我们从童真迈向年少的轻狂走向稳重成熟。当我们的大学生活匆匆来临又匆匆溜走。当我们迈出校门走向社...
    暖暖南风阅读 345评论 0 1
  • 毛毛虫_小姐阅读 281评论 0 5