面试笔记——Java集合篇

Java集合框架体系

在这里插入图片描述
重点:单列集合——ArrayList、LinkedList;双列集合——HashMap、ConcurrentHashMap。

List相关

数组(Array) 是一种用连续的内存空间存储相同数据类型数据的线性数据结构。
数组获取其他元素:
在这里插入图片描述
为什么数组索引从0开始呢?假如从1开始不行吗?

  • 在根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式是:数组的首地址+索引乘以存储数据的类型大小在这里插入图片描述
    在这里插入图片描述
  • 如果数组的索引从1开始,寻址公式中,就需要增加一次减法操作,对于CPU来说就多了一次指令,性能不高。
    在这里插入图片描述
    查找的时间复杂度:
  • 随机(通过下标)查询的时间复杂度是O(1)
  • 查找元素(未知下标)的时间复杂度是O(n)
  • 查找元素(未知下标但排序)通过二分查找的时间复杂度是O(logn)

插入和删除时间复杂度:

  • 插入和删除的时候,为了保证数组的内存连续性,需要挪动数组元素,平均时间复杂度为O(n)
ArrayList源码分析

成员变量:

    /**
     * 默认初始的容量(CAPACITY)
     */
    private static final int DEFAULT_CAPACITY = 10;
    /**
     * 用于空实例的共享空数组实例
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};
    /**
     * 用于默认大小的空实例的共享空数组实例。
     * 我们将其与 EMPTY_ELEMENTDATA 区分开来,以了解添加第一个元素时要膨胀多少
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    /**
     * 存储 ArrayList 元素的数组缓冲区。 ArrayList 的容量就是这个数组缓冲区的长度。
     * 当添加第一个元素时,任何具有 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList
     * 都将扩展为 DEFAULT_CAPACITY
     * 当前对象不参与序列化
     */
    transient Object[] elementData; // non-private to simplify nested class access
    /**
     * ArrayList 的大小(它包含的元素数量)
     *
     * @serial
     */
    private int size;

构造方法:
构造方法1——带初始化容量的构造函数:

	public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    } 

构造方法2——无参构造函数,默认创建空集合:

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

构造方法3——将collection对象转换成数组,然后将数组的地址的赋给elementData

    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

关键方法
第一次添加数据(调用add方法添加数据)流程:
在这里插入图片描述
第十次添加数据:
在这里插入图片描述
第十一次添加数据:
在这里插入图片描述

总结:

  • ArrayList的底层使用动态的数组实现的;
  • 它的初始容量为0,当第一次添加数据的时,会初始化容量为10;
  • 在扩容后,容量是原来容量的1.5倍,每次扩容都需要拷贝数据;
  • 添加逻辑:
    • 确保数组已使用长度(size)加1之后足够存下下一个数据​
    • 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
    • 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。​
    • 返回添加成功布尔值。

问题: ArrayList list=new ArrayList(10)中的list扩容几次?
回答: 该语句只是声明和实例了一个 ArrayList,指定了容量为 10,未扩容。

Array与List相互转换

  • 数组转List ,使用JDK中java.util.Arrays工具类的asList方法
  • List转数组,使用List的toArray方法。无参toArray方法返回 Object数组,传入初始化长度的数组对象,返回该对象数组。
    //数组转List
    public static void testArray2List() {
        String[] strs = {"aaa", "bbb", "ccc"};
        List<String> list = Arrays.asList(strs);
        for (String s : list) {
            System.out.println(s);
        }
    }

    //List转数组
    public static void testList2Array() {
        List<String> list = new ArrayList<String>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        String[] array = list.toArray(new String[list.size()]);
        for (String s : array) {
            System.out.println(s);
        }
    }

