序列化反序列化( transient/static )

      平时我们在Java内存中的对象,是无 法进行IO操作或者网络通信的,因为在进行IO操作或者网络通信的时候,人家根本不知道内存中的对象是个什么东西,因此必须将对象以某种方式表示出来。一个Java对象的表示有各种各样的方式,Java本身也提供给了用户一种表示对象的方式,那就是序列化。换句话说,序列化只是表示对象的一种方式而已。OK,有了序列化,那么必然有反序列化,我们先看一下序列化、反序列化是什么意思。

  • 序列化:将一个对象转换成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。
  • 反序列化:将字节数组重新构造成对象。

    我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。

    然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。如下:

import java.io.*;

/**
 * @author Heian
 * @time 19/01/30 21:40
 * @copyright(C) 2019 深圳市北辰德科技股份有限公司
 * 用途:transient  static  序列化反序列化
 */
public class SerializableDemo {
    //内部类
   static class User implements Serializable {
        private String accNo;
        private static String userName;
        private transient String passWord;

        public String getAccNo() {
            return accNo;
        }

        public void setAccNo(String accNo) {
            this.accNo = accNo;
        }

        public String getUserName() {
            return userName;
        }

        public void setUserName(String userName) {
            this.userName = userName;
        }

        public String getPassWord() {
            return passWord;
        }

        public void setPassWord(String passWord) {
            this.passWord = passWord;
        }

        @Override
        public String toString() {
            return "User{" +
                    "accNo='" + accNo + "\'" +
                    ",userName='" + userName +  "\'" +
                    ", passWord='" + passWord  +  "\'" +  
                    '}';
        }
    }

    public static void main(String[] args) throws FileNotFoundException, IOException,ClassNotFoundException {
        SerializableDemo serializableDemo = new SerializableDemo();
        User user = new User();
        user.setAccNo ("123456"); user.setUserName ("Heian");user.setPassWord ("abc");
        System.out.println ("序列化前:" + user);//序列化前:User{accNo='123456',userName='Heian', passWord='abc'}
        //定义一个临时文件用来装载此对象(被transient修饰的字段不会序列化进到二进制数组中)
        ObjectOutputStream objOut = new ObjectOutputStream (new FileOutputStream ("D:a.log"));//此时会在该目录下生成一个临时文件,文件夹内容为二进制字节数组文件
        objOut.writeObject (user);
        objOut.close ();
        user.setUserName ("jack");
        //再将序列化之后的字节数取到,转成对象
        ObjectInputStream objIn = new ObjectInputStream (new FileInputStream ("D:a.log"));
        Object obj = (User)objIn.readObject ();
        System.out.println ("反序列化之后的信息:" + obj);//反序列化之后的信息:User{accNo='123456',userName='jack', passWord='null'}
        /*可以很清晰的看到加了关键字transient密码是没有被序列化到二进制  字节数组中的,而且加了对userName用static修饰符修饰也不会被序列化到二进制字节数组中,
        它取出的只是JVM内存中的值(序列化之前以对象保存的形式是Heian而反序列化之后取出的确实Jack证明字节数组无该值,而且取出的jack是对象中的值)*/

    }


}
aaaa
二进制表示的字节数组User对象

总结:

  • 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
  • transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
  • 被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化(被声明为static的属性不会被序列化,这个问题可以这么理解,序列化保存的是对象的状态,但是static修饰的变量是属于类的而不是属于变量的,因此序列化的时候不会序列化它)。
  • 为什么要不被序列化呢,主要是为了节省存储空间,而是为了对字段进行加密。

手动指定序列化过程

    Java并不强求用户非要使用默认的序列化方式,用户也可以按照自己的喜好自己指定自己想要的序列化方式----只要你自己能保证序列化前后能得到想要的数据就好了。手动指定序列化方式的规则是:

     进行序列化、反序列化时,虚拟机会首先试图调用对象里的writeObject和readObject方法,进行用户自定义的序列化和反序列化。如果没有这样的方法,那么默认调用的是ObjectOutputStream的writeObject以及ObjectInputStream的 readObject方法。如果该类有这样的方法,则按照自定义规则,用户可以自己控制序列化和反序列 化的过程。

这是非常有用的。比如:

1、有些场景下,某些字段我们并不想要使用Java提供给我们的序列化方式,而是想要以自定义的方式去序列化它,比如ArrayList的 elementData、HashMap的table(至于为什么在之后写这两个类的时候会解释原因),就可以通过将这些字段声明为transient, 然后在writeObject和readObject中去使用自己想要的方式去序列化它们

