Collection接口是集合类的根接口,Java中没有提供这个接口的直接的实现类。但是却让其被继承产生了两个接口,就是Set和List。Set中不能包含重复的元素。List是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。
Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分。Map包含了key-value对。Map不能包含重复的key,但是可以包含相同的value。
Iterator,所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含以下三种方法: 1.hasNext()是否还有下一个元素。 2.next()返回下一个元素。 3.remove()删除当前元素。
有索引,有序可重复。ArrayList与Vector底层是数组,查询快增删慢。LinkedList底层是双向链表,查询慢增删快。ArrayList,LinkedList都是线程不安全,Vector线程安全。
List遍历
①普通for循环遍历List删除指定元素
for(int i=0; i < list.size(); i++){
if(list.get(i) == 5)
list.remove(i);
}
② 迭代遍历,用list.remove(i)方法删除元素
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
Integer value = it.next();
if(value == 5){
list.remove(value);
}
}
③foreach遍历List删除元素
for(Integer i:list){
if(i==3) list.remove(i);
}
无索引。无序不重复,Set实质上使用的是Map的Key存储,如果要将自定义的类存储到Set中,需要重写equals和hashCode方法。HashSet底层是通过Hashmap来实现的,HashSet是无序不重复的,且不能排序,集合元素可以是null,但只能放入一个null
LinkedHashSet底层是链表(保证有序)+哈希表(保证集合的唯一性),查询慢增删快,它是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的顺序所以遍历的时候会按照添加时的顺序来访问。
TreeSet底层是红黑树,一般用于排序,可以使用compareTo进行排序方法来比较元素之间大小关系,然后将元素按照升序排列,有序。
Map: Key无序不重复,Value可重复。
HashMap底层是数组+链表,它根据键的HashCode值存储数据,根据键可以直接获取它的值,访问速度很快。所以在Map中插入、删除和定位元素比较适合用hashMap。
LinkedHashMap底层是链表+哈希表,它是HashMap的一个子类,如果需要读取的顺序和插入的相同,可以用LinkedHashMap来实现。
TreeMap底层是红黑树,与TreeSet类似,取出来的是排序后的键值对。但如果是要按自然顺序或自定义顺序遍历键,那么TreeMap会更好,有序。
底层是 数组和链表 结合在⼀起使⽤也就是链表散列。如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。扩容翻转时顺序不一致使用头插法会产生死循环,导致cpu100%
底层数据结构上采用了数组+链表+红黑树;当链表⻓度⼤于阈值(默认为 8-泊松分布),数组的⻓度大于 64时,链表将转化为红⿊树,以减少搜索时间。(解决了tomcat臭名昭著的url参数dos攻击问题)
1.首先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
2.由于底层数据结构不同,他们所适用的场景也不同,ArrayList更适合查找,LinkedList更适合删除和添加
3.另外ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使用
4.ArratList的底层使用动态数组,默认容量为10,当元素数量到达容量时,生成一个新的数组,大小为前一次的1.5倍,然后将原来的数组copy过来;
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,value统一为present,因此 HashSet 的实现比
较简单,相关 HashSet 的操作,基本上都是直接调用底层HashMap 的相关方法来完成,HashSet 不允许重复的值。
List:有序、可重复集合。按照对象插入的顺寻保存数据,允许多个Null元素对象,可以使用iterator迭代器遍历,也可以使用get(int index)方法获取指定下标元素。
Set:无序、不可重复集合只允许有一个Null元素对象,取元素时,只能使用iterator迭代器逐一遍历。
Map : key-value键值对形式的集合,添加或获取元素时,需要通过key来检索到value。
HashMap 基于 Hash 算法实现的:
1.当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
2.存储时,如果出现hash值相同的key,此时有两种情况。
(1)如果key相同,则覆盖原始值;
(2)如果key不同(出现冲突),则将当前的key-value放入链表中
3.获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
4.理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
数组加链表(1.8以前),1.8之后添加了红黑树,基于hash表的map接口实现
阈值(边界值)>8并且桶位数(数组长度)大于64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。
(1)HashTable的每个方法都用synchronized修饰,因此是线程安全的,但同时读写效率很低
(2)HashTable的Key不允许为null
(3)HashTable只对key进行一次hash,HashMap进行了两次Hash
(4)HashTable底层使用的数组加链表
(5)HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
在HashMap扩容的是时候会调用resize()方法中的transfer()方法,在这里由于是头插法所以在多线程情况下可能出现循环链表,所以后面的数据定位到这条链表的时候会造成数据丢失。和读取的可能导致死循环。
1.并发修改导致数据不一致
HashMap的数据结构是基于数组和链表实现的。在进行插入或删除操作时,如果不同线程同时修改同一个位置的元素,就会导致数据不一致的情况。具体来说,当两个线程同时进行插入操作时,假设它们都要插入到同一个数组位置,并且该位置没有元素,那么它们都会认为该位置可以插入元素,最终就会导致其中一个线程的元素被覆盖掉。此外,在进行删除操作时,如果两个线程同时删除同一个元素,也会导致数据不一致的情况。
2.并发扩容导致死循环或数据丢失
当HashMap的元素数量达到一定阈值时,它会触发扩容操作,即重新分配更大的数组并将原来的元素重新映射到新的数组上。然而,在进行扩容操作时,如果不加锁或者加锁不正确,就可能导致死循环或者数据丢失的情况。具体来说,当两个线程同时进行扩容操作时,它们可能会同时将某个元素映射到新的数组上,从而导致该元素被覆盖掉。此外,在进行扩容操作时,如果线程不安全地修改了next指针,就可能会导致死循环的情况。
(1)使用ConcurrentHashMap
(2)使用HashTable
(3)Collections.synchronizedHashMap()方法
1、判断数组是否为空,为空进行初始化;
2、不为空,则计算 key 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
3、查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
4、存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据;
5、若不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;
6、若不是红黑树,创建普通Node加入链表中;判断链表长度是否大于 8,大于则将链表转换为红黑树;
7、插入完成之后判断当前节点数是否大于阈值,若大于,则扩容为原数组的二倍
通过调用key的hashCode方法获取哈希值找到存放的数组下标,通过遍历此位置的key与插入的key通过equals比较,如果已存在则替换
值,不存在则插入进来。
Map实现类调用entrySet方法获得一个Entry类型的Set,通过遍历这个Set集合获取Entry调用getKey或者getValue获取值
ConcurrentHashMap 使用分段锁的方式来实现线程安全,它将一个大的哈希表分成多个小的哈希表(段),每个小的哈希表都有自己的锁。这样,不同的线程可以同时访问不同的小哈希表,从而避免了多个线程同时竞争同一个锁的情况,提高了并发性能。
ConcurrentHashMap 的扩容机制与 HashMap 类似,它会在哈希表的负载因子达到阈值时进行扩容。扩容的过程中,ConcurrentHashMap 会将原来的小哈希表逐一复制到新的大哈希表中,这个过程中仍然可以保证线程安全。扩容后,ConcurrentHashMap 会继续使用分段锁的方式来维护新的小哈希表。
ConcurrentHashMap 的 get() 方法不需要加锁,因为它是线程安全的。在并发访问时,ConcurrentHashMap 使用了 volatile 和 CAS 等机制来保证数据的一致性和可见性,所以可以保证多个线程同时访问时不会出现数据竞争和不一致的情况。
ConcurrentHashMap 和 Hashtable 都是线程安全的哈希表,但是它们有很大的区别。ConcurrentHashMap 使用了分段锁的方式来提高并发性能,而 Hashtable 使用了一个全局锁来保证线程安全,所以并发性能比 ConcurrentHashMap 差很多。此外,ConcurrentHashMap 允许空键和空值,而 Hashtable 不允许。另外,ConcurrentHashMap 支持更多的操作,比如 ConcurrentHashMap 支持的批量操作和原子操作等,Hashtable 不支持。
1.开放定址法
当关键字key的哈希地址p =H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,若p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
即:Hi=(H(key)+di)% m (i=1,2,…,n)
开放定址法有下边三种方式:
线性探测再散列 顺序查看下一个单元,直到找出一个空单元或查遍全表 di=1,2,3,…,m-1 二次(平方)探测再散列 在表的左右进行跳跃式探测,直到找出一个空单元或查遍全表 di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 ) 伪随机探测再散列 建立一个伪随机数发生器,并给一个随机数作为起点 di=伪随机数序列。具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。
优点
容易序列化 若可预知数据总数,可以创建完美哈希数列
缺点
占空间很大。(开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间) 删除节点很麻烦。不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。
2.再哈希法
提供多个哈希函数,如果第一个哈希函数计算出来的key的哈希值冲突了,则使用第二个哈希函数计算key的哈希值。
优点
不易产生聚集
缺点
增加了计算时间
3.链地址法(hashmap使用此法)
对于相同的哈希值,使用链表进行连接
优点
处理冲突简单,无堆积现象。即非同义词决不会发生冲突,因此平均查找长度较短; 适合总数经常变化的情况。(因为拉链法中各链表上的结点空间是动态申请的) 占空间小。装填因子可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计 删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
缺点
查询时效率较低。(存储是动态的,查询时跳转需要更多的时间) 在key-value可以预知,以及没有后续增改操作时候,开放定址法性能优于链地址法。 不容易序列化
4.建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时 候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这 个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集 合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next() 遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍 历;否则抛出异常,终止遍历。 解决办法: 在遍历过程中,所有涉及到改变modCount值的地方全部加上synchronized。 使用CopyOnWriteArrayList来替换ArrayList