JAVA数据结构

强颜欢笑 提交于 2019-12-04 13:27:46

JAVA中的常用的数据结构、每个特定类的使用和功能适用的场合。

一、数据结构

1.逻辑结构和物理结构

逻辑结构:反映数据元素之间的逻辑关系,其中的逻辑关系是指数据元素之间的前后件关系,而与他们在计算机中的存储位置无关。常见的逻辑结构有集合、线性结构、树形结构、图形结构。

  集合:数据结构中的元素之间除了同属一个集合”的相互关系外,别无其他关系。
  线性结构:数据结构中的元素存在一对一的相互关系。每个数据元素只有一个直接前驱和一个直接后继,线性结构有线性表,栈,队列,双队列,串,一般多维数组、广义表不是线性结构。
  树形结构:数据结构中的元素存在一对多的相互关系。且具有明显的层次关系,每一层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。树形结构可以表示从属关系、并列关系。数据结构中各种树形状都是树形结构。
  图形结构:数据结构中的元素存在多对多的相互关系。结点之间的关系可以是任意的,任意两个数据元素之间都可能相关,图形结构常被用于描述各种复杂的数据对象。

物理结构:指数据的逻辑结构在计算机存储空间的存放形式。包括数据元素的机内表示和关系的机内表示,数据元素的机内表示指用二进制位的位串表示数据元素,称这种位串为节点。当数据元素有若干个数据项组成时,位串中与多个数据项对应的子位串称为数据域。关系的机内表示则可以分为顺序映像和非顺序映像,常用的存储结构有顺序存储结构、链式存储结构、索引存储和哈希存储。

  顺序存储:把逻辑上相邻的结点存储在物理位置上相邻的存储单元,结点间的逻辑关系由存储单元的邻接关系来体现,主要用于线性的数据结构。
  链式存储:不要求逻辑上相邻的结点在物理位置上亦相邻,结点间的邻接关系由附加的指针字段表示。
  索引存储:就是在存储信息的同时,还建立附加的索引表。索引表由若干索引项组成,每个结点在索引表中都要一个索引项,根据一个结点或者一组结点对应一个索引项分为稠密索引和稀疏索引。
  哈希(散列)存储:根据结点的关键字直接计算出该结点的存储地址。

数据结构:是带有结构特性的数据元素的集合,它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系,并对这种结构定义相适应的运算,设计出相应的算法,并确保经过这些运算以后所得到的新结构仍保持原来的结构类型。因此数据结构是计算机存储、组织数据的方式,更可以指代相互之间存在一种或多种特定关系的数据元素的集合。

  数据结构的意义在于将有关系,关系密切,有着某种贡献关系的数据组织到一起。当数据以适当的方式组织到一起的时候,是可以形成一定的组织规律的,如面向对象就是组织数据的一种方式。而数据合理的组织,我们就可以根据一个元素,高效率获取关联元素,可以加快我们的搜索效率、并且利用数据关联性解决问题。

2.常见的几种数据结构

数组:属于构造(聚合)数据类型,一个数组可以分解为多个数组元素,这些数组元素可以是基本数据类型或是构造类型。即数组是把具有相同类型的若干元素按无序的形式组织起来的一种形式,是一种同类数据元素的集合。

  对于一维数组,除了第一个元素和最后一个元素没有前驱结点和后继结点,其他元素均有一个前驱结点和后继结点,所以一维数组是线性结构,而对于多维数组,则不存在一对一的关系,多维数组多用于表示矩阵。数组使用时、必须先定义数据类型、指定数组容量,而后申请一块连续的存储空间存储数据,因此数组是顺序存储结构。

链表:是一种线性结构,在物理存储上是非连续、非线性的。数据元素的逻辑顺序是通过链表中的指针链接次序实现的,链表由一系列结点组成,每个结点包括存储数据元素的数据域和存储下一个结点地址的指针域。

  链表在使用时可以根据需求扩容而不像数组那要要预先定义容量大小。链表的非顺序存储结构,使得链表插入、删除的时间大大减小,但是查找元素需要从头开始遍历,链表还有其他的形式,如循环链表、双向链表等。

:是一种运算受限的线性表,限定了插入和删除操作只能够在表尾进行,对于可操作的一端被称为栈顶,相对的另一端被称为栈底,因此对于栈而言栈顶没有后继结点。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素。从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

  栈典型的性质就是后进先出,也叫做先进后出表,其可以用来在函数调用的时候存储断点,做递归时也要用到栈。栈是一种重要的线性结构,其可以用数组或者链表实现。

队列:和栈很相似,是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,其中进行插入操作的一端称为队尾,而进行删除操作的一端称为队头。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。队列中没有元素时称为空队列。

  因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出线性表。队列也有分循环队列、双端队列、优先队列等,也可以用数组或者链表实现队列,但是如果使用数组实现,队列则有最大容量限制,而且可能出现假上溢现象。

  一般用数组实现队列,我们要维护两个指针,其中队头指针指向实际队头元素、队尾指针指向实际队尾指针的下一个位置,初始时,两指针置于0处表示队空,当队列中元素不断出队,此时队头指针会往后移,那么每次添加元素只能在队尾指针处插入,因此当队尾指向数组最后一个位置插入元素就会溢出,但是实际队头指针前面还有空间,因此称为假溢出。

  解决假溢出可以每次将队列元素向低地址区移动,但是效率很低。还有一种方法就是循环队列,将数组存储区看成一个首尾相接的环形区域,也就是数据存放到第n-1位置后,下一个存储空间为0(下标从0开始)。同时将队头指针front和队尾指针rear相等作为队空的条件,为了区分队空,我们少用一个元素,将(rear+1)%maxsize=front作为队满的条件。

