起航学习网

- 让每个人都能学到最前沿新知识、新技能!
起航学习网
当前位置: 起航学习网 > 短期培训 > 编程语言 > 零基础学习java开发 你真的了解Java的序列化吗

零基础学习java开发 你真的了解Java的序列化吗

时间:2018-03-19 11:20:49来源:Java培训网 作者:IT培训网 已有: 名学员访问该课程

前言:在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的。只

你真的以为你了解Java的序列化了吗,在学习java的路上,你遇到了哪些难题呢?

Java对象的序列化与反序列化。

序列化与反序列化  

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等。在网络传输过程中,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。这个相反的过程又称为反序列化。

Java对象的序列化与反序列化  

在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的。只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。

但是在真实的应用场景中,我们需要将这些对象持久化下来,并且能够在需要的时候把对象重新读取出来。Java的对象序列化可以帮助我们实现该功能。

对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。

在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。

相关接口及类  

Java为了方便开发人员将Java对象进行序列化及反序列化提供了一套方便的API来支持。其中包括以下接口和类:

java.io.Serializable

java.io.Externalizable

ObjectOutput

ObjectInput

ObjectOutputStream

ObjectInputStream

Serializable 接口  

类通过实现java.io.Serializable接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。

当试图对一个对象进行序列化的时候,如果遇到不支持 Serializable 接口的对象。在此情况下,将抛出NotSerializableException

虽然Serializable接口中并没有定义任何属性和方法,但是如果一个类想要具备序列化能力也比必须要实现它。其实,主要是因为序列化在真正的执行过程中会使用instanceof判断一个类是否实现类Serializable,如果未实现则直接抛出异常。关于这部分内容,我会单开一篇文章讲解。

如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该集成java.io.Serializable接口。

下面是一个实现了java.io.Serializable接口的类

packagecom.hollischaung.serialization.SerializableDemos;importjava.io.Serializable;/*** Created by hollis on 16/2/17.* 实现Serializable接口*/publicclassUser1implementsSerializable{privateString name;privateintage;publicString getName(){returnname;}publicvoidsetName(String name){this.name = name;}publicintgetAge(){returnage;}publicvoidsetAge(intage){this.age = age;}@OverridepublicString toString(){return"User{"+"name='"+ name + '\''+", age="+ age +'}';}}

通过下面的代码进行序列化及反序列化

