今天介绍一下Java的两个集合类,ArrayList和LinkedList,这两个集合的知识点几乎能够说面试必问的。java
对于这两个集合类,相信你们都不陌生,ArrayList能够说是平常开发中用的最多的工具类了,也是面试中几乎必问的,LinkedList可能用的少点,但大多数的面试也会有所涉及,尤为是关于这二者的比较能够说是屡见不鲜,因此,不管从使用上仍是在面试的准备上,对于这两个类的知识点咱们都要有足够的了解。node
ArrayList是List接口的一个实现类,底层是基于数组实现的存储结构,能够用于装载数据,数据都是存放到一个数组变量中,web
transient Object[] elementData;
transient是一个关键字,它的做用能够总结为一句话:将不须要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。 你可能会以为奇怪,ArrayList能够被序列化的啊,源码但是实现了java.io.Serializable接口啊,为何数组变量还要用transient
定义呢?面试
别急,关于这个问题,咱们后面会讨论到,不卖个关子,大家怎么会看到最后,而后给我点在看呢?数组
当咱们新建一个实例时,ArrayList会默认帮咱们初始化数组的大小为10编辑器
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
但请注意,这个只是数组的容量大小,并非List真正的大小,List的大小应该由存储数据的数量决定,在源码中,获取真实的容量实际上是用一个变量size
来表示,工具
private int size;
在源码中,数据默认是从数组的第一个索引开始存储的,当咱们添加数据时,ArrayList会把数据填充到上一个索引的后面去,因此,ArrayList的数据都是有序排列的。并且,因为ArrayList自己是基于数组存储,因此查询的时候只须要根据索引下标就能够找到对于的元素,查询性能很是的高,这也是咱们很是青睐ArrayList的最重要的缘由。性能
可是,数组的容量是肯定的啊,若是要存储的数据大小超过了数组大小,那不就有数组越界的问题?flex
关于这点,咱们不用担忧,ArrayList帮咱们作了动态扩容的处理,若是发现新增数据后,List的大小已经超过数组的容量的话,就会新增一个为原来1.5倍容量的新数组,而后把原数组的数据原封不动的复制到新数组中,再把新数组赋值给原来的数组对象就完成了。优化
扩容以后,数组的容量足够了,就能够正常新增数据了。
除此以外,ArrayList提供支持指定index新增的方法,就是能够把数据插入到设定的索引下标,好比说我想把元素4插入到3后面的位置,也就是如今5所在的地方,
插入数据的时候,ArrayList的操做是先把3后面的数组所有复制一遍,而后将这部分数据日后移动一位,其实就是逐个赋值给后移一位的索引位置,而后3后面就能够空出一个位置,把4放入就完成了插入数据的操做了
删除的时候也是同样,指定index,而后把后面的数据拷贝一份,而且向前移动,这样原来index位置的数据就删除了。
到这里咱们也不难发现,这种基于数组的查询虽然高效,但增删数据的时候却很耗性能,由于每增删一个元素就要移动对应index后面的全部元素,数据量少点还无所谓,但若是存储上千上万的数据就很吃力了,因此,若是是频繁增删的状况,不建议用ArrayList。
既然ArrayList不建议用的话,这种状况下有没有其余的集合可用呢?
固然有啊,像我这样的暖男确定是第一时间告诉大家的,这就引出了咱们下面要说的LinkedList。
LinkedList 是基于双向链表实现的,不须要指定初始容量,链表中任何一个存储单元均可以经过向前或者向后的指针获取到前面或者后面的存储单元。在 LinkedList 的源码中,其存储单元用一个Node
类表示:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
Node中包含了三个成员,分别是存储数据的item
,指向前一个存储单元的点 prev
和指向后一个存储单元的节点 next
,经过这两个节点就能够关联先后的节点,组装成为链表的结构,
由于有保存先后节点的地址,LinkedList增删数据的时候不须要像ArrayList那样移动整片的数据,只须要经过引用指定index位置先后的两个节点便可,好比咱们要在李白和韩信之间插入孙悟空的节点,只须要像这样处理下节点之间的指向地址:
删除数据也是一样原理,只须要改变index位置先后两个节点的指向地址便可。
这样的链表结构使得LinkedList能很是高效的增删数据,在频繁增删的情景下能很好的使用,但不足之处也是有的。
虽然增删数据很快,但查询就不怎么样了,LinkedList是基于双向链表存储的,当查询对应index位置的数据时,会先计算链表总长度一半的值,判读index是在这个值的左边仍是右边,而后决定从头结点仍是从尾结点开始遍历,
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
虽然已经二分法来作优化,但依然会有遍历一半链表长度的状况,若是是数据量很是多的话,这样的查询无疑是很是慢的。
这也是LinkedList最无奈的地方,鱼和熊掌不可兼得,咱们既想查的快,又想增删快,这样的好事怎么可能都让咱们遇到呢?因此,通常建议LinkedList使用于增删多,查询少的情景。
除此以外,LinkedList对内存的占用也是比较大的,毕竟每一个Node都维护着先后指向地址的节点,数据量大的话会占用很多内存空间。
讲到这,你是否是对标题的那个问题胸有成竹了?
下次有面试官问你,ArrayList和LinkedList哪一个更占空间时,你就能够信誓旦旦的说,LinkedList更占空间,我看了薛大佬的文章,确定不会错。说完你就能够安心坐着,等待面试官露出满意的笑容,告诉你经过面试的消息,成功拿下offer指日可待。
若是你真的这么答的话,我也相信面试官必定会被你的回答所征服,他听完必定会点点头,嘴角开始上扬,而后笑容满面的告诉你,
感谢你今天过来面试,你能够回去等通知了。。。。
哈哈,开个玩笑,不凑多点字可不是个人风格。
言归正传,表面上看,LinkedList的Node存储结构彷佛更占空间,但别忘了前面介绍ArrayList扩容的时候,它会默认把数组的容量扩大到原来的1.5倍的,若是你只添加一个元素的话,那么会有将近原来一半大小的数组空间被浪费了,若是原先数组很大的话,那么这部分空间的浪费也是很多的,
因此,若是数据量很大又在实时添加数据的状况下,ArrayList占用的空间不必定会比LinkedList空间小,这样的回答就显得谨慎些了,听上去也更加让人容易认同,但你觉得这样回答就完美了吗?非也
还记得我前面说的那个transient变量吗?它的做用已经说了,不想序列化的对象就能够用它来修饰,用transient修饰elementData意味着我不但愿elementData数组被序列化。为何要这么作呢?
这是由于序列化ArrayList的时候,ArrayList里面的elementData,也就是数组未必是满的,比方说elementData有10的大小,可是我只用了其中的3个,那么是否有必要序列化整个elementData呢? 显然没有这个必要,所以ArrayList中重写了writeObject方法:
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();
}
}
每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData这个数组对象不去序列化它,而是遍历elementData,只序列化数组里面有数据的元素,这样一来,就能够加快序列化的速度,还可以减小空间的开销。
加上这个知识点后,咱们对上面那个问题就能够有更加全面的回答了,若是你下次也遇到这个问题的话,你能够参考一下个人说法:
通常状况下,LinkedList的占用空间更大,由于每一个节点要维护指向先后地址的两个节点,但也不是绝对,若是恰好数据量超过ArrayList默认的临时值时,ArrayList占用的空间也是不小的,由于扩容的缘由会浪费将近原来数组一半的容量,不过,由于ArrayList的数组变量是用transient关键字修饰的,若是集合自己须要作序列化操做的话,ArrayList这部分多余的空间不会被序列化。
怎么样,这样的回答是否是更加的说服力,不只更加全面,还可能会给面试官留下好印象,让他以为你是个有本身思考的求职者,说不定当场就让你面试经过了呢。就冲这点,大家是否是应该给我来个点个赞呢,哈哈。
做者:鄙人薛某,一个不拘于技术的互联网人,欢迎关注个人公众号,这里不只有技术干货,还有吹水~~~