前言:
抽时间看了一下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:如果觉得有帮助,可以请作者喝一瓶肥宅快乐水,有意见也可以在评论区里交流!