集合:是不同对象的无序聚集,集合成员是无序的,每个成员都只在集合中出现一次。集合作为一种数据类型,具有自己的运算操作。我们规定两个集合的并集为包含两个集合所有元素的集合,两个集合的交集为只包含两个集合中同时存在的元素的集合,集合的差集定义为差集中的元素只包含在其中一个集合,且不属于另一个集合。

  我们通常使用集合来归类数据,集合可以使用不同的存储结构实现,只要符合集合的性质。为了更好的定义集合的操作,集合引入空集、子集以及集合相等的定义。同时集合还具有空集律、幂等律、交换律、结合律、分配律、合并律、德摩根定律。

哈希表:也叫散列表,是根据关键码值而直接进行访问的数据结构,也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。

  主要思想是通过一个哈希函数,在所有可能的键与槽位之间建立一张映射表。哈希函数每次接受一个键将返回与键相对应的哈希编码或哈希值。键的数据类型可能多种多样,但哈希值的类型只能是整型。通常不同的键值通过哈希函数的计算可能得到相同的哈希值,称为冲突,为了解决哈希表的冲突,可以引入链式哈希表,其相对于一组链表,每个链表则存放哈希值相同的键。如果单个链表过长会减低哈希表检索效率,因此为实现均匀散列,通常会引入负载因子规定单个链表元素个数最大值,当然选择恰当的哈希函数也可以提高性能。

:是一种由n个有限结点组成一个具有层次关系的集合,其中每个结点有零个或多个子结点,没有父结点的结点称为根结点,没有子结点的的结点叫做叶子结点,每一个非根结点有且只有一个父结点,除了根结点外,每个子结点可以分为多个不相交的子树。

  树的实现方法与表示方法有很多种,如父结点表示法、孩子链表示法、左孩子右兄弟表示法等。同时树还依据不同性质,有二叉树、有序树、哈夫曼树、平衡树、字典树等,为了方便表示,在树中规定树的高度为根结点的高度,而结点的高度定义为结点与其叶子结点之间最长路径上边的个数。结点的深度则是根结点到该结点边的个数,结点的度表示子节点个数。

  父结点表示法:记录树中节点与节点之间的父子关系,为每个结点增加一个parent域记录该结点的父结点。一般可以用数组实现,parent[j] = i表示结点j的父结点为i。因此寻找结点的双亲方便,但是查找儿子结点耗时。
  孩子链表示法:对于n个结点的树、维护n个链表,每个链表存放该结点的所有儿子结点。寻找孩子方便、寻找双亲困难。
  左孩子右兄弟表示法:对于树中的每个结点,都存在左引用和右引用,其中左引用指向该结点的一个孩子,而右引用则指向其兄弟结点。因此把树转化为二叉树的形式存储,实现方便

  二叉树是使用最广泛的一种树结构、其每个结点最多有两个子树的树结构,称为左子树和右子树,且结点只有一个左子树和只有一个右子树是不同的结构。一棵深度为k的二叉树最多有2(k)-1个结点,这时称二叉树为满二叉树。具有n个结点的不同结构二叉树数目为卡特兰数(f(n)=f(n-1)+f(n-2)×f(1)+···+f(1)×f(n-2)+f(n-1))。

  二叉树的前序遍历:访问顺序是从根结点开始访问、依次左子树、右子树。可以想象成从树根开始绕着整课树的外围转一圈。
  二叉树的中序遍历:先访问左子树、根结点再右子树。相当于把树画好然后投影到底下的就是中序遍历序列。
  二叉树的后序遍历:访问左子树、右子树完、最后才访问根结点。我们可以从根结点开始绕着整棵树依次移出叶子结点(移除后产生的新叶子结点遍历到时也要移出),最后就得到后序遍历序列。

二叉堆:二叉堆是一种特殊的二叉树, 它总是保证一棵树的最小元素(最小堆)或者最大元素(最大堆)处于树根上。堆中每个结点的值都是大于或者小于其子结点的,因此二叉堆的任何一个子树也是一个二叉堆。

  堆是一棵完全二叉树,其第一层到倒数第二层的结点都是满的,并且有且一个结点的结点必须是没有右结点。可以将一个数组构建成一个二叉堆,从最后一个非叶子结点开始往下建堆,时间复杂度为O(log2n)。每次添加元素则是将元素插入堆尾然后通过比较上浮,而删除元素将堆顶与最后一个元素交换、移出队尾后对堆顶进行下沉操作,添加和删除元素时间复杂度都是O(log2n),因为n个结点的完全二叉树高度为log2n。

:是一种非线性的数据结构,主要有顶点和边组成,图中所有的顶点构成一个顶点集合,所有的边构成边的集合,一个完整的图结构就是由顶点集合和边集合组成。图根据边有无方向分为有向图和无向图,根据每个顶点间是否存在边分来判断是不是完全图。

  图的每个顶点都可能与任意的其他顶点存在边,我们把一条边的两个顶点叫做邻接顶点,所以实现图我们可以使用邻接矩阵表示法和邻接表表示法。而图的遍历又分为深度优先遍历和广度优先遍历(也适用于树)。

  邻接矩阵:假设图有n个顶点,那么可以定义一个n*n的二维矩阵来表示图。矩阵中元素(i,j)则表示顶点i到j是否有一条边,我们可以规定值为0表示边不存在。表示无向图时,邻接矩阵就是一个对称矩阵,而对于有向图(i,j)和(j,i)的取值可能不同。
  邻接表:邻接矩阵表示法在删除和添加边时效率很大,但是存在空间的浪费。而邻接表则是对于n个顶点的图定义n个链表,链表中存放该顶点的邻接顶点,因此由于链表的动态空间分配、节省了空间。

  深度优先遍历:从图中某个顶点出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和该顶点有路径相通的顶点都被访问到。
  广度优先遍历:也是从图中某个顶点出发,访问了该顶点后要依次访问其邻接顶点,访问完其所有邻接顶点才访问邻接顶点的邻接顶点。直至图中所有顶点都被访问到。

