Java集合主要用来存储数值数据和引用数据
Java集合框架图
- 圆点为接口
- 方框为具体实现类
- Collections和Arrays是集合工具类类
- 集合的顶层都是接口,Collection是单列集合的顶层接口,Map是双列集合的顶层接口
- Collection下的集合类都包含(add,remove,contains,isEmpty)等方法,也都包括Iterator方法,可以使用具体对象.iterator()方法获取迭代器,其中List接口继承了ListIterator接口,可以在使用迭代器进行遍历的过程中,添加删除元素
- SortedSet和SortedMap是可指定排序的接口,TreeSet、SortedMap继承,可以实现自定义排序
List/Set/Map之间的基本区别
- List:有序的,可重复的
- Set:无序的,不可重复的
- Map:使用K-V结构存储的,Key是无序的、不可重复的,Value是无序的、可重复的
List/Set/Map之间的区别不绝对
-
我们常说的集合有序无序是指的是“插入顺序”,是指在插入的时候,插入的顺序是否保持,当遍历集合时是否会按照插入的顺序展示,像TreeSet和TreeMap这种集合底层是红黑树,可以通过构造方法加入比较器进行排序,也就是实现自己的排序,跟插入的顺序无关,所以它不一定是集合的有序无序
-
我们常见的集合中(有无序指插入时的顺序在遍历时是否可以保持):
- 有序的包括:(ArrayList、LinkedList、LinkedHashSet、LinkedHashMap)
- 无序的包括:(HashSet、HashMap、HashTable、TreeSet、TreeMap)
- 可以自定义排序的包括:(TreeMap、TreeSet)
上述的基本区别是指比较笼统的,实际上在Java集合类中不然
- 比如(LinkedHashSet和LinkedHashMap)底层是通过双向链表维持插入顺序,所以说它们是有序的,这里说的是Set的值和Map的Key值是有序的
底层构造
- ArrayList:动态数组
- LinkedList:双向链表
- HashSet:HashMap
- LinkedHashSet: HashSet+双向链表 / LinkedHashMap
- HashMap:数组+链表+红黑树
- LinkedHashMap:HashMap+双向链表
- TreeSet:红黑树
- TreeMap:红黑树
ArrayList:
- 底层是一个动态数组 Object[],默认不指定数组大小为10,在add数据的时候才会开创数组的真正内存空间,并且每次add会判断是否达到数组的最大值,比如10,当添加到10后,想再添加数据,发现添加后长度为11,大于数组空间的长度10,这个时候就会扩容,扩容为当前容量的1.5倍。ArrayList是线程不安全的容器。每次add都是添加到数组的末尾,尾插法
LinkedList:
- 底层是一个双向链表,只能顺序访问数据,可以快速的增加或删除元素,没有初始大小,也没有扩容机制,就是在前面或者后面新增或者删除就好
- 同样长度的ArrayList和LinkedList进行比较,LinkedList占用的内存空间会更多,因为ArrayList是顺序分配内存,LinkedList是随机分配内存,并且每一个节点多占用了指向前后节点的指针。而ArrayList的空间浪费主要体现在Object数组的结尾会预留一定的容量空间
ArrayList和Vector的区别:
- 都是动态数组,ArrayList线程不安全,Vector线程安全
HashMap:
- 1.7之前底层是数组+链表,1.8之后是数组+链表+红黑树。链表主要是为了解决哈希冲突,红黑树主要是为了提高查询的效率
- HashMap是通过Key值的hashcode()值经过“扰动函数”的处理后得到的hash值,然后通过(length-1)& hash判断当前元素所在的位置,如果发生碰撞,进行equals比较,如果Key值是一样的,则直接覆盖,否则加在桶链表的后面,“扰动函数”主要是为了减少碰撞
- “扰动函数”其实就是HashMap的hash方法,是对hashcode()值进行再次hash()。此hash()是对hashcode值与高16位做异或运算,得到最后的hash值,然后通过(length-1)& hash计算出该key所在的index位置
- HashMap是由Entry数组实现,Entry数组本质是一个Hash表,Entry数组中的每个元素称为Bucket桶,桶有自己的索引,所以可以是链表也可以是红黑树
- HashMap的put()过程,先对key的hashcode()取hash后计算下标,看看对应的桶中是否有元素,如果没有元素直接添加,如果有元素,先比较key值,如果key值相同(key为基本数据类型 / 引用数据类型,先比较hashcode,随后用==运算符和equals()来判断该key是否相同)则覆盖value值,若key值不同则插在此桶链表的后面。如果当链表长度大于8并且Entry数组大于等于64则转换为红黑树。如果Entry数组不够64,先resize()对Entry数组扩容。如果红黑树大小为6,会退化成链表
- HashMap resize()扩容,默认的Entry数组长度为16,负载因子为0.75,可以通过构造函数指定。如果Entry数组达到了 16 x 0.75 = 12,此时达到阈值,进行resize()扩容到原先Entry数组长度的2倍。扩容会打乱原本K-V值在Entry数组的位置,resize() 在1.7会重新计算hash,1.8进行了优化,不需要全部重新计算hash
- HashMap Entry数组的长度只能为2次幂的原因,是因为只有大小取2次幂时,才能合理运用「位运算」替代取模(扰动函数:通常使用mod取模计算)
- 链表、红黑树比较:
- 链表:
- 查询速度:o(n)
- 插入速度:o(1)
- 红黑树:
- 查询速度:o(log(n))
- 插入速度:o(log(n))
- 链表:
- HashMap线程不安全可能出现的问题i:
- 线程A put()进去,线程B get()不出来
- 线程A put()修改,线程B put()修改,线程A修改的数据丢失
- 达到resize()阈值时,线程A和线程B同时 put()会造成同时resize(),HashMap可能会变成环
LinkedHashMap:
- 底层结构:HashMap+双向链表
- 继承了HashMap,同时多维护了一个双向链表,实现了插入有序
- 遍历时通过双向链表进行遍历,所以LinkedHashMap的大小不会影响到遍历的性能
TreeMap:
- 底层结构:红黑树
- 通过Comparator实现key的自定义排序,如果TreeMap不指定Comparator进行排序,则会使用自然排序
- 节点的key不能为null,如果为null则无法排序
红黑树特点:
- 红黑树并不追求完美的平衡,也就是叶子结点之间可能高度大于2,为了防止二叉查找树退化成链表,而使用的红黑树
- 红黑树降低了旋转的需求,AVL树基本上每次插入都需要旋转,红黑树最多旋转三次 O(1)
- 搜索的次数 > 插入和删除 选择AVL
- 搜索的次数 < 插入和删除 选择红黑树
HashMap和HashTable的区别:
- HashMap可以存储Null key和Null value,key只能出现一次,value可以出现多次,而HashTable的key和value都不可以存储Null
- HashMap是非线程安全的,HashTable是线程安全的。
- HashMap初始值为16,2倍扩容,HashTable初始值为11,2n+1倍扩容
- HashMap若给定容量初始值,会将其扩充到2的幂次方,而HashTable直接使用给定的初始值
- HashMap底层数据结构为数组+链表+红黑树,HashTable只是数组+链表,因为HashTable不再更新了,两者都为Map接口子类
HashMap和HashSet的区别:
- HashSet底层就是HashMap,在HashMap上源码改动的比较少
- HashMap是K-V结构,而HashSet是Set结构,HashSet底层的Set数据其实是存储在HashMap的Key上的,而HashSet底层的HashMap上的Value存储的是PRESENT常量值
- HashSet在add方法时会调用hashcode()和equals()方法进行HashMap上的Key的比较,如果相同则覆盖,不相同产生哈希冲突加在链表后面或红黑树
ConcurrentHashMap与HashTable的区别:
- 两者都是线程安全的,只不过实现线程安全的方法不同
- 底层数据结构:
- 1.7 ConcurrentHashMap采用数组+链表 1.8 采用数组+链表+红黑树
- 1.7 HashTable采用数组+链表 1.7 还是使用 数组+链表
- 实现线程安全的方式:
- ConcurrentHashMap:
- 1.7: 分段的数组+链表实现,所以采用了分段锁对Entry数组进行分段(Segment),每一把锁只能锁住一段数据
- Segment 数组 + HashEntry 数组 + 链表实现
- Segment实现了ReentrantLock,是一种可重入锁,在需要修改Segment中的数组时,必须先获取Segment的锁
- 1.8: 抛弃了分段锁,转而使用数组+链表+红黑树进行实现,使用Sychronized + CAS进行实现
- Node 数组 + 链表 / 红黑树
- Sychronized只锁住了当前链表和红黑树的首节点,所以只要hash不冲突,就不会产生锁冲突,效率提升非常多
- 1.7: 分段的数组+链表实现,所以采用了分段锁对Entry数组进行分段(Segment),每一把锁只能锁住一段数据
- HashTable:
- 全表锁,使用Synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低
- ConcurrentHashMap:
- 线程安全的粒度:
- ConcurrentHashMap(多个线程put()可能会触发锁,hash不冲突就不会触发锁,多线程读时无锁,Node的val和next使用volatile修饰,读写线程对该变量互相可见):
- 1.7:分段锁(锁住一个Segment的数据)
- 1.8:Sychronized + CAS(锁住链表或红黑树头节点)
- HashTable(多个线程get()、put()都会触发锁):
- 全表锁(所有方法Synchronized)
- ConcurrentHashMap(多个线程put()可能会触发锁,hash不冲突就不会触发锁,多线程读时无锁,Node的val和next使用volatile修饰,读写线程对该变量互相可见):
线程安全/不安全:
- 线程安全:
- Vector(对应ArrayList)
- HashTable(对应HashMap)
- JUC下集合类(CopyOnWriteArrayLIst(对于add()操作使用ReentrantLock进行加锁)、ConcurrentHashMap)
- 线程不安全;
- 其余都为线程不安全