Java集合常见面试题(四)

news2025/1/15 23:04:50

Map 接口


HashMap 的底层实现

JDK1.8 之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。

HashMap 通过 key 的 hashcode 经过扰动函数(hash函数)处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

如果想详细了解 hashcode 的,可以参考一下这篇博客——必须掌握的hashcode()方法。

拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。如下图所示:
在这里插入图片描述

JDK1.8及之后

相比于之前的版本, JDK1.8 及之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

在这里插入图片描述

TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉搜索树的缺陷,
因为二叉搜索树在某些情况下会退化成一个线性结构。

来结合源码分析一下 HashMap 链表到红黑树的转换

  1. putVal 方法中执行链表转红黑树的判断逻辑

链表的长度大于 8 的时候,就执行 treeifyBin (转换红黑树)的逻辑


static final int TREEIFY_THRESHOLD = 8;

// 遍历链表
for (int binCount = 0; ; ++binCount) {
    // 遍历到链表最后一个节点
    if ((e = p.next) == null) {
        p.next = newNode(hash, key, value, null);
        // 如果链表元素个数大于等于TREEIFY_THRESHOLD(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;
}
  1. treeifyBin 方法中判断是否真的转换为红黑树

static final int MIN_TREEIFY_CAPACITY = 64;

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 判断当前数组的长度是否小于 64
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        // 如果当前数组的长度小于 64,那么会选择先进行数组扩容
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 否则才将列表转换为红黑树

        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。

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 的长度为什么是 2 的幂次方,可以参考这篇文章——为什么HashMap的长度一定是2的次幂?

解决hash冲突的办法有哪些?HashMap用的哪种?

解决Hash冲突方法有:开放定址法、再哈希法、链地址法(拉链法)、建立公共溢出区。HashMap中采用的是 链地址法

  • 开放定址法也称为再散列法: 基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H§,如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。 因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。

  • 再哈希法(双重散列,多重散列): 提供多个不同的hash函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。 这样做虽然不易产生堆集,但增加了计算的时间。

  • 链地址法(拉链法): 将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。

  • 建立公共溢出区: 将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。

为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

HashMap默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ?

回答这个问题前,我们来先看下HashMap的默认构造函数:

     int threshold;             // 容纳键值对的最大值
     final float loadFactor;    // 负载因子
     int modCount;  
     int size;  

Node[] table 的初始化长度 length (DEFAULT_INITIAL_CAPACITY,默认值是16),Load factor为负载因子(默认值是0.75),threshold 是 HashMap 所能容纳键值对的最大值。threshold = length * loadfactor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

默认的 loadFactor 是0.75,0.75是对空间和时间效率的一个平衡选择,一般不要修改,除非在时间和空间比较特殊的情况下 :

  • 如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值 。

  • 相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。

我们来追溯下作者在源码中的注释(JDK1.7):

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. 
Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the 
HashMap class, including get and put).The expected number of entries in the map and its load factor should be taken 
into account when setting its initial capacity, so as to minimize the number of rehash operations.
If the initial capacity is greater than the maximum number of entries divided by the load factor, 
no rehash operations will ever occur.

翻译过来大概的意思是:作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,并且尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生。

如果想要详细了解这个问题,可以参考这篇文章——面试官:为什么 HashMap 的加载因子是0.75?

HashMap 中 key 的存储索引是怎么计算的?

首先根据 key 的值计算出 hashcode 的值,然后根据 hashcode 计算出 hash 值,最后通过 hash&(length-1)计算得到存储的位置。看看源码的实现:

// jdk1.7
方法一:
static int hash(int h) {
    int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

    h ^= k.hashCode(); // 为第一步:取hashCode值
    h ^= (h >>> 20) ^ (h >>> 12); 
    return h ^ (h >>> 7) ^ (h >>> 4);
}
方法二:
static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但实现原理一样
     return h & (length-1);  //第三步:取模运算
}
// jdk1.8
static final int hash(Object key) {   
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    /* 
     h = key.hashCode() 为第一步:取hashCode值
     h ^ (h >>> 16)  为第二步:高位参与运算
    */
}

这里的 Hash 算法本质上就是三步:取key的 hashCode 值、根据 hashcode 计算出hash值、通过取模计算下标。其中,JDK1.7和1.8的不同之处,就在于第二步。我们来看下详细过程,以JDK1.8为例,n为table的长度。

