java ArrayList的序列化分析

一、绪论

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

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

通过对 ArrayList 源码的分析,可以知道 ArrayList 的数据存储依赖于 elementData 数组,它的声明为:
transient Object[] elementData;
注意 transient 修饰着 elementData 这个数组。

先看看 transient 关键字的作用

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

然而在实际开发过程中,我们常常会遇到这样的问题,类的有些属性需要序列化,有些属性不需要序列化,打个比方,例如用户的敏感信息(如密码,银行卡号等),不希望在网络(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上 transient 关键字。

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

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

二、序列化流程

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

在序列化和反序列化过程中需要特殊处理的类,必须严格遵从下面的写法(私有,void返回值类型,函数名,参数):

private void writeObject(java.io.ObjectOutputStream out)

private void readObject(java.io.ObjectInputStream in)

对象序列化步骤

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

三、ArrayList解决序列化

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 修饰,不能被序列化,但是我们可以将它的值取出来,然后将该值写入输出流。

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

3、调用

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

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

import java.io.*;

public class TestSerialization implements Serializable{
	private static final long serialVersionUID = 5732067711721143635L;
	private transient int num;

	public int getNum(){
		return num;
	}

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

	private void writeObject(ObjectOutputStream s){
		try {
			s.defaultWriteObject();
			s.writeObject(num);
			System.out.println("writeObject of "+this.getClass().getName());
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private void readObject(ObjectInputStream s){
		try {
			s.defaultReadObject();
			num = (Integer) s.readObject();
			System.out.println("readObject of "+this.getClass().getName());
		} catch (ClassNotFoundException | IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

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

输出:
序列化之前的值: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) 和 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 数组的容量可能是 8x1.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,说明序列化时,只需序列化实际存储的元素,而不是整个数组。
参考:
java.io.Serializable浅析
java serializable深入了解
ArrayList源码分析——如何实现Serializable
java序列化和反序列话总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ArrayListJava编程语言中的一种动态数组,它可以根据需要自动调整大小,并且支持任意类型的对象。如果您想将ArrayList序列化,您可以按照以下步骤进行操作: 1. 导入Java IO库中的ObjectOutputStream和FileOutputStream类,以便可以将ArrayList写入文件中。 ```java import java.io.FileOutputStream; import java.io.ObjectOutputStream; ``` 2. 创建一个ArrayList对象并将元素添加到其中。例如,以下代码创建了一个包含整数的ArrayList对象: ```java import java.util.ArrayList; ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(2); list.add(3); ``` 3. 使用FileOutputStream和ObjectOutputStream将ArrayList写入文件。例如,以下代码将ArrayList对象写入名为"list.ser"的文件中: ```java FileOutputStream fos = new FileOutputStream("list.ser"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(list); oos.close(); fos.close(); ``` 4. 要读取序列化ArrayList,请使用ObjectInputStream和FileInputStream。例如,以下代码将从名为"list.ser"的文件中读取ArrayList对象: ```java import java.io.FileInputStream; import java.io.ObjectInputStream; FileInputStream fis = new FileInputStream("list.ser"); ObjectInputStream ois = new ObjectInputStream(fis); ArrayList<Integer> list = (ArrayList<Integer>) ois.readObject(); ois.close(); fis.close(); ``` 在反序列化ArrayList时,您需要将Object类型的读取结果强制转换为ArrayList类型。如果文件中包含的对象不是ArrayList,则会抛出ClassCastException异常。 ### 回答2: ArrayList 是一个动态数组,它继承了AbstractList类,并实现了List接口。Arraylist 可以存储任意类型的对象,而不仅限于原始数据类型。它提供了方便的方法来操作对象的集合,如添加、删除、修改和查找等。 当需要ArrayList 对象序列化时,我们可以使用Java提供的 ObjectOutputStream类和 ObjectInputStream类。 ObjectOutputStream类能够将对象序列化为字节流,而 ObjectInputStream类则能够将字节流反序列化为对象。要对ArrayList进行序列化和反序列化,我们需要确保其中的对象都是可序列化的。 在将ArrayList对象序列化时,需要注意以下几点: 1. ArrayList 类本身是可序列化的,因为它继承自AbstractList类,AbstractList类实现了Serializable接口; 2. 如果ArrayList中存储的对象是自定义类的实例,确保自定义类实现了Serializable接口; 3. 当ArrayList中存储的对象还包含其他引用类型字段时,需要确保这些字段所属的类也实现了Serializable接口; 4. 序列化和反序列化的操作可以使用try-catch语句捕获IOException异常; 5. 序列化和反序列化可以使用文件流(FileInputStream和FileOutputStream)将对象保存在文件中,也可以使用网络流(Socket)进行传输。 对于ArrayList序列化,可以通过以下步骤实现: 1. 创建一个ArrayList对象,并添加需要存储的元素; 2. 创建一个ObjectOutputStream对象,并将ArrayList对象写入文件或流中,使用writeObject()方法; 3. 关闭ObjectOutputStream对象; 4. 在需要序列化时,创建一个ObjectInputStream对象,读取文件或流中的数据; 5. 使用readObject()方法将读取的数据转换为ArrayList对象; 6. 关闭ObjectInputStream对象。 总的来说,ArrayList序列化是将ArrayList对象转换为字节流进行存储或传输,而反序列化则是将字节流转换为ArrayList对象,以便在需要时使用。序列化和反序列化的过程需要确保存储和读取的对象都是可序列化的。 ### 回答3: ArrayList序列化是将ArrayList对象转化为字节流的过程,以便能够将其保存到文件或通过网络传输。序列化是一种将对象状态转换为可存储或传输的格式的机制。在Java中,使用序列化可以轻松地将ArrayList对象保存到磁盘文件或数据库中。 ArrayList实现了Serializable接口,这意味着它的对象可以被序列化。要对ArrayList进行序列化需要执行以下几个步骤: 1. 创建一个FileOutputStream对象,并通过它创建一个ObjectOutputStream对象。这些对象是用于将ArrayList对象写入到文件中。 2. 调用ObjectOutputStream对象的writeObject()方法,将ArrayList对象写入文件。这个方法会将对象转化为字节流,并将其写入文件中。 3. 关闭ObjectOutputStream和FileOutputStream对象,释放资源。 要从文件中反序列化ArrayList对象,需要执行以下几个步骤: 1. 创建一个FileInputStream对象,并通过它创建一个ObjectInputStream对象。这些对象是用于从文件中读取ArrayList对象。 2. 调用ObjectInputStream对象的readObject()方法,从文件中读取ArrayList对象。这个方法会将字节流转化为ArrayList对象。 3. 关闭ObjectInputStream和FileInputStream对象,释放资源。 值得注意的是,在进行ArrayList序列化时,需要确保ArrayList中的元素也实现了Serializable接口,以确保其能够被序列化。如果ArrayList中的元素未实现Serializable接口,将会抛出NotSerializableException异常。 总结来说,ArrayList序列化是将ArrayList对象转化为字节流的过程,以便能够将其保存到文件或通过网络传输。这在Java中是通过实现Serializable接口来实现的。序列化和反序列化的过程可以通过ObjectOutputStream和ObjectInputStream类来完成。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值