ArrayList的size、内部数组的length和序列化及反序列化

前言:
抽时间看了一下ArrayList的代码,主要关注了它的序列化和反序列化这一块,因为这里有一个很有意思的点——用于保存数据的内部数组是使用transient修饰的。了解序列化知识的人都知道,如果一个变量被transient修饰的话,那么在序列化的时候它就会被忽略(当然了这里是针对Serialization这个接口),通常我们使用它来修饰一些我们不希望被序列化的数据,以达到保护的目的。但是,这里就会产生一个疑问,为什么ArrayList用于存储数据的内部数组elementData要使用transient修饰的呢?

 /**
  * The array buffer into which the elements of the ArrayList are stored.
  * The capacity of the ArrayList is the length of this array buffer. Any
  * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  * will be expanded to DEFAULT_CAPACITY when the first element is added.
  */
 transient Object[] elementData; // non-private to simplify nested class access


ArrayList的size和内部数组的length

带着上面那个疑问,让我们先来了解一下这两个变量的关系吧。
size变量是记录ArrayList中的元素的个数的,而内部数组length呢?这里我们首先需要知道ArrayList的实现是基于数组的,但是数组的长度是可以动态扩增的。因此,初始化创建一个ArrayList的,它会分配一个长度很小的数组,随着数据的增大,数组的长度也会进行动态扩增的。

举一个例如,如下是一个模拟的数据,假设这是一个ArrayList对象的内部数组,现在数组的length为7,ArrayList对象的size为5。
在这里插入图片描述

所以,现在可以知道size<length,至于每次扩容是扩大多少呢?肯定不会是一个一个的扩容的,因为Java中数组的扩容其实很麻烦的,所以需要减少扩容的次数,但是又不能一次分配一个很大的容量,因为内存是很宝贵的资源的。所以,这就是在性能和内存之间限制了扩容的大小。

扩容的源码如下:

	/**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

它有很多种情况,因为作为一个工业级的List是需要考虑很多种情况的,但是我们通常也不会遇到那几张特殊的情况。这里只需要关注下面这一种即可。

int newCapacity = oldCapacity + (oldCapacity >> 1);

这个代码是一个位运算,而 >> 1 表示除2。例如 100002 >> 1 = 010002。然后再加上原来的容量,每次扩容就是上一次的1.5倍(这里要考虑到不是整除哦!)。

那么就有一个问题了,如果我现在有n个元素,刚经过扩容,那么现在size还是n,但是数组的length变为了1.5n。如果我现在对此list进行序列化,那么对数组直接序列化了会造成很大的存储空间浪费。极端情况下,如果扩容之后进行又清空了所有的元素,但是数组的大小可不会自己调整了(确实有方法可以手动调整数组的大小,但是不是自动的,所以很多人可能就会忽略了)。所以,如果直接序列化ArrayList的话,那么会造成很大的空间浪费,因此transient的作用就在于此!



缩减内部数组的大小

我们再来看一段代码,它的作用是反过来的,即减小内部数组的大小。我也没有使用过这个方法,但是考虑到内存其实也是很宝贵的资源,如果数组中的元素数量很少,而数据经过扩容以后,分配的空间很多,这就会造成很大的浪费,甚至会严重影响程序的性能。

	/**
     * Trims the capacity of this <tt>ArrayList</tt> instance to be the
     * list's current size.  An application can use this operation to minimize
     * the storage of an <tt>ArrayList</tt> instance.
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

代码验证

Talk is cheap, show me your code!
上面都是一些理论上的描述,虽然已经可以解释清楚问题了。但是,我还是想尝试用代码来进行实际的验证,证明size和length之间的差距,所以我设计了一个小的实验。

实验思路

首先创建一个空的ArrayList对象,然后存入一个元素(这时的数组大小为10,这是由内部代码决定的),此时size=1,length=10,然后我执行上面那个trimToSize方法,此时我期望的是size=1,length=1。
这里可能会有一个疑问,我怎么能获得内部数组的length呢?这确实是一个问题,但并非不可能的!

实验代码

使用了反射代码,不然我是无法获取到那些些变量的,这里可以看出反射代码的能力强大之处,但是它是一把双刃剑,所以还是要谨慎使用。这个代码看起来可能不是那么直观,你可以先去了解一下反射,或者你可以直接看运行的截图。

package dragon;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class ArrayListTest {
	public static void main(String[] args) throws Exception{
		Class<?> clazz = ArrayList.class;       // 获取ArrayList的Class对象
		// clazz.newInstance();  这个在 JDK9 中被抛弃了!
		Object obj = clazz.getDeclaredConstructor(new Class[] {})
				.newInstance(new Object[] {});
		Method mtd = clazz.getMethod("add", Object.class);  // 调用ArrayList对象的add方法,添加一个元素
		mtd.invoke(obj, "小黑");
		
		mtd = clazz.getMethod("size", new Class<?>[] {});   // 获取ArrayList对象的size所代表的Method对象
		int size = (int) mtd.invoke(obj, new Object[] {});  // 调用ArrayList对象的size方法,获取它的size
		System.out.println("ArrayList对象的size: " + size);	        // 打印输出ArrayList的size
		
		Field f = clazz.getDeclaredField("elementData");  // 获取内部的数组elementData,这是一个Field对象
		f.setAccessible(true);  // 设置可访问权限,不然会报错误:can not access a member of class java.util.ArrayList with modifiers "transient"
		Object[] data = (Object[]) f.get(obj);   // 获取该Field对象的值,即该elementData数组 
		System.out.println("缩减前的:" + data);  
		String name1 = (String) data[0];         
		System.out.println("缩减前内部数组的长度:" + data.length);
		
		mtd = clazz.getMethod("trimToSize", new Class<?>[] {});  // 获取ArrayList对象的trimToSize方法所代表的Method对象
		mtd.invoke(obj, new Object[] {});            // 调用ArrayList对象的trimToSize,缩减内部数组的长度。
        data = (Object[]) f.get(obj);                // 再次获取该数组对象,之所以再次,是有原因的!
        System.out.println("缩减后的:" + data);
        String name2 = (String) data[0];
		System.out.println("缩减后内部数组的长度:" + data.length);
		System.out.println("name1 == name2: " + (name1 == name2));  // 此处说明它是浅克隆,之所以前后引用地址改变是因为克隆的机制。
		System.out.println("name1: " + name1);
		System.out.println("name2: " + name2);
	}
}

实验结果

在这里插入图片描述
这个结果很完美,它验证了直接序列化数组可能造成的空间浪费和使用trimToSize对空间的节约。这里前者指的是外存或者带宽(通过网络传输序列化对象),后者指的是内存。但是这里还有一个另外的问题没有解决:

data = (Object[]) f.get(obj); // 再次获取该数组对象,之所以再次,是有原因的!

为什么要再次获取呢?难道第一次获取的数组不是原来的数组了吗?我还专门写了一个代码来验证,证明了每次获取的引用类型是同一个,但是这里就矛盾了呀。后来才发现,原来是trimToSize这个方法会改变数组的引用地址。它是一个native方法,有点类似于浅克隆。即克隆前后的对象引用改变了,但是引用的引用没有改变,上面也有代码进行验证了。


ArrayList对象不能序列化了吗?

这个问题其实不好回答, 直接来试一试就知道了,还是要实践出真知!

深克隆

ArrayList对象的序列化实现深克隆代码

package dragon;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;

public class ArrayListTest2 {
	public static void main(String[] args) throws Exception {
		List<String> list = new ArrayList<>();
		list.add("小黑");
		list.add("小华");
		
		// 序列化到磁盘上面,还需要使用一个文件来存储,干脆我就直接序列化到内存中了,
		// 这样反序列化也快。反正也是序列化,并不影响我的解释。
		ByteArrayOutputStream output = new ByteArrayOutputStream();
		ObjectOutputStream objOutput = new ObjectOutputStream(output);
		
		objOutput.writeObject(list);
		
		ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
		ObjectInputStream objInput = new ObjectInputStream(input);
		
		@SuppressWarnings("unchecked")
		List<String> newList = (List<String>) objInput.readObject();
		System.out.println("size: " + newList.size());
		newList.forEach(System.out::println);
	}
}

序列化和反序列化结果:
在这里插入图片描述
结论:
Transient修饰了内部的数组,并部影响ArrayList对象的序列化。而且,序列化只是序列化了size个对象,并非length个对象。

实现序列化和反序列化的秘密

由于使用了transient修饰了内部的数组,所以是不能直接序列化的,那么既然可以序列化,必然是有其它原因的!

查看源码可知以下两个方法:writeObject和readObject,这两个方法是用来实现定制化的序列化的,因为有时候单纯的序列化可能不是我们想要的,例如ArrayList的这个类,或者是序列化可能导致某种问题,例如打破了单例模式,也可以通过重写这两个方法来避免这个问题。

	/**
     * Save the state of the <tt>ArrayList</tt> instance to a stream (that
     * is, serialize it).
     *
     * @serialData The length of the array backing the <tt>ArrayList</tt>
     *             instance is emitted (int), followed by all of its elements
     *             (each an <tt>Object</tt>) in the proper order.
     */
    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

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

