浅谈java中的ArrayList 和 LinkedList 和 Vector 的区别(部分源码分析)

参考博客:

blog.csdn.net/kangxidageg…

blog.csdn.net/qq_25806863…

在java中,Collection是必须了解的。

Collection主要为两大分类,一是链表List,一是集合Set(当然还有其他分支)。现在我们把注意力放到链表List上面来,链表List是我们再日常的开发中经常会用到的数据结构。

List主要分为三大类,ArrayList,LinkedList,Vector。那么,接下来我们来了解这三个数据结构。

ArrayList的源码分析:

ArrayList是我们最常用的数据结构,但是,我们有没有去真正的了解这个数据结构呢,有没有去了解一下这个数据结构的内部源码呢?

从这部分的源码可以看出,ArrayList是List的实现类,也是可以序列化的,而且它的初始容量是10。
从这部分可以看出,ArrayList是可以在创建的时候指定集合的长度的。

  List<String> list1=new ArrayList<>();
  List<String> list2=new ArrayList<>(20);
复制代码

我们可以看到,list2就是我指定长度为20创建的。让我们继续来看源码。

通过addAll()方法,我们来追踪到了当容量不够的时候,ArrayList的扩容方法,也就是如图的grow()方法。 代码如下:

    private void grow(int minCapacity) {
       int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
          newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
          newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
复制代码

从代码中我们可以看出,参数minCapacity是当前集合需要的容量,参数oldCapacity是之前集合的容量,参数newCapacity是根据旧的集合容量扩充后的容量长度,扩充方法是:

int newCapacity = oldCapacity + (oldCapacity >> 1)
复制代码

是通过二进制的位移方法计算的,当参数oldCapacity为10的时候,也就是“1010”,向右位移一位ie,变成了“101”,换算成10进制,那就是5,也就是说,当oldCapacity为10的时候,经过计算后的newCapacity是变成了15,这也就是我们经常说的,ArrayList的扩充方式是扩充1.5倍。

当然,实际的扩充方法没有说的这么简单,我们发现当发现我们扩充后的容量还是小于需要的容量的时候,我们直接将需要的容量设置为当前的集合的容量大小,这个时候,是没有遵循1.5倍的扩充逻辑的。

而且,ArrayList集合的容量大小,其实是不能大于“Integer.MAX_VALUE - 8”的(也就是int的最大值减去8),当大于这个值了,那么就会设置当前集合的容量为Integer.MAX_VALUE。而到这里,也是没有遵循ArrayList的1.5倍扩充机制的。

Vector的源码分析:

Vector其实是和ArrayList的模式有极多相似之处,通过源码我们可以来分析一下:

在这里,我们看到了,虽然Vector和ArrayList有很多相似的地方,但是Vector在这里有了一个参数是ArrayList没有的,就是参数capacityIncrement。

参数capacityIncrement是Vector的扩容增加量,是可以人为添加修改的,初始化是在Vector的构造函数里面,如下图:

而这个参数,是ArrayList里面没有的。

当然,ArrayList中同样也有Vector中没有的参数,Vector是没有默认容量长度这个参数的,因为当你写代码的时候,如下:

List<String> list=new Vector<>();
复制代码

这个时候,虽然我们没有设置Vector的默认长度,但是我们深入到源码中去看,发现实际调用了Vector的另外一个构造函数,给Vector设置默认的容量长度10。源码如下:

接下来,让我们来分析一下Vector的扩容机制吧。

在上面,我们有说到,Vector是可以自己设置扩容增加量的,那么,我们跟踪一下代码,来看看Vector的扩容机制吧。

在这段源码中,我们可以看到,这个方法是由synchronized修饰的,也就是说Vector是线程安全的。不过我们暂时不讨论这点,继续往下看。

好的,我们将扩容的方法grow()的代码复制过来,如下:

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
             newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
复制代码

参数capacityIncrement就是我们设置的扩容增加量,当我们给capacityIncrement赋值了,那么扩容的时候就根据我们的设置扩容,否则就直接翻倍。

int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
复制代码

后面的代码我们不做详细分析了,因为是和ArrayList里的扩容机制是一样的。

LinkedList的源码分析:

LinkedList和ArrayList和Vector是完全不同的,在之前的源码分析中我们可以看到,ArrayList和Vector的底层都是数组,而LinkedList的底层是截然不同的,它底层是链表,那么我们通过源码来了解一下LinkedList吧。

这段源码中出现的三个参数,都是由transient修饰的,也就是代表着不能被序列化。

看到这里,我们发现LinkedList的底层是一个双向链表,因为它一个节点包含了两个指针,一个前指针,指向前一个元素,一个后指针,指向后一个元素。

还有一部分关于构造函数的和其他添加删除的方法函数,我就不展现源码给大家了,一个带参数的构造,一个不带参数的构造,大家可以自己去看看源码。

ArrayList 和 LinkedList 和 Vector 的区别:

ArrayList:

ArrayList是底层是由可变长度数组(当元素个数超过数组的长度时,会产生一个新的数组,将原数组的数据复制到新数组,再将新的元素添加到新数组中。)组成的,初始容量为10,扩充一般扩充1.5倍。它是线程不安全的,查询速度比较快,删减增加速度比较慢。

Vector:

Vector的底层也是由可变长度数组(当元素个数超过数组的长度时,会产生一个新的数组,将原数组的数据复制到新数组,再将新的元素添加到新数组中。)组成的,初始容量也是10,扩充一般默认情况下扩充为2倍。它是线程安全的,它大部分的方法都包含关键字synchronized,所以不管是查询速度还是删减增加速度都是比较慢。

ArrayList和Vector中,从指定的位置检索一个对象,或在集合的末尾插入、删除一个元素的时间是一样的,时间复杂度都是O(1)。但是如果在其他位置增加或者删除元素花费的时间是O(n)。

LinkedList:

LinkedList的底层是双向链表,是线程不安全的。

LinkedList中,在插入、删除任何位置的元素所花费的时间都是一样的,时间复杂度都为O(1),但是他在检索一个元素的时间复杂度为O(n)

扩展:

O(1)和O(n):

在描述算法复杂度时,经常用到o(1), o(n), o(logn), o(nlogn)来表示对应算法的时间复杂度, 这里进行归纳一下它们代表的含义: 这是算法的时空复杂度的表示。不仅仅用于表示时间复杂度,也用于表示空间复杂度。

O(n):O后面的括号中有一个函数,指明某个算法的耗时/耗空间与数据增长量之间的关系。其中的n代表输入数据的量。 代表数据量增大几倍,耗时也增大几倍。比如常见的遍历算法。

O(1):就是最低的时空复杂度了,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。 哈希算法就是典型的O(1)时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(不考虑冲突的话)

数组和链表:

一. 数组

数组静态分配内存。

缺点:在内存中,数组是一块连续的区域。数组需要预留空间,每次申请数组之前必须规定数组的大小,如果大小不合理,则可能会浪费内存。而且它对内存空间要求高,必须有足够的连续内存空间。而且数组大小固定,不能动态拓展。

优点:数组利用下标定位,时间复杂度为O(1),数组插入或删除元素的时间复杂度O(n)。

二. 链表

链表动态分配内存。

优点:链表在内存中是不连续的,插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素)。 内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间)。 大小没有固定,拓展很灵活。