二、JAVA集合框架

JAVA为了提高性能,同时让不同类型的集合以相同方式工作、实现易扩展、高适应、高度的互操作性,为我们提供了功能强大的集合框架。整个框架围绕一组统一标准的接口,我们可以直接使用这些接口的标准实现或者扩展定义自己的数据结构。


  Iterable接口是对可迭代对象Iterator进行了封装,同时实现Iterable接口的类可以使用for each方法进行元素遍历,其主要方法有iterator()、forEach()、spliterator()。Collection接口为容器提供一套实现标准,也是JAVA中List、Set、Queue的公共父类,其定义了所有这些容器实现类公共的方法,诸如size()、isEmpty()、add()、remove()、contains()等。List、Set、Queue也是接口类,相比于Collection对所有这些容器的通用方法进行了定义,它们则对应列表、集合、队列的概念,是在Collection的基础上进一步分离出的不同容器。
  Dictionary和Map都是以键值对方式存储元素的容器,两者都定义了对键值对容器操作的标准。Dictionary是已经过时的接口、因此现在均是使用Map接口进行具体的实现。

1.List篇

List是继承自Collection的接口,是一个有序的集合,也叫做序列。允许对List容器中元素的插入位置进行精确的控制,同时可以根据元素的整数索引访问元素、并搜索元素,允许存放重复的元素和空元素。AbstractList是实现List接口的一个抽象类,其实现了List中部分方法以便进一步扩展。


ArrayList

//添加方法
add(Object obj)   //在集合的末尾添加元素
add(int index, Object obj)   //用来向集合指定位置添加元素,其他元素位置相对向后偏移。
addAll(int index, Collection coll)   //在指定索引处添加指定集合的全部元素
//删除方法
remove(int index)   //清除指定索引位置的元素,其之后的元素位置往前偏移
remove(Object obj)   //删除序列中第一个出现的相同对象,内部使用equals方法比较,因此自定义对象应该注意重写方法。
removeAll(Collection coll)   //从集合中删除指定集合中对应匹配的元素
removeIf(Predicate filter)   //按照一定规则过滤集合中元素,Predicate是一个函数式接口来定义过滤规则
//修改方法
set(int index, Object obj)   //将集合中对应索引的对象修改为指定对象
//查找方法
get(int index)   //获取指定位置的元素
subList(int fromIndex, int toIndex)   //查找当前集合指定访问的指针,返回的是原集合的视图,因此实际的修改会映射回原集合

  ArrayList是AbstractList的一个实现类,其底层是使用数组存放元素的,因此有查询速度快、增删速度慢的特点。由于底层用数组存放元素,所以当元素超过数组容量就需要扩容,初始时可以为ArrayList指定容量,如果未指定默认为10。
  ArrayList的扩容策略是当数组已满不能再添加元素时,内部会将数组的容量调到原来的1.5倍,并使用Arrays.copyOf()方法进行数据迁移。另外ArrayList是线程不安全的,也就是其在添加或删除元素时内部没有用到任何同步互斥机制,因此当多线程操作会发生下标越界等异常问题。

Vector

//添加方法
addElement(Object obj)   //在集合的末尾添加元素,与add的区别是返回值为void
insertElementAt(Object obj, int index)   //在指定索引处插入元素,之后的元素往后偏移
//删除方法
removeElement(Object obj)  //删除向量中第一个出现的相同对象,内部使用equals方法比较,因此自定义对象应该注意重写方法。
removeAllElement()   //删除向量中所有元素
removeElementAt(int index)   //删除指定下标处的元素
//修改方法
setElementAt(Object obj,int index)   //将向量中对应索引的对象修改为指定对象
setSize(int newSize)   //定义向量大小,若成员个数超过此大小,超出元素会丢失
//查找方法
firstElement()   //获取向量中首个元素
lastElement()   //获取向量中最后一个元素
//另外Vector实现了List接口也支持List中定义的操作方法

  Vector是矢量队列,底层也是使用数组实现,默认初始容量是10,除了允许指定初始容量还可以指定扩展容量,未指定扩展容量则默认扩展一倍。Vector还是线程安全的,其内部使用synchronized关键字实现同步,因此也带来了性能消耗,所以没有多线程操作下ArrayList效率更高。

Stack

//添加方法
push(Object obj)   //向栈顶插入一个元素
//删除方法
pop()   //移除栈顶元素,并返回该元素
//查找方法
peek()   //返回栈顶元素,不删除
search(Object obj)   //查找元素距离栈顶的位置,规定栈顶元素位置为1

  Stack是继承自Vector实现的一个标准后进先出栈,因此其包括Vector的方法,内部实现都是调用Vector的方法执行的,因此其也是线程安全的。

LinkedList

//添加方法
addFirst(Object obj)、offerFirst(Object obj)、push(Object obj)   //在链表头部插入一个元素
addLast(Object obj)、offer(Object obj)、offerLast(Object obj)   //在链表尾部插入一个元素
//删除方法
removeFirst()、pollFirst()、pop()、poll()   //从链表中删除头
removeLast()、pollLast()   //从链表中删除尾
//查找方法
getFirst()、peekFirst()、element()、peek()   //获取第一个元素,不删除
getLast()、peekLast()   //获取最后一个元素、不删除

  LinkedList不仅仅继承自AbstractSequentialList类,还实现Deque接口、因此其也具体队列的性质。底层使用链表实现、增删速度快、查询速度慢,并且内部维护了头尾两个指针、因此是个双向链表、每个节点本身除了值外还拥有prev、next两个引用来维护双链表关系。遍历LinkedList链表使用迭代器可以获得很高的效率。

