美文网首页
集合List接口详解【Java提高十六】

集合List接口详解【Java提高十六】

作者: Java帮帮 | 来源:发表于2018-05-12 18:53 被阅读0次

    在编写java程序中,我们最常用的除了八种基本数据类型,String对象外还有一个集合类,在我们的的程序中到处充斥着集合类的身影!java中集合大家族的成员实在是太丰富了,有常用的ArrayList、HashMap、HashSet,也有不常用的Stack、Queue,有线程安全的Vector、HashTable,也有线程不安全的LinkedList、TreeMap等等!

           上面的图展示了整个集合大家族的成员以及他们之间的关系。下面就上面的各个接口、基类做一些简单的介绍(主要介绍各个集合的特点。区别),更加详细的介绍会在不久的将来一一讲解。

    一、Collection接口

           Collection接口是最基本的集合接口,它不提供直接的实现,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。Collection所代表的是一种规则,它所包含的元素都必须遵循一条或者多条规则。如有些允许重复而有些则不能重复、有些必须要按照顺序插入而有些则是散列,有些支持排序但是有些则不支持。

           在Java中所有实现了Collection接口的类都必须提供两套标准的构造函数,一个是无参,用于创建一个空的Collection,一个是带有Collection参数的有参构造函数,用于创建一个新的Collection,这个新的Collection与传入进来的Collection具备相同的元素。

    二、List接口

           List接口为Collection直接接口。List所代表的是有序的Collection,即它用某种特定的插入顺序来维护元素顺序。用户可以对列表中每个元素的插入位置进行精确地控制,同时可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。

    2.1、ArrayList

    ArrayList是一个动态数组,也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括null。每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。

           size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间运行,也就是说,添加 n 个元素需要 O(n) 时间(由于要考虑到扩容,所以这不只是添加元素会带来分摊固定时间开销那样简单)。

    ArrayList擅长于随机访问。同时ArrayList是非同步的。

    ArrayList详解:

    一、ArrayList概述

          ArrayList是实现List接口的动态数组,所谓动态就是它的大小是可变的。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。

          每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。默认初始容量为10。随着ArrayList中元素的增加,它的容量也会不断的自动增长。在每次添加新的元素时,ArrayList都会检查是否需要进行扩容操作,扩容操作带来数据向新数组的重新拷贝,所以如果我们知道具体业务数据量,在构造ArrayList时可以给ArrayList指定一个初始容量,这样就会减少扩容时数据的拷贝问题。当然在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。

       注意,ArrayList实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。所以为了保证同步,最好的办法是在创建时完成,以防止意外对列表进行不同步的访问:

    List list = Collections.synchronizedList(new ArrayList(...));

    二、ArrayList源码分析    

          ArrayList我们使用的实在是太多了,非常熟悉,所以在这里将不介绍它的使用方法。ArrayList是实现List接口的,底层采用数组实现,所以它的操作基本上都是基于对数组的操作。

          2.1、底层使用数组

          transient??为java关键字,为变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。

          这里Object[] elementData,就是我们的ArrayList容器,下面介绍的基本操作都是基于该elementData变量来进行操作的。

          2.2、构造函数

         ArrayList提供了三个构造函数:

         ArrayList():默认构造函数,提供初始容量为10的空列表。

         ArrayList(int initialCapacity):构造一个具有指定初始容量的空列表。

         ArrayList(Collection c):构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。

          2.3、新增

          ArrayList提供了add(E e)、add(int index, E element)、addAll(Collection c)、addAll(int index, Collection c)、set(int index, E element)这个五个方法来实现ArrayList增加。

    add(E e):将指定的元素添加到此列表的尾部。

    这里ensureCapacity()方法是对ArrayList集合进行扩容操作,elementData(size++) = e,将列表末尾元素指向e。

    add(int index, E element):将指定的元素插入此列表中的指定位置。

          在这个方法中最根本的方法就是System.arraycopy()方法,该方法的根本目的就是将index位置空出来以供新数据插入,这里需要进行数组数据的右移,这是非常麻烦和耗时的,所以如果指定的数据集合需要进行大量插入(中间插入)操作,推荐使用LinkedList。

    addAll(Collection c):按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。

    这个方法无非就是使用System.arraycopy()方法将C集合(先准换为数组)里面的数据复制到elementData数组中。这里就稍微介绍下System.arraycopy(),因为下面还将大量用到该方法。该方法的原型为:public static voidarraycopy(Object src, int srcPos, Object dest, int destPos, int length)。它的根本目的就是进行数组元素的复制。即从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。将源数组src从srcPos位置开始复制到dest数组中,复制长度为length,数据从dest的destPos位置开始粘贴。

    addAll(int index, Collection c):从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。

    set(int index, E element):用指定的元素替代此列表中指定位置上的元素。

          2.4、删除

          ArrayList提供了remove(int index)、remove(Object o)、removeRange(int fromIndex, int toIndex)、removeAll()四个方法进行元素的删除。

    remove(int index):移除此列表中指定位置上的元素。

    remove(Object o):移除此列表中首次出现的指定元素(如果存在)。

          其中fastRemove()方法用于移除指定位置的元素。如下

          removeRange(int fromIndex, int toIndex):移除列表中索引在fromIndex(包括)和toIndex(不包括)之间的所有元素。

    removeAll():是继承自AbstractCollection的方法,ArrayList本身并没有提供实现。

          2.5、查找

          ArrayList提供了get(int index)用读取ArrayList中的元素。由于ArrayList是动态数组,所以我们完全可以根据下标来获取ArrayList中的元素,而且速度还比较快,故ArrayList长于随机访问。

         2.6、扩容

          在上面的新增方法的源码中我们发现每个方法中都存在这个方法:ensureCapacity(),该方法就是ArrayList的扩容方法。在前面就提过ArrayList每次新增元素时都会需要进行容量检测判断,若新增元素后元素的个数会超过ArrayList的容量,就会进行扩容操作来满足新增元素的需求。所以当我们清楚知道业务数据量或者需要插入大量元素前,我可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。

          在这里有一个疑问,为什么每次扩容处理会是1.5倍,而不是2.5、3、4倍呢?通过google查找,发现1.5倍的扩容是最好的倍数。因为一次性扩容太大(例如2.5倍)可能会浪费更多的内存(1.5倍最多浪费33%,而2.5被最多会浪费60%,3.5倍则会浪费71%……)。但是一次性扩容太小,需要多次对数组重新分配内存,对性能消耗比较严重。所以1.5倍刚刚好,既能满足性能需求,也不会造成很大的内存消耗。

          处理这个ensureCapacity()这个扩容数组外,ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize()方法来实现。该方法可以最小化ArrayList实例的存储量。

    2.2、LinkedList

           同样实现List接口的LinkedList与ArrayList不同,ArrayList是一个动态数组,而LinkedList是一个双向链表。所以它除了有ArrayList的基本操作方法外还额外提供了get,remove,insert方法在LinkedList的首部或尾部。

           由于实现的方式不同,LinkedList不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。这样做的好处就是可以通过较低的代价在List中进行插入和删除操作。

    与ArrayList一样,LinkedList也是非同步的。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:

    List list = Collections.synchronizedList(new LinkedList(...));

    LinkedList详解:

    一、概述

           LinkedList与ArrayList一样实现List接口,只是ArrayList是List接口的大小可变数组的实现,LinkedList是List接口链表的实现。基于链表实现的方式使得LinkedList在插入和删除时更优于ArrayList,而随机访问则比ArrayList逊色些。

           LinkedList实现所有可选的列表操作,并允许所有的元素包括null。

    除了实现 List 接口外,LinkedList 类还为在列表的开头及结尾 get、remove 和 insert 元素提供了统一的命名方法。这些操作允许将链接列表用作堆栈、队列或双端队列。

           此类实现 Deque 接口,为 add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作。

           所有操作都是按照双重链接列表的需要执行的。在列表中编索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。

    同时,与ArrayList一样此实现不是同步的。

    (以上摘自JDK 6.0 API)。

    二、源码分析

    2.1、定义

           首先我们先看LinkedList的定义:

    从这段代码中我们可以清晰地看出LinkedList继承AbstractSequentialList,实现List、Deque、Cloneable、Serializable。其中AbstractSequentialList提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作,从而以减少实现List接口的复杂度。Deque一个线性 collection,支持在两端插入和移除元素,定义了双端队列的操作。

    2.2、属性

           在LinkedList中提供了两个基本属性size、header。

           其中size表示的LinkedList的大小,header表示链表的表头,Entry为节点对象。

           上面为Entry对象的源代码,Entry为LinkedList的内部类,它定义了存储的元素。该元素的前一个元素、后一个元素,这是典型的双向链表定义方式。

    2.3、构造方法

           LinkedList提高了两个构造方法:LinkedLis()和LinkedList(Collection c)。

           LinkedList()构造一个空列表。里面没有任何元素,仅仅只是将header节点的前一个元素、后一个元素都指向自身。

           LinkedList(Collection c): 构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。该构造函数首先会调用LinkedList(),构造一个空列表,然后调用了addAll()方法将Collection中的所有元素添加到列表中。以下是addAll()的源代码:

           在addAll()方法中,涉及到了两个方法,一个是entry(int index),该方法为LinkedList的私有方法,主要是用来查找index位置的节点元素。

           从该方法有两个遍历方向中我们也可以看出LinkedList是双向链表,这也是在构造方法中为什么需要将header的前、后节点均指向自己。

           如果对数据结构有点了解,对上面所涉及的内容应该问题,我们只需要清楚一点:LinkedList是双向链表,其余都迎刃而解。

    由于篇幅有限,下面将就LinkedList中几个常用的方法进行源码分析。

    2.4、增加方法

           add(E e): 将指定元素添加到此列表的结尾。

    该方法调用addBefore方法,然后直接返回true,对于addBefore()而已,它为LinkedList的私有方法。

           在addBefore方法中无非就是做了这件事:构建一个新节点newEntry,然后修改其前后的引用。

           LinkedList还提供了其他的增加方法:

    add(int index, E element):在此列表中指定的位置插入指定的元素。

    addAll(Collection c):添加指定 collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序。

    addAll(int index, Collection c):将指定 collection 中的所有元素从指定位置开始插入此列表。

    AddFirst(E e): 将指定元素插入此列表的开头。

    addLast(E e): 将指定元素添加到此列表的结尾。

    2.5、移除方法

           remove(Object o):从此列表中移除首次出现的指定元素(如果存在)。该方法的源代码如下:

           该方法首先会判断移除的元素是否为null,然后迭代这个链表找到该元素节点,最后调用remove(Entry e),remove(Entry e)为私有方法,是LinkedList中所有移除方法的基础方法,如下:

           其他的移除方法:

    clear(): 从此列表中移除所有元素。

    remove():获取并移除此列表的头(第一个元素)。

    remove(int index):移除此列表中指定位置处的元素。

    remove(Objec o):从此列表中移除首次出现的指定元素(如果存在)。

    removeFirst():移除并返回此列表的第一个元素。

    removeFirstOccurrence(Object o):从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。

    removeLast():移除并返回此列表的最后一个元素。

    removeLastOccurrence(Object o):从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。

    2.5、查找方法

           对于查找方法的源码就没有什么好介绍了,无非就是迭代,比对,然后就是返回当前值。

    get(int index):返回此列表中指定位置处的元素。

    getFirst():返回此列表的第一个元素。

    getLast():返回此列表的最后一个元素。

    indexOf(Object o):返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。

    lastIndexOf(Object o):返回此列表中最后出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。

    2.3、Vector

           与ArrayList相似,但是Vector是同步的。所以说Vector是线程安全的动态数组。它的操作与ArrayList几乎一样。

    Vector详解

    一、Vector简介

            Vector可以实现可增长的对象数组。与数组一样,它包含可以使用整数索引进行访问的组件。不过,Vector的大小是可以增加或者减小的,以便适应创建Vector后进行添加或者删除操作。

            Vector实现List接口,继承AbstractList类,所以我们可以将其看做队列,支持相关的添加、删除、修改、遍历等功能。

            Vector实现RandmoAccess接口,即提供了随机访问功能,提供提供快速访问功能。在Vector我们可以直接访问元素。

            Vector 实现了Cloneable接口,支持clone()方法,可以被克隆。

            Vector提供了四个构造函数:

            在成员变量方面,Vector提供了elementData , elementCount, capacityIncrement三个成员变量。其中

            elementData :"Object[]类型的数组",它保存了Vector中的元素。按照Vector的设计elementData为一个动态数组,可以随着元素的增加而动态的增长,其具体的增加方式后面提到(ensureCapacity方法)。如果在初始化Vector时没有指定容器大小,则使用默认大小为10.

    elementCount:Vector对象中的有效组件数。

            capacityIncrement:向量的大小大于其容量时,容量自动增加的量。如果在创建Vector时,指定了capacityIncrement的大小;则,每次当Vector中动态数组容量增加时>,增加的大小都是capacityIncrement。如果容量的增量小于等于零,则每次需要增大容量时,向量的容量将增大一倍。

            同时Vector是线程安全的!

    二、源码解析

            对于源码的解析,LZ在这里只就增加(add)删除(remove)两个方法进行讲解。

    2.1增加:add(E e)

            add(E e):将指定元素添加到此向量的末尾。  

            这个方法相对而言比较简单,具体过程就是先确认容器的大小,看是否需要进行扩容操作,然后将E元素添加到此向量的末尾。

            对于Vector整个的扩容过程,就是根据capacityIncrement确认扩容大小的,若capacityIncrement <= 0 则扩大一倍,否则扩大至capacityIncrement 。当然这个容量的最大范围为Integer.MAX_VALUE即,2^32 - 1,所以Vector并不是可以无限扩充的。

    2.2、remove(Object o)

            因为Vector底层是使用数组实现的,所以它的操作都是对数组进行操作,只不过其是可以随着元素的增加而动态的改变容量大小,其实现方法是是使用Arrays.copyOf方法将旧数据拷贝到一个新的大容量数组中。Vector的整个内部实现都比较简单,这里就不在重述了。

    三、Vector遍历

            Vector支持4种遍历方式。

    3.1、随机访问

            因为Vector实现了RandmoAccess接口,可以通过下标来进行随机访问。

    3.2、迭代器

    3.3、for循环

    3.4、Enumeration循环

    2.4、Stack

           Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop 方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。

    Stack详解:

    在Java中Stack类表示后进先出(LIFO)的对象堆栈。栈是一种非常常见的数据结构,它采用典型的先进后出的操作方式完成的。每一个栈都包含一个栈顶,每次出栈是将栈顶的数据取出,如下:

            Stack通过五个操作对Vector进行扩展,允许将向量视为堆栈。这个五个操作如下:

            Stack继承Vector,他对Vector进行了简单的扩展:

    view plai

            Stack的实现非常简单,仅有一个构造方法,五个实现方法(从Vector继承而来的方法不算与其中),同时其实现的源码非常简单

            Stack的源码很多都是基于Vector

    List总结

    一、List接口概述

    List接口,成为有序的Collection也就是序列。该接口可以对列表中的每一个元素的插入位置进行精确的控制,同时用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。 下图是List接口的框架图:

            通过上面的框架图,可以对List的结构了然于心,其各个类、接口如下:

            Collection:Collection 层次结构 中的根接口。它表示一组对象,这些对象也称为 collection 的元素。对于Collection而言,它不提供任何直接的实现,所有的实现全部由它的子类负责。

            AbstractCollection:提供 Collection 接口的骨干实现,以最大限度地减少了实现此接口所需的工作。对于我们而言要实现一个不可修改的 collection,只需扩展此类,并提供 iterator 和 size 方法的实现。但要实现可修改的 collection,就必须另外重写此类的 add 方法(否则,会抛出 UnsupportedOperationException),iterator 方法返回的迭代器还必须另外实现其 remove 方法。

            Iterator:迭代器。

            ListIterator:系列表迭代器,允许程序员按任一方向遍历列表、迭代期间修改列表,并获得迭代器在列表中的当前位置。

            List:继承于Collection的接口。它代表着有序的队列。

            AbstractList:List 接口的骨干实现,以最大限度地减少实现“随机访问”数据存储(如数组)支持的该接口所需的工作。

            Queue:队列。提供队列基本的插入、获取、检查操作。

            Deque:一个线性 collection,支持在两端插入和移除元素。大多数 Deque 实现对于它们能够包含的元素数没有固定限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列。

            AbstractSequentialList:提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作。从某种意义上说,此类与在列表的列表迭代器上实现“随机访问”方法。

            LinkedList:List 接口的链接列表实现。它实现所有可选的列表操作。

            ArrayList:List 接口的大小可变数组的实现。它实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。

            Vector:实现可增长的对象数组。与数组一样,它包含可以使用整数索引进行访问的组件。

            Stack:后进先出(LIFO)的对象堆栈。它通过五个操作对类 Vector 进行了扩展 ,允许将向量视为堆栈。

            Enumeration:枚举,实现了该接口的对象,它生成一系列元素,一次生成一个。连续调用 nextElement 方法将返回一系列的连续元素。

    二、使用场景

            学习知识的根本目的就是使用它。每个知识点都有它的使用范围。集合也是如此,在Java中集合的家族非常庞大,每个成员都有最适合的使用场景。在刚刚接触List时,LZ就说过如果涉及到“栈”、“队列”、“链表”等操作,请优先考虑用List。至于是那个List则分如下:

            1、对于需要快速插入、删除元素,则需使用LinkedList。

            2、对于需要快速访问元素,则需使用ArrayList。

            3、对于“单线程环境”或者“多线程环境,但是List仅被一个线程操作”,需要考虑使用非同步的类,如果是“多线程环境,切List可能同时被多个线程操作”,考虑使用同步的类(如Vector)。

    2.1ArrayList、LinkedList性能分析

            在List中我们使用最普遍的就是LinkedList和ArrayList,同时我们也了解了他们两者之间的使用场景和区别。

            运行结果:

            从上面的运行结果我们可以清晰的看出ArrayList、LinkedList、Vector增加、删除、遍历的效率问题。下面我就插入方法add(int index, E element),delete、get方法各位如有兴趣可以研究研究。

            首先我们先看三者之间的源码:

            ArrayList

            rangeCheckForAdd、ensureCapacityInternal两个方法没有什么影响,真正产生影响的是System.arraycopy方法,该方法是个JNI函数,是在JVM中实现的。声明如下:

           事实上我们只需要了解该方法会移动index后面的所有元素即可,这就意味着ArrayList的add(int index, E element)方法会引起index位置之后所有元素的改变,这真是牵一处而动全身。

    LinkedList

            该方法比较简单,插入位置在末尾则调用linkLast方法,否则调用linkBefore方法,其实linkLast、linkBefore都是非常简单的实现,就是在index位置插入元素,至于index具体为知则有node方法来解决,同时node对index位置检索还有一个加速作用,如下:

            所以linkedList的插入动作比ArrayList动作快就在于两个方面。

    1:linkedList不需要执行元素拷贝动作,没有牵一发而动全身的大动作。

    2:查找插入位置有加速动作即:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。

            Vector

            Vector的实现机制和ArrayList一样,同样是使用动态数组来实现的,所以他们两者之间的效率差不多,add的源码也一样,如下:

            上面是针对ArrayList、LinkedList、Vector三者之间的add(int index,E element)方法的解释,解释了LinkedList的插入动作要比ArrayList、Vector的插入动作效率为什么要高出这么多!至于delete、get两个方法LZ就不多解释了。

    2.2、Vector和ArrayList的区别

    相关文章

      网友评论

          本文标题:集合List接口详解【Java提高十六】

          本文链接:https://www.haomeiwen.com/subject/ikuhdftx.html