Java中常用集合:Map 和 Collection 接口相关实现类
|- Collection
|-List
|-ArrayList
|-LinkedList
|-Vector
|-CopyOnWriteArrayList (JUC包)
|-Set
|-HashSet
|-LinkedHashSet
|-TreeSet
|- Map
|-HashMap
|-Hashtable
|-ConcurrentHashMap (JUC包)
|-TreeMap
一、List
1、ArrayList
底层基于数组实现,数据保存在了elementData属性中。
* 当创建时不传入大小或者传入的为0, elementData = {}。此时第一次add elementData的大小将扩容至10
* 之后使用add方法添加时判断是否需要扩容(当前数组大小 < 添加新元素后的大小),扩容规则为 newCapacity = oldCapacity + (oldCapacity >> 1)
* 扩容的新数组将会重新申请一块连续空间,然后将数据拷贝过去。
* 常用方法为 add 、 get、 remove
* 插入删除效率不高。应为是基于数组实现,删除时后面所有的内存块都需要移动。插入时扩容需要重新申请空间然后进行老到新的复制比较耗时
* 不支持并发
其他点
* elementData属性 使用transient修饰符
* transient用来表示一个域不是该对象序行化的一部分,
* 当一个对象被序行化的时候,transient修饰的变量的值是不包括在序行化的表示中的。
* 为什么要用transient修饰符?
* 原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,
* 那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化
* 实际存储的那些元素,而不是整个数组,从而节省空间和时间。
* ArrayList在序列化的时候会调用writeObject,直接将size和element写入ObjectOutputStream;
* 反序列化时调用readObject,从ObjectInputStream获取size和element,再恢复到elementData。
2、LinkedList
linkedList底层使用双向链表结构, 插入删除效率高,查询和随机访问的效率不高(按照位置访问时也需要从first几点一个 一个向下找)
3、 Vector
数据存储结构和ArrayList类似,对操作方法加锁。
4、CopyOnWriteArrayList
JUC包下的线程安全的ArrayList 。写入时复制(CopyOnWrite,简称COW)思想 ,读写分离。读时不加锁,写加锁。并且当写入时会先复制一份原本的数组,不影响在此期间的度操作。当写入完成之后将数组引用指向复制后的新地址。
List小结 :
1、ArrayList 、Vector、 CopyOnWriteArrayList的区别?
Vector使用synchronized对get、add等方法进行了同步设置,是线程安全集合。arrayList没有任何同步机制,线程不安全
CopyOnWriteArrayList是线程安全的并且比Vector效率更高。因为只在写入时加锁。
2、 ArrayList和LinkedList 的区别
两者底层实现不同,arrayList使用的是数组实现,linkedlist 使用的是双向链表。
由于数组是一块连续的固定大小的内存空间,当arrayList插入时如果达到最大容量需要扩容,
还需要进行老到新的数据全量拷贝,耗时较长。并且在数据中插入除了在尾部插入,在其他数组任何位置插入都需要移动部分数组元素,比较耗时。
LinkedList不需要有扩容这一操作,双向链表可以是分散的内存空间,对于插入操作也可快速操作。但是如果需要随机访问,
链表需要从头结点开始直到到达访问节点,所以访问空间复杂度O(n/2)不如数组快捷。
二、Map接口
1、HashMap
底层使用数组+链表+红黑树实现。 JDK1.7之前只有数组+链表,1.8进行优化,当链表节点大于8时链表将转化为红黑树形式,红黑树结构下当小于6时退化为链表。两种数据结构中,数组作为哈希桶,链表是为了解决哈希冲突。HashMap初始化的时候,数组的默认大小为16,还会有一个默认的加载因子0.75,然后通过加载因子和数组大小计算出一个阈值,当数组里已经使用的元素大于这个阈值时,会触发扩容resize()方法进行扩容到原来的两倍(数组当前大小进行左移以为操作),然后重新哈希寻找各自的位置。在1.7之前扩容这个地方,使用的是头部插入,当扩容过程中使用get操作的时候容易出现循环链表,在1.8的时候对这个做了优化,使用尾部插入。不会出现循环链表,但是还是线程不安全的。如果需要保证线程安全的需要使用ConcurrentHashMap、Hashtable。推荐使用的是ConcurrentHashMap
那么问题来了
为什么选择6和8中间差一个7呢,不直接使用8呢?
这是因为防止当进行插入和删除频繁操作时,链表和树之间的频繁转换消耗性能。
为什么使用红黑树呢?
当链表节点多了以后,遍历链表的效率会变低,影响整体性能。红黑树作为一种平衡二叉树,元素很多的情况下,层级会控制的很好。对于n个元素的链表遍历的时间复杂度为O(n),但是红黑树的遍历复杂度只是O(logN)。对于AVL树来说,红黑树在查询复杂度上和AVL树差别不大,但是对于最求绝对平衡来说,红黑树在插入和删除上效率会更高。
为什么不直接采用红黑树呢,而是 链表+红黑树?
查询和插入删除的时间平衡,红黑树的查询虽然快,但是在数据小于8的时候,插入的时候依然伴随着左右旋操作,耗时会比链表插入长。
HashMap中如果自定义key元素需要重写hashcode和equels方法?
hashCode 和equels作为Object方法,但是默认实现hashCode为内存地址。hashCode方法在Hashmap中,相同的对象hash值必须一样,但是我们知道地址不一样对象也可能一样,所以必须重写hashCode方法。在put 和get操作时会用到对比hash值和equels方法,虽然相同的对象hash值一样,但是也会存在不同的对象hash值一样的情况,而Object默认实现equels是通过hashcode比较的,所以要重写equels方法确保对象一致。
2、Hashtable
类似于HashMap 数据结构,对多操作方法加synchronized 关键字进行并发控制。
3、ConcurrentHashMap
是在HashMap的基础上实现线程安全而且兼顾了效率。在1.7之前,ConcurrentHashMap使用的分段锁的技术,初始化时会有一个并发数参数,默认是16。会存在两种数组结构。当元素增多时,锁的粒度会越来越大。到1.8时,放弃了分段锁,使用的是CAS+synchronized,当插入头结点的时候使用CAS(比较-交换),当。已经有头结点时,使用synchronized将头结点锁住然后进行添加。这个过程会自旋添加直到成功。比如所第一次进来发现头节点没有值,然后进行下一次循环发现有值之后如果发现正在扩容,该线程会帮助扩容线程进行扩容,如果发现没有扩容,加锁然后把数据加入链表。