2.Set篇

Set和Collection的方法基本一致、代表不能拥有重复元素的容器类型,同时可以发现内部主要是使用对象的equals判断元素是否重复,因此为了保证元素不重复,自定义的类需要确保equals()、hashCode()方法有效。AbstractSet则是在Set接口上扩展,实现了equals()、hashCode()、removeAll()方法。


TreeSet

//添加方法
add(Object obj)、addAll(Collection coll)
//删除方法
remove(Object obj)、removeAll(Collection coll)、removeIf(Predicate filter)
pollFirst()、pollLast()
//查找方法
ceiling(Object obj)   //查找大于等于给定元素的最小元素
first()   //返回最小的元素,即第一个元素
floor(Object obj)   //返回小于等于给定元素的最大元素
headSet(Object obj)   //返回元素小于给定元素的部分视图SortedSet
higher(Object obj)   //返回严格大于给定元素的最小元素
last()   //返回最大的元素,即最后一个元素
lower(Object obj)   //返回严格小于给定元素的最大元素
subSet(Object from, Object to)   //返回指定访问的部分视图

  TreeSet是实现Set接口的有序集合,其支持自然排序和自定义排序,当使用自定义排序时需实现compare方法,因此如果在compare方法中未做处理TreeSet插入空值是会报空指针异常的。TreeSet的底层实现依赖于TreeMap,可以发现其构造函数创建了TreeMap实例用来存放元素,底层则是使用二叉树进行排序,是非同步、线程不安全的。

HashSet

//添加方法
add(Object obj)、addAll(Collection coll)
//删除方法
remove(Object obj)、removeAll(Collection coll)、removeIf(Predicate filter)
//查找方法
iterator()   //使用Iterator对象实现迭代遍历

  HashSet是一个无序的集合,即不保证迭代顺序和存放顺序一致,因此其无法根据索引获取元素,允许存放空值。实际上HashSet的底层是由HashMap来实现的,在构造方法时创建HashMap对象,其它方法则调用HashMap内部方法实现。是非同步,线程不安全的。

LinkedHashSet

//添加方法
add(Object obj)、addAll(Collection coll)
//删除方法
remove(Object obj)、removeAll(Collection coll)、removeIf(Predicate filter)
//查找方法
iterator()   //使用Iterator对象实现迭代遍历

  LinkedHashSet是可预知迭代顺序的集合,也就是说其迭代顺序和存放顺序是一致的,主要因为由哈希表和链接列表实现,通过链接列表定义的迭代顺序实现了顺序迭代。同时为了维护链表开销,性能会比HashSet差。其构造时调用了HashSet的构造方法,而HashSet里面创建的是LinkedHashMap对象,因此LinkedHashSet实现是由LinkedHashMap实现的,允许存放空元素,也是非同步,线程不安全的。

EnumSet

allOf(Class elementType)   // 创建一个包含指定元素类型的所有元素的EnumSet
complementOf(EmumSet e)   //创建一个类型与指定枚举集合相同,且包含指定枚举集合中所不包含的那部分元素的EnumSet
copyOf(Collection coll)   //创建一个从指定集合初始化的EnumSet
copyOf(EnumSet e)   //创建一个与指定枚举集合一模一样的EnumSet
noneOf(Class elementType)   //创建一个指定类型的空枚举集合
of(E first, E... rest)   //创建一个包含指定元素的EnumSet,所以指定元素属于同一个枚举类

  EnumSet是一个枚举类的专用集合,且要求枚举集合内部所有元素来自同一个枚举类。不同于其他集合类的实现,枚举集合内部是使用位向量定义操作的,即使用某一位为0或者1代表该元素是否存在,因此其操作高效且性能良好,适合应用于集合常用的操作多的情况。其不允许插入空元素,是非同步、线程不安全的。

3.Queue篇

Queue即队列,在Collection的基础上添加了offer()、poll()、peek()、element()等方法用来定义队列的操作,其默认的操作规则就是先进先出。Deque则是继承它的子接口,表示双端队列,因此添加了头尾安全、不安全的一套插入、移除、查询操作,这些方法在LinkedList中体现到。AbstractQueue则是实现Queue的一个抽象类,用于进一步扩展。


ArrayDeque

//添加方法
addFirst(Object obj)、offerFirst(Object obj)、push(Object obj)
addLast(Object obj)、offer(Object obj)、offerLast(Object obj)
//删除方法
removeFirst()、pollFirst()、pop()、poll()   //从队列中删除头
removeLast()、pollLast()   //从队列中删除尾
//查找方法
getFirst()、peekFirst()、peek()、element()   //获取第一个元素,不删除
getLast()、peekLast()   //获取最后一个元素、不删除
//offer、poll系列操作是安全的,队列空移除时、或者空间溢出会使用boolean或null来确定是否成功,但是add、remove系列失败则抛异常

  ArrayDeque的底层存储是使用可变数组实现的,而且是循环数组。其默认初始空间为16,扩容新长度为旧长度的两倍。其禁止存放空元素,由于是双端对列,因此和LinkedList一样可以当栈、也可以当队列使用,不过ArrayDeque底层用数组实现、以及不用维护多余的引用性能则优于LinkedList。是非同步、线程不安全的。

PriorityQueue

//添加方法
add(Object obj)、offer(Object obj)   //将指定元素插入此优先级队列(实际位置根据堆进行存放)
//删除方法
poll()   //获取并移除队列头部
remove(Object obj)   //从此队列中移除指定元素的单个实例
//查找方法
peek()、element()   //获取队列头,但不删除

  PriorityQueue是一个基于优先级堆的无界优先级队列,优先级顺序自然排序也可自定义,不允许存放空元素和不可比较的对象。其优先级排序是通过二叉小顶堆实现的,内部维护一个数组作为底层存储,同时使用动态扩容实现无界性。是非同步、线程不安全的。

