Java集合汇总HashMap、ConcurrentHashMap

java Collection

特点:

  • 高性能
  • 允许不同类型的集合,具有高度的互操作性
  • 扩展和适应简单

java集合继承关系图

在这里插入图片描述

集合与数组区别

  • 数组定长;集合长度可变
  • 数组可以存储任意类型;集合只能存储对象
  • 数组存储的元素为相同类型;集合存储对象类型可以不同

集合各接口的实现类

Collection:List、Set、Queue

List:有序容器,元素可以重复,可有多个null元素,存在索引(ArrayList、LinkedList、Vector)

Set:无序容器元素不可重复,只允许一个null元素(HashSet(LinkedHashSet)、SortedSet(NavigableSet)(TreeSet))

Map:键值对集合,key无序,唯一,value可以重复(HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap)

集合框架底层数据结构

Arraylist: Object数组
Vector: Object数组
LinkedList: 双向循环链表

HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
LinkedHashSet: 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)。

HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
TreeMap: 红黑树(自平衡的排序二叉树)

线程安全的集合

Vector、HashTable、Statck、enumeration

fail-fast机制

当有线程在访问集合时,同时有另外的线程对集合的结构进行修改时就会触发fail-fast机制,返回ConcurrentModificationException 异常

原因:线程访问集合都会拿到一个modCount变量,当有线程对集合进行改变时modCount会改变值;访问集合线程每次迭代拿集合值之前都会判断modCount值是否被改变,发生改变就会抛出异常

解决:对modCount变量加锁

集合迭代器

Iterator 接口提供遍历任何 Collection 的接口,从集合中调用迭代器方法来实例化迭代器。迭代器可以遍历集合中元素也可以移除元素

List<String>  li = new ArrayList<>();
Iterator<String> it =li.iterator();
while(it.hasNext()){
	String obj =it.next();//迭代器获取对象
	it.remove();//迭代器移除对象
}

Iterator 和 ListIterator:

  • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。

  • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)

  • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能

最佳实践:

Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access(支持用for)(不支持如LinkedList)

支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历

如何实现数组和 List 之间的转换:

  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法。

ArrayList和Vector:

  • Vector线程安全
  • ArrayList性能更高
  • Vector每次扩容增加一倍,ArrayList每次扩容百分之五十

多线程场景下如何使用 ArrayList:

Collections 的 synchronizedList 方法将其转换成线程安全的容器

ArrayList 的 elementData 加上 transient 修饰:

ArrayList 实现了 Serializable 接口,ransient 的作用是不希望 elementData 数组被序列化,重写了 writeObject 实现,每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小

队列

java.util.Queue

  • add、remove、element 操作失败抛出异常;
  • offer 操作失败返回 false 或抛出异常,poll、peek 操作失败返回 null;
public interface Queue<E> extends Collection<E> {
    //插入元素,成功返回true,失败抛出异常
    boolean add(E e);

    //插入元素,成功返回true,失败返回false或抛出异常 
    boolean offer(E e);

    //取出并移除头部元素,空队列抛出异常 
    E remove();

    //取出并移除头部元素,空队列返回null 
    E poll();

    //取出但不移除头部元素,空队列抛出异常 
    E element();

    //取出但不移除头部元素,空队列返回null 
    E peek();
}

Queue 作为先进先出队列,只能从头部取元素、插入元素到尾部。Java 同样定义了双向队列 Deque,可以同时在头部、尾部插入和取出元素,接口定义如下所示。Deque 也同样定义了两套队列操作方法,针对头部操作方法为 xxxFirst、针对尾部操作方法为 xxxLast:

  • add、remove、get 操作失败抛出异常;
  • offer 操作失败返回 false 或抛出异常,poll、peek 操作失败返回 null;

Deque 另外还有 removeFirstOccurrence、removeLastOccurrence 方法用于删除指定元素,元素存在则删除,不存在则队列不变。

public interface Deque<E> extends Queue<E> {
    //插入元素到队列头部,失败抛出异常 
    void addFirst(E e);

    //插入元素到队列尾部,失败抛出异常  
    void addLast(E e);

    //插入元素到队列头部,失败返回false或抛出异常 
    boolean offerFirst(E e);

    //插入元素到队列尾部,失败返回false抛出异常  
    boolean offerLast(E e);

    //取出并移除头部元素,空队列抛出异常 
    E removeFirst();

    //取出并移除尾部元素,空队列抛出异常 
    E removeLast();

