美文网首页一些收藏Android开发
Java小白系列(一):关键字transient

Java小白系列(一):关键字transient

作者: 青叶小小 | 来源:发表于2021-01-27 00:17 被阅读0次

    宗旨

    本系列意在用最浅显的文字和代码示例,让大家实实在在的掌握Java最基础的知识。

    一、前言

    我们在学习JDK源码时,如果大家有留心,应该会经常看到关键字:transient !但不知道有多少人去真正了解过该关键字的作用。

    二、初识transient

    transient,中文意思:短暂的!那么,它有什么用呢?这就涉及到 Java 持久化机制。

    2.1、Java 持久化

    Java为我们提供了一种非常方便的持久化实例机制:Serializable !

    可能有人要说:Serializable 不是用于序列化与反序列化么?
    嗯,对!没错,那我想问:一个实例序列化后干啥用?

    一个实例的序列化,可用于数据传输,然而,其最大的作用还是用于写入磁盘,从而防止数据丢失。

    2.2、Java 序列化

    我们在编写可用于序列化/反序列化的类时,都会去实现 Serializable,但是有时候,我们又不想序列化某些字段,那么,我们就可以用关键字 transient 来修饰该字段。

    // BankCard.java
    import java.io.Serializable;
    
    public class BankCard implements Serializable {
        private String cardNo;
        private transient String password;
    
        public String getCardNo() {
            return cardNo;
        }
    
        public void setCardNo(String cardNo) {
            this.cardNo = cardNo;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        @Override
        public String toString() {
            return "BankCard{cardNo='" + cardNo + '\'' + ", password='" + password + '\'' + '}';
        }
    }
    

    编写测试类

    public class Main {
        public static void main(String[] args) {
            try {
                serializeBankCard();
                Thread.sleep(2000);
                deSerializeBankCard();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private static void serializeBankCard() throws Exception {
            BankCard bankCard = new BankCard();
            bankCard.setCardNo("CardNo: 1234567890");
            bankCard.setPassword("********");
    
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("./bank.txt"));
            os.writeObject(bankCard);
            os.close();
            System.out.println("序列化 " + bankCard.toString());
        }
    
        private static void deSerializeBankCard() throws Exception {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("./bank.txt"));
            BankCard bankCard = (BankCard) is.readObject();
            is.close();
            System.out.println("反序列化 " + bankCard.toString());
        }
    }
    
    // 打印输出
    // 序列化 BankCard{cardNo='CardNo: 1234567890', password='********'}
    // 反序列化 BankCard{cardNo='CardNo: 1234567890', password='null'}
    

    我们可以看到,我们先创建『银行卡』类,然后将数据序列化写到磁盘上,过了2秒后,我们再从磁盘上读取数据,打印发现,『password』字段是『null』。

    我们打开磁盘上的文件,用十六进制查看数据,也可以看到,文件中,只有『CardNo』,没有『password』:

    serializable + transient.png

    三、深入分析 transient

    在深入分析之前,我先抛出几个问题,然后,再带大家一一去解惑:

    • transient 实现原理;
    • transient 修饰的字段真的无法被序列化?
    • 静态变量能被序列化么?
    • transient + static 修饰的字段能被序列化么?

    OK,带着以上问题,我们去看源码!

    3.1、Serializable 类

    /**
     * ......
     * The writeObject method is responsible for writing the state of the
     * object for its particular class so that the corresponding
     * readObject method can restore it.  The default mechanism for saving
     * the Object's fields can be invoked by calling
     * out.defaultWriteObject. The method does not need to concern
     * itself with the state belonging to its superclasses or subclasses.
     * State is saved by writing the individual fields to the
     * ObjectOutputStream using the writeObject method or by using the
    * methods for primitive data types supported by DataOutput.
    *
    * The readObject method is responsible for reading from the stream and
    * restoring the classes fields. It may call in.defaultReadObject to invoke
    * the default mechanism for restoring the object's non-static and
    * non-transient fields.
    * 
    * .........
    *
    * @see java.io.ObjectOutputStream
    * @see java.io.ObjectInputStream
    * @see java.io.ObjectOutput
    * @see java.io.ObjectInput
    * @see java.io.Externalizable
    * @since   JDK1.1
    */
    public interface Serializable {
    }
    

    Serializable 的注释就说到了:

    • writeObject 负责写数据,readObject 负责读数据;
    • 数据(状态)的读写,并不关心它的父类或子类;
    • ObjectOutputSteam 是基于 DataOutput 实现的写;
    • readObject 负责读流(stream),并存到类成员变量中,它的默认存到非static成员和非transient成员中;

    3.2、ObjectOutputStream 类

    开头的注释也说了:默认的 Serializable 机制,只写类的对象、签名以及非 transient 和非 static 字段

    /*
     * The default serialization mechanism for an object writes the class of the
     * object, the class signature, and the values of all non-transient and
     * non-static fields.
     */
     public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants {
        private final BlockDataOutputStream bout;
        private static class BlockDataOutputStream extends OutputStream implements DataOutput {
            ......
        }
        
        public final void writeObject(Object obj) throws IOException {
            ......
            try {
                writeObject0(obj, false);
            }
            ......
        }
        
        private void writeObject0(Object obj, boolean unshared) throws IOException {
            boolean oldMode = bout.setBlockDataMode(false);
            depth++;
            try {
                ......
                
                ObjectStreamClass desc;
                for (;;) {
                    ......
                    // 重点:获取 Serializable对象的 desc
                    // desc 中的 fields 会过滤 static 和 transient
                    // 具体代码流程继续看 3.3
                    desc = ObjectStreamClass.lookup(cl, true);
                    ......
                }
                
    
                } else if (obj instanceof Serializable) {
                    writeOrdinaryObject(obj, desc, unshared); // obj 根据 desc.fields 序列化数据
                }
                ......
                
            } finally {
                depth--;
                bout.setBlockDataMode(oldMode);
            }
        }
        
        private void writeOrdinaryObject(Object obj,
                                         ObjectStreamClass desc,
                                         boolean unshared) throws IOException {
            if (extendedDebugInfo) {
                debugInfoStack.push(
                    (depth == 1 ? "root " : "") + "object (class \"" +
                    obj.getClass().getName() + "\", " + obj.toString() + ")");
            }
            try {
                desc.checkSerialize();
                // 最终调用 bout 来写数据
                bout.writeByte(TC_OBJECT);
                
                // 所有的 writeXXX 都将调用 bout 来写数据
                writeClassDesc(desc, false);
                handles.assign(unshared ? null : obj);
                if (desc.isExternalizable() && !desc.isProxy()) {
                    writeExternalData((Externalizable) obj);
                } else {
                    writeSerialData(obj, desc);
                }
            } finally {
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }
        }
    }
    

    3.3、ObjectStreamClass类

    3.3.1、ObjectStreamClass.lookup

    public class ObjectStreamClass implements Serializable {
        static ObjectStreamClass lookup(Class<?> cl, boolean all) {
            ......
            if (entry == null) {
                try {
                    entry = new ObjectStreamClass(cl);
                } catch (Throwable th) {
                    entry = th;
                }
               ......
            }
        
            ......
        }
    }
    

    3.3.2、ObjectStreamClass构造函数

    public class ObjectStreamClass implements Serializable {
        private ObjectStreamClass(final Class<?> cl) {
            ......
            if (serializable) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        ......
                        try {
                            fields = getSerialFields(cl);  // 获取可序列化的字段
                            computeFieldOffsets();
                        } catch (InvalidClassException e) {
                            ......
                            fields = NO_FIELDS;
                        }
                        ......
                    }
                }
            }
        }
    }
    

    3.3.3、ObjectStreamClass.getSerialFields

    public class ObjectStreamClass implements Serializable {
        private static ObjectStreamField[] getSerialFields(Class<?> cl) throws InvalidClassException {
            ObjectStreamField[] fields;
            if (Serializable.class.isAssignableFrom(cl) &&
                !Externalizable.class.isAssignableFrom(cl) &&
                !Proxy.isProxyClass(cl) &&
                !cl.isInterface())
            {
                if ((fields = getDeclaredSerialFields(cl)) == null) {
                    fields = getDefaultSerialFields(cl);
                }
                Arrays.sort(fields);
            } else {
                fields = NO_FIELDS;
            }
            return fields;
        }
    }
    

    3.3.4、ObjectStreamClass.getDefaultSerialFields

    public class ObjectStreamClass implements Serializable {
        private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
            Field[] clFields = cl.getDeclaredFields();
            ArrayList<ObjectStreamField> list = new ArrayList<>();
            int mask = Modifier.STATIC | Modifier.TRANSIENT; // 重点1:掩码 mask
        
            for (int i = 0; i < clFields.length; i++) {
                // 重点2:过滤 static 或者 transient
                if ((clFields[i].getModifiers() & mask) == 0) {
                    list.add(new ObjectStreamField(clFields[i], false, true));
                }
            }
            int size = list.size();
            return (size == 0) ? NO_FIELDS : list.toArray(new ObjectStreamField[size]);
        }
    }
    

    3.4、断点查看运行时JDK源码

    jdk_runtime.png

    简单的看完以上源码,至少回答了我们这么几个问题:

    • static 变量是无法序列化的,无论是否加了 transient 修饰符;
    • transient 修饰的变量也是无法序列化的;

    那么,还有一个问题,transient 修饰的变量真的无法序列化么?

    再回答该问题前,首先我想问下大家,大家知道 Java 除了提供 Serializable 序列化外,还有提供其它方式么?

    答案是:Java 提供了两种序列化方式

    1. Serializable,序列化与反序列化由 Java 实现(ObjectOutputStream / ObjectInputStream 的默认机制);
    2. Externalizable,序列化与反序列化由开发者自主来实现;

    先来看看示例

    // BankCardExt.java
    import java.io.Externalizable;
    import java.io.IOException;
    import java.io.ObjectInput;
    import java.io.ObjectOutput;
    
    public class BankCardExt implements Externalizable {
        private String cardNo;
        private transient String password;
    
        public String getCardNo() {
            return cardNo;
        }
    
        public void setCardNo(String cardNo) {
            this.cardNo = cardNo;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeObject(cardNo);
            out.writeObject(password);
        }
    
        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            cardNo = (String) in.readObject();
            password = (String) in.readObject();
        }
    
        @Override
        public String toString() {
            return "BankCardExt{cardNo='" + cardNo + "\', password='" + password + "\'}";
        }
    }
    

    这个类的读和写,由我们来实现。

    再来个测试类

    public class Main {
    
        public static void main(String[] args) {
            try {
                serializeBankCard();
                Thread.sleep(2000);
                deSerializeBankCard();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private static void serializeBankCard() throws Exception {
            BankCardExt bankCard = new BankCardExt();
            bankCard.setCardNo("CardNo: 1234567890");
            bankCard.setPassword("********");
    
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("./bankExt.txt"));
            os.writeObject(bankCard);
            os.close();
            System.out.println("序列化 " + bankCard.toString());
        }
    
        private static void deSerializeBankCard() throws Exception {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("./bankExt.txt"));
            BankCardExt bankCard = (BankCardExt) is.readObject();
            is.close();
            System.out.println("反序列化 " + bankCard.toString());
        }
    }
    
    // 打印输出
    // 序列化 BankCardExt{cardNo='CardNo: 1234567890', password='********'}
    // 反序列化 BankCardExt{cardNo='CardNo: 1234567890', password='********'}
    

    我们看到,读的时候,数据也回来了。

    exteralizable.png

    查看文件,我们也看到了『CardNo』和『password』。

    这里还有一点需要注意:写的顺序与读的顺序要保持一致!如果我们顺充不一致,结果将如下:

    {
        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeObject(cardNo);
            out.writeObject(password);
        }
        
        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            password = (String) in.readObject();
            cardNo = (String) in.readObject();
        }
    }
    
    // 打印输出
    // 序列化 BankCardExt{cardNo='CardNo: 1234567890', password='********'}
    // 反序列化 BankCardExt{cardNo='********', password='CardNo: 1234567890'}
    

    OK,最后一个问题我们也解答了,当我们实现 Externalizable 时,需要我们自主选择哪些字段需要序列化与反序列化,因此,此时的 transient 修饰符将没有作用。

    四、总结

    本文分析了 transient 的序列化底层机制:

    • 当我们的类实现了 Serializable 时,transient 修饰的变量只能存在于内存中,ObjectOutputStream 将忽略 transient 和 static 修饰的变量;
    • 当我们的类实现了 Externalizable 时,此时 transient 关键字将失效,类的成员变量的序列化与反序列化将由我们自己控制;

    相关文章

      网友评论

        本文标题:Java小白系列(一):关键字transient

        本文链接:https://www.haomeiwen.com/subject/qjhozktx.html