4.Map篇

Map表达的是映射的意思,提供了键到值的一一映射,因此我们每次插入元素都是以键值对的形式,每个键最多只能映射到一个值,可以通过键来获取值。此接口还用来取代Dictionary类,同时提供键集、值集、键值映射关系集三种视图。AbstractMap是实现Map的一个抽象类,实现了一些简单、通用的方法。


HashTable

//添加方法
put(Object key, Object value)   //将key映射到value并存入哈希表中
putAll(Map map)   //将指定映射的所有映射添加到哈希表中
//删除方法
remove(Object key)   //从表中移除该key即其对应值
//查找方法
elements()、keys()   //返回Enumeration接口用于遍历哈希表中所有值或键
entrySet()、keySet()、values()   //返回此哈希表的键值映射的视图、键视图、值视图
get(Object key)   //返回此哈希表中对应key的值,不包含则返回null

  HashTable表示的是哈希表,任何非空对象都可以用作键和值,存放空对象则会报空指针异常,同时不保证存放顺序和遍历顺序一致。为了成功获取和存储对象,用作键值的对象必须实现hashCode()和equals()方法,保证equals()相等的对象hashCode()也相等。因为内部存对象是根据hashCode值和数组长度计算获取数组下标位置,而数组存放的是链表,查元素或者存储元素都是在链表里面用equals()方法进行比较然后搜索和存放。所以其底层是通过链表数组(Entry数组)存放元素的,并用链表类似桶的方式解决哈希冲突,其有两个性能指标,分别是初始容量和加载因子,初始容量就是初始链表(桶)的数量,加载因子表示哈希表容量增长前可以达到多满的一个尺度,加载因子过高可以减少空间开销,但是单链表查询耗时,因此应该折衷选择。另外它是同步、线程安全的。

HashMap

//添加方法
put(Object key, Object value)   //将key映射到value并存入哈希表中
putAll(Map map)   //将指定映射的所有映射添加到哈希表中
//删除方法
remove(Object key)   //从表中移除该key即其对应值
//查找方法
entrySet()、keySet()、values()   //返回此哈希表的键值映射的视图、键视图、值视图
get(Object key)   //返回此哈希表中对应key的值,不包含则返回null

  HashMap是哈希表的Map接口的实现,同时不保证映射的顺序,特别是不保证顺序恒久不变。该类和HashTable大致相同,但是它允许存放空值和空键,底层也是使用链表数组方式实现的,也是使用链表来解决哈希冲突,内部也有初始容量和加载因子来调整性能。另外HashMap不是同步的,是非线程安全的,因此单线程情况下性能会比HashTable好。

LinkedHashMap

//添加方法
put(Object key, Object value)   //将key映射到value并存入哈希表中
putAll(Map map)   //将指定映射的所有映射添加到哈希表中
//删除方法
remove(Object key)   //从表中移除该key即其对应值
//查找方法
entrySet()、keySet()、values()   //返回此哈希表的键值映射的视图、键视图、值视图
get(Object key)   //返回此哈希表中对应key的值,不包含则返回null

  LinkedHashMap是HashMap的子类,是哈希表和链接链表的实现,其具有可预知的迭代顺序,即迭代顺序就是元素添加顺序。其大部分的方法都是调用HashMap实现的,但是内部额外维护了一个双重链接列表来保证迭代顺序,因此性能上比HashMap差,也允许存放空值和空元素。和HashMap一样是非同步、线程不安全的。

TreeMap

//添加方法
put(Object key, Object value)   //将key映射到value并存入哈希表中
putAll(Map map)   //将指定映射的所有映射添加到哈希表中
//删除方法
remove(Object key)   //从表中移除该key即其对应值
pollFirstEntry()、pollLastEntry()   //在哈希表中移除最小键、最大键的映射
//查找方法
entrySet()、keySet()、values()   //返回此哈希表的键值映射的视图、键视图、值视图
get(Object key)   //返回此哈希表中对应key的值,不包含则返回null
ceilingEntry(Object key)、ceilingKey(Object key)   //返回大于等于指定键的最小键关联、或键
descendingKeySet()、descendingMap   //返回键的逆序视图、映射的逆序视图
firstEntry()、firstKey()、lastEntry()、lastKey()   //返回最小(最大)映射、最小(最大)键值
floorEntry()、floorKey()   //返回小于等于指定键的最大键关联、或键
headMap(Object key)   //返回部分视图、键值严格小于指定键
higherEntry()、higherKey()、lowerEntry()、lowerKey()   //严格大于(小于)指定键的最小(最大)键
subMap(Object fromKey, Object toKey)   //返回部分视图,键值在指定范围

  TreeMap是个有序的哈希表,我们以任意顺序放置元素,遍历元素时可以按照自然排序顺序呈现,同时也支持自定义排序,因此不建议放置null,否则会抛出空指针异常,且规定equals()结果为true的两个对象是相等的。内部排序则是使用红黑树实现的,也是一种平衡的二叉树,因此插入、删除都能在O(log2n)时间开销。

WeakHashMap

//添加方法
put(Object key, Object value)   //将key映射到value并存入哈希表中
putAll(Map map)   //将指定映射的所有映射添加到哈希表中
//删除方法
remove(Object key)   //从表中移除该key即其对应值
//查找方法
entrySet()、keySet()、values()   //返回此哈希表的键值映射的视图、键视图、值视图
get(Object key)   //返回此哈希表中对应key的值,不包含则返回null

  WeakHashMap是以弱键实现的哈希表,弱键的意思就是即使映射存在也不能阻止垃圾回收器对该键的丢弃,这就使得该键被回收,其条目也被移除。该类也是支持空键和空值的,内部实现和HashMap很像,也是使用链表数组存放元素,也具有初始容量和加载因子。而其实现弱键则是通过弱引用实现的,通过弱引用机制来实现自动回收,因此非常适合做缓存用途,如果不用键值对形式,则可以通过Collections的newSetFromMap方法来创建一个WeakHashSet供使用。WeakHashMap也是非同步、线程不安全的。

