Java集合框架
Arraylist与LinkedList
1,是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全;
线程安全解决办法 :
方法1: Collections.synchronizedList(new LinkedList())
方法2: LinkedList和ArrayList换成线程安全的集合,如CopyOnWriteArrayList,ConcurrentLinkedQueue…
方法3:Vector(内部主要使用synchronized关键字实现同步)
2.底层数据结构:ArrayList底层使用的是Object动态数组;LinkedList底层使用的是双向链表数据结构,(JDK1.6之
前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别:);
3,插入和删除是否受元素位置的影响:
ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。
比如:执行、add添加方法时(remove用作删除元素),ArrayList会默认将指定元素追加到此列表的末尾,但是如果要在指定位置上插入或删除元素的话,动态数组的元素就会向前或向后执行移一位的操作。
LinkedList采用链表结构,所以插入或者删除不受元素位置的影响,只要指针指向新的值,新值的指针指向下一个值就完成了增加,删除同理,
4,是否支持快速随机访问:LinkedList不支持高效的随机访问,而ArrayList支持,快速随机访问就是通过元素序号快速获取元素对象。相比ArrayList的高效访问,LinkedList的插入删除效率又高于ArrayList,所以需要大量查询操作时,如商城首页的搜索使用ArrayList集合,反之需要大量增删改操作时,如购物车,则推荐使用LinkedList。
5,内存空间占用:ArrayList的空间浪费主要体现在在List列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
下面再总结一下 list 的遍历方式选择:
实现了RandomAccess接口的list,优先选择普通for循环 ,其次foreach,
未实现RandomAccess接口的list, 优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大
size的数据,千万不要使用普通for循环
补充:数据结构基础之双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从
双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表,如
下图所示,同时下图也是LinkedList 底层使用的是双向循环链表数据结构。
ArrayList和Vector区别
ArrayList和Vector基本一样,唯一的区别就是Vector是线程安全的, Vector 是线程同步的,即线程安全, Vector类的操作方法带有synchronized 。
HashMap的底层实现
JDK1.8之前HashMap底层是数组和链表结合在一起使用,也就是链表散列。JDK1.8之后《底层结构变为数组加链表加红黑树,当链表长度大于8的时候链表结构会变为红黑树结构继续存储键值对,红黑树长度小于6时重新变为链表结构》HashMap 通过 key 的 hashCode 经
过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的
长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的
话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的
hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞
“拉链法” :将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希
冲突,则将冲突的值加到链表中即可。
DK1.8之后
相比于之前的版本,JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
HashMap和Hashtable的区别
线程是否安全,HashMap是非线程安全的,HashTable是线程安全的,HashTable内部的方法基本都经过synchronized修饰。(如果你要保证线程安全的话就使用,ConcurrentHashMap吧!);
效率因为线程安全的问题,HashMap要比HashTable效率高,另外,HashTable基本被淘汰,不要在代码中使用。
对Null Key和Null value的支持: HashMap中,null可以作为键,这样的键只有一个,可以有多个值为null,但是在HashTable中put进的键值只要有一个为null,直接抛出空指针异常(NullPointerException)
初始化容量大小和每次扩容量大小的不同: 创建时如果不指定容量初始值,HashTable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。创建时如果给定了容量初始值,那么HashTable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小(HashMap 中的 tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总
是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
底层数据结构:JDK1.8以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。HashTable没有这样的机制。
HashMap的长度为什么是2的幂次方
为了能让HashMap存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲过,Hash值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射的比较均匀松散,一般应用是很难出现碰撞的,但问题是一个40亿长度的数组,内存是放不下的,所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标,这个数组下标的计算方法是“ (n - 1) & hash ”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点是:“取余(%)操作中如果除数是2的幂次则等价于与其
除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采
用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
HashMap多线程操作导致死循环问题
在多线程下,进行put操作会导致HashMap死循环,原因在于HashMap的扩容resize()方法。由于扩容是新建一个数组,复制原数组到数组。由于数组下标挂有链表,所有需要复制链表,但是多线程操作有可能导致环形链表。复制链表过程如下:
这个过程为,先将 A 复制到新的 hash 表中,然后接着复制 B 到链头(A 的前边:B.next=A),本来 B.next=null,
到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,将 B.next=A,所以,这里继续复制A,让
A.next=B,由此,环形链表出现:B.next=A; A.next=B
注意:JDK已经解决了死循环的问题
HashSet和HashMap区别
如果你看过HashSet源码的话就应该知道:HashSet底层就是基于HashMap实现的。(HashSet的源码非常非常少,因为除了clone()方法,writeObject()方法、readObject()方法是 HashSet 自己不得不实现之外,其他方法都是
直接调用 HashMap 中的方法。)
ConcurrentHashMap 和 Hashtable 的区别
并发又安全的ConcurrentHashMap
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同
底层数据结构 JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表+红黑二叉树。HashTable和JDK1.8之前的HashMap的底层数据结构类似都是采用数组+链表的形式,数组是HashMap的主体,链表则是主要为了解决哈西冲突而存在的;
实现线程安全的方式(重要): 直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。
① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了
分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁
竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑
树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很
多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,
但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,
效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用
put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
ConcurrentHashMap线程安全的具体实现方式/底层具体实现
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment实现了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色。HashEntry用于存储键值对数据。
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8
的结构类似,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍
集合框架底层数据结构总结
Collection
1,List
ArrayList:Object数组
Vector:Object数组
LinkedList:双向链表(JDK1.6之前为循环列表,JDK1.7取消了循环)。
2,Set
HashSet (无序,唯一): 基于HashMap实现的,底层采用HashMap来保存元素
LinkedHashSet:LinkedHashSet继承于HashSet,并且其内部是通过LinkedHashMap来实现的。有点类似于我们之前说的LinkedHashMap其内部是基于HashMap实现一样,不过还是有一点点区别。
TreeSet (有序,唯一):红黑树(自平衡的排序二叉树)
Map
HashMap:JDK1.8之前HashMap由数组+链表组成,之后加了红黑树,数组是HashMap的主体,链表则是主要为了解决哈希冲突而设定的,1.8之后。当链表阈值大于8的时候,将链表转换为红黑树,减少搜索时间。
LinkedHashMap LinkedHashMap 继承自HashMap,所以它的底层仍然是基于拉链式散列结构即有数组和链表或红黑树组成,另外,LinkedHashMap在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
HashTable: 数组+链表组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,
TreeMap 红黑树(自平衡的排序二叉树)