注意:

  • Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址。源码如下图所示:
    在这里插入图片描述

  • list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响。源码如下图所示:
    在这里插入图片描述
    测试代码:

    //数组转List
    public static void testArray2List() {
        String[] strs = {"aaa", "bbb", "ccc"};
        List<String> list = Arrays.asList(strs);
        for (String s : list) {
            System.out.println(s);
        }
        strs[1] = "ddd";
        System.out.println("================");
        for (String s : list) {
            System.out.println(s);
        }
    }

    //List转数组
    public static void testList2Array() {
        List<String> list = new ArrayList<String>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        String[] array = list.toArray(new String[list.size()]);
        for (String s : array) {
            System.out.println(s);
        }
        list.add("ddd");
        System.out.println("================");
        for (String s : array) {
            System.out.println(s);
        }
    }

ArrayList和LinkedList的区别

  1. 底层数据结构

    • ArrayList 使用动态数组实现,它内部维护一个 Object 数组,在添加或删除元素时会涉及到数组的扩容和拷贝操作。
    • LinkedList 使用双向链表实现,它的每个元素都包含了对前后元素的引用,添加或删除元素时只需要修改相邻节点的引用,不涉及数组的扩容和拷贝操作。
  2. 访问效率

    • ArrayList 支持随机访问(通过索引访问),因为它是基于数组实现的,可以通过索引直接定位到元素。因此,对于随机访问的操作效率比较高,时间复杂度为 O(1)。
    • LinkedList 不支持随机访问,因为它是基于链表实现的,需要从头节点或尾节点开始顺序查找,时间复杂度为 O(n),其中 n 为链表长度。但在插入和删除操作上,LinkedList 由于不涉及数组的扩容和拷贝,且只需修改相邻节点的引用,因此插入和删除操作的效率较高,时间复杂度为 O(1)。
  3. 新增和删除

    • ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)。
    • LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)。
  4. 空间复杂度

    • ArrayList 在添加元素时可能需要进行数组的扩容,而扩容操作需要重新分配内存空间并将原数组的元素拷贝到新数组中,因此可能会导致额外的内存消耗。
    • LinkedList 每个元素都需要额外的空间来存储前后节点的引用,因此相比 ArrayList 需要更多的内存空间。
  5. 线程安全

    • ArrayList和LinkedList都不是线程安全的。
    • 如果需要保证线程安全,有两种方案:
      • 在方法内使用,局部变量则是线程安全的
      • 使用线程安全的ArrayList和LinkedList,如
      List<Object> syncArrayList = Collections.synchronizedList(new ArrayList<>());
      List<Object> syncLinkedList = Collections.synchronizedList(new LinkedList<>());
      

HashMap相关

二叉树:每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。
二叉树每个节点的左子树和右子树也分别满足二叉树的定义。
如图所示:
在这里插入图片描述

Java中有两个方式实现二叉树:数组存储,链式存储。
基于链式存储的树的节点可定义如下:

    public class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;

        TreeNode() {}
        TreeNode(int val) {
            this.val = val;
        }
        TreeNode(int val, TreeNode left, TreeNode right) {
            this.val = val;
            this.left = left;
            this.right = right;
        }
    }

在这里插入图片描述
在二叉树中,比较常见的二叉树有:

  • 满二叉树
  • 完全二叉树
  • 二叉搜索树
  • 红黑树

二叉搜索树(Binary Search Tree,BST) 又名二叉查找树,有序二叉树或者排序二叉树,是二叉树中比较常用的一种类型。
二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。
在这里插入图片描述
二叉树的插入,查找,删除的时间复杂度O(logn),最坏的情况下,二叉树会退化成链表,此时的查找时间复杂度为O(n),如图:

在这里插入图片描述
红黑树(Red Black Tree):也是一种自平衡的二叉搜索树(BST),之前叫做平衡二叉B树(Symmetric Binary B-Tree),它在每个节点上增加了一个额外的表示节点颜色的属性,可以是红色或黑色,如下图所示。红黑树通过一系列的规则确保了树的平衡,从而保证了在最坏情况下的基本操作(如查找、插入和删除)具有较低的时间复杂度。
在这里插入图片描述

