Java集合类知识

Java 容器

(部分图片来源于cyc作者)

一、概览

容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。

Collection

Java集合类知识_第1张图片

#### 1. Set
  • TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。

  • HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。

  • LinkedHashSet:具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。

2. List

  • ArrayList:基于动态数组实现,支持随机访问。

  • Vector:和 ArrayList 类似,但它是线程安全的。

  • LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。

3. Queue

  • LinkedList:可以用它来实现双向队列。

  • PriorityQueue:基于堆结构实现,可以用它来实现优先队列。

Map

Java集合类知识_第2张图片

- TreeMap:基于红黑树实现。
  • HashMap:基于哈希表实现。

  • HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 HashTable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。

  • LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。

二、容器中的设计模式

迭代器模式

Java集合类知识_第3张图片

Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。

从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for (String item : list) {
     
    System.out.println(item);
}

适配器模式

java.util.Arrays#asList() 可以把数组类型转换为 List 类型。

@SafeVarargs
public static <T> List<T> asList(T... a)

应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。

Integer[] arr = {
     1, 2, 3};
List list = Arrays.asList(arr);

也可以使用以下方式调用 asList():

List list = Arrays.asList(1, 2, 3);

三、ArrayList和Vector(重点)

ArrayList 是基于数组实现的,所以支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。数组的默认大小为 10。

 //默认容量是10
 private static final int DEFAULT_CAPACITY = 10;

 //说明调用的是有参构造器,初始容量就是自己传的initialCapacity的值
 private static final Object[] EMPTY_ELEMENTDATA = {
     };

 //说明调的是无参构造器,默认容量是10(第1次添加元素时初始化容量为10)
 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
     };

 transient Object[] elementData; // 实际存放元素的地方,transient该关键字声明数组默认不会被序列化。
Java集合类知识_第4张图片

**添加元素并且是否需要扩容:**

添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),即 oldCapacity+oldCapacity/2。其中 oldCapacity >> 1 需要取整,所以新容量大约是旧容量的 1.5 倍左右。(oldCapacity 为偶数就是 1.5 倍,为奇数就是1.5倍-0.5)

扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

		 //添加
        public boolean add(E e) {
     
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }

        private void ensureCapacityInternal(int minCapacity) {
     
            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
        }

        private static int calculateCapacity(Object[] elementData, int minCapacity) {
     
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
     
                return Math.max(DEFAULT_CAPACITY, minCapacity); //如果是通过无参方式构造的 就直接把默认值(10)返回
            }
            return minCapacity; //否则就返回minCapacity
        }

        private void ensureExplicitCapacity(int minCapacity) {
     
            modCount++;  //modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。并发修改异常,而Vector是线程安全的 就不需要modCount
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

         //开始进行扩容
        private void grow(int minCapacity) {
     
            int oldCapacity = elementData.length;//数组原长度
            int newCapacity = oldCapacity + (oldCapacity >> 1); //新长度=10+5=15 右移操作就是除2
            if (newCapacity - minCapacity < 0)  //如果新长度比需要的最小长度还小的话
                newCapacity = minCapacity;   //就把最小长度给新长度
            if (newCapacity - MAX_ARRAY_SIZE > 0) //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 如果新长度比int最大值还大
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
*/

删除元素:

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的。

public E remove(int index) {
     
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

ArrayList它里面的clone()方法是深拷贝

假设B复制了A,修改A的时候,看B是否发生变化:
浅拷贝只是增加了一个指针指向已存在的内存地址
深拷贝是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,跟原对象是相互独立的

ArrayList的subList方法:subList方法只是保存了下标,新集合和原集合共用一个引用:

ArrayList<String> list = new ArrayList<>();
        list.add("e");
        list.add("1");
        list.add("2");
        list.add("3");
        List<String> strings = list.subList(0, 3);
        list.set(0,"6666");
        System.out.println(strings .get(0));//打印 6666  说明subList方法只是保存了下标,新集合和原集合共用一个引用

Arrays.asList方法:

Integer[] arr = {
     1, 2, 3};
//注意asList() 的参数不能使用基本类型数组作为参数,只能使用相应的包装类型数组。
//因为引用类型的数组,a=[]  基本类型数组:a= [long[] 会把他变成二维数组.
List list1 = Arrays.asList(arr);
System.out.println(list1.size());// 1 因为是基本类型的数组,如果把int换成Integer,则size=3

Fail-Fast机制:

modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小。在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException,即并发修改异常

**Vector同步:**它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。

Vector与 ArrayList 的比较

  • Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
  • Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍

替代方案:可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。

List<String> synList = Collections.synchronizedList(new ArrayList<>());

也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

List<String> list = new CopyOnWriteArrayList<>();

四、LinkedList

基于双向链表实现,使用 Node 存储链表节点信息。

Java集合类知识_第5张图片

**LinkedList 与 ArrayList 的比较:**

ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。ArrayList 和 LinkedList 的区别可以归结为数组和链表的区别:

  • 数组支持随机访问,但插入删除的代价很高,需要移动大量元素;
  • 链表不支持随机访问,但插入删除只需要改变指针。

五、HashMap(重点)

参考地址:https://joonwhee.blog.csdn.net/article/details/78996181?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&dist_request_id=1328642.24557.16156230262267211&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control和https://gitee.com/gu_chun_bo/java-construct/blob/d466640dc977a61433fecbbb069ffe7a56d46d1a/java%E9%9B%86%E5%90%88/HashMap.md

面试环节:https://blog.csdn.net/v123411739/article/details/106324537?spm=1001.2014.3001.5502

HashMap底层结构: 数组+链表+红黑树 JDK8是尾插法
内部包含了一个 Node 类型的数组 table。Node 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Node 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的Node

img
基本参数:
	 //无参构造的时候,默认初始化容量为 2^4=16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    static final int MAXIMUM_CAPACITY = 1 << 30; 	//最大容量 2^32

    static final float DEFAULT_LOAD_FACTOR = 0.75f; 	//无参构造的时候,默认负载因子

   //树形化阈值为8 即当链表的长度大于8的时候,会将链表转为红黑树,优化查询效率。链表查询的时间复杂度为o(n) , 红黑树查询的时间复杂度为 o(log n)
    static final int TREEIFY_THRESHOLD = 8;

   //解除树形化阈值,就是当红黑树的节点个数小于等于6时,会将红黑树结构转为链表结构。
    static final int UNTREEIFY_THRESHOLD = 6;

   /*树形化的最小容量为64;前面我们看到有一个树形化阈值,就是当链表的长度大于8的时候,会从链表转为红黑树,其实不一定是这样的。转为红黑树有两个条件:
     ① 链表的长度大于8 
     ② HashMap数组的容量大于等于64  需要两个条件都成立的情况下,链表结构才会转为红黑树,否则还是扩容。*/
    static final int MIN_TREEIFY_CAPACITY = 64;

    //hash表什么时候初始化? 在第一次插入值的时候才初始化
    transient Node<K,V>[] table;

    transient int size;//当前hash表中的元素个数

    transient int modCount;//当前hash表的结构修改次数,这个就是快速失败的机制

    //扩容阈值,也就是初始化的大小 当hash表中的元素超过阈值 就会触发扩容
    int threshold;

    final float loadFactor;//负载因子 使用默认的0.75就行
含有2个参数的构造方法解读:
public HashMap(int initialCapacity, float loadFactor) {
     
        //下面三个if主要是进行合法校验
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity); //如果传入的容量大小不是2的次方,就会找传入值最近的2的次方
    }
      //返回一个大于等于当前值cap的一个数字,并且这个数字一定是2的次方数,比如传10,就会返回16。
      static final int tableSizeFor(int cap) {
     
        int n = cap - 1;//为什么减1呢?如果传入16,则会返回32,变为了2倍浪费了很大的空间
        n |= n >>> 1;   n |= n >>> 2;  n |= n >>> 4;  n |= n >>> 8; n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
那么怎么定位哈希桶数组索引位置?

对于任意给定的对象,只要它的 hashCode() 返回值相同,那么计算得到的 hash 值总是相同的。我们首先想到的就是把 hash 值对 table 长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是模运算消耗还是比较大的,所以就改为了位运算,将高位也参与计算,目的是为了降低 hash 冲突的概率。如下代码:

// 计算key的hash值
static final int hash(Object key) {
      
    int h;
    //如果键为null,就直接存到index为0的位置,否则先拿到key的hashCode值,再将hashCode的高16位参与运算,作用就是让key的hash值的高16位也参与路由运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
int n = tab.length;//获得数组的长度
int index = (n - 1) & hash;// 将(tab.length - 1) 与 hash值进行&运算,获取桶的下标
put 操作
 public V put(K key, V value) {
     
        return putVal(hash(key), key, value, false, true);
    }

    //首先来看hash(key)方法,通过计算返回一个hash值
      static final int hash(Object key) {
     
        int h;
       //如果键为null,就直接存到index为0的位置,否则就让hash值和它右移16位之后的值进行异或运算,作用就是让key的hash值的高16位也参与路由运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
     
    //tab:引用当前hashMap的散列表  p: 表示当前散列表的元素  n:散列表数组的长度  i: 表示路由寻址的下标
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        //延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中的最耗费内存的散列表
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        //i = (n - 1) & hash 这个就是确定桶的下标的
        //如果当前桶下标的值为null,说明还没有存值,直接封装为一个Node节点放在该位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

        else {
     
        //e: nodel临时元素  k: 表示临时的一个key
            Node<K,V> e; K k;
        //如果插入的节点与当前节点的key一样,则更新value值就行
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
          //前面不满足 说明key不一样,则如果是树结构的话,就根据红黑树的插入规则进行插入
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //前面 不是树,那就只有是链表
            else {
     
                //遍历链表
                for (int binCount = 0; ; ++binCount) {
     
                    //如果当前结点的下一个结点是null,就将key value 进行封装 插入到末尾
                    if ((e = p.next) == null) {
     
                        p.next = newNode(hash, key, value, null);
                        //插入完成之后 判断是否达到了树的阈值,如果达到了,就要进行树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果在遍历链表的时候 找到了一个key一样的 就更新这个节点的value值
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

            //更新值,返回旧值
            if (e != null) {
      // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //插入新元素之后,size+1,如果达到了扩容阈值,就会触发扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
resize扩容

为什么需要扩容?缓解hash冲突导致的链表过长的问题,缓解查询效率:

final Node<K,V>[] resize() {
     
        Node<K,V>[] oldTab = table; //引用扩容之前的表
        int oldCap = (oldTab == null) ? 0 : oldTab.length;  //获得扩容之前数组的长度
        int oldThr = threshold;  //获得扩容之前的阈值
        int newCap, newThr = 0; //newCap:扩容之后数组的大小 newThr: 扩容之后,再次触发扩容的条件

        //条件老表长度>0 说明hashMap中的散列表数组已经初始化过了,此次是正常扩容
        if (oldCap > 0) {
     
            //如果扩容之前的数组长度已经大于了可设置的最大值,则不扩容,设置扩容条件为int的最大值,这种情况还是比较少的
            if (oldCap >= MAXIMUM_CAPACITY) {
     
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //否则就让oldCap左移1位实现数值大小翻倍赋值给newCap 如果newCap小于数组的可设置的最大程度且原数组的oldCap >= 了默认的16 也让新的扩容阈值等于原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

        /*下面两种情况oldCap=0也就是散列表没有初始化,为null,第一次添加元素需要初始化 能进入此逻辑说明			是通过有参构造的,因为这三个构造器都能给threshold赋值
        1.new HashMap( initCap, loadFactor) ;
        2. new HashMap(initCap)
        3. new HashMap(map); 并且这个map有数据
        直接让数组的大小等于threshold,因为前面计算的threshold一定是2的次方*/
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

        //这种情况就是无参构造 也就是直接 new HashMap()。设置散列表大小为16,扩容阈值为0.75*16=12
        else {
                    
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //newThr为零时,通过newCap和loadFactor计算出一个newThr
        if (newThr == 0) {
     
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        //上面的事情就是计算新数组的大小以及扩容的阈值

       Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创造出一个更长更大的数组
       table = newTab; //赋值给table

        //如果旧表不为空 表明扩容之前有数据
        if (oldTab != null) {
     
        //就进入循环,一个桶一个桶的处理
            for (int j = 0; j < oldCap; ++j) {
     
                Node<K,V> e;//当前节点
                
                //如果当前桶的头结点不为空
                if ((e = oldTab[j]) != null) {
     
                    //将该桶置空,方便回收,因为该节点的值已经赋值给e了,所以就直接操作e
                    oldTab[j] = null;
                    //满足此条件 说明当前位置只有一个数据,直接把该节点数据放到新表对应的位置 
                    if (e.next == null)
                        //index = (length - 1) & hash 这个就是确定桶的下标的
                        newTab[e.hash & (newCap - 1)] = e;
                    
                    //如果是树形结构,就树化操作
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        
                    //否则就进行链表操作,高低位
                    else {
      
                     // 低位链表:存放在扩容之后的数组下标的位置,与当前数组下标位置一致的元素
                     // 高位链表:存放在扩容之后的数组下标的位置 为当前数组下标位置+ 扩容之前数组长度的元素
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                       // 循环操作,将链表拆分为高低链
                        do {
     
                            next = e.next;
                            //如果当前hash值 & 旧数组大小 == 0 就放在低位链
                            if ((e.hash & oldCap) == 0) {
     
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //否则就放在高位链
                            else {
     
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //将低位链的最后一个指向null,并且把它放在与原数组index一样的位置
                        if (loTail != null) {
     
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                         //将高位链的最后一个指向null,并且把它放在新数组index下标为(原数组index+原数组长度)的位置
                        if (hiTail != null) {
     
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
get方法分析
public V get(Object key) {
     
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

   final Node<K,V> getNode(int hash, Object key) {
     
        // tab:引用当前hashmap的table   first:桶位中的头元素
        // n:table的长度  e:是临时Node元素  k:是key的临时变量
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

       // 1.如果哈希表为空,或key对应的桶为空,返回null,否则进入到if中
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
     

            // 2.这个桶的头元素就是想要找的,直接返回这个头元素
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;

            // 说明当前桶位不止一个元素,可能是链表,也可能是红黑树
            if ((e = first.next) != null) {
     
                // 3.树化了
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);

                // 4.链表
                do {
     
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
remove方法分析
public V remove(Object key) {
     
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
    }

    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
     
        // tab:引用当前hashmap的table  p:当前的node元素
        // n:当前的散列表数组长度  index:表示寻址结果的下标
        Node<K,V>[] tab; Node<K,V> p; int n, index;

        // 1.如果数组table为空或key映射到的桶为空,返回null。否则进入if条件进行查找
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
     

            // node:查找到的结果  e:当前Node的下一个元素
            Node<K,V> node = null, e; K k; V v;

            // 2.如果桶位的头元素就是我们要找的
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;

            else if ((e = p.next) != null) {
     
                // 3.如果是树形结构
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                // 4.如果是链表
                else {
     
                    do {
     
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
     
                            node = e; //找到了就赋值给node 然后跳出循环
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }

            // 如果node不为null,说明按照key查找到想要删除的数据了
            if (node != null && (!matchValue || (v = node.value) == value ||
                    (value != null && value.equals(v)))) {
     
                // 是树,删除节点
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                // 删除桶的第一个元素
                else if (node == p)
                    tab[index] = node.next;
                // 不是第一个元素,就删除链表上的那个节点
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
hash函数的作用

当我们有一个需求,比如40亿个数,然后内存只有1G的大小,统计数中出现最多的数?

我们最简单的思路就是使用HashMap,然后遍历,每新出现一个就加到map中,然后初始化为1,遇到一样的就进行++操作。但是表中,key和value都是整型,4个字节,所以一个数就会需要8个字节来统计,如果出现最坏的情况,每个数都不一样,则需要8*40亿字节大小=32G > 1G,这个时候内存就会出现不够用的情况。所以换个思路。

我们可以设计一个hash函数,再创建100个小文件。然后让当前的数据进行hash,得到hash值后,然后再模上100,则相同的数或者成100倍数的数都可以均匀分到100个小文件。然后再对这100个小文件分别使用HashMap统计每个小文件的最大数,统计完一个小文件,内存释放掉,然后统计另外一个小文件,然后再比较获得最大的数

总结

增加、删除、查找时,定位到哈希桶数组的位置是很关键的一步,源码中是通过下面3个操作来完成这一步:

​ 1)拿到 key 的 hashCode 值;

​ 2)将 hashCode 的高位参与运算,重新计算 hash 值;

​ 3)将计算出来的 hash 值与 “table.length - 1” 进行 & 运算。

HashMap 的默认初始容量(capacity)是 16,capacity 必须为 2 的幂次方;默认负载因子(load factor)是 0.75;实际能存放的节点个数(threshold,即触发扩容的阈值)= capacity * load factor。

HashMap 在触发扩容后,阈值会变为原来的 2 倍,并且会对所有节点进行重 hash 分布,重 hash 分布后节点的新分布位置只可能有两个:“原索引位置” 或 “原索引+oldCap位置”。例如 capacity 为16,索引位置 5 的节点扩容后,只可能分布在新表 “索引位置5” 和 “索引位置21(5+16)”。导致 HashMap 扩容后,同一个索引位置的节点重 hash 最多分布在两个位置的根本原因是:1)table的长度始终为 2 的 n 次方;2)索引位置的计算方法为 “(table.length - 1) & hash”。

HashMap 是非线程安全的,在并发场景下使用 ConcurrentHashMap 来代替,该源码可以看我写的JUC笔记

与 Hashtable 的比较:

  • HashMap 是非线程安全的,Hashtable是线程安全的,Hashtable 使用 synchronized 来进行同步。
  • HashMap 可以插入键为 null 的 Entry, Hashtable 不允许 。
  • HashMap 的迭代器是 fail-fast (快速失败)迭代器,有一个modCount 用来记录结构发生变化的次数。在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出ConcurrentModificationException,即并发修改异常。
  • HashMap 的 hash 值重新计算过,它根据(size-1)& hash来计算下标,Hashtable 直接使用 hashCode。

六、LinkedHashMap

存储结构

继承自 HashMap,因此具有和 HashMap 一样的快速查找特性。

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。

LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。

void afterNodeAccess(Node<K,V> p) {
      }
void afterNodeInsertion(boolean evict) {
      }

afterNodeAccess()

当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。

void afterNodeAccess(Node<K,V> e) {
      // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
     
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
     
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

afterNodeInsertion()

在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。

evict 只有在构建 Map 的时候才为 false,在这里为 true。

void afterNodeInsertion(boolean evict) {
      // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
     
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
     
    return false;
}

LRU 缓存

以下是使用 LinkedHashMap 实现的一个 LRU 缓存:

  • 设定最大缓存空间 MAX_ENTRIES 为 3;
  • 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
  • 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
     
    private static final int MAX_ENTRIES = 3;

    protected boolean removeEldestEntry(Map.Entry eldest) {
     
        return size() > MAX_ENTRIES;
    }

    LRUCache() {
     
        super(MAX_ENTRIES, 0.75f, true);
    }
}
public static void main(String[] args) {
     
    LRUCache<Integer, String> cache = new LRUCache<>();
    cache.put(1, "a");
    cache.put(2, "b");
    cache.put(3, "c");
    cache.get(1);
    cache.put(4, "d");
    System.out.println(cache.keySet());
}
[3, 1, 4]

你可能感兴趣的:(java知识点复习,java,map,java)