专题二、ArrayList序列化技术细节详解

图片 2

Java语言的transient不像class、synchronized和其他熟悉的关键字那样众所周知,因而它会出现在一些面试题中。这篇文章我将为大家讲解transient。

一、绪论

所谓的JAVA序列化与反序列化,序列化就是将JAVA
对象以一种的形式保持,比如存放到硬盘,或是用于传输。反序列化是序列化的一个逆过程。

JAVA规定被序列化的对象必须实现java.io.Serializable这个接口,而我们分析的目标ArrayList同样实现了该接口。

通过对ArrayList源码的分析,可以知道ArrayList的数据存储都是依赖于elementData数组,它的声明为:

transient Object[] elementData;

注意transient修饰着elementData这个数组。
 - Transient关键字

transient的用途

Q:transient关键字能实现什么?

A:当对象被序列化时(写入字节序列到目标文件)时,transient阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。例如,当反序列化对象——数据流(例如,文件)可能不存在时,原因是你的对象中存在类型为java.io.InputStream的变量,序列化时这些变量引用的输入流无法被打开。

1、先看看transient关键字的作用

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

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

总之,java
的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

具体详见:Java
transient关键字使用小记

既然elementData被transient修饰,按理来说,它不能被序列化的,那么ArrayList又是如何解决序列化这个问题的呢?

Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想
用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。
transient是Java语言的关键字,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,
transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。
注意static变量也是可以串行化的
同时,通过反序列化得到的对象是不同的对象,而且得到的对象不是通过构造器得到的,
也就是说反序列化得到的对象不执行构造器。

transient使用介绍

Q:如何使用transient?

A:包含实例变量声明中的transient修饰符。片段1提供了小的演示。

import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class ClassLib implements Serializable {
    private transient InputStream is;

    private int majorVer;
    private int minorVer;

    ClassLib(InputStream is) throws IOException {
        System.out.println("ClassLib(InputStream) called");
        this.is = is;
        DataInputStream dis;
        if (is instanceof DataInputStream)
            dis = (DataInputStream) is;
        else
            dis = new DataInputStream(is);
        if (dis.readInt() != 0xcafebabe)
            throw new IOException("not a .class file");
        minorVer = dis.readShort();
        majorVer = dis.readShort();
    }

    int getMajorVer() {
        return majorVer;
    }

    int getMinorVer() {
        return minorVer;
    }

    void showIS() {
        System.out.println(is);
    }
}

public class TransDemo {
    public static void main(String[] args) throws IOException {
        if (args.length != 1) {
            System.err.println("usage: java TransDemo classfile");
            return;
        }
        ClassLib cl = new ClassLib(new FileInputStream(args[0]));
        System.out.printf("Minor version number: %d%n", cl.getMinorVer());
        System.out.printf("Major version number: %d%n", cl.getMajorVer());
        cl.showIS();

        try (FileOutputStream fos = new FileOutputStream("x.ser");
                ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(cl);
        }

        cl = null;

        try (FileInputStream fis = new FileInputStream("x.ser");
                ObjectInputStream ois = new ObjectInputStream(fis)) {
            System.out.println();
            cl = (ClassLib) ois.readObject();
            System.out.printf("Minor version number: %d%n", cl.getMinorVer());
            System.out.printf("Major version number: %d%n", cl.getMajorVer());
            cl.showIS();
        } catch (ClassNotFoundException cnfe) {
            System.err.println(cnfe.getMessage());
        }
    }
}

片段1:序列化和反序列化ClassLib对象

片段1中声明ClassLib和TransDemo类。ClassLib是一个读取Java类文件的库,并且实现了java.io.Serializable接口,从而这些实例能被序列化和反序列化。TransDemo是一个用来序列化和反序列化ClassLib实例的应用类。

ClassLib声明它的实例变量为transient,原因是它可以毫无意义的序列化一个输入流(像上面讲述的那样)。事实上,如果此变量不是transient的话,当反序列化x.ser的内容时,则会抛出java.io.NotSerializableException,原因是InputStream没有实现Serializable接口。

编译片段1:javac TransDemo.java;带一个参数TransDemo.class运行应用:java TransDemo TransDemo.class。你或许会看到类似下面的输出:

ClassLib(InputStream) called
Minor version number: 0
Major version number: 51
java.io.FileInputStream@79f1e0e0

Minor version number: 0
Major version number: 51
null

以上输出表明:当对象被重构时,没有构造方法调用。此外,is假定默认为null,相比较,当ClassLib对象序列化时,majorVer和minorVer是有值的。