红黑树具有以下特性:

  1. 节点颜色:每个节点都是红色或黑色。
  2. 根节点:根节点是黑色的。
  3. 叶子节点:叶子节点(NIL 节点或空节点)是黑色的。
  4. 红色节点:红色节点的两个子节点都是黑色的。
  5. 路径规则:从任一节点到其每个叶子节点的路径上,黑色节点的数量相同。

当插入和删除操作可能会破坏红黑树的特性,因此需要进行修正,以恢复树的平衡。

红黑树的复杂度:

  • 查找:
    红黑树也是一棵BST(二叉搜索树)树,查找操作的时间复杂度为:O(log n)
  • 添加:
    添加先要从根节点开始找到元素添加的位置,时间复杂度O(log n)
    添加完成后涉及到复杂度为O(1)的旋转调整操作
    故整体复杂度为:O(log n)
  • 删除:
    首先从根节点开始找到被删除元素的位置,时间复杂度O(log n)
    删除完成后涉及到复杂度为O(1)的旋转调整操作
    故整体复杂度为:O(log n)

HashMap

散列表: 散列表(Hash Table)又名哈希表/Hash表,是根据键(Key)直接访问在内存存储位置值(Value)的数据结构,它是由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性。
如图:
在这里插入图片描述
将键(key)映射为数组下标的函数叫做散列函数。 可以表示为:hashValue = hash(key)。

散列函数的基本要求:

  • 散列函数计算得到的散列值必须是大于等于0的正整数,因为hashValue需要作为数组的下标
  • 如果key1==key2,那么经过hash后得到的哈希值也必相同即:hash(key1) == hash(key2)
  • 如果key1 != key2,那么经过hash后得到的哈希值也必不相同即:hash(key1) != hash(key2)——但不太好实现

实际的情况下想找一个散列函数能够做到对于不同的key计算得到的散列值都不同几乎是不可能的,即便像著名的MD5,SHA等哈希算法也无法避免这一情况,这就是散列冲突(或者哈希冲突,哈希碰撞,就是指多个key映射到同一个数组下标位置)

避免Hash冲突——拉链法:在散列表中,数组的每个下标位置我们可以称之为桶(bucket)或者槽(slot),每个桶(槽)会对应一条链表, 所有散列值相同的元素我们都放到相同槽位对应的链表中。如图所示:
在这里插入图片描述
基于拉链法,分析散列表的时间复杂度:

  1. 插入操作:
    • 通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可,插入的时间复杂度是 O(1)
  2. 查找、删除操作:
    • 当查找、删除一个元素时,同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除
      • 平均情况下基于链表法解决冲突时查询的时间复杂度是O(1)
      • 散列表可能会退化为链表,查询的时间复杂度就从 O(1) 退化为 O(n)
      • 将链表法中的链表改造为其他高效的动态数据结构,比如红黑树,查询的时间复杂度是 O(logn)
HashMap实现原理

HashMap的数据结构: 底层使用hash表数据结构,即数组和链表或红黑树

  1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况。
    a. 如果key相同,则覆盖原始值;
    b. 如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中
  3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。

在这里插入图片描述
HashMap的jdk1.7和jdk1.8有什么区别:

  • JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
  • JDK1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时并且数组长度达到64时,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。
HashMap的put方法的具体流程

HashMap常见属性:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient HashMap.Node<K,V>[] table;
transient int size;
DEFAULT_INITIAL_CAPACITY   默认的初始容量
DEFAULT_LOAD_FACTOR        默认的加载因子

扩容阈值 == 数组容量  *  加载因子

其中 table变量的所属类:

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;

            return o instanceof Map.Entry<?, ?> e
                    && Objects.equals(key, e.getKey())
                    && Objects.equals(value, e.getValue());
        }
    }

HashMap的无参构造方法:

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