EnumMap

//添加方法
put(Object key, Object value)   //将key映射到value并存入哈希表中
putAll(Map map)   //将指定映射的所有映射添加到哈希表中
//删除方法
remove(Object key)   //从表中移除该key即其对应值
//查找方法
entrySet()、keySet()、values()   //返回此哈希表的键值映射的视图、键视图、值视图
get(Object key)   //返回此哈希表中对应key的值,不包含则返回null

  EnumMap是与枚举类型键一起使用的Map,枚举映射中的所有键必须来自同一个枚举类。EnumMap不允许键为空,而值可以为空,其存放顺序是按照枚举类中枚举常量顺序,内部则用数组来表示映射关系,一个数组根据枚举类存放所有的键,另一个数组则存放于键对应的值,某个键没有值则另一个数组对应位置为null。另外其collection返回的视图的迭代器是弱一致的,因此迭代时修改映射结构不会抛异常,也不保证迭代时能及时发现修改。是非同步、线程不安全的。

IdentityHashMap

//添加方法
put(Object key, Object value)   //将key映射到value并存入哈希表中
putAll(Map map)   //将指定映射的所有映射添加到哈希表中
//删除方法
remove(Object key)   //从表中移除该key即其对应值
//查找方法
entrySet()、keySet()、values()   //返回此哈希表的键值映射的视图、键视图、值视图
get(Object key)   //返回此哈希表中对应key的值,不包含则返回null

  IdentityHashMap也实现了Map接口,但是不同于其他哈希表,它是使用引用相等性来代替对象相等性,也就是用等号相等才判断两个键相等而不是用equals()方法,因此其可以说是违反了Map的常规协定。允许存放空值和空键,同时不保证元素存放顺序,内部使用对象数组来存放元素,不是同步的。这样的哈希表可以用于拓扑保留对象图形转换如序列化或深层复制,还有便是用来维护代理对象。

三、JAVA并发包下的集合框架

1.List

并发包下实现List接口的集合类有CopyOnWriteArrayList。

CopyOnWriteArrayList
  CopyOnWrite简称COW即写入时复制,是计算机程序设计领域中的一种优化策略。核心思想就是当有多个调用者同时要请求相同资源时,会共同获得执行相同资源的引用,而只有在某个调用着要求修改资源内容时,系统才会复制一份真正资源的副本给该调用者,这对其他调用者是透明的,他们所见到的最初资源仍然保持不变。因此这种做法当没有调用者要修改资源时,始终不会有副本产生,多个调用者读取时是共享一个资源的。
  CopyOnWriteArrayList实现则是基于COW原理,内部通过数组存放元素,每次增加、删除、修改元素时都会进行加锁操作,这样就保证多线程最多只有一个线程会对其做出修改,同时每次修改之后复制一个副本,修改完成后则将副本赋值回原数组。每次读操作则不需要加锁,这就允许多个线程同时读取内容,但是若读和写同时发生,在写未完成时,读到的还是未更改前的内容。
  CopyOnWriteArrayList的每次修改都需要重新复制一遍数组,因此带来的内存消耗比较大,但是并发情况下读操作是不用加锁的,所以更适用于在多线程环境下遍历操作远远大于可变操作数据集的存储。

2.Map

并发包下ConcurrentMap是继承Map的一个接口,同时用做扩展,其实现类有ConcurrentHashMap和ConcurrentSkipListMap

ConcurrentHashMap
  ConcurrentHashMap主要是解决HashMap线程不安全和HashTable效率不高的问题而引入的,同时不同的JDK版本其实现也不同。在JDK1.7之前使用的是分段锁实现,而JDK1.8更新后,则是使用数组、链表、红黑树这些数据结构和CAS原子操作实现的。
  虽然之前HashTable可以实现线程安全,但由于使用Synchronized关键字锁住了整个对象,从而使得效率低下。ConcurrentHashMap就是通过减少锁竞争,将全局加锁改成局部加锁操作来提高性能的。JDK1.7中ConcurrentHashMap的实现就是数组+Segment+分段锁,使用分段锁技术将数据分成一段一段的存储,每一段都相当于一个哈希表,内部则维护一个数组,每个数组元素就是链表。然后给每一段数据配一把锁,因此不同段的数据是允许被同时访问的。那么定位哈希元素就可以通过两次哈希映射,第一次映射到段,第二次则定位到段内具体的链表头。虽然可能每次映射多了一步,但是细化到段并发提高了性能。
  JDK1.7的并发程度会受到段数量的限制,而JDK1.8则是使用数组+链表+红黑树以及内部大量的CAS操作完成的。CAS(compare and swap)就是比较和交换的意思,是一种基于锁的操作,而且是乐观锁。JAVA中锁分为悲观锁和乐观锁,悲观锁就是就是将资源锁住,等获得锁的线程释放,下一个线程才能访问。乐观锁则采用宽泛的态度,通过不加锁来处理资源,比如添加版本号的处理,因此性能提高很多。CAS有三个操作数分别是内存位置、预期原值和新值,若内存地址里的值和预期原值是一样的,那么就可以执行更新把值改为新值,否则会不断循环获取数据比较。
  JDK1.8的ConcurrentHashMap内部维护一个Node数组,一个Node是一个继承自Map.Entry的链表,其包括key、key的哈希值、value和next,其中value和next用volatile修辞保证可见性。而当链表数据大于8,则将链表升级为红黑树,因此提高了查找速度。ConcurrentHashMap的每次插入操作都会不断的CAS循环,直到获得更新机会,保证了同步。删除操作时如果在扩容会先协助扩容,然后使用synchronized关键字上锁,之后删除元素。读取操作则不需要同步控制。