二、序列化工作流程

类通过实现java.io.Serializable接口可以启用其序列化功能。要序列化一个对象,必须与一定的对象输出/输入流联系起来,通过对象输出流将对象状态保存下来,再通过对象输入流将对象状态恢复。

在序列化和反序列化过程中需要特殊处理的类必须使用下列准确签名来实现特殊方法:

private void writeObject(java.io.ObjectOutputStream out) throws
IOException

private void readObject(java.io.ObjectInputStream in) throws
IOException, ClassNotFoundException

 

下面进行测试:
新建一个javabean类,代码:

类中的成员变量和transient

Q:类中的成员变量中可以使用transient吗?

A:问题答案请看片段2

public class TransDemo {
    public static void main(String[] args) throws IOException {
        Foo foo = new Foo();
        System.out.printf("w: %d%n", Foo.w);
        System.out.printf("x: %d%n", Foo.x);
        System.out.printf("y: %d%n", foo.y);
        System.out.printf("z: %d%n", foo.z);
        try (FileOutputStream fos = new FileOutputStream("x.ser");
                ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(foo);
        }

        foo = null;

        try (FileInputStream fis = new FileInputStream("x.ser");
                ObjectInputStream ois = new ObjectInputStream(fis)) {
            System.out.println();
            foo = (Foo) ois.readObject();
            System.out.printf("w: %d%n", Foo.w);
            System.out.printf("x: %d%n", Foo.x);
            System.out.printf("y: %d%n", foo.y);
            System.out.printf("z: %d%n", foo.z);
        } catch (ClassNotFoundException cnfe) {
            System.err.println(cnfe.getMessage());
        }
    }
}

片段2:序列化和反序列化Foo对象

片段2有点类似片段1。但不同的是,序列化和反序列化的是Foo对象,而不是ClassLib。此外,Foo包含一对变量,w和x,以及实例变量y和z。

编译片段2(javac TransDemo.java)并运行应用(java TransDemo)。你可以看到如下输出:

w: 1
x: 2
y: 3
z: 4

w: 1
x: 2
y: 3
z: 0

这个输出告诉我们,实例变量y是被序列化的,z却没有,它被标记transient。但是,当Foo被序列化时,它并没有告诉我们,是否变量w和x被序列化和反序列化,是否只是以普通类初始化方式初始。对于答案,我们需要查看x.ser的内容。

下面显示x.ser十六进制:

00000000 AC ED 00 05 73 72 00 03 46 6F 6F FC 7A 5D 82 1D ....sr..Foo.z]..
00000010 D2 9D 3F 02 00 01 49 00 01 79 78 70 00 00 00 03 ..?...I..yxp....

由于JavaWorld中的“The Java serialization algorithm revealed”这篇文章,我们发现输出的含义:

  • AC ED 序列化协议标识
  • 00 05 流版本号
  • 73 表示这是一个新对象
  • 72 表示这是一个新的类
  • 00 03 表示类名长度(3)
  • 46 6F 6F 表示类名(Foo)
  • FC 7A 5D 82 1D D2 9D 3F 表示类的串行版本标识符
  • 02 表示该对象支持序列化
  • 00 01 表示这个类的变量数量(1)
  • 49 变量类型代码 (0×49, 或I, 表示int)
  • 00 01 表示变量名长度(1)
  • 79 变量名称(y)
  • 78 表示该对象可选的数据块末端
  • 70 表示我们已经到达类层次结构的顶部
  • 00 00 00 03 表示y的值(3)

显而易见,只有实例变量y被序列化。因为z是transient,所以不能序列化。此外,即使它们标记transien,w和x不能被序列化,原因是它们类变量不能序列化。

1、对象序列化步骤

a) 写入

  • 首先创建一个OutputStream输出流;
  • 然后创建一个ObjectOutputStream输出流,并传入OutputStream输出流对象;
  • 最后调用ObjectOutputStream对象的writeObject()方法将对象状态信息写入OutputStream。

b)读取

  • 首先创建一个InputStream输入流;
  • 然后创建一个ObjectInputStream输入流,并传入InputStream输入流对象;
  • 最后调用ObjectInputStream对象的readObject()方法从InputStream中读取对象状态信息。

举例说明:

 

public class Box implements Serializable {
    private static final long serialVersionUID = -3450064362986273896L;
    
    private int width;
    private int height;
    