在这里插入图片描述

HashMap 的put方法流程?

简要流程如下:

  1. 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;

  2. 如果数组是空的,则调用 resize 进行初始化;

  3. 如果没有哈希冲突直接放在对应的数组下标里;

  4. 如果冲突了,且 key 已经存在,就覆盖掉 value;

  5. 如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;

  6. 如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。

在这里插入图片描述

HashMap 的扩容方式?

HashMap在容量超过负载因子所定义的容量之后,就会扩容。java里的数组是无法自己扩容的,将HashMap的大小扩大为原来数组的两倍。

我们来看jdk1.8扩容的源码

  final Node<K,V>[] resize() {
        //oldTab:引用扩容前的哈希表
        Node<K,V>[] oldTab = table;
        //oldCap:表示扩容前的table数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //获得旧哈希表的扩容阈值
        int oldThr = threshold;
        //newCap:扩容之后table数组大小
        //newThr:扩容之后下次触发扩容的条件
        int newCap, newThr = 0;
        //条件成立说明hashMap中的散列表已经初始化过了,是一次正常扩容
        if (oldCap > 0) {
            //判断旧的容量是否大于等于最大容量,如果是,则无法扩容,并且设置扩容条件为int最大值,
            //这种情况属于非常少数的情况
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }//设置newCap新容量为oldCap旧容量的二倍(<<1),并且<最大容量,而且>=16,则新阈值等于旧阈值的两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //如果oldCap=0并且边界值大于0,说明散列表是null,但此时oldThr>0
        //说明此时hashMap的创建是通过指定的构造方法创建的,新容量直接等于阈值
        //1.new HashMap(intitCap,loadFactor)
        //2.new HashMap(initCap)
        //3.new HashMap(map)
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //这种情况下oldThr=0;oldCap=0,说明没经过初始化,
        //创建hashMap的时候是通过new HashMap()的方式创建的
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //newThr为0时,通过newCap和loadFactor计算出一个newThr
        if (newThr == 0) {
            //容量*0.75
            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指向新创建的数组
        table = newTab;
        //本次扩容之前table不为null
        if (oldTab != null) {
            //对数组中的元素进行遍历
            for (int j = 0; j < oldCap; ++j) {
                //设置e为当前node节点
                Node<K,V> e;
                //当前桶位数据不为空,但不能知道里面是单个元素,还是链表或红黑树,
                //e = oldTab[j],先用e记录下当前元素
                if ((e = oldTab[j]) != null) {
                    //将老数组j桶位置为空,方便回收
                    oldTab[j] = null;
                    //如果e节点不存在下一个节点,说明e是单个元素,则直接放置在新数组的桶位
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果e是树节点,证明该节点处于红黑树中
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //e为链表节点,则对链表进行遍历
                    else { // preserve order
                        //低位链表:存放在扩容之后的数组的下标位置,与当前数组下标位置一致
                        //loHead:低位链表头节点
                        //loTail低位链表尾节点
                        Node<K,V> loHead = null, loTail = null;
                        //高位链表,存放扩容之后的数组的下标位置,=原索引+扩容之前数组容量
                        //hiHead:高位链表头节点
                        //hiTail:高位链表尾节点
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //oldCap为16:10000,与e.hsah做&运算可以得到高位为1还是0
                            //高位为0,放在低位链表
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    //loHead指向e
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //高位为1,放在高位链表
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //低位链表已成,将头节点loHead指向在原位
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //高位链表已成,将头节点指向新索引
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

扩容之后原位置的节点只有两种调整
保持原位置不动(新bit位为0时)
散列原索引+扩容大小的位置去(新bit位为1时)
扩容之后元素的散列设置的非常巧妙,节省了计算hash值的时间,我们来看一 下具体的实现

在这里插入图片描述

当数组长度从16到32,其实只是多了一个bit位的运算,我们只需要在意那个多出来的bit为是0还是1,是0的话索引不变,是1的话索引变为当前索引值+扩容的长度,比如5变成5+16=21。

在这里插入图片描述
这样的扩容方式不仅节省了重新计算hash的时间,而且保证了当前桶中的元素总数一定小于等于原来桶中的元素数量,避免了更严重的hash冲突,均匀的把之前冲突的节点分散到新的桶中去。

一般用什么作为HashMap的key?

一般用Integer、String 这种不可变类当 HashMap 当 key,而且 String 最为常用。

  • 因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。这就是 HashMap 中的键往往都使用字符串的原因。
  • 因为获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非常重要的,这些类已经很规范的重写了 hashCode() 以及 equals() 方法。

HashMap为什么线程不安全?

在这里插入图片描述

  • 多线程下扩容死循环。JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。

  • 多线程的put可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。

  • put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。此问题在JDK 1.7和 JDK 1.8 中都存在。

如果想要详细了解这个问题,可以参考这篇文章——面试官:HashMap 为什么线程不安全?

HashMap 和 Hashtable 的区别

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

  • 效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它;

  • 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。

  • 初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,上面已经介绍过为什么是 2 的幂次方。

  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable 没有这样的机制。

HashMap 中带有初始容量的构造函数:

public HashMap(int initialCapacity, float loadFactor) {
        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);
    }
     public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

下面这个方法保证了 HashMap 总是使用 2 的幂作为哈希表的大小。

/**
  * Returns a power of two size for the given target capacity
  * 返回给定目标容量的两个大小的幂
  */
 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        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;
    }

HashMap 和 HashSet 区别

如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。

HashMapHashSet
实现了 Map 接口实现了 Set 接口
存储键值对仅存储对象
调用 put()向 map 中添加元素调用 add()方法向 Set 中添加元素
HashMap 使用键(Key)计算 hashcodeHashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性
HashMap 相对于 HashSet 较快,因为它使用唯一的键获取对象HashSet 较 HashSet 来说比较慢

补充HashSet的实现:HashSet的底层其实就是HashMap,只不过我们HashSet是实现了Set接口并且把数据作为K值,而V值一直使用一个相同的虚值来保存。如源码所示:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
}

由于 HashMap 的 K 值本身就不允许重复,并且在 HashMap 中如果 K/V 相同时,会用新的 V 覆盖掉旧的 V,然后返回旧的 V,那么在HashSet 中执行这一句话始终会返回一个 false,导致插入失败,这样就保证了数据的不可重复性。

HashMap 和 TreeMap 区别

TreeMap 和HashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口。

在这里插入图片描述
在这里插入图片描述

实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。
实现SortedMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下:

package com.lzl.gather;

public class Student implements Comparable<Student>{

    public String sname;

    public Integer sage;

    public String getSname() {
        return sname;
    }

    public void setSname(String sname) {
        this.sname = sname;
    }

    public Integer getSage() {
        return sage;
    }

    public void setSage(Integer sage) {
        this.sage = sage;
    }

    public Student(String sname, Integer sage) {
        this.sname = sname;
        this.sage = sage;
    }


    @Override
    public int compareTo(Student o) {

        if(this.sage > o.sage){
            return 1;
        }else if (this.sage < o.sage){
            return -1;
        }else {
            return 0;
        }

    }
}

public static void main(String[] args) {


       TreeMap<Student,String> map = new TreeMap<>((p1,p2)-> p2.sage - p1.sage);

       map.put(new Student("张三",5),"张三");
       map.put(new Student("李四",7),"李四");
       map.put(new Student("王五",2),"王五");
       map.put(new Student("赵六",9),"赵六");

       System.out.println(map);


    }

Output:

{com.lzl.gather.Student@6d311334=赵六, com.lzl.gather.Student@682a0b20=李四,
 com.lzl.gather.Student@3d075dc0=张三, com.lzl.gather.Student@214c265e=王五}

可以看出,TreeMap 中的元素已经是按照 Person 的 age 字段的升序来排列了。

综上,相比于HashMap来说 TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。

HashSet 如何检查重复?

当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。

在 JDK1.8 中,HashSet的add()方法只是简单的调用了HashMap的put()方法,并且判断了一下返回值以确保是否有重复元素。直接看一下HashSet中的源码:

// Returns: true if this set did not already contain the specified element
// 返回值:当 set 中没有包含 add 的元素时返回真
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}

而在HashMap的putVal()方法中也能看到如下说明:

// Returns : previous value, or null if none
// 返回值:如果插入位置没有元素返回null,否则返回上一个元素
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
...
}

也就是说,在 JDK1.8 中,实际上无论HashSet中是否已经存在了某元素,HashSet都会直接插入,只是会在add()方法的返回值处告诉我们插入前是否存在相同元素。

HashMap 有哪几种常见的遍历方式?

如果想了解这个问题,可以参考这篇文章——HashMap 的 7 种遍历方式与性能分析!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JAVA基础知识08集合基础

目录 1. 集合 1.1 什么是集合&#xff1f; 1.2 ArrayList 1.2.1 ArrayList 长度可变原理 1.2.2 集合和数组的使用选择 1.2.3 ArrayList 集合常用成员方法 1. 集合 1.1 什么是集合&#xff1f; 集合是一种容器&#xff0c;用来装数据的&#xff0c;类似于数组。 其长度可…

线段树的懒标记与应用

目录 一、前言 二、Lazy-tag技术 1、update() 中的lazy-tag 三、例题 1、区间修改、区间查询&#xff08;lanqiaoOJ 1133&#xff09; 一、前言 本文主要讲了线段树的Lazy-tag技术和一道例题&#xff0c;建议自己要多练习线段树的题目。 二、Lazy-tag技术 背景&#xf…

水面漂浮物垃圾识别检测系统 YOlOv7

水面漂浮物垃圾识别检测系统通过PythonYOLOv7网络模型&#xff0c;实现对水面漂浮物以及生活各种垃圾等全天候24小时不间断智能化检测。Python是一种由Guido van Rossum开发的通用编程语言&#xff0c;它很快就变得非常流行&#xff0c;主要是因为它的简单性和代码可读性。它使…

Linux- 系统随你玩之--文本处理三剑客-带头一哥-awk

文章目录1、awk概述2、awk原理2.1、 awk 工作原理2.2、 与sed工作原理比较2.3、 awk与sed的区别3、使用方法及原理3.1、格式如下&#xff1a;3.2、 匹配规则3.3、 参数说明3.4、处理规则与流程控制3.5、 常用 awk 内置变量3.6、 awk 正则表达式解释4、操作实例4.1、 准备工作4.…

(十七)抽象队列同步器AQS

AQSAbstractQueuedSynchronizer抽象同步队列简称AQS&#xff0c;它是实现同步器的基础组件&#xff0c;并发包中锁的底层就是使用AQS实现。类图如下&#xff0c;AbstractQueuedLongSynchronizer与AbstractQueuedSynchronizer结构一模一样&#xff0c;只是AbstractQueuedSynchro…

Springboot+java师生交流答疑作业系统

&#xff0c;本系统拥有学生&#xff0c;教师&#xff0c;管理员三个角色&#xff0c;学生可以注册登陆系统&#xff0c;查看新闻&#xff0c;查看教学&#xff0c;在线提问答疑&#xff0c;提交作业&#xff0c;发布交流&#xff0c;留言反馈等功能&#xff0c;教师可以发布教…

恶意代码分析实战 14 反虚拟机技术

14.1 Lab17-01 题目 这个恶意代码使用了什么反虚拟机技术&#xff1f; 恶意代码用存在漏洞的x86指令来确定自己是否运行在虚拟机中。 如果你有一个商业版本IDAPro&#xff0c;运行第17章中代码清单17-4所示的IDAPython脚本&#xff08;提供如jindAniM.py&#xff09;&#…

spring boot前后端交互之数据格式转换

在前后端分离开发的项目种&#xff0c;前端获取数据的方式基本都是通过Ajax。请求方法也有所不同,常见的有POST,GET,PUT,DELETE等。甚至连请求的数据类型都不一样&#xff0c;x-www-form-urlencodeed,form-data,json等。 那么在前后端交互过程中&#xff0c;具体的数据该如何接…

ESP32设备驱动-8x8LED点阵驱动(基于Max7219+SPI)

8x8LED点阵驱动(基于Max7219+SPI) 1、Max7219介绍 MAX7219/MAX7221是紧凑型串行输入/输出共阴极显示驱动器,可将微处理器(Ps)连接到多达8位的7段数字LED显示器、条形图显示器或64个独立LED。片上包括一个 BCD 代码 B 解码器、多路扫描电路、段和数字驱动器,以及存储每个数字…

通信电子、嵌入式类面试题刷题计划04

文章目录036——看门狗电路的作用是什么&#xff1f;【社招】037——你了解CAN总线协议吗&#xff1f;说一说你的理解【社招】038——锁存器、触发器、寄存器三者的区别&#xff1f;【校招】039——D触发器和D锁存器的区别是什么&#xff1f;【校招】040——三极管和MOS管的区别…

Cadence PCB仿真使用Allegro PCB SI生成单网络EMI报告Single Net EMI Report及报告导读图文教程

🏡《Cadence 开发合集目录》   🏡《Cadence PCB 仿真宝典目录》 目录 1,概述2,生成报告3,报告导读4,总结1,概述 单网络EMI报告是值将差分模式下的网络视为单个网络,分析来自时钟上升沿的辐射影响。本文简单介绍使用Allegro PCB SI生成单网络EMI报告的方法,及Singl…

搜索引擎位置跟踪应用SerpBear

什么是 SerpBear ? SerpBear 是一款开源搜索引擎位置跟踪应用程序。它允许你跟踪你的网站在谷歌中的关键词位置&#xff0c;并得到他们的位置通知。 软件特点&#xff1a; 无限关键词&#xff1a;添加无限域名和无限关键词以跟踪其 SERP电子邮件通知&#xff1a;每天/每周/每…

车载以太网简介

车载以太网简介 基本概念 传统车载网络 LIN&#xff1a;用于通信速率低的场景&#xff0c;比如车窗、座椅等。CAN&#xff1a;目前车载网络首先&#xff0c;低成本高可靠。FlexRay &#xff1a;具备故障容错的车载总线系统。MOST&#xff1a;内置流媒体数据信道&#xff0c;…

2023年企业信息安全缺陷和解决方案,防止职员外泄信息

随着网络的发展和普及&#xff0c;信息安全与每个人息息相关&#xff0c;包含方方面。每个人既是独立个体又必须和社会交换资源。这就需要把控一个尺度。 要了解信息安全&#xff0c;首先需要对信息有个大体了解。从拥有者和使用者分类分为&#xff0c;个人&#xff0c;企业&a…

恶意代码分析实战 11 恶意代码的网络特征

11.1 Lab14-01 问题 恶意代码使用了哪些网络库&#xff1f;它们的优势是什么&#xff1f; 使用WireShark进行动态分析。 使用另外的机器进行分析对比可知&#xff0c;User-Agent不是硬编码。 请求的URL值得注意。 回答&#xff1a;使用了URLDownloadToCacheFileA函数&#…

JavaEE多线程-定时器

目录一、定时器1.1 什么是定时器&#xff1f;1.2 定时器的构成二、简单实现定时器一、定时器 1.1 什么是定时器&#xff1f; 定时器是多线程编码中的一个重要组件,它就好比一个闹钟,例如我们想去坐车,但是不想现在去坐车,想8:30去坐车,于是我们订了一个8点钟的闹钟,也就是说定…

Linux内核驱动初探(四) 内部看门狗

目录 0. 前言 1. menuconfig 2. 设备树 3. 拓展试验 0. 前言 这次的内部看门狗驱动也比较顺利&#xff0c;重点看了 原理图和4.19.x 内核的配置。 内部看门狗设备名叫做 /dev/watchdog 。 1. menuconfig 我们在 linux-menuconfig 里面如下设置&#xff1a;进入 Device D…

[Java]JavaWeb学习笔记(动力节点老杜2022)

文章目录&#x1f97d; Tomcat服务器&#x1f30a; 下载与安装&#x1f30a; 关于Tomcat服务器的目录&#x1f30a; 启动Tomcat&#x1f30a; 实现一个最基本的web应用&#xff08;这个web应用中没有java小程序&#xff09;&#x1f97d; 静态资源与动态资源&#x1f97d; 模拟…

GPU虚拟化(留坑)

文章内容大程度参考B站王利明老师对《GPU虚拟化技术分享》的演讲&#xff1a;https://b23.tv/uQKBpcK GPU 有什么用&#xff1f; GPU可以用于图形渲染&#xff0c;也能够用于高性能计算和编解码等场景。 图&#xff1a;GPU 的典型软件架构&#xff08;不含虚拟化&#xff09; …

注解存储对象到Spring,详解 五大类注解 和方法注解

上一篇博客我们介绍了如何使用xml来引入bean对象&#xff0c;当项目多的时候&#xff0c;显然那样是比较麻烦的。现在我们只需要 个注解就可以替代了。注意&#xff1a;注解和xml可以同时使用准备工作:配置扫描路径我们需要配置 下存储对象的扫描包路径&#xff0c;只有被配置的…