    //取出并移除头部元素,空队列返回null
    E pollFirst();

    //取出并移除尾部元素,空队列返回null
    E pollLast();

    //取出但不移除头部元素,空队列抛出异常
    E getFirst();

    //取出但不移除尾部元素,空队列抛出异常
    E getLast();

    //取出但不移除头部元素,空队列返回null
    E peekFirst();

    //取出但不移除尾部元素,空队列返回null
    E peekLast();

    //移除指定头部元素,若不存在队列不变,移除成功返回true 
    boolean removeFirstOccurrence(Object o);

    //移除指定尾部元素,若不存在队列不变,移除成功返回true 
    boolean removeLastOccurrence(Object o);
}

Java.util.concurrent.BlockingQueue是一个阻塞队列,在进行检索或移除一个元素的时候,它会等待队列变不为空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

HashMap 实现

HashMap 基于 Hash 算法实现

  • 往Hashmap中put元素,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标

  • 存储时,如果key相同,则覆盖原始值,如果key不同(出现冲突),则将当前的key-value放入链表中

  • 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。

解决hash冲突的问题,核心是使用数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

Jdk 1.8中对HashMap的优化:

  • 当链表中的节点数据超过阈值(默认8)(数组长度大于64)之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

拉链法:

  • 数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;将数组和链表结合在一起,发挥两者各自的优势

JDK1.7与1.8对比:

不同JDK 1.7JDK 1.8
存储结构数组 + 链表数组 + 链表 + 红黑树
初始化方式单独函数:inflateTable()直接集成到了扩容函数resize()
hash值计算方式扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式头插法(先将原位置的数据移到后1位,再插入数据到该位置)尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

HashSet 实现

HashSet的实现基于HashMap来实现的,HashSet的值于HashMap的Key上HashSet的add()方法调用HashMap的put()方法,k为HashSet的值,value为PRESENT(一个虚拟值)

HashSet较HashMap慢

HashMap的put方法

当put的时,首先计算 key的hash值,这里调用了 hash方法,hash方法实际是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//实现Map.put和相关方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 步骤①:tab为空则创建 
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 步骤②:计算index,并对null做处理  
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 步骤③:节点key存在,直接覆盖value 
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // 步骤④:判断该链为红黑树 
        // hash值不相等,即key不相等;为红黑树结点
        // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 步骤⑤:该链为链表 
        // 为链表结点
        else {
            // 在链表最末插入结点
            for (int binCount = 0; ; ++binCount) {
                // 到达链表的尾部
                
                //判断该链表尾部指针是不是空的
                if ((e = p.next) == null) {
                    // 在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                    //判断链表的长度是否达到转化红黑树的临界值,临界值为8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //链表结构转树形结构
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 步骤⑥:超过最大容量就扩容 
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

HashMap的扩容操作

①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;

②.每次扩展的时候,都是扩展2倍;

③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

哈希冲突

把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值),

当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

哈希扰动

数据结构:

在这里插入图片描述

将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以需要对hashCode作一定的优化

hash():

使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,这样的操作称为扰动

static final int hash(Object key) {//jdk1.8
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}

HashMap适合作为K:

用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了

HashMap 与 HashTable

  • 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果要保证线程安全的话就使用 ConcurrentHashMap )

  • 效率:HashMap 要比 HashTable 效率高一点。

对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。在 HashTable (不能有null出现)中 put 进的键值有一个 null,直接抛NullPointerException。

  • 初始容量大小和每次扩充容量大小的不同 :

①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。

②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。 HashMap 总是使用2的幂作为哈希表的大小。

HashMap 和TreeMap

对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。需要对一个有序的key集合进行遍历,TreeMap是更好的选择。

HashMap 和 ConcurrentHashMap

  • ConcurrentHashMap对整个数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)

  • HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

ConcurrentHashMap 和 Hashtable

  • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

实现线程安全的方式(重要):

① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;

② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

ArrayList和LinkedList

ArrayList:底层是动态数组,适合于查询

LinkedList:底层是双向链表,适合于插入、删除

ArrayList扩容:初始默认长度为10,之后扩容按照1.5倍增长扩容(10,15,22,33)

Array 和 ArrayList

  • Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。

  • Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。

  • Array 内置方法没有 ArrayList 多

  • Array 转 List: Arrays. asList(array) ;

  • List 转 Array:List 的 toArray() 方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

snack-counter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值