Java序列化与反序列化

1. 序列化的含义、意义及使用场景

Java平台允许创建在内存中可以复用的java对象,但一般情况下,对象只存在于虚拟机运行期,当JVM结束运行,对象就不存在了。序列化就是将内存中的对象,持久化保存,并可以在将来可以重新恢复成对象。

  • **序列化:**将对象写到IO流中
  • **反序列化:**从IO流中恢复对象
  • **意义:**序列化机制允许将实现序列化的Java对象,转换为字节数组,这些字节数组可以保存到磁盘,或者通过网络传输,已达到后来恢复成原来的对象。序列化机制使得对象可以脱离程序而存在。
  • **使用场景:**所以在网络上传输的对象必须要实现序列化(远程方法调用的bean必须实现序列化),所有需要保存到磁盘的java对象都必须是可序列化的。通常建议:程序创建的每个JavaBean类都实现Serializeable接口。

2. 序列化与反序列化

在Java中,只需要给类实现接口Serializable,标识这个类可以被序列化。

package interview.SerializableDemo;

import java.io.Serializable;
import java.util.Date;

/**
 * 创建可以被实例化的类
 *
 * @author: mahao
 * @date: 2019/8/24
 */
public class User implements Serializable {
	//指定序列号id,如果不指定JVM会自动计算出来,序列号不变,都可以进行反序列操作。
    private static final long serialVersionUID = 8683452581122892180L;

    private String name;
    private int age;
    private Date time;
    private boolean isMan;
    //static String flag = "static field";

    public User(String name, int age, Date time, boolean isMan) {
        System.out.println("init method be invoke ... ");
        this.name = name;
        this.age = age;
        this.time = time;
        this.isMan = isMan;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", time=" + time +
                ", isMan=" + isMan +
                '}';
    }
}

序列化 反序列化

package interview.SerializableDemo;

import org.junit.Test;

import java.io.*;
import java.util.Date;

/**
 * 序列化和反序列化对象:
 * 步骤:序列化
 * 1.创建一个ObjectOutputStream输出流:
 * 2.调用ObjectOutputStream对象的writerObject输出可序列化对象
 * <p>
 * 反序列化:
 * 1.创建一个ObjectInputStream输入流
 * 2.调用ObjectInputStream对象的readObject得到序列化的对象。
 * <p>
 * <p>
 * 输出告诉我们,反序列化并不会调用构造方法。
 * 反序列的对象是由JVM自己生成的对象,不通过构造方法生成。
 *
 * @author: mahao
 * @date: 2019/8/24
 */
public class UserMain {

    public static void main(String[] args) {
        //序列化对象
        User user = new User("mahao", 18, new Date(), true);
        ObjectOutputStream out = null;
        ObjectInputStream in = null;
        try {
            out = new ObjectOutputStream(new FileOutputStream("user.obj"));
            out.writeObject(user);

            //反序列化
            in = new ObjectInputStream(new FileInputStream("user.obj"));
            User serUser = (User) in.readObject();
            System.out.println(user == serUser);
            System.out.println(serUser);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null)
                    out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


    }

    @Test
    public void test2() throws Exception {
        //反序列化
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.obj"));
        User serUser = (User) in.readObject();
        // System.out.println(user == serUser);
        System.out.println(serUser);
    }
}


输出结果:

init method be invoke ... 
false
User{name='mahao', age=18, time=Sat Aug 24 11:32:48 CST 2019, isMan=true}

成员也必须是支持序列化的:

package interview.SerializableDemo;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 *属性中的引用类型也必须是Serializable的子类
 *
 * @author: mahao
 * @date: 2019/8/24
 */
public class MainClass2 {
    /*
    Exception in thread "main" java.io.NotSerializableException: interview.SerializableDemo.Dog

     */
    public static void main(String[] args) throws Exception {
        Person p = new Person();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.obj"));
        oos.writeObject(p);
    }
}

class Person implements Serializable {
    private String name;
    private Dog dog = new Dog();
}

class Dog {

}

3. 同一对象序列化多次的机制

==同一个对象序列化多次,会将这个对象序列化多次吗?==答案是否定了。

Java序列化算法:

  1. 所有保存到磁盘的对象都有一个序列化编码号
  2. 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
  3. 如果此对象已经序列化过,则直接输出编号即可。