秘密就在这里:
序列化:

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

反序列化:

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

可以看出来,它就是在序列化的时候,一个一个的写入;然后反序列化的时候,再一个一个的读取,原因很简单,哈哈!

注意:

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

// 反序列化
// Read in capacity
s.readInt(); // ignored

这两段代码其实是没有作用的。因为上面的s.defaultReadObject();s.defaultReadObject();,已经包括了size的写入和读取。之所以,安排这样多余的代码,实际上是为了兼容性考虑——Write out size as capacity for behavioural compatibility with clone()。通过了解到,是以前的某一个版本使用了size,所以后来的版本为了达到兼容的目的,也必须这样去做,具体原因可看下面这个回答。
Why does Java ArrayList write the size field in serialization stream explicitly?

说明

ArrayList还有很多的知识点,这里只是其中的一个部分,不过也花了好久才明白一点。可见这些大牛设计的代码真的的巧妙呀,还要继续学习!


PS:如果觉得有帮助,可以请作者喝一瓶肥宅快乐水,有意见也可以在评论区里交流!
感谢打赏!

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是Java中ArrayList类的部分源代码(摘自OpenJDK 8): ``` public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { //默认初始容量 private static final int DEFAULT_CAPACITY = 10; //空数组实例 private static final Object[] EMPTY_ELEMENTDATA = {}; //缺省空数组实例,用于默认构造函数创建空列表 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //元素数组 transient Object[] elementData; // non-private to simplify nested class access //构造函数 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } //其他方法,如get、set、size等省略 } ``` 上面的代码展示了ArrayList的实现方式。ArrayList使用一个数组来存储元素,当元素数量达到数组容量时,会自动扩容。默认情况下,数组容量为10,但是在构造函数中可以指定初始容量。如果元素数量为0,则使用一个空数组ArrayList还实现了List和RandomAccess接口,以及其他一些方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值