2、因为序列化并不安全,因此有些场景下我们需要对一些敏感字段进行加密再序列化,然后再反序列化的时候按照同样的方式进行解密,就在一定程度上保证了安全性了。 要这么做,就必须自己写writeObject和readObject,writeObject方法在序列化前对字段加密,readObject方法在序 列化之后对字段解密

import java.io.*;

/**
 * @author Heian
 * @time 19/01/30 21:40
 * @copyright(C) 2019 深圳市北辰德科技股份有限公司
 * 用途:transient  static  序列化反序列化
 */
public class SerializableDemo2 {
    //内部类
   static class User implements Serializable {
        private String accNo;
        private static String userName;
        private transient String passWord;

        public String getAccNo() {
            return accNo;
        }

        public void setAccNo(String accNo) {
            this.accNo = accNo;
        }

        public String getUserName() {
            return userName;
        }

        public void setUserName(String userName) {
            this.userName = userName;
        }

        public String getPassWord() {
            return passWord;
        }

        public void setPassWord(String passWord) {
            this.passWord = passWord;
        }

        @Override
        public String toString() {
            return "User{" +
                    "accNo='" + accNo + "\'" +
                    ",userName='" + userName +  "\'" +
                    ", passWord='" + passWord  +  "\'" +  
                    '}';
        }
        /**
         * objOut.writeObject (user);会进入该方法
         */
        private void writeObject(java.io.ObjectOutputStream s) throws Exception {
            System.out.println ("我想自己控制序列化的过程");
            s.defaultWriteObject ();
            //按道理这里应该一些对密码转加密操作,实际上这里啥也没做,就是把字符串序列化而已
            s.writeInt (passWord.length ());
            for (int i = 0; i < passWord.length (); i++)
                s.writeChar (passWord.charAt (i));
        }
        private void readObject(java.io.ObjectInputStream s) throws Exception {
            System.out.println("我想自己控制反序列化的过程");
            s.defaultReadObject();
            int length = s.readInt();
            char[] cs = new char[length];
            for (int i = 0; i < length; i++)
                cs[i] = s.readChar();
            //按道理这里应该做一些解密操作,将从字节数组中的转加密在解密,然后输出到对象,这里就不演示那么复杂的了
            passWord = new String(cs, 0, length);
        }
    }

    public static void main(String[] args) throws FileNotFoundException, IOException,ClassNotFoundException {
        SerializableDemo2 serializableDemo = new SerializableDemo2 ();
        User user = new User();
        user.setAccNo ("123456"); user.setUserName ("Heian");user.setPassWord ("abc");
        System.out.println ("序列化前:" + user);//序列化前:User{accNo='123456',userName='Heian', passWord='abc'}
        //定义一个临时文件用来装载此对象(被transient修饰的字段不会序列化进到二进制数组中)
        ObjectOutputStream objOut = new ObjectOutputStream (new FileOutputStream ("D:a.log"));//此时会在该目录下生成一个临时文件,文件夹内容为二进制字节数组文件
        objOut.writeObject (user);
        objOut.close ();
        //再将序列化之后的字节数取到,转成对象
        ObjectInputStream objIn = new ObjectInputStream (new FileInputStream ("D:a.log"));
        Object obj = (User)objIn.readObject ();
        System.out.println ("反序列化之后的信息:" + obj);//反序列化之后的信息:User{accNo='123456',userName='Heian', passWord='abc'}


    }


}
二进制字节数组表示的user对象

    先通过defaultWriteObject和defaultReadObject方法序列化、反序列化对象,然后在文件结尾追加需要额外序列化的内容/从文件的结尾读取额外需要读取的内容。 

复杂序列化情况总结

虽然Java的序列化能够保证对象状态的持久保存,但是遇到一些对象结构复杂的情况还是比较难处理的,最后对一些复杂的对象情况作一个总结:

1、当父类继承Serializable接口时,所有子类都可以被序列化

2、子类实现了Serializable接口,父类没有,父类中的属性不能序列化(不报错,数据丢失),但是在子类中属性仍能正确序列化

3、如果序列化的属性是对象,则这个对象也必须实现Serializable接口,否则会报错

4、反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错

5、反序列化时,如果serialVersionUID被修改,则反序列化时会失败

(摘自:https://blog.csdn.net/yx_keith/article/details/80306071  https://www.cnblogs.com/szlbm/p/5504166.html

其实我一直有个疑问?为什么被transient修饰的变量不能打印出来,也就是说难道jvm内存找不到它的地址吗?static修饰的userName存于方法区,而被transient修饰的呢?希望有人能帮我答疑解惑。

猜你喜欢

转载自blog.csdn.net/qq_40826106/article/details/86706900