ConcurrentSkipListMap
  ConcurrentSkipListMap和TreeMap一样是有序哈希表,也支持自然排序和自定义排序,但是ConcurrentSkipListMap是线程安全的,适用于高并发场景。且ConcurrentSkipListMap的内部结构并不是红黑树,而是使用SkipList(跳表)实现的。
  跳表是一种随机化的数据结构,内部则是由不同层次的有序链表组成,且只有第一层包含所有的数据,其它层相当于索引,且上一层的元素是下一层元素的子集,层次越高,跳跃性越大,包含的数据越少。跳表有一个表头,上层的每个节点都有指向其下一个元素和下层对应元素的引用,所以查找数据则从上往下、从左往右进行查找,由于每一层都有序,查找时就是通过不断缩小查找范围最终确定元素位置。每次插入结点,都会随机化确定结点所要占据的层数,然后在着些层都要插入该结点。而删除元素则会删除所有层对应元素,如果某一层删除后只剩头尾两个指针,则要删除整层。因此跳表的空间是可收缩的。
  在ConcurrentSkipListMap中使用内部类Node包含key、value、next成员来做基准结点,使用内部类Index包含node、down、right来做索引结点。通过volatile关键字和基于乐观锁的CAS实现并发操作,插入和删除都是判断结点的邻接结点是否变化确定其他线程是否和自己发生冲突。

3.Set

并发包下实现的Set集合主要继承自AbstractSet抽象类,有CopyOnWriteArraySet和ConcurrentSkipListSet

CopyOnWriteArraySet
  CopyOnWriteArraySet也是基于COW的原理实现的,且其内部就是维护了一个CopyOnWriteArrayList对象,由于是Set因此不提供set操作,其添加操作和删除操作都是调用CopyOnWriteArrayList的方法进行加锁和复制数组,不同的是添加时会查找元素是否已经在数组中,如果存在就返回false不插入,因此保证了元素的单一性。读操作则是通过迭代遍历且支持多调用方共享,由于内部使用的是List存放,因此读顺序和存放顺序是一样的。和CopyOnWriteArrayLis适用于多线程且读多写少的情况。

ConcurrentSkipListSet
  ConcurrentSkipListSet是线程安全的有序集合,ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,其内部维护着一个ConcurrentSkipListMap对象,添加、删除元素都是调用ConcurrentSkipListMap的方法完成,因此特性和ConcurrentSkipListMap很像。

4.Queue

Queue接口分别有抽象类AbstractQueue和一个专门用于实现并发的接口BlockingQueue也叫阻塞队列。在AbstractQueue抽象类下实现的并发集合有ConcurrentLinkedQueue。而BlockingQueue接口下扩展的并发集合则比较多,分别有ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、LinkedBlockingDeque、DelayQueue、SynchronousQueue、LinkedTransferQueue

ConcurrentLinkedQueue
  ConcurrentLinkedQueue是一个线程安全队列,它采用先进先出的规则对节点进行排序。其结点结构使用内部类Node实现,Node有item和next两个成员来实现链表,因此ConcurrentLinkedQueue是一个基于链接结点的无界队列。
  由于队列的先进先出,每次添加元素都是插入到队尾,而当多个线程执行入队操作,就可能竞争队尾指针导致入队失败。因此ConcurrentLinkedQueue在入队时则使用CAS算法来实现并发操作。每次取到尾结点都要确保其下一个引用为空,否则说明当前其他线程有入队操作,则要重新取尾结点重试直至操作成功,然后需要更新尾结点,更新尾结点则是判断插入的地方是否和之前取到的尾结点一样,不一样则要将尾结点往后移,否则会影响下次入队效率。
  出队也是一样的原理,如果取到的队头没有数据,说明已经被其它线程出队,因此使用CAS尝试来清除数据实现删除,删除成功后则需要更新头结点。

阻塞队列:是在队列的基础上支持两个附加操作的队列,即队列满时,队列会阻塞插入线程,直到队列不满。而当队列空时,获取元素的线程会等待直到队列非空。因此形成非常典型的生成者、消费者的场景,生产者则向队列中添加元素,消费者则获取元素,存在同步制约关系。

ArrayBlockingQueue
  ArrayBlockingQueue就是典型的用数组实现的阻塞队列,底层则用循环数组来存放数据,同时支持阻塞和非阻塞的入队、出队操作。创建ArrayBlockingQueue必须要指定队列的初始容量,且一旦确定了初始容量,就不能够再试图增加容量,因此是无界阻塞队列。ArrayBlockingQueue内部使用ReentrantLock锁保证同步和线程安全,并且读和写使用的是同一把锁,因此导致读和写不能并发进行,通信则是使用notEmpty和notFull两个condition来实现。

LinkedBlockingQueue
  LinkedBlockingQueue也是一个阻塞队列,和ArrayBlockingQueue不同的是,其内部使用一个链表来存放元素,如果初始化时不指定大小,默认容量为Integer.MAX_VALUE,那么不断添加元素可能内存溢出,此时其就是无界队列。LinkedBlockingQueue也支持阻塞和非阻塞的入队、出队操作,另外不同的是其读和写分别用读锁和写锁两把锁控制并发,读锁拥有notEmpty的condition,写锁拥有一个notFull的condition,因此读写是可并发的,且不是每次添加或者删除结束后就获取对方的锁通知对方,而是在边界条件才触发通知,所以性能会有很大提高。