在这里插入图片描述

 public static void main(String[] args) throws Exception {
        User u1 = new User("mahao", 18, new Date(), true);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MainClass3.obj"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("MainClass3.obj"));
        oos.writeObject(u1);
        oos.writeObject(u1);
        User u2 = (User) ois.readObject();
        User u3 = (User) ois.readObject();
        System.out.println(u1 == u2);//false u2是虚拟机新生成的对象,和u1不是同一个
        System.out.println(u2 == u3);//true  u3和u2是同一对象,u2 u3是同一个对象序列化而来的,不会将这个对象序列化多次。

        /*
        结论:
        从输出结果可以看出,Java序列化同一对象,并不会将此对象序列化多次得到多个对象。序列化时,如果已经
        序列化过某个对象,则第二次序列化时,不会对他进行多次序列化,只会输出编号。

        Java序列化算法:
        1.所有保存到磁盘的对象都有一个序列化编码号

        2.当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有对象从未在此虚拟机上序列化过,
        才会将对象对象序列化为字节序列输出;

        3.如果已经序列化过了,则会直接输出编号即可。

         */

    }

序列化算法的问题:

由于java序利化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。

 /*
    java序列化算法也会带来问题,他不会重复序列化一个已经序列化过的对象,只会记录那个序列化后的对象的编号。
    如果序列化一个可变对象,更改了对象的内容,再次序列化,则不会再次序列化,而是只保存上次的编号。
     */
    @Test
    public void testTrouble() throws Exception {
        User u1 = new User("mahao", 18, new Date(), true);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MainClass3tr.obj"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("MainClass3tr.obj"));
        oos.writeObject(u1);
        u1.setName("ABC");
        oos.writeObject(u1);
        User u2 = (User) ois.readObject();
        User u3 = (User) ois.readObject();
        System.out.println(u2);
        System.out.println(u3);
    }

//结果
init method be invoke ... 
User{name='mahao', age=18, time=Sat Aug 24 16:03:49 CST 2019, isMan=true}
User{name='mahao', age=18, time=Sat Aug 24 16:03:49 CST 2019, isMan=true}

4. transient

有些时候,我们有这样的需求,某些属性不需要序列化。使用transient关键字选择不需要序列化的字段。

从输出我们看到,使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。

package interview.SerializableDemo;

import java.io.*;
import java.util.Date;

/**
 * transient关键字的使用:
 * <p>
 * 些时候,我们有这样的需求,某些属性不需要序列化。
 * 使用transient关键字选择不需要序列化的字段。
 * <p>
 * 从输出我们看到,使用transient修饰的属性,java序列化时,
 * 会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。
 * 对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
 *
 * @author: mahao
 * @date: 2019/8/24
 */
public class TransientTest {


    public static void main(String[] args) throws Exception {
        User u1 = new User("mahao", 18, new Date(), true);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("TransientTest.obj"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("TransientTest.obj"));
        oos.writeObject(u1);
        User u2 = (User) ois.readObject();
        System.out.println(u1);
        System.out.println(u2);

    }


    static class User implements Serializable {

        private String name;
        private int age;
        private Date time;
        private transient boolean isMan;
        private transient Dog dog = new Dog();

        public User(String name, int age, Date time, boolean isMan) {
            System.out.println("init method be invoke ... ");
            this.name = name;
            this.age = age;
            this.time = time;
            this.isMan = isMan;
        }

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", time=" + time +
                    ", isMan=" + isMan +
                    ", dog=" + dog +
                    '}';
        }
    }

}

//结果
init method be invoke ... 
User{name='mahao', age=18, time=Sat Aug 24 16:16:30 CST 2019, isMan=true, dog=interview.SerializableDemo.Dog@5b480cf9}
User{name='mahao', age=18, time=Sat Aug 24 16:16:30 CST 2019, isMan=false, dog=null}

5. 自定义序列化

在java集合体系下,很多的类都实现了Serializable接口,但是内部实现的数据结构却定义了成了transient,比如ArrayList,内部的实现数组定义的是忽略序列化的属性。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	 private static final long serialVersionUID = 8683452581122892189L;
	 
	 transient Object[] elementData; // non-private to simplify nested class access
	  
	 private int size;

省略了其他成员变量,从上面的代码中可以知道ArrayList实现了java.io.Serializable接口,那么我们就可以对它进行序列化及反序列化。因为elementDatatransient的,所以我们认为这个成员变量不会被序列化而保留下来。我们写一个Demo,验证一下我们的想法:

public static void main(String[] args) throws Exception {
        ArrayList<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        System.out.println("serializable  before : " + list);

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MainClass4.obj"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("MainClass4.obj"));
        oos.writeObject(list);
        ArrayList<String> list2 = (ArrayList<String>) ois.readObject();
        System.out.println(list2);

    }

//
serializable  before : [aaa, bbb, ccc]
[aaa, bbb, ccc]

了解ArrayList的人都知道,ArrayList底层是通过数组实现的。那么数组elementData其实就是用来保存列表中的元素的。通过该属性的声明方式我们知道,他是无法通过序列化持久化下来的。那么为什么code 4的结果却通过序列化和反序列化把List中的元素保留下来了呢?

6. writeObjectreadObject方法

在ArrayList中定义了来个方法: writeObjectreadObject

这里先给出结论:

在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。

如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。

用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