说明:

  • HashMap是懒惰加载,在创建对象时并没有初始化数组
  • 在无参的构造函数中,设置了默认的加载因子是0.75

添加数据的流程图:
在这里插入图片描述

  1. 判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)
  2. 根据键值key计算hash值得到数组索引
  3. 判断table[i]==null,条件成立,直接新建节点添加
  4. 如果table[i]==null ,不成立
    • 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
    • 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
    • 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操 作,遍历过程中若发现key已经存在直接覆盖value
  5. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。

put方法流程

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

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断数组是否未初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        //如果未初始化,调用resize方法 进行初始化
        n = (tab = resize()).length;
    //通过 & 运算求出该数据(key)的数组下标并判断该下标位置是否有数据
    if ((p = tab[i = (n - 1) & hash]) == null)
        //如果没有,直接将数据放在该下标位置
        tab[i] = newNode(hash, key, value, null);
    //该数组下标有数据的情况
    else {
        Node<K,V> e; K k;
        //判断该位置数据的key和新来的数据是否一样
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //如果一样,证明为修改操作,该节点的数据赋值给e,后边会用到
            e = p;
        //判断是不是红黑树
        else if (p instanceof TreeNode)
            //如果是红黑树的话,进行红黑树的操作
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //新数据和当前数组既不相同,也不是红黑树节点,证明是链表
        else {
            //遍历链表
            for (int binCount = 0; ; ++binCount) {
                //判断next节点,如果为空的话,证明遍历到链表尾部了
                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;
                }
                //判断链表当中有数据相同的值,如果一样,证明为修改操作
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //把下一个节点赋值为当前节点
                p = e;
            }
        }
        //判断e是否为空(e值为修改操作存放原数据的变量)
        if (e != null) { // existing mapping for key
            //不为空的话证明是修改操作,取出老值
            V oldValue = e.value;
            //一定会执行  onlyIfAbsent传进来的是false
            if (!onlyIfAbsent || oldValue == null)
                //将新值赋值当前节点
                e.value = value;
            afterNodeAccess(e);
            //返回老值
            return oldValue;
        }
    }
    //计数器,计算当前节点的修改次数
    ++modCount;
    //当前数组中的数据数量如果大于扩容阈值
    if (++size > threshold)
        //进行扩容操作
        resize();
    //空方法
    afterNodeInsertion(evict);
    //添加操作时 返回空值
    return null;
}
HashMap的扩容

流程图:
在这里插入图片描述

  • 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)
  • 每次扩容的时候,都是扩容之前容量的2倍;
  • 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
    • 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
    • 如果是红黑树,走红黑树的添加
    • 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元 素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

扩容

