- Java集合类主要由两个接口派生而出:Collection和Map
从上面的集合框架图可以看到,Java集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection接口又有3种子类型,List、Set和Queue,再下面是一些抽象类,最后是具体实现类,常用的有ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap等等。
- Collection接口是处理对象集合的根接口,其中定义了很多对元素进行操作的方法,AbstractCollection是提供Collection部分实现的抽象类。
- List
List接口扩展自Collection,它可以定义一个允许重复的有序集合,从List接口中的方法来看,List接口主要是增加了面向位置的操作,允许在指定位置上操作元素,同时增加了一个能够双向遍历线性表的新列表迭代器ListIterator。AbstractList类提供了List接口的部分实现,AbstractSequentialList扩展自AbstractList,主要是提供对链表的支持。下面介绍List接口的两个重要的具体实现类,也是我们可能最常用的类,ArrayList和LinkedList。
- ArrayList
- ArrayList是List接口的可变数组的实现。底层使用数组保存所有元素。其操作基本上是
对数组的操作
- 实现了所有可选列表操作,并允许包括 null 在内的所有元素。
- 除了实现 List接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。
- 每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。
它总是至少等于列表的大小。
- 随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的
重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。
注意:
- 当增加数据的时候,如果ArrayList的大小已经不满足需求时,那么就将数组变为原长度的1.5倍,之后的操作就是把老的数组拷到新的数组里面。
- LinkList
List 接口的链接列表实现。实现所有可选的列表操作,并且允许所有元素(包括 null)。除了实现 List 接口外,LinkedList 类还为在列表的开头及结尾 get、remove 和 insert 元素提供了统一的命名方法。这些操作允许将链接列表用作堆栈、队列或双端队列。
此类实现 Deque 接口,为 add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作。
LinkList的实现原理总结
- 数据存储是基于双向链表实现的。
- ②插入数据很快。先是在双向链表中找到要插入节点的位置index,找到之后,再插入一个新节点。 双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。
③删除数据很快。先是在双向链表中找到要插入节点的位置index,找到之后,进行如下操作:node.previous.next = node.next;node.next.previous = node.previous;node = null 。查找节点过程和插入一样。
- 获取数据很慢,需要从Head节点进行查找。
- ⑤遍历数据很慢,每次获取数据都需要从头开始。
注:
- 以双向链表实现。链表无容量限制,但双向链表本身使用了更多空间,也需要额外的链表指针操作。
- Set
Set接口扩展自Collection,它与List的不同之处在于,规定Set的实例不包含重复的元素。在一个规则集内,一定不存在两个相等的元素。AbstractSet是一个实现Set接口的抽象类,Set接口有三个具体实现类,分别是散列集HashSet、链式散列集LinkedHashSet和树形集TreeSet。
- HashSet
HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素。HashSet中不允许有重复元素,这是因为HashSet是基于HashMap实现的,HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个private static final Object PRESENT = new Object();。HashSet跟HashMap一样,都是一个存放链表的数组。
HashSet是基于HashMap来实现的,操作很简单,更像是对HashMap做了一次"封装",而且只使用了HashMap的key来实现各种特性
HashSet的实现原理总结如下:
①是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。
②当我们试图把某个类的对象当成 HashMap的 key,或试图将这个类的对象放入 HashSet 中保存时,重写该类的equals(Object obj)方法和 hashCode() 方法很重要,而且这两个方法的返回值必须保持一致:当该类的两个的 hashCode() 返回值相同时,它们通过 equals() 方法比较也应该返回 true。通常来说,所有参与计算 hashCode() 返回值的关键属性,都应该用于作为 equals() 比较的标准。
③HashSet的其他操作都是基于HashMap的。
- LinkedHashSet
LinkedHashSet通过继承HashSet,底层使用LinkedHashMap,以很简单明了的方式来实现了其自身的所有功能。
- TreeSet
TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, java.io.Serializable接口。
*TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法。
*TreeSet 实现了NavigableSet接口,意味着它支持一系列的导航方法。比如查找与指定目标最匹配项。
*TreeSet 实现了Cloneable接口,意味着它能被克隆。
*TreeSet 实现了java.io.Serializable接口,意味着它支持序列化。
*reeSet是基于TreeMap实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。
*reeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。
* reeSet是非同步的。 它的iterator 方法返回的迭代器是fail-fast的。
- Queue
队列是一种先进先出的数据结构,元素在队列末尾添加,在队列头部删除。Queue接口扩展自Collection,并提供插入、提取、检验等操作。
poll()与remove()方法都是移除队列头部的元素,两者的区别在于如果队列为空,那么poll()返回的是null,而remove()会抛出一个异常。方法element()与peek()主要是获取头部元素,不删除。
接口Deque,是一个扩展自Queue的双端队列,它支持在两端插入和删除元素,因为LinkedList类实现了Deque接口,所以通常我们可以使用LinkedList来创建一个队列。PriorityQueue类实现了一个优先队列,优先队列中元素被赋予优先级,拥有高优先级的先被删除。实际上有多个Queue的实现,有的是采用线性表实现,有的基于链表实现。还有的适用于多线程的环境。Java中具有Queue功能的类主要有如下几个:AbstractQueue, ArrayBlockingQueue, ConcurrentLinkedQueue, LinkedBlockingQueue, DelayQueue, LinkedList, PriorityBlockingQueue, PriorityQueue和ArrayDqueue。
- Map
是一种存储键值对映射的容器类,在Map中键可以是任意类型的对象,但不能有重复的键,每个键都对应一个值,真正存储在图中的是键值构成的条目。从上面这张图中我们可以看到接口Map提供了很多查询、更新和获取存储的键值对的方法,更新包括方法clear()、put()、putAll()、remove()等等,查询方法包括containsKey、containsValue等等。Map接口常用的有三个具体实现类,分别是HashMap、LinkedHashMap、TreeMap。
在实际使用中,如果更新图时不需要保持图中元素的顺序,就使用HashMap,如果需要保持图中元素的插入顺序或者访问顺序,就使用LinkedHashMap,如果需要使图按照键值排序,就使用TreeMap。
- HashMap
HashMap实现了Map接口,继承子AbstractMap。其中,Map接口定义了键映射到值的
规则。Java中HashMap是由数组和引用实现的"链表散列"。HashMap底层实现是数组,但是数组的每一项都是一个链表,其中initialCapacity就代表了数组的长度。Entry为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值。这个内部类非常重要,正是由于Entry才构成table数组的项为链表。
初始化HashMap时,系统会创建一个长度为capacity的Entry数组,这个数组里可以存储元素的位置被称为"桶(bucket)" 每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。无论何时,HashMap 的每个"桶"只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。
数组中存储的是一个链表的头结点
threshold: 初始容量,表示哈希表中桶的数量。
loadFactor:负载因子,表示当前哈希表的最大填满比例。当threshold * loadFactor < 当前哈希表中桶数目时,哈希表的threshold需要扩大为当前的2倍。
- HashMap的使用场景:当需要存储键值对时需要使用HashMap,它可以接收key为null
的键值对,但是是非线程同步的。
- HashMap的工作原理:
HashMap底层是数组实现的,数组的每个元素是链表,由Entry内部类实现。HashMap通过put方法存储对象,通过get方法获取对象。
put存储对象时,我们将K/V键值对传给put方法,它首先调用hash方法计算K的hash值,取余HashMap数组长度后获取该键值对所在链表的数组下标,进一步存储时,会适当调整数组大小,并且采用头插法将Entry键值对插入到链表中。
步骤:1)对key的hashCode()做hash,然后再计算index;
2)如果没碰撞直接放到bucket里;
3)如果碰撞了,以链表的形式存在buckets后;
4)如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
5)如果节点已经存在就替换old value(保证key的唯一性)
6)如果bucket满了(超过load factor*current capacity),就要resize。
获取对象时,我们将K传给get方法,也是先调用hash方法计算hash值获取数组中所在链表的下标。然后,顺序遍历链表,查找相同Entry的key的value值。
- LinkedHashMap
LinkedHashMap继承自HashMap,它主要是用链表实现来扩展HashMap类,HashMap中条目是没有顺序的,但是LinkedHashMap通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序,LinkedHashMap中元素既可以按照它们插入map的顺序排序,也可以按它们最后一次被访问的顺序排序。 对于LinkedHashMap而言,它继承与HashMap、底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。
关 注 点 | 结 论 |
LinkedHashMap是否允许空 | Key和Value都允许空 |
LinkedHashMap是否允许重复数据 | Key重复会覆盖、Value允许重复 |
LinkedHashMap是否有序 | 有序(指插入顺序等) |
LinkedHashMap是否线程安全 | 非线程安全 |
注:
- LinkedHashMap可以认为是HashMap+LinkedList,即它既使用HashMap操作数据结构,又使用LinkedList维护插入元素的先后顺序。
- TreeMap
TreeMap基于红黑树数据结构的实现,键值可以使用Comparable或Comparator接口来排序。TreeMap继承自AbstractMap,同时实现了接口NavigableMap,而接口NavigableMap则继承自SortedMap。SortedMap是Map的子接口,使用它可以确保图中的条目是排好序的。
映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。TreeMap的基本操作containsKey、get、put、remove方法,它的时间复杂度是log(n)
TreeMap本质是Red-Black Tree,它包含几个重要的成员变量:root、size、comparator。其中root是红黑树的根节点。它是Entry类型,Entry是红黑树的节点,它包含了红黑树的6个基本组成:key、value、left、right、parent和color。Entry节点根据根据Key排序,包含的内容是value。Entry中key比较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。
- 其他集合类
- Vector
Vector 是矢量队列。和ArrayList不同,Vector中的操作是线程安全的。因为Vector底层是使用数组实现的,所以它的操作都是对数组进行操作,只不过其是可以随着元素的增加而动态的改变容量大小,其实现方法是是使用Arrays.copyOf方法将旧数据拷贝到一个新的大容量数组中。
1) vector可以实现可增长的对象数组。与数组一样,它包含可以使用整数索引进行访问的组件。不过,Vector的大小是可以增加或者减小的,以便适应创建Vector后进行添加或者删除操作。
2)Vector实现List接口,继承AbstractList类,所以我们可以将其看做队列,支持相关的添加、删除、修改、遍历等功能。
3)Vector实现RandmoAccess接口,即提供了随机访问功能,提供提供快速访问功能。在Vector我们可以直接访问元素。
4)Vector 实现了Cloneable接口,支持clone()方法,可以被克隆。
- ConcurrentHashMap
前提:HashMap中未进行同步考虑,而Hashtable则使用了synchronized,带来的直接影响就是可选择,我们可以在单线程时使用HashMap提高效率,而多线程时用Hashtable来保证安全。通过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,安全的背后是巨大的浪费。
左边便是Hashtable的实现方式---锁整个hash表;而右边则是ConcurrentHashMap的实现方式---锁桶(或段)。ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。
ConcurrentHashMap只有在求size等操作时才需要锁定整个表。而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。
ConcurrentHashMap中主要实体类就是三个:
1) ConcurrentHashMap(整个Hash表);
2) Segment(桶);
3) HashEntry(节点)
ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
- CopyOnWriteArrayList
CopyOnWriteArrayList,是一个线程安全的List接口的实现,它使用了ReentrantLock锁来保证在并发情况下提供高性能的并发读取。
Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
CopyOnWriteArrayList中add方法的实现(向CopyOnWriteArrayList里添加元素),可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。
读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
缺点:
- 内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。
- 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。