    public static void main(String[] args) {
        Box myBox=new Box();
        myBox.setWidth(50);
        myBox.setHeight(30);
        try {
            FileOutputStream fs=new FileOutputStream("F:\foo.ser");
            ObjectOutputStream os=new ObjectOutputStream(fs);
            os.writeObject(myBox);
            os.close();
            FileInputStream fi=new FileInputStream("F:\foo.ser");
            ObjectInputStream oi=new ObjectInputStream(fi);
            Box box=(Box)oi.readObject();
            oi.close();
            System.out.println(box.height+","+box.width);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public int getWidth() {
        return width;
    }
    public void setWidth(int width) {
        this.width = width;
    }
    public int getHeight() {
        return height;
    }
    public void setHeight(int height) {
        this.height = height;
    }
}
import java.util.Date;
public class LoggingInfo implements java.io.Serializable   
{   
    private static Date loggingDate = new Date();   
    private String uid;   
    private transient String pwd;   

    LoggingInfo(String user, String password)   
    {   
        uid = user;   
        pwd = password;   
    }   
    public String toString()   
    {   
        String password=null;   
        if(pwd == null)   
        {   
        password = "NOT SET";   
        }   
        else  
        {   
            password = pwd;   
        }   
        return "logon info: n   " + "user: " + uid +   
            "n   logging date : " + loggingDate.toString() +   
            "n   password: " + password;   
    }   
}   

三、ArrayList解决序列化

测试类,调用上面的javabean类,进行序列化和反序列化,代码如下:

1、序列化

从上面序列化的工作流程可以看出,要想序列化对象,使用ObjectOutputStream对象输出流的writeObject()方法写入对象状态信息,即可使用readObject()方法读取信息。

那是不是可以在ArrayList中调用ObjectOutputStream对象的writeObject()方法将elementData的值写入输出流呢?

见源码:

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

虽然elementData被transient修饰,不能被序列化,但是我们可以将它的值取出来,然后将该值写入输出流。

// 片段1 它的功能等价于片段2
s.writeObject(elementData[i]);  // 传值时,是将实参elementData[i]赋给s.writeObject()的形参
//  片段2
Object temp = new Object();     // temp并没有被transient修饰
temp = elementData[i];
s.writeObject(temp);

 

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class Test{
    public static void main(String[] args){
        LoggingInfo loggingInfo = new LoggingInfo("longyin", "123");
        System.out.println("写入:"+loggingInfo);
        ObjectOutputStream objectOutput = null;
        ObjectInputStream objectInput = null;
        try {
            objectOutput = new ObjectOutputStream(
                    new FileOutputStream("test.txt"));
            objectInput = new ObjectInputStream(
                    new FileInputStream("test.txt"));
            objectOutput.writeObject(loggingInfo);
            LoggingInfo info = (LoggingInfo) objectInput.readObject();
            System.out.println("读取:"+info);
            System.out.println("是否相等:"+(info==loggingInfo));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally{
            if (objectInput != null) {
                try {
                    objectInput.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (objectOutput != null) {
                try {
                    objectOutput.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2、反序列化

ArrayList的反序列化处理原理同上,见源码:

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的,那什么时候能调用呢?

执行结果:
图片 1

3、调用

如果一个类不仅实现了Serializable接口,而且定义了
readObject(ObjectInputStream in)和 writeObject(ObjectOutputStream
out)方法,那么将按照如下的方式进行序列化和反序列化:

ObjectOutputStream会调用这个类的writeObject方法进行序列化,ObjectInputStream会调用相应的readObject方法进行反序列化。

事情到底是这样的吗?我们做个小实验,来验明正身。
实验1:

public class TestSerialization implements Serializable
{
    private transient int    num;

    public int getNum()
    {
        return num;
    }

    public void setNum(int num)
    {
        this.num = num;
    }

    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException
    {
        s.defaultWriteObject();
        s.writeObject(num);
        System.out.println("writeObject of "+this.getClass().getName());
    }

    private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException
    {
        s.defaultReadObject();
        num = (Integer) s.readObject();
        System.out.println("readObject of "+this.getClass().getName());
    }

    public static void main(String[] args)
    {
        TestSerialization test = new TestSerialization();
        test.setNum(10);
        System.out.println("序列化之前的值:"+test.getNum());
        // 写入
        try
        {
            ObjectOutputStream outputStream = new ObjectOutputStream(
                    new FileOutputStream("D:\test.tmp"));
            outputStream.writeObject(test);
        } catch (FileNotFoundException e)
        {
            e.printStackTrace();
        } catch (IOException e)
        {
            e.printStackTrace();
        }
        // 读取
        try
        {
            ObjectInputStream oInputStream = new ObjectInputStream(
                    new FileInputStream("D:\test.tmp"));
            try
            {
                TestSerialization aTest = (TestSerialization) oInputStream.readObject();
                System.out.println("读取序列化后的值:"+aTest.getNum());
            } catch (ClassNotFoundException e)
            {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e)
        {
            e.printStackTrace();
        } catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}

输出:

序列化之前的值:10
writeObject of TestSerialization
readObject of TestSerialization
读取序列化后的值:10

实验结果证明,事实确实是如此:

ObjectOutputStream会调用这个类的writeObject方法进行序列化,ObjectInputStream会调用相应的readObject方法进行反序列化。
那么ObjectOutputStream又是如何知道一个类是否实现了writeObject方法呢?又是如何自动调用该类的writeObject方法呢?

答案是:是通过反射机制实现的。

部分解答:

ObjectOutputStream的writeObject又做了哪些事情。它会根据传进来的ArrayList对象得到Class,然后再包装成
ObjectStreamClass,在writeSerialData方法里,会调用ObjectStreamClass的
invokeWriteObject方法,最重要的代码如下:

writeObjectMethod.invoke(obj, new Object[]{ out });

实例变量writeObjectMethod的赋值方式如下:

writeObjectMethod = getPrivateMethod(cl, "writeObject", 
                new Class[] { ObjectOutputStream.class }, 
                Void.TYPE);

 private static Method getPrivateMethod(Class cl, String name,
        Class[] argTypes, Class returnType)
{
    try
    {
        Method meth = cl.getDeclaredMethod(name, argTypes);
        // *****通过反射访问对象的private方法
        meth.setAccessible(true);
        int mods = meth.getModifiers();
        return ((meth.getReturnType() == returnType)
                && ((mods & Modifier.STATIC) == 0) && ((mods & Modifier.PRIVATE) != 0)) ? meth
                : null;
    } catch (NoSuchMethodException ex)
    {
        return null;
    }
}

在做实验时,我们发现一个问题,那就是为什么需要s.defaultWriteObject();和s.defaultReadObject();语句在readObject(ObjectInputStream o)
and writeObject(ObjectOutputStream o)之前呢?

它们的作用如下:

1、It reads and writes
all the non transient fields of the class respectively.

2、 These methods also helps in backward and future compatibility. If in
future you add some non-transient field to the class and you are
trying to deserialize it by the older version of class then the
defaultReadObject() method will neglect the newly added field, similarly
if you deserialize the old serialized object by the new version then the
new non transient field will take default value from JVM

通过执行结果,可以对照上面的分析,说明上面的分析是正确的。

四、为什么使用transient修饰elementData?

既然要将ArrayList的字段序列化(即将elementData序列化),那为什么又要用transient修饰elementData呢?

回想ArrayList的自动扩容机制,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数。

比如,现在实际有了8个元素,那么elementData数组的容量可能是8×1.5=12,如果直接序列化elementData数组,那么就会浪费4个元素的空间,特别是当元素个数非常多时,这种浪费是非常不合算的。

所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。

见源码:

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

从源码中,可以观察到 循环时是使用i<size而不是
i<elementData.length,说明序列化时,只需实际存储的那些元素,而不是整个数组。

参考:

 

 

 

1、java.io.Serializable浅析

2、java
serializable深入了解

3、ArrayList源码分析——如何实现Serializable

4、java序列化和反序列话总结

  • 下面说说Volatile关键字

Java 语言提供了一种稍弱的同步机制,即 volatile
变量.用来确保将变量的更新操作通知到其他线程,保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新.
当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的.
volatile 变量对所有线程是立即可见的,对 volatile
变量所有的写操作都能立即反应到其他线程之中,换句话说:volatile
变量在各个线程中是一致的,所以基于 volatile 变量的运算是线程安全的.
对于以上的说法,我没有想到如何用实例进行验证。
下面只是个人的理解:
1。如果在类中使用volatile修饰一个变量,并且是static的类型,那么该变量属于类,是类变量,那么即使多个线程访问该变量访问的也是同一个,哪个线程改变它的话,其他线程在访问它的时候就是最新的值。不存在不同步的问题。

2。如果在类中使用volatile修饰的变量没有使用static修饰,那就属于成员变量,那么多个线程在访问的时候,访问同一个对象下的该成员变量也不存在不同步的问题。对于同一个对象,该成员变量就一个!线程无论何时访问都是最新的。

所以能用到volatile关键字解决多线程的不同步问题相当少了。

  • 序列化和反序列化

正常情况下,一个类实现java序列化很简单,只需要implements
Serializable接口即可,之后该类在跨jvm的传输过程中会遵照默认java序列化规则序列化和反序列化;不同jvm版本之间序列化方式稍有不同,但基本上都是兼容的。
在某些特殊情况下,可能需要自定义序列化和反序列化的行为,看下面例子:

 class AbstractSerializeDemo {     
    private int x, y;     

    public void init(int x, int y) {     
        this.x = x;     
        this.y = y;     
    }     

    public int getX() {     
        return x;     
    }     

    public int getY() {     
        return y;     
    }     

    public void printXY() {     
        System.out.println("x:" + x + ";y:" + y);     
    }     
}     

public class SerializeDemo extends AbstractSerializeDemo implements Serializable {     
    private int z;     

    public SerializeDemo() {     
        super.init(10, 50);     
        z = 100;     
    }     

    public void printZ() {     
        super.printXY();     
        System.out.println("z:" + z);     
    }     

    public static void main(String[] args) throws IOException, ClassNotFoundException {     
        ByteArrayOutputStream bos = new ByteArrayOutputStream();     
        ObjectOutputStream out = new ObjectOutputStream(bos);     
        SerializeDemo sd = new SerializeDemo();     
        sd.printZ();     
        out.writeObject(sd);     
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));     
        SerializeDemo sd2 = (SerializeDemo) in.readObject();     
        sd2.printZ();     
    }     
}  

这段程序表示了一个可序列化的类继承自一个非序列化的有状态超类,期望的结果是,子类序列化以后传输并反序列化回来,原先的值域包括超类的值域都保持不变。

但是输出是:
x:10;y:50
z:100
x:0;y:0
z:100
结果和期望不符,子类的值域保留下来了,但是超类的值域丢失了,这对jvm来说是正常的,因为超类不可序列化;

为了解决这个问题,只能自定义序列化行为,具体做法是在SerializeDemo里加入以下代码:

private void writeObject(ObjectOutputStream os) throws IOException {     
      os.defaultWriteObject();//java对象序列化默认操作     
     os.writeInt(getX());     
      os.writeInt(getY());     
  }     

  private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {     
      is.defaultReadObject();//java对象反序列化默认操作     
      int x=is.readInt();     
      int y=is.readInt();     
      super.init(x,y);     
  }   

writeObject和readObject方法为JVM会在序列化和反序列化java对象时会分别调用的两个方法,修饰符都是private,没错。

我们在序列化的默认动作之后将超类里的两个值域x和y也写入object流;与之对应在反序列化的默认操作之后读入x和y两个值,然后调用超类的初始化方法。

再次执行程序之后的输出为:

x:10;y:50
z:100
x:10;y:50
z:100
另外还有两个自定义序列化方法writeReplace和readResolve,分别用来在序列化之前替换序列化对象

在反序列化之后的对返回对象的处理。一般可以用来避免singleTon对象跨jvm序列化和反序列化时产生多个对象实例,事实上singleTon的对象一旦可序列化,它就不能保证singleTon了。JVM的Enum实现里就是重写了readResolve方法,由JVM保证Enum的值都是singleTon的,所以建议多使用Enum代替使用writeReplace和readResolve方法。

private Object readResolve()     
    {     
        return INSTANCE;     
    }     

    private Object writeReplace(){     
        return INSTANCE;     
    }    

注:writeReplace调用在writeObject前执行;readResolve调用在readObject之后执行。
(以上序列化反序列化机制部分摘自:)

上面的INSTANCE是单例类的实例。通过上面的代码可以是单例类在序列化和反序列化后得到同一个对象!!
还有需要注意的是,上面的两个方法签名就是那样的方法签名,记住就可以了。如果非要问为什么?那应该从源码的角度看看对象的序列化和反序列化的过程。

  • 使用java.io.Externalizable进行序列化和反序列化

序列化和反序列化还有一种方法就是实现上面的接口,实现上面的接口需要实现两个方法:

@Override
public void writeExternal(ObjectOutput out) throws IOException {
    // TODO Auto-generated method stub
}
@Override
public void readExternal(ObjectInput in) throws IOException,
        ClassNotFoundException {
    // TODO Auto-generated method stub
}

上面的两个方式是实现Externalizable接口必须实现的方法。通过这两个方法的名字我们也该知道,它所实现的功能和

private void writeObject(ObjectOutputStream os) throws IOException {     
     //......   
  }     

  private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {     
      //......   
  }   

和这两个方法实现功能一样,都是自定义序列化和反序列化。
不同的是:
1、writeObject、readObject两个方法的实现并不是强制的,实现一个或者两个方法都实现都是可以的。而方法writeExternal、readExternal是实现接口是必须实现的方法。
2、writeObject、readObject两个方法的实现是实现序列化和反序列化时程序自己调用的。也就是说在程序如下:

out = new ObjectOutputStream(new          
            FileOutputStream("test2.txt"));
            System.out.println("写入:"+sigleCls);
            out.writeObject(sigleCls);

上面的程序使用ObjectOutputStream的write方法序列化对象sigleCls的时候,会自动调用上面的writeObject、readObject方法,如果sigleCls类实现了这两个方法的话。不用显式调用。

而writeExternal、readExternal也不用显式调用,这一点同上面的一样的。不同的是,实现这两个方法进行序列化的时候,必须在实现类中有公共的无参数的构造器!!!否则抛出异常!!
3、如果实现了Externalizable接口,同时实现private Object readResolve(){}
、private Object writeReplace(){ } 方法,也是生效的。
注:writeReplace调用在writeExternal前执行;readResolve调用在readExternal之后执行。
4、在此writeExternal 和readExternal 的作用与writeObject和readObject
一样,当我们同时实现了两个interface的时候,JVM只运行Externalizable
接口里面的writeExternal 和readExternal 方法对序列化内容进行处理。

最后给出一个实例代码:
单例类:

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SigleCls implements Serializable,Externalizable
{

    private static final long serialVersionUID = 1L;
    private static SigleCls sigleCls;
    public SigleCls(){}
    public static SigleCls getInstance(){
        if (sigleCls == null) {
            sigleCls = new SigleCls();
        }
        return sigleCls;
    }
    private String name;
    private transient String psw;

    public void setName(String name) {
        this.name = name;
    }

    public void setPsw(String psw) {
        this.psw = psw;
    }

    private Object readResolve()
    {
        System.out.println("SigleCls.readResolve()");
           return sigleCls;
    }

   private Object writeReplace(){
        System.out.println("SigleCls.writeReplace()");
         return sigleCls;
   }


@Override
public String toString() {
    return "name="+name+",pwd="+psw;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
    System.out.println("SigleCls.writeExternal()");
    out.writeObject(sigleCls);

}

private void writeObject(ObjectOutputStream out) throws IOException{
    System.out.println("LoggingInfo.writeObject()");
    out.defaultWriteObject();
    out.writeInt(4);
}
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException{
    System.out.println("LoggingInfo.readObject()");
    in.defaultReadObject();
    System.out.println("整数="+in.readInt());
}

@Override
public void readExternal(ObjectInput in) throws IOException,
        ClassNotFoundException {
    // TODO Auto-generated method stub
    System.out.println("SigleCls.readExternal()");
    in.readObject();
}
}
/**
 * 注意实现Externalizable接口的类,在发序列化时,将会执行构造函数,
 * 因为对于流操作而言,此对象是有明确类型的(Serializable接口是个标记接口).
 * 而且,如果实现了writeExternal和readExternal,
 * 将不会在执行readObject和writeObject,
 * 因为此时这两个方法已经被"擦除".
 */

测试类:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Test2 {

    public static void main(String[] args) {
        ObjectOutputStream out = null;
        ObjectInputStream in = null;
        SigleCls sigleCls = SigleCls.getInstance();
        sigleCls.setName("longyin");
        sigleCls.setPsw("23456");
        try {
            out = new ObjectOutputStream(new FileOutputStream("test2.txt"));
            System.out.println("写入:"+sigleCls);
            out.writeObject(sigleCls);
            out.flush();
            in = new ObjectInputStream(new FileInputStream("test2.txt"));
            SigleCls sig = (SigleCls) in.readObject();
            System.out.println("读取:"+sig);
            System.out.println("相等与否:"+(sig==sigleCls));
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

结果:
图片 2
大家可以通过结果,验证上面的理论部分是否正常。应该说结果证明了上面的理论部分!!!

这篇文章是一个博士所写!非常好!值得好好看看!!

以上博文参考内容比较多,如果有什么不对的地方,请批评指正!谢谢~~【握手~】

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图