//扩容、初始化数组
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
    	//如果当前数组为null的时候,把oldCap老数组容量设置为0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //老的扩容阈值
    	int oldThr = threshold;
        int newCap, newThr = 0;
        //判断数组容量是否大于0,大于0说明数组已经初始化
    	if (oldCap > 0) {
            //判断当前数组长度是否大于最大数组长度
            if (oldCap >= MAXIMUM_CAPACITY) {
                //如果是,将扩容阈值直接设置为int类型的最大数值并直接返回
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果在最大长度范围内,则需要扩容  OldCap << 1等价于oldCap*2
            //运算过后判断是不是最大值并且oldCap需要大于16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold  等价于oldThr*2
        }
    	//如果oldCap<0,但是已经初始化了,像把元素删除完之后的情况,那么它的临界值肯定还存在,       			如果是首次初始化,它的临界值则为0
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //数组未初始化的情况,将阈值和扩容因子都设置为默认值
    	else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    	//初始化容量小于16的时候,扩容阈值是没有赋值的
        if (newThr == 0) {
            //创建阈值
            float ft = (float)newCap * loadFactor;
            //判断新容量和新阈值是否大于最大容量
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
    	//计算出来的阈值赋值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //根据上边计算得出的容量 创建新的数组       
    	Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    	//赋值
    	table = newTab;
    	//扩容操作,判断不为空证明不是初始化数组
        if (oldTab != null) {
            //遍历数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //判断当前下标为j的数组如果不为空的话赋值个e,进行下一步操作
                if ((e = oldTab[j]) != null) {
                    //将数组位置置空
                    oldTab[j] = null;
                    //判断是否有下个节点
                    if (e.next == null)
                        //如果没有,就重新计算在新数组中的下标并放进去
                        newTab[e.hash & (newCap - 1)] = e;
                   	//有下个节点的情况,并且判断是否已经树化
                    else if (e instanceof TreeNode)
                        //进行红黑树的操作
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //有下个节点的情况,并且没有树化(链表形式)
                    else {
                        //比如老数组容量是16,那下标就为0-15
                        //扩容操作*2,容量就变为32,下标为0-31
                        //低位:0-15,高位16-31
                        //定义了四个变量
                        //        低位头          低位尾
                        Node<K,V> loHead = null, loTail = null;
                        //        高位头		   高位尾
                        Node<K,V> hiHead = null, hiTail = null;
                        //下个节点
                        Node<K,V> next;
                        //循环遍历
                        do {
                            //取出next节点
                            next = e.next;
                            //通过 与操作 计算得出结果为0
                            if ((e.hash & oldCap) == 0) {
                                //如果低位尾为null,证明当前数组位置为空,没有任何数据
                                if (loTail == null)
                                    //将e值放入低位头
                                    loHead = e;
                                //低位尾不为null,证明已经有数据了
                                else
                                    //将数据放入next节点
                                    loTail.next = e;
                                //记录低位尾数据
                                loTail = e;
                            }
                            //通过 与操作 计算得出结果不为0
                            else {
                                 //如果高位尾为null,证明当前数组位置为空,没有任何数据
                                if (hiTail == null)
                                    //将e值放入高位头
                                    hiHead = e;
                                //高位尾不为null,证明已经有数据了
                                else
                                    //将数据放入next节点
                                    hiTail.next = e;
                               //记录高位尾数据
                               	hiTail = e;
                            }
                            
                        } 
                        //如果e不为空,证明没有到链表尾部,继续执行循环
                        while ((e = next) != null);
                        //低位尾如果记录的有数据,是链表
                        if (loTail != null) {
                            //将下一个元素置空
                            loTail.next = null;
                            //将低位头放入新数组的原下标位置
                            newTab[j] = loHead;
                        }
                        //高位尾如果记录的有数据,是链表
                        if (hiTail != null) {
                            //将下一个元素置空
                            hiTail.next = null;
                            //将高位头放入新数组的(原下标+原数组容量)位置
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
    	//返回新的数组对象
        return newTab;
    }
HashMap的get 方法
public V get(Object key) {
    Node<K,V> e;
    //hash(key),获取key的hash值
    //调用getNode方法,见下面方法
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}


final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //找到key对应的桶下标,赋值给first节点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //判断hash值和key是否相等,如果是,则直接返回,桶中只有一个数据(大部分的情况)
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        
        if ((e = first.next) != null) {
            //该节点是红黑树,则需要通过红黑树查找数据
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            
            //链表的情况,则需要遍历链表查找数据
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
HashMap的寻址算法

在HashMap中,计算hash值时用到的关键方法:
在这里插入图片描述

计算hash值时用到的两个技巧:

  • 将key的hashCode右移16位后再进行异或运算——(h = key.hashCode()) ^ (h >>> 16),这称为扰动算法,使hash值更加均匀,从而减少hash冲突。
  • (n-1)&hash : 得到数组中的索引,代替取模,性能更好(运算结果与取模运算一样)。数组长度必须是2的n次幂

HashMap的数组长度一定是2的次幂:

  • 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
  • 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/490031.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

为什么在vite中使用eslint报错‘__dirname‘ is not defined?

问题分析 发生这种情况是因为 ESLint 不知道 vite.config.js 中的代码在 Node.js 中使用&#xff0c;__dirname 未在浏览器中定义&#xff0c;也未在 ES 模块中定义。因此要告诉 ESLint 代码将作为 CommonJS 模块在 Node.js 中运行。 解决方案 请打开 ESLint 配置并在该 env …

关于 boost::asio::strand 初始化 socket、stream、resolver、deadline_timer 对象

在 boost::asio 之中默认情况下&#xff0c;大家使用 io_context 来为这些对象初始化传递的执行者&#xff0c;但我需要这里说明。 对于 boost::asio 构造类似 socket 对象必须构造传递 io_context 是个伪命题&#xff0c;boost::asio 对象并非只允许传递 boost::asio::io_cont…

pyrealsense2获取保存点云

一、第一种实现代码 Python import sys import cv2 import pyrealsense2 as rs import numpy as np import keyboard import open3d as o3d import osif __name__ "__main__":output_folder output_data/os.makedirs(output_folder, exist_okTrue)pipeline rs.p…

git cherry pick merge部分提交

cherry pick merge 指定某次提交 1. git history 选择要从哪个分支merge 2. 找到提交记录,选择cherry pick 3.这个时候就可以直接push了

【面试题】ES文档写入和读取流程详解

前言&#xff1a;在回答这个问题之前我们先要搞清楚一个问题那就是什么是文档&#xff0c;避免不知所云&#xff01; 一、什么是文档&#xff1f; 在Elasticsearch中&#xff0c;文档&#xff08;Document&#xff09;是最基本的信息单元&#xff0c;用于表示和存储数据。文…

数据采集用,集成了主流工业通讯协议

IoTClient 是一个物联网设备通讯协议实现客户端&#xff0c;集成了主流工业通讯协议&#xff0c;包括主流PLC通信读取、ModBus协议、Bacnet协议等。该组件基于.NET Standard 2.0&#xff0c;适用于.NET的跨平台开发&#xff0c;可在Windows、Linux等系统上运行&#xff0c;甚至…

LinkedIn账号为什么被封?被封后如何解决?

近期会有一些小伙伴说自己遇到了帐号无法登录的情况&#xff0c;其实出现领英帐号被封号(被限制登录)主要会有两类情况&#xff0c;今天就给大家分享一下如果被封该如何解决&#xff0c;强烈建议收藏。 在电脑领英官网或者手机领英APP上&#xff0c;输入领英帐号密码点击登录后…

数据结构(五)单链表专题

在开始之前&#xff0c;我先来给大家讲一下顺序表与链表的区别&#xff1a; 它们在堆上存储的差异&#xff1a; 我们可以很容易的知道&#xff0c;循序表是连续的有序的&#xff0c;但链表是杂乱的&#xff0c;它们通过地址彼此联系起来。 1. 链表的概念及结构 概念&#xff1…

【光伏科普】光伏投融资计算的意义

光伏产业&#xff0c;作为清洁能源的重要组成部分&#xff0c;近年来在全球范围内得到了广泛的关注与发展。而在光伏项目的实施过程中&#xff0c;投融资计算显得尤为重要。本文旨在探讨光伏投融资计算的意义&#xff0c;以及它如何影响光伏产业的可持续发展。 首先&#xff0c…

无法找到filesystem头文件

无法找到filesystem头文件 一、前言 这段时间接老板命令&#xff0c;做目标识别模型的嵌入式部署。需要将模型运行环境编译后打包到瑞芯微开发板上运行&#xff0c;在此之前我对原C文件做过修改&#xff0c;为了能实现与厂商提供的数据接口对接。 我在用CMake打包过程中&…

jmeter接口测试及详细步骤以及项目实战教程

在接口测试项目实战中&#xff0c;JMeter是一款非常强大和流行的自动化测试工具&#xff0c;它可以测试各种类型的应用程序&#xff0c;并通过采样和报告来识别性能瓶颈和API的问题。本文将为你提供一个基于实际项目的JMeter接口测试项目实战教程&#xff0c;指导你如何使用JMe…

腾讯VS网易:一场不见终局的游戏未来之战

国内游戏霸主腾讯最近赚足了眼球。 总体上看&#xff0c;腾讯手握“游戏社交”两大王牌&#xff0c;最近发布的财报十分亮眼&#xff0c;其2023年总营收和净利润分别同比增长10%和36%&#xff0c;展现了互联网巨头的强劲活力。 然而巨头亦有焦虑&#xff0c;增值服务营收同比…

数学算法(算法竞赛、蓝桥杯)--分解质因数、唯一分解定理

1、B站视频链接&#xff1a;G07 分解质因数 唯一分解定理 试除法_哔哩哔哩_bilibili 题目链接&#xff1a;质因子分解 - 洛谷 #include <bits/stdc.h> using namespace std;int n; int a[100010];//质因子的个数void decompose(int x){for(int i2;i*i<x;i){//i增加&a…

Fastgpt 无法启动或启动后无法正常使用的讨论(启动失败、用户未注册等问题这里)

FastGPT 是一个基于 LLM 大语言模型的知识库问答系统&#xff0c;提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排&#xff0c;从而实现复杂的问答场景&#xff01; FastGPT是非常实用并且相当厉害的个人知识库AI项目&#xff0c;项目是非常…

Linux Tomcat的服务器如何查看接口请求方式?

问题描述 最近在和安卓开发对接接口&#xff0c;遇到一个接口总是报405错误&#xff0c;有对接经验的开发应该都知道是请求方式不对&#xff0c;假如接口定义为POST请求的&#xff0c;但是客户端却用GET请求&#xff0c;这时候就会报这个错误。Android客户端那边使用xUtils框架…

扫雷大师:用C语言揭秘自动展开盘面与智能扫雷策略

目录 扫雷自动展开盘面智能扫雷更优策略完整代码 扫雷 扫雷游戏是一款经典的单人电脑游戏&#xff0c;其主要规则如下&#xff1a; 游戏目标&#xff1a;游戏的目标是在不触发任何地雷的情况下&#xff0c;找出所有非雷区域。玩家需要根据格子周围的数字来推断哪些格子含有地雷…

MFC(二)集成基础控件

目录 OnCreateCStatic【标签&#xff0c;图片】CEdit【文本框&#xff0c;密码框&#xff0c;数值框&#xff0c;文本区】CButton【按钮&#xff0c;单选按钮&#xff0c;多选按钮】CComboBox【下拉列表&#xff0c;列表】CSliderCtrl【滑动条】CListCtrl【表格】CAnimateCtrl【…

第十二届蓝桥杯JavaB组省赛真题 - ASC

解题思路&#xff1a; 这是目前为止做到过最简单的了 public class Main {public static void main(String[] args) {int res L-A 65;System.out.print(res);} }

东联直播音效助手

东方联盟创始人郭盛华为广大主播免费开发的一款专用的音效场控工具&#xff0c;通过这款软件&#xff0c;主播使用各种精彩的音效&#xff0c;避免直播间过于低沉和尴尬&#xff0c;从而更好的拉近观众的距离。音效有掌声、爆笑声、尖叫声、关注点赞、任务等各种音效. 【东方联…

【win10 win11添加右键】git bash

打开注册表编辑器。 按下Win键 R&#xff0c;然后输入”regedit”并按下回车键来打开注册表编辑器。计算机\HKEY_CLASSES_ROOT\Directory\Background\shell\git_bash\command2. 导航到注册表路径&#xff1a;依次展开”HKEY_CLASSES_ROOT\Directory\Background\shell”。右键…
最新文章