方法具体实现:

	//* Save the state of the <tt>ArrayList</tt> instance to a stream (that
    // * is, serialize it).
	private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);//序列化元素个数

        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);//序列化每个元素
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    
	//* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
    //* deserialize it).
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored  
		//因为size属性是可以被序列化了,所以size已经是有值的了, s.readInt(); 可以被忽略
        //而且可以使用size的原因。
        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

那么为什么ArrayList要用这种方式来实现序列化呢?

因为ArrayList是动态数组,数组可能扩充到了很多元素,但是实际元素存储的少,导致有很多空的元素,为了保证null的元素浪费空间,自定义实现序列化,把数组定义为transient。

writeObject and readObject

为了防止一个包含了大量空对象的数组序列化,为了优化存储,所以ArrayList使用transient来声明elementData。但作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来,所以,通过重写writeObjectreadObject方法的方式把其中的元素保留下来。

writeObject方法把elementData数组中的元素遍历的保存到输出流(ObjectOutputStream)中。

readObject方法从输入流(ObjectInputStream)中读出对象并保存赋值到elementData数组中。

自定义序列化策略如何被调用:

通过之前的定义,ObjectOutputStream在调用writeObject(Object obj)方法时,会先去序列化的对象中寻找方法writeObject ,直接给出调用栈:

writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject

这里看一下invokeWriteObject

void invokeWriteObject(Object obj, ObjectOutputStream out)
        throws IOException, UnsupportedOperationException
    {
        if (writeObjectMethod != null) {
            try {
                writeObjectMethod.invoke(obj, new Object[]{ out });
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof IOException) {
                    throw (IOException) th;
                } else {
                    throwMiscException(th);
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

所以,会先去判断序列化对象是否存在自定义的序列化方法,如果存在,则通过反射区调用序列化的方法去完成自定义的序列化操作。

7. Serializable 作用

writeObject0方法中有这么一段代码:

	if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }

在进行序列化操作时,会判断要被序列化的类是否是Enum、Array和Serializable类型,如果不是则直接抛出NotSerializableException

8. 自定义实现序列化策略

package interview.SerializableDemo;

import org.junit.Test;

import java.io.*;
import java.util.Arrays;

import java.util.List;

/**
 * 自定义实现序列化和反序列化策略
 *
 * @author: mahao
 * @date: 2019/8/24
 */
public class MainClass5 {

    public static void main(String[] args) throws Exception {
        Bean.Dog d1 = new Bean.Dog("d1");
        Bean.Dog d2 = new Bean.Dog("d2");
        Bean.Dog d3 = new Bean.Dog("d3");

        Bean bean = new Bean("mahao", 10.5f, new Bean.Dog[]{d1, d2, d3},
                Arrays.asList("aa", "bb", "cc"));

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MainClass5.obj"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("MainClass5.obj"));
        oos.writeObject(bean);

        Bean bean1 = (Bean) ois.readObject();

        System.out.println(bean);
        System.out.println(bean1);

    }

}

//transient加上则属性就不会去序列化了,但是其他的属性仍然会序列化,
//所以transient的使用时,对某些需要自定义序列化的属性,进行加上该关键字,然后在
//writeObject中实现该属性的自定义序列化实现,并不是,自定义了实现,该类就不执行默认的
//序列化了。
class Bean implements Serializable {

    private String name;

    private float sum;

    private Dog[] dogs;

    private transient List<String> list;

    public Bean(String name, float sum, Dog[] dogs, List<String> list) {
        this.name = name;
        this.sum = sum;
        this.dogs = dogs;
        this.list = list;
    }


    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
        System.out.println("writeObject be invoke");
        s.defaultWriteObject();

        s.writeObject(name);//写入名字
        //忽略sum
        //写入数组大小
        s.writeInt(dogs.length);
        for (Dog d : dogs) {//写入数组
            s.writeObject(d);
        }

        s.writeObject(list);//list直接调用他的自定义实现

    }

    //自定义读取
    private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
        System.out.println("readObject be invoke ");
        s.defaultReadObject();
        name = (String) s.readObject();
        int length = s.readInt();
        dogs = new Dog[length];
        for (int i = 0; i < length - 1; i++) {
            dogs[i] = (Dog) s.readObject();
        }
        s.readObject();
        list = (List<String>) s.readObject();
    }


    @Override
    public String toString() {
        return "Bean{" +
                "name='" + name + '\'' +
                ", sum=" + sum +
                ", dogs=" + Arrays.toString(dogs) +
                ", list=" + list +
                '}';
    }

    static class Dog implements Serializable {
        String name;

        public Dog(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "Dog{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
}

案例地址: https://github.com/mashenghao/javaSe/tree/master/src/interview/SerializableDemo

参考:

  • https://www.hollischuang.com/archives/1140
  • https://juejin.im/post/5ce3cdc8e51d45777b1a3cdf
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值