PriorityBlockingQueue
  PriorityBlockingQueue是一个支持优先级的无界阻塞队列,默认情况下元素采用自然顺序升序排列,也可自定义排序规则。其底层数据结构是一个基于数组实现的最小二叉堆,初始容量为11。内部使用一个ReentrantLock和一个Condition实现同步机制,Condition用于获取元素时队列为空进行的通信,由于其内部只使用ReentrantLock一把锁,因此读写、读读、写写不同时发生。在读的时候,可能会出现数组容量不够,因此需要扩容。扩容是根据实际大小选择扩容为原来的2倍或是1.5倍,容量小时扩容就比较快。扩容过程中是有一小段时间释放锁的,提供了扩容和出队的并发,同时使用CAS防止多线程同时扩容,最后在更换数组引用和拷贝旧数组数据之前要及时加锁。

LinkedBlockingDeque
  LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可实现队列的两端进行插入和移除操作,同时减少竞争。当我们不指定初始容量,它就是无界的,默认为Integer.MAX_VALUE。因为内部是一个由Node结点(包括item、prev、next)组成的双链表,所以既可以当队列,也可以当做栈来使用。LinkedBlockingDeque内部维护一个ReentrantLock锁和notEmpty、notFull两个Condition来控制并发操作,所以读写是不可并发的,同样支持阻塞和非阻塞的插入、删除操作。

DelayQueue
  DelayQueue是一个支持延时获取元素的无界阻塞队列,内部实例化一个PriorityQueue存放元素,因此是无界且是有序的。首先DelayQueue中的元素必须要实现Delayed接口,而Delayed接口又继承自Comparable接口,因此DelayQueue中添加的元素必须要实现对应接口的方法即getDelay()和compareTo()方法。PriorityQueue内部排序调用实现的compareTo(),而compareTo()应该要依赖于getDelay()方法,即实际上我们应该遵守DelayQueue的规范,让队首元素是延迟期满后保存时间最长的元素,那么就应该定义内部排序按照过期时间最近的排在最前面,如果不符合规范,执行出队操作可能已经有过期元素,但是实际获取到空值。
  根据DelayQueue的特性,可以应用到缓存系统的设计和定时任务的设计。DelayQueue内部维护一个ReentrantLock和一个Condition,存取是不可并发的,支持阻塞和非阻塞的调用。调用Size()方法获取的是过期和未过期元素总数,但是如果优先队列中全部都是未过期元素,那么调用获取元素方法返回的是空值。DelayQueue内部还有一个leader的Thread对象,leader对象主要用来减少不必要的运行时间,在take()方法中就是通过leader是否为空,判断是否已经有线程在取了,如果有则直接阻塞等待,否则就设置当前线程为leader,等待delay延迟到期再poll掉元素。

SynchronousQueue
  SynchronousQueue是没有容量的阻塞队列。插入元素到队列的线程会被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。因此其size()、isEmpty()、peek()方法都被写死来表示其内部没有元素。SynchronousQueue内部有公平模式和非公平模式之分,非公平模式下使用LIFO的TransferStack类存放元素,而公平模式下则使用FIFO的TransferQueue来存放元素。实际上存取元素都由抽象类Transferer的transfer()方法实现,TransferStack和TransferQueue都是基于链表实现了Transferer类,它们的transfer()方法则是通过是否存入元素来判断是生产者线程还是消费者线程。
  非公平模式下采用TransferStack这种先进后出的方式进行非公平处理,其内部有三种状态,分别是REQUEST,DATA,FULFILLING。REQUEST代表的是数据请求操作,DATA代表的是数据存放操作,在线程执行操作调用transfer()方法时,内部通过判断是否有传入元素决定是REQUEST还是DATA状态。FULFILLING是当栈顶非空且要压入的模式和栈顶模式是不一样的,也就是说遇到了一个匹配操作,那么就把当前模式和FULFILLING模式做或操作后压入栈顶,之后便是往下找匹配操作之后出栈。而当其他模式请求遇到头结点是FULFILLING模式,是会帮助进行匹配的。内部基于SNode实现了栈结构,如果匹配的话,是从栈顶往下匹配,因此最先到的请求处于栈底就会最后匹配,所以实现了非公平模式。内部同步操作则使用CAS实现。
  公平模式则使用内部类TransferQueue实现,TransferQueue类内部用一个标识isData来判断是存操作还是取操作,同时用来区分结点的不同状态,因此和非公平模式很像,遇到队空或相同模式就要入队等待,如果是互补的模式就代表匹配出队。内部采用基于QNode实现的队列结构,每个最新到的请求会优先和队头也就是最先入队的操作匹配,因此是公平模式。内部也是采用CAS实现同步机制。

LinkedTransferQueue
  LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列,可以看成是SynchronousQueue和LinkedBlockingQueue的超集。LinkedTransferQueue采用一种预占模式,也就是说消费者线程取数据,如果队列不为空,则直接取走数据,否则会生成一个请求结点入队,消费者线程则阻塞,直到生产者线程存放数据,发现有请求结点则不入队,而是填充结点数据然后唤醒消费者线程取走数据。内部支持阻塞和非阻塞的入队、出队操作,同步则是基于CAS的原理实现。

四、集合迭代的快速失败和安全失败

快速失败(fail-fast):在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了增加或删除,则会抛出Concurrent Modification Exception。(使用迭代器本身的remove()方法不会抛出异常)

  迭代器在遍历集合元素过程中,会维护一个modCount变量,当集合在遍历期间内容发生修改,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,不是就抛出异常。JAVA的Util包下的集合基本是快速失败的,但是EnumSet和EnumMap是安全失败的。

安全失败(fail-safe):采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。因此安全失败迭代器在迭代中被修改,不会抛出任何异常。

  虽然基于拷贝避免了抛异常,但也使得迭代器不能访问到修改后的内容。即迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。JAVA的Concurrent包下的容器都是安全失败的,可以在多线程下并发使用、并发修改。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!