Java序列化基本原理

作者: jiangmo | 来源:发表于2018-11-23 13:57 被阅读6次

序列化是一种对象持久化的手段。普遍应用在网络传输、RPC、RMI等场景中。
序列化有多种协议(如Thrift、protobuf、hessian、kryo等),本文主要说Jdk自带的序列化。

什么是IO流?

byte序列的读写,Java中的IO流是实现输入/输出的基础.

Java将数据从源(文件、内存、键盘、网络)读入到内存 中,形成了流,然后将这些流还可以写到另外的目的地(文件、内存、控制台、网络),之所以称为流,是因为这个数据序列在不同时刻所操作的是源的不同部分。按照不同的分类标准,IO流分为不同类型。主要有以下几种方式:按照数据流方向、数据处理的单位和功能。

不管流的分类是多么的丰富和复杂,其根源来自于四个基本的类。这个四个类的关系如下:

字节(1 byte)流 字符(1 char)流
抽象基类 InputStream、OutputStream Reader、Writer
文件 FileInputStream、FileOutputStream FileReader、FileWriter
数组 ByteArrayInputStream、ByteArrayOutputStream CharArrayReader、CharArrayWriter
管道 PipedInputStream、PipedOutputStream PipedReader、PipedWriter
字符串 StringReader、StringWriter
缓冲流 BufferedInputStream、BufferedOutputStream BufferedReader、BufferedWriter
转换流 InputStreamReader、OutputStreamWriter
对象流 ObjectInputStream、ObjectOutputStream
抽象基类 FilterInputStream、FilterOutputStream FilterReader、FilterWriter
打印流 PrintStream PrintWriter
推回输入流 PushbackInputStream PushbackReader
特殊流 DataInputStream、DataOutputStream

序列化的目的:

1)永久的保存对象,保存对象的字节序列到本地文件中;
2)通过序列化对象在网络中传递对象;
3)通过序列化对象在进程间传递对象。

几个问题:

  • 怎么实现Java的序列化
  • 为什么实现了java.io.Serializable接口才能被序列化
  • transient的作用是什么
  • 怎么自定义序列化策略
  • 自定义的序列化策略是如何被调用的
  • ArrayList对序列化的实现有什么好处

怎么实现Java的序列化

只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。

public class User implements Serializable{
    // 可序列化对象的版本  
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    public User(){}
...
}

// -------- test
   public static void main(String[] args) {
        //Initializes The Object
        User user = new User();
        user.setName("hollis");
        user.setAge(23);
     
        //Write Obj 
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(oos);
        }

        //Read Obj 
        File file = new File("tempFile");
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(file));
            User newUser = (User) ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(ois);
            try {
                FileUtils.forceDelete(file);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

序列化及反序列化相关知识

1、在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。

2、通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化

3、虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

4、序列化并不保存静态变量。

5、要想将父类对象也序列化,就需要让父类也实现Serializable 接口。

6、Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

7、serialVersionUID :在对象进行序列化或者反序列化操作的时候,要考虑JDK版本的问题,如果序列化的JDK版本和反序列化的JDK版本不统一则就可能造成异常,所以在序列化操作中引入了一个serialVersionUID的常量,可以通过此常量来验证版本的一致性,在进行反序列化时,JVM会将传过来的字节流中的serialVersionUID与本地相应实体的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就抛出不一致的异常。

8、服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

为什么实现了java.io.Serializable接口才能被序列化

以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接口,那么我们就可以对它进行序列化及反序列化。因为elementData是transient的,所以我们认为这个成员变量不会被序列化而保留下来

public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<String> stringList = new ArrayList<String>();
        stringList.add("hello");
        stringList.add("world");
        stringList.add("hollis");
        stringList.add("chuang");
        System.out.println("init StringList" + stringList);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("stringlist"));
        objectOutputStream.writeObject(stringList);

        IOUtils.close(objectOutputStream);
        File file = new File("stringlist");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        List<String> newStringList = (List<String>)objectInputStream.readObject();
        IOUtils.close(objectInputStream);
        if(file.exists()){
            file.delete();
        }
        System.out.println("new StringList" + newStringList);
    }