缺点:不能随机查找,必须从第一个开始遍历,查找效率低。

单链表和双链表的区别:

单链表只有一个指向下一结点的指针,也就是只能next。 双链表除了有一个指向下一结点的指针外,还有一个指向前一结点的指针,可以通过prev()快速找到前一结点,顾名思义,单链表只能单向读取。

双链表的优点:

1、删除单链表中的某个结点时,一定要得到待删除结点的前驱,得到该前驱有两种方法,第一种方法是在定位待删除结点的同时一路保存当前结点的前驱。第二种方法是在定位到待删除结点之后,重新从单链表表头开始来定位前驱。尽管通常会采用方法一。但其实这两种方法的效率是一样的,指针的总的移动操作都会有2*i次。而如果用双向链表,则不需要定位前驱结点。因此指针总的移动操作为i次。

2、查找时也一样,我们可以借用二分法的思路,从head(首节点)向后查找操作和last(尾节点)向前查找操作同步进行,这样双链表的效率可以提高一倍。

为什么目前市场应用上单链表的应用要比双链表的应用要广泛的多呢?

从存储结构来看,每个双链表的节点要比单链表的节点多一个指针,而长度为n就需要 n*length(这个指针的length在32位系统中是4字节,在64位系统中是8个字节) 的空间,这在一些追求时间效率不高应用下并不适应,因为它占用空间大于单链表所占用的空间;这时设计者就会采用以时间换空间的做法,使用单链表,这时一种工程总体上的衡量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值