package com.hollischaung.serialization.SerializableDemos;importjava.io.File;importjava.io.FileInputStream;importjava.io.FileOutputStream;importjava.io.IOException;importjava.io.ObjectInputStream;importjava.io.ObjectOutputStream;/*** Created by hollis on 16/2/17.* SerializableDemo1 结合SerializableDemo2说明 一个类要想被序列化必须实现Serializable接口*/publicclassSerializableDemo1{publicstaticvoidmain(String[] args){//Initializes The ObjectUser1 user = newUser1();user.setName("hollis");user.setAge(23);System.out.println(user);//Write Obj to Filetry(FileOutputStream fos = newFileOutputStream("tempFile"); ObjectOutputStream oos = newObjectOutputStream(fos)) {oos.writeObject(user);} catch(IOException e) {e.printStackTrace();}//Read Obj from FileFile file = newFile("tempFile");try(ObjectInputStream ois = newObjectInputStream(newFileInputStream(file))) {User1 newUser = (User1)ois.readObject();System.out.println(newUser);} catch(IOException | ClassNotFoundException e) {e.printStackTrace();}}}//OutPut://User{name='hollis', age=23}//User{name='hollis', age=23}

如果你观察够细微的话,你可能会发现,我在上面的测试代码中使用了IO流,但是我并没有显示的关闭他。这其实是Java 7中的新特性try-with-resources。这其实是Java中的一个语法糖,背后原理其实是编译器帮我们做了关闭IO流的工作。后面我会单独出一篇文章介绍下如何使用语法糖提高代码质量。

上面的代码中,我们将代码中定义出来的User对象通过序列化的方式保存到文件中,然后再从文件中将他到序列化成Java对象。结果是我们的对象的属性均被持久化了下来。

Externalizable接口  

除了Serializable 之外,java中还提供了另一个序列化接口Externalizable

为了了解Externalizable接口和Serializable接口的区别,先来看代码,我们把上面的代码改成使用Externalizable的形式。

packagecom.hollischaung.serialization.ExternalizableDemos;importjava.io.Externalizable;importjava.io.IOException;importjava.io.ObjectInput;importjava.io.ObjectOutput;/*** Created by hollis on 16/2/17.* 实现Externalizable接口*/publicclassUser1implementsExternalizable{privateString name;privateintage;publicString getName(){returnname;}publicvoidsetName(String name){this.name = name;}publicintgetAge(){returnage;}publicvoidsetAge(intage){this.age = age;}publicvoidwriteExternal(ObjectOutput out)throwsIOException {}publicvoidreadExternal(ObjectInput in)throwsIOException, ClassNotFoundException {}@OverridepublicString toString(){return"User{"+"name='"+ name + '\''+", age="+ age +'}';}}
package com.hollischaung.serialization.ExternalizableDemos;importjava.io.*;/*** Created by hollis on 16/2/17.* 对一个实现了Externalizable接口的类进行序列化及反序列化*/publicclassExternalizableDemo1{publicstaticvoidmain(String[] args){//Write Obj to fileUser1 user = newUser1();user.setName("hollis");user.setAge(23);try(ObjectOutputStream oos = newObjectOutputStream(newFileOutputStream("tempFile"))){oos.writeObject(user);} catch(IOException e) {e.printStackTrace();}//Read Obj from fileFile file = newFile("tempFile");try(ObjectInputStream ois =  newObjectInputStream(newFileInputStream(file))){User1 newInstance = (User1) ois.readObject();//outputSystem.out.println(newInstance);} catch(IOException | ClassNotFoundException e ) {e.printStackTrace();}}}//OutPut://User{name='null', age=0}

通过上面的实例的输出结果可以发现,对User1类进行序列化及反序列化之后得到的对象的所有属性的值都变成了默认值。也就是说,之前的那个对象的状态并没有被持久化下来。这就是Externalizable接口和Serializable接口的区别:

Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。

由于上面的代码中,并没有在这两个方法中定义序列化实现细节,所以输出的内容为空。还有一点值得注意:在使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public的无参的构造器。

如果实现了Externalizable接口的类中没有无参数的构造函数,在运行时会抛出异常:java.io.InvalidClassException。如果一个Java类没有定义任何构造函数,编译器会帮我们自动添加一个无参的构造方法,可是,如果我们在类中定义了一个有参数的构造方法了,编译器便不会再帮我们创建无参构造方法,这点需要注意。

按照要求修改之后代码如下:

packagecom.hollischaung.serialization.ExternalizableDemos;importjava.io.Externalizable;importjava.io.IOException;importjava.io.ObjectInput;importjava.io.ObjectOutput;/*** Created by hollis on 16/2/17.* 实现Externalizable接口,并实现writeExternal和readExternal方法*/publicclassUser2implementsExternalizable{privateString name;privateintage;publicString getName(){returnname;}publicvoidsetName(String name){this.name = name;}publicintgetAge(){returnage;}publicvoidsetAge(intage){this.age = age;}publicvoidwriteExternal(ObjectOutput out)throwsIOException {out.writeObject(name);out.writeInt(age);}publicvoidreadExternal(ObjectInput in)throwsIOException, ClassNotFoundException {name = (String) in.readObject();age = in.readInt();}@OverridepublicString toString(){return"User{"+"name='"+ name + '\''+", age="+ age +'}';}}

再执行测试得到以下结果

//OutPut://User{name='hollis', age=23}

这次,就可以把之前的对象状态持久化下来了。

ObjectOutput和ObjectInput 接口  

上面的writeExternal方法和readExternal方法分别接收ObjectOutput和ObjectInput类型参数。这两个类作用如下。

ObjectInput 扩展自 DataInput 接口以包含对象的读操作。

DataInput 接口用于从二进制流中读取字节,并根据所有 Java 基本类型数据进行重构。同时还提供根据 UTF-8 修改版格式的数据重构 String 的工具。

对于此接口中的所有数据读取例程来说,如果在读取所需字节数之前已经到达文件末尾 (end of file),则将抛出 EOFException(IOException 的一种)。如果因为到达文件末尾以外的其他原因无法读取字节,则将抛出 IOException 而不是 EOFException。尤其是,在输入流已关闭的情况下,将抛出 IOException。

ObjectOutput 扩展 DataOutput 接口以包含对象的写入操作。

DataOutput 接口用于将数据从任意 Java 基本类型转换为一系列字节,并将这些字节写入二进制流。同时还提供了一个将 String 转换成 UTF-8 修改版格式并写入所得到的系列字节的工具。

对于此接口中写入字节的所有方法,如果由于某种原因无法写入某个字节,则抛出 IOException。

ObjectOutputStream、ObjectInputStream类  

通过前面的代码片段中我们也能知道,我们一般使用ObjectOutputStream的writeObject方法把一个对象进行持久化。再使用ObjectInputStream的readObject从持久化存储中把对象读取出来。

更多关于ObjectInputStream和ObjectOutputStream的相关知识,我会单独有一篇文章介绍,敬请期待。

transient 关键字  

transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。关于transient 关键字的拓展同样下一篇文章介绍。

序列化ID  

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

序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。

你真的以为你了解Java的序列化了吗?

在《Java对象的序列化与反序列化》中,简单介绍了Java中对象的序列化和反序列化的一些基础知识。看文那篇文章后,有小伙伴留言说:我终于了解了Java的序列化了。我只想说:小伙子,你真的是图样图森破啊 。

通过那篇文章,读者可以学会如何对Java对象进行序列化和反序列化。但是,还有些原理性知识没有深入讲解。本文,在上一篇文章的基础上,抽丝剥茧,深入底层原理,主要围绕以下几个问题展开:

  • 怎么实现Java的序列化?

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

  • transient的作用是什么?

  • 如果在序列化中破坏transitent的限制?

  • 怎么自定义序列化策略?

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

  • ArrayList对序列化的实现有哪些好处?

零基础学习java开发 你真的了解Java的序列化吗_www.epx365.cn

序列化基础知识

关于序列化的用法及基础知识,由于不是本文重点,这里不再详细介绍了,详情请参阅Java对象的序列化与反序列化。这里只做简单回顾。

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

ArrayList的序列化

为了深入的介绍序列化,我们这篇文章准备从Java源码中的ArrayList类入手。看看Java自身是如何使用序列化的。在介绍ArrayList序列化之前,先来考虑一个问题:

“问:如何自定义的序列化和反序列化策略?”

带着这个问题,我们来看java.util.ArrayList的源码

code 1

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的,所以我们认为这个成员变量的内容不会被序列化而保留下来。我们写一个Demo,验证一下我们的想法:

code 2

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]

了解ArrayList的人都知道,ArrayList底层是通过数组实现的。那么数组elementData其实就是用来保存列表中的元素的。通过该属性的声明方式,我们认为,他应该是无法通过序列化持久化下来的。

&ldquo;问:为什么code 2的结果却通过序列化和反序列化把List中的元素保留下来了呢?&rdquo;

writeObject和readObject方法

在ArrayList中定义了来个方法: writeObject和readObject。

这里先给出结论:

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

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

也就述说,用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。我们发现ArrayList中有这两个方法的实现,那么基本可以确定,elementData能被序列化持久下来,肯定和这两个方法有关,虽然他被声明为transitent,那么我们来看一下ArrayList类中这两个方法的具体实现:

code 3

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

code 4

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

通过上面两段代码,我们发现,raedObject方法和writeObjec方法中定义了关于elementData的序列化策略。现在,我们可以回答刚刚的问题了。

&ldquo;问:为什么code 2的结果却通过序列化和反序列化把List中的元素保留下来了呢?

答:ArrayList中定义了raedObject和writeObject方法,这两个方法中定义了elementData的序列化及反序列化策略。&rdquo;

那么,问题又来了。

&ldquo;问:为什么ArrayList要用这种方式来实现序列化呢?&rdquo;

why transient

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

why writeObject and readObject

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

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

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

至此,我们先试着来回答刚刚提出的问题:

&ldquo;问:为什么ArrayList要用这种方式来实现序列化呢?

答:避免elementData数组中过多的无用的null被序列化。

问:如何自定义的序列化和反序列化策略?

答:可以通过在被序列化的类中增加writeObject 和 readObject方法。&rdquo;

那么问题又来了,虽然ArrayList中写了writeObject 和 readObject 方法,但是这两个方法并没有显示的被调用啊。

&ldquo;问:如果一个类中包含writeObject 和 readObject 方法,那么这两个方法是怎么被调用的呢?&rdquo;

ObjectOutputStream

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

为了节省篇幅,这里给出ObjectOutputStream的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();        }    }

其中writeObjectMethod.invoke();是关键,通过反射的方式调用writeObjectMethod方法。官方是这么解释这个writeObjectMethod的:

class-defined writeObject method, or null if none

在我们的例子中,这个方法就是我们在ArrayList中定义的writeObject方法。通过反射的方式被调用了。

至此,我们先试着来回答刚刚提出的问题:

&ldquo;问:如果一个类中包含writeObject 和 readObject 方法,那么这两个方法是怎么被调用的?

答:在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法时,会通过反射的方式调用。&rdquo;

至此,我们已经介绍完了ArrayList的序列化方式。那么,不知道有没有人提出这样的疑问:

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

Serializable接口的定义:

public interface Serializable {  }

如果尝试对一个未实现Serializable接口的类进行序列化,会抛出java.io.NotSerializableException。这是为什么呢?Serializable只是一个空接口,如何实现的呢?

其实这个问题也很好回答,我们再回到刚刚ObjectOutputStream的writeObject的调用栈:

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

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。

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

答:在类的序列化过程中,会使用instanceof关键字判断一个类是否继承了Serializable类,如果没有,则直接抛出NotSerializableException异常。&rdquo;

总结

1、如果一个类想被序列化,需要实现Serializable接口。否则将抛出NotSerializableException异常,这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于Enum、Array和Serializable类型其中的任何一种。

2、在变量声明前加上该关键字,可以阻止该变量被序列化到文件中。

3、在类中增加writeObject 和 readObject 方法可以实现自定义序列化策略。

文章出自:http://qh.itpxw.cn/peixun/software/201838941.html

文章标题:零基础学习java开发 你真的了解Java的序列化吗



免责声明:本站文章均由入驻起航学习网的会员所发或者网络转载,所述观点仅代表作者本人,不代表起航学习网立场。如有侵权或者其他问题,请联系举报,必删。侵权投诉

(责任编辑:IT培训网)
顶一下
(0)
0%
踩一下
(0)
0%
------分隔线----------------------------
培训学校
IT培训网 访问该机构站点 报名留言 加为好友 用户等级:注册会员 用户级别:10 机构名称:IT培训网 联 系 人:罗老师 联系电话:13783581536 联系手机:13783581536 在线客服:起航学习网客服 在 线 QQ:起航学习网客服 电子邮件: 网站域名:http://www.itpxw.cn 注册时间:2016-07-18 11:07 最后登录:2024-02-20 13:02
推荐内容