//init StringList[hello, world, hollis, chuang]
//new StringList[hello, world, hollis, chuang]

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

关键是重写了writeObject和readObject方法

在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。
如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。
用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

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

        if (size > 0) {
            // be like clone(), allocate array based upon size not 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();
            }
        }
    }

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();
        }
    }

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

why transient

ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient。

why writeObject and readObject

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

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

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

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

对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的,那么带着刚刚的问题,我们来分析一下ArrayList中的writeObject 和 readObject 方法到底是如何被调用的呢?

ObjectOutputStream的writeObject的调用栈:

writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->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();
        }
    }

其中writeObjectMethod.invoke(obj, new Object[]{ out });是关键,通过反射的方式调用writeObjectMethod方法。

Serializable明明就是一个空的接口,它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?

            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

其他

路径分隔符

在Windows下的路径分隔符和Linux下的路径分隔符是不一样的,当直接使用绝对路径时,跨平台会暴出“No such file or diretory”的异常。

比如说要在temp目录下建立一个test.txt文件,在Windows下应该这么写:
File file1 = new File ("C:\tmp\test.txt");
在Linux下则是这样的:
File file2 = new File ("/tmp/test.txt");

如果要考虑跨平台,则最好是这么写:
File myFile = new File("C:" + File.separator + "tmp" + File.separator, "test.txt");

File类有几个类似separator的静态字段,都是与系统相关的,在编程时应尽量使用。

  • public static final char separatorChar
    与系统有关的默认名称分隔符。此字段被初始化为包含系统属性 file.separator 值的第一个字符。在 UNIX 系统上,此字段的值为 '/';在 Microsoft Windows 系统上,它为 ''。

  • public static final String separator
    与系统有关的默认名称分隔符,为了方便,它被表示为一个字符串。此字符串只包含一个字符,即 separatorChar。

  • public static final char pathSeparatorChar
    与系统有关的路径分隔符。此字段被初始为包含系统属性 path.separator 值的第一个字符。此字符用于分隔以路径列表 形式给定的文件序列中的文件名。在 UNIX 系统上,此字段为 ':';在 Microsoft Windows 系统上,它为 ';'。

  • public static final String pathSeparator
    与系统有关的路径分隔符,为了方便,它被表示为一个字符串。此字符串只包含一个字符,即 pathSeparatorChar。

相关文章

  • Java-序列化-反序列化

    Thanks Java基础学习总结——Java对象的序列化和反序列化java序列化反序列化原理Java 序列化的高...

  • java序列化那些事儿

    java序列化作用 在说java序列化的作用之前,先说下什么是java序列化吧。java序列化是指把java对象转...

  • Java序列化

    Java序列化的几种方式以及序列化的作用 Java基础学习总结——Java对象的序列化和反序列化

  • Java序列化机制

    Java序列化机制 序列化和反序列化 Java序列化是Java内建的数据(对象)持久化机制,通过序列化可以将运行时...

  • Java 序列化 之 单例模式

    序列化相关文章: Java 序列化 之 Serializable Java 序列化之 Externalizable...

  • java 序列化 原理解析

    序列化相关文章: Java 序列化 之 Serializable Java 序列化之 Externalizable...

  • 认识Java序列化

    我的个人博客,认识Java序列化 引言 将 Java 对象序列化为二进制文件的 Java 序列化技术是 Java ...

  • JDK 序列化

    序列化和分序列化概念 什么是序列化和反序列化 Java序列化是指把Java对象转换为字节序列的过程,而Java反序...

  • Spark序列化

    Java序列化 有关Java对象的序列化和反序列化也算是Java基础的一部分,首先对Java序列化的机制和原理进行...

  • Serializable中为什么要设置UID

    1、什么是Java序列化与反序列化 Java序列化是指把Java对象保存为二进制字节码的过程,Java反序列化是指...

网友评论

    本文标题:Java序列化基本原理

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