面试题七:集合框架

Map接口

HashMap

  1. HashMap的容量有什么特点?

    HashMap的默认容量是16,默认的最大容量是2^30,默认的加载因子是0.75,在容量达到12*0.75=12时会触发扩容。扩容时,如果扩容后的容量超过最大值,那么容量为最大值,一般情况下为原始容量的2倍。

  2. HashMap是怎么进行扩容的?

    1. 如果使用默认的构造方法,那么第一次插入元素时初始化为默认值,初始容量为16,负载因子为0.7,下一次扩容的门槛是12;
    2. 如果使用非默认的构造方法,那么第一次插入元素时初始化为扩容门槛,扩容门槛的大小等于传入容量向上最近的2的n次方;
    3. 如果原始容量大于0,将容量扩充为原来的2倍,扩容门槛也扩充为原来的2倍,扩充后的容量不能大于最大容量;
    4. 按照新的容量创建一个Node数组;
    5. 迁移元素,原来的链表拆为2个链表,低位链表保留在原始的位置,高位链表迁移到旧位置+旧容量的新位置;
  3. HashMap的put过程?

    1. 调用hash(key)计算key的hash值,如果key为null,返回0,否则调用key.hashcode()获取hashcode后,让hashcode值得高16位与整个hashcode值异或位操作,以使计算出的hash值更分散
    2. 如果桶的数量为0,则初始化桶;
    3. 如果key所在的桶没有元素,直接插入元素;
    4. 如果key所在的桶中的第一个元素的key与传入的key值相同,说明找到了元素,转步骤9执行;
    5. 如果第一个元素是树节点,则调用树节点的putTreeVal()寻找元素或者插入树节点;
    6. 如果不是以上三种情况,则遍历所在桶的链表,查询key是否存在于链表中;
    7. 如果找到了key,则转步骤9执行;
    8. 如果没找到key,则将元素插入队列尾部,并且判断是否需要树化;
    9. 如果找到了对应的key,则判断是否需要替换旧值,并直接返回旧值;
    10. 如果插入了元素,则数量加1并判断是否需要扩容;
  4. HashMap的get过程?

    1. 调用hash(key)计算key的hash值,如果key为null,返回0,否则调用key.hashcode()获取hashcode后,让hashcode值得高16位与整个hashcode值异或位操作,以使计算出的hash值更分散
    2. 找到key所在的桶以及第一个元素;
    3. 如果第一个元素的key等于待查找的key,直接返回;
    4. 如果第一个元素是树节点,则按照树节点的方式查找,否则按照链表的方式查找;
  5. HashMap中的元素是否是有序的?有哪些顺序的hashMap实现?

    HashMap中的元素是无序的;

    顺序的HashMap实现:

    1. LinkedHashMap

      基于元素进入集合的顺序或者被访问的先后顺序排序

    2. TreeMap

      基于元素的固有顺序(Comparator或者Comparable决定)

  6. HashMap何时进行树化?何时进行反树化?

    树化:

    当桶的数量(数组长度)小于64时,直接扩容,不进行树化;如果桶的数量等于64并且插入新节点后链表长度大于8,此时就会触发树化;

    反树化:

    当单个桶中元素数量小于6时,进行反树化;

  7. HashMap是怎么进行缩容的?

    hashMap没有缩容

  8. HashMap查询、插入、删除的时间复杂度各是多少?

    HashMap的数据结构包含数组、链表、红黑树,数组的查询时间复杂度是O(1),链表的查询时间复杂度是O(k),红黑树的查询时间复杂度是O(logk),k为桶中的元素个数,因此当元素数量非常多的时候,转化为红黑树能极大的提高性能。

  9. HashMap中的红黑树实现部分可以用其他数据结构代替吗?

    //TODO

  10. HashMap的工作原理是什么?

    java8中使用数组+链表+红黑树的数据结构实现,实际上HashMap是一个链表散列

    使用put(key,value)方法来存储对象,使用get(key)方法从HashMap中获取对象;

    当我们给put(key, value)方法传递键和值得时候,首先调用hash(key)方法,返回的hashcode用来找到桶的未来以存储Entry对象。

  11. 当两个对象的hashCode值相等会发生什么?

    因为hashcode值相等,那么就会映射到相同的桶中,出现哈希碰撞

    此时会以链表的方式存储数据;

  12. 如何解决哈希碰撞?

    理想情况下,使用散列函数计算出来的每一个关键字的散列值都应该不同,但是现实是这种情况很难出现,如果两个不同的关键词通过散列函数获取到了相同的散列值,那么此时就会出现哈希碰撞。

    解决方法:

    • 直接定址法
    • 开放定址法
    • 链地址法(HashMap使用的就是链地址放)
    • 除留余数法

    链地址法的优点:

    1. 处理简单,且无堆积现象,即非同义词绝不会发生冲突,因此平均查找长度较短;
    2. 各链表上的节点空间是动态申请的,因此适合用于造表前无法确切知道表长的场景;

    链地址法的缺点:

    1. 指针需要额外的空间;
    2. 查找时候需要遍历链表;
  13. hashcode和equals方法为什么重要?

    HashMap使用key对象的hashcode()equals()去确定键值对的索引。当从HashMap中查询数据的时候这两个方法也会被用到。

    如果这两个方法没有被正确实现,在这种情况下,两个不同的key也许会产生相同的hashcode()equals()输出,此时HashMap将会认为这两者是相同的,然后就会覆盖掉一个值,而不是存储到不同的地方。

  14. 能否使用任何类作为Map的实现?

    可以,遵循以下几点即可:

    1. 如果类重写了equals方法,那么也要重写hashCode方法;
    2. 类的所有实例都需要遵循与equalshashcode相关的规则;
    3. 如果一个类没有使用equals方法,那么不应该使用它的hashcode方法;
    4. 用户自定义key类的最佳实践是使之为不可变的,这样hashcode值可以被缓存起来,拥有更好的性能。
  15. HashMap的长度为什么是2的幂次方?

    为了能让HashMap存取更加高效,尽量减少碰撞,也就是尽量把数据分散均匀,每个链表/红黑树的长度大致相同。

    主要原因:

    1. 计算方便:当容量一定是2^n时,h & (length - 1) == h % length,扩容时非常方便计算新的位置,只需要是原来的位置+旧容量就是新的位置;
    2. 分布均匀:如果不是2的幂次方,那么有些位置永远不会被使用到,有些位置碰撞的概率大大增加,这显然于hash均匀分布的原则不符合;
    3. 提高效率: 二进制位操作&的效率高于取余操作%;
  16. HashMap在多线程环境中什么时候会出现问题?

ConcurrentHashMap

  1. HashMap和ConcurrentHashMap的区别;

    1. HashMap是线程不安全的,ConcurrentHashMap是线程安全的;

    2. ConcurrentHashMap不能存储key或者value为null的元素,HashMap支持;

    3. HashMap中有初始容量和负载因子且可以手动传入,ConcurrentHashMap没有直接传入负载因子的地方,直接写死是0.75;

    4. ConcurrentHashMap中通过sizeCtl来表达各个阶段;

      • -1,表示正在初始化;
      • 0,默认值,表示后续在真正初始化时使用默认容量;
      • >0,在初始化之前,存储的是传入的容量,在初始化或者扩容之后存储的是下一次扩容的门槛;
      • sizeCtl = (resizeStamp << 16) + (1 + nThreads),表示正在进行扩容,高位存储扩容邮戳,低位存储扩容线程数加1;
    5. ConcurrentHashMap扩容时使用链表逆序的方式遍历链表;

    不同之处:ConcurrentHashMap 是线程安全的,多线程环境下,无需加锁直接使用ConcurrentHashMap 多了转移节点,主要用户保证扩容时的线程安全;

    相同之处:都是数组 +链表+红黑树的数据结构(JDK8之后),所以基本操作的思想一致都实现了Map接口,继承了AbstractMap 操作类,所以方法大都相似,可以相互切换.

  2. ConcurrentHashMap的存储结构?

    数组+链表+红黑树

  3. ConcurrentHashMap是怎么保证并发安全的?

    主要是使用CAS操作+Synchronized保证并发安全;

    CAS负责并发安全的修改对象属性的值或者数据某个位置的值;

    Synchronized负责给桶加锁,使用分段锁提高并发能力;

  4. ConcurrentHashMap使用了哪些锁?

    put过程中使用的锁有CAS,自旋锁,Synchronized, 分段锁

  5. ConcurrentHashMap是怎么扩容的?

    1. 元素个数的存储方式类似于LongAdder类,存储在不同的段上,减少不同线程同时更新size时的冲突;
    2. 计算元素个数时把这些段的值及baseCount相加算出总的元素个数;
    3. 正常情况下sizeCtl存储着扩容门槛,扩容门槛为容量的0.75倍;
    4. 扩容时sizeCtl高位存储扩容邮戳(resizeStamp),低位存储扩容线程数加1(1+nThreads);
    5. 其它线程添加元素后如果发现存在扩容,也会加入的扩容行列中来;
  6. ConcurrentHashMap的size()方法实现?

    获取元素个数时是没有加锁的;

    步骤:

    1. 元素的个数依据不同的线程存在在不同的段里;
    2. 计算CounterCell所有段及baseCount的数量之和;
    3. 获取元素个数没有加锁;
  7. ConcurrentHashMap是强一致性的吗?

    ConcurrentHashMap查询操作没有加锁,因此不是强一致性的。

  8. ConcurrentHashMap不能解决什么问题?

    private static final Map<Integer, Integer> map = new ConcurrentHashMap<>();
    
    public void unsafeUpdate(Integer key, Integer value) {
        Integer oldValue = map.get(key);
        if (oldValue == null) {
            map.put(key, value);
        }
    }
    

    这里如果有多个线程同时调用unsafeUpdate()这个方法,ConcurrentHashMap还能保证线程安全吗?

    答案是不能。因为get()之后if之前可能有其它线程已经put()了这个元素,这时候再put()就把那个线程put()的元素覆盖了。此时可以使用putIfAbsent()

    不能听说ConcurrentHashMap是线程安全的,就认为它无论什么情况下都是线程安全的

  9. ConcurrentHashMap哪些地方用到了分段锁的思想?

    put插入数据时使用了分段锁;

    size()查询个数时也用到了分段思想;

    remove()删除数据时使用了分段锁;

    transfer()扩容时迁移元素时用到了分段锁;

  10. ConcurrentHashMap中put()过程?

    1. 判断桶是否已经被初始化了,未初始化时需要先初始化;
    2. 计算待插入的元素的key的hash值,从而确定桶的位置;
    3. 如果桶中没有元素,则尝试(CAS)将待插入元素插入到该桶的第一个位置;
    4. 如果正在扩容,那么当前线程加入帮助扩容;
    5. 如果当前桶即非空,也没有在扩容,那么使用分段锁锁住该桶;
    6. 如果当前桶中以链表的方式存储数据,那么在链表中查找或者插入数据;
    7. 如果当前桶中以树的方式存储数据,那么在红黑树中查找或者插入数据;
    8. 如果元素存在,那么就返回旧值;
    9. 如果元素不存在,整个Map的个数加1,并检查是否需要扩容;

    put过程中的分段锁为什么使用Synchronized而不是ReentrantLock?

    因为Synchronized经过优化之后,某些情况下性能并不比ReentrantLock差。

  11. 扩容期间在未迁移到的hash桶插入数据会发生什么?

    只要插入的位置扩容线程还未迁移到,就可以插入,当迁移到该插入的位置时,就会阻塞等待插入操作完成再继续迁移 。

  12. 正在迁移的hash桶遇到 get 操作会发生什么?

    在扩容过程期间形成的 hn 和 ln链 是使用的类似于复制引用的方式,也就是说 ln 和 hn 链是复制出来的,而非原来的链表迁移过去的,所以原来 hash 桶上的链表并没有受到影响,因此如果当前节点有数据,还没迁移完成,此时不影响读,能够正常进行。如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时get线程会帮助扩容

  13. 正在迁移的hash桶遇到 put/remove 操作会发生什么?

    如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时写线程会帮助扩容,如果扩容没有完成,当前链表的头节点会被锁住,所以写线程会被阻塞,直到扩容完成。

  14. 如果 lastRun 节点正好在一条全部都为高位或者全部都为低位的链表上,会不会形成死循环?

    在数组长度为64之前会导致一直扩容,但是到了64或者以上后就会转换为红黑树,因此不会一直死循环 。

  15. 扩容后 ln 和 hn 链不用经过 hash 取模运算,分别被直接放置在新数组的 i 和 n + i 的位置上,那么如何保证这种方式依旧可以用过 h & (n - 1) 正确算出 hash 桶的位置?

    如果 fh & n-1 = i ,那么扩容之后的 hash 计算方法应该是 fh & 2n-1 。 因为 n 是 2 的幂次方数,所以 如果 n=16, n-1 就是 1111(二进制), 那么 2n-1 就是 11111 (二进制) 。 其实 fh & 2n-1 和 fh & n-1 的值区别就在于多出来的那个 1 => fh & (10000) 这个就是两个 hash 的区别所在 。而 10000 就是 n 。所以说 如果 fh 的第五 bit 不是 1 的话 fh & n = 0 => fh & 2n-1 == fh & n-1 = i 。 如果第5位是 1 的话 。fh & n = n => fh & 2n-1 = i+n 。

  16. 并发情况下,各线程中的数据可能不是最新的,那为什么 get 方法不需要加锁?

    get操作全程不需要加锁是因为Node的成员val是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。

  17. ConcurrentHashMap 和 Hashtable 的区别?

    ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

    底层数据结构:JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable是采用 数组+链表 的形式。

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

    ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。

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

  18. 扩容过程中,读访问能否访问的到数据?怎么实现的?

    可以的。当数组在扩容的时候,会对当前操作节点进行判断,如果当前节点还没有被设置成fwd节点,那就可以进行读写操作,如果该节点已经被处理了,那么当前线程也会加入到扩容的操作中去。

  19. 为什么超过冲突超过8才将链表转为红黑树而不直接用红黑树?

    默认使用链表, 链表占用的内存更小正常情况下,想要达到冲突为8的几率非常小,如果真的发生了转为红黑树可以保证极端情况下的效率

  20. ConcurrentHashMap 和HashMap的扩容有什么不同?

    HashMap的扩容是创建一个新数组,将值直接放入新数组中,JDK7采用头链接法,会出现死循环,JDK8采用尾链接法,不会造成死循环;

    ConcurrentHashMap 扩容是从数组队尾开始拷贝,拷贝槽点时会锁住槽点,拷贝完成后将槽点设置为转移节点。所以槽点拷贝完成后将新数组赋值给容器;

  21. ConcurrentHashMap 是如何发现当前槽点正在扩容的?

    ConcurrentHashMap 新增了一个节点类型,叫做转移节点,当我们发现当前槽点是转移节点时(转移节点的 hash 值是 -1),即表示 Map 正在进行扩容.

  22. 描述一下 CAS 算法在 ConcurrentHashMap 中的应用?

    CAS是一种乐观锁,在执行操作时会判断内存中的值是否和准备修改前获取的值相同,如果相同,把新值赋值给对象,否则赋值失败,整个过程都是原子性操作,无线程安全问题;

    ConcurrentHashMap 的put操作是结合自旋用到了CAS,如果hash计算出的位置的槽点值为空,就采用CAS+自旋进行赋值,如果赋值是检查值为空,就赋值,如果不为空说明有其他线程先赋值了,放弃本次操作,进入下一轮循环

LinkedHashMap

  1. LinkedHashMap是怎么实现的?

    LinkedHashMap继承了HashMap并且实现了Map接口,拥有HashMap的所有特性。

    在HashMap的数组+链表+红黑树的基础上,又添加了双向链表的结构存储数据的顺序,因此在删除和添加元素的过程中,除了要维护数组、链表、红黑树三种数据结构外,还需要维护LinkedList中的存储,效率要比HashMap低。

  2. LinkedHashMap是有序的吗?怎么个有序法?

    默认是按照插入顺序排序,可以通过传入参数实现按照访问顺序排序;

  3. LinkedHashMap如何实现LRU缓存淘汰策略?

    只需要设置accessOrder=true,并且重写removeEldestEntry()方法即可,示例代码如下:

    class LRU<k,v> extends LinkedHashMap<k,v> {
    
        private int capacity;
    
        public LRU(int initialCapacity, float loadFactor) {
            super(initialCapacity, loadFactor, true);
            this.capacity = initialCapacity;
        }
    
        @Override
        protected boolean removeEldestEntry(Map.Entry<k, v> eldest) {
            return size()>capacity;
        }
    }
    

    原理:

    1. 在插入元素后,回调钩子方法afterNodeInsertion(boolean evict),HashMap的put()方法默认参数evict为true;
    2. ``afterNodeInsertion()在HashMap中为空实现,在LinkedHashMap中有具体的实现,判断evict && (first = head) != null && removeEldestEntry(first),即判断evict 为true,双向链表头结点不为空,以及removeEldestEntry()的返回值,默认该方法返回false`,因此只需要重写该方法即可;
    3. 在节点被访问之后,put()或者get()被调用,将回调afterNodeAccess()方法,如果此时accessOrder=true,那么将当前节点移动到链表的末尾,末尾是最新访问的元素;

TreeMap

  1. TreeMap中是怎么遍历的?

    按照key值的大小进行遍历

  2. TreeMap插入、删除、查询元素的时间复杂度各是多少?

    约等于O(n)

  3. TreeMap就有序的吗?怎么个有序法?

    按照key值大小排序,初始化时要么传入比较器,要么就将key值实现Comparable接口

  4. TreeMap是否需要扩容?

    没有扩容的概念

  5. TreeMap和LinkedHashMap的区别?

    1. 数据结构不同;
    2. 有序的方式不同,LinkedHashMap是插入序或者访问序有序,TreeMap是key值大小排序;
  6. TreeMap数据结构?

    只有一颗红黑树

WeakHashMap

  1. WeakHashMap使用的数据结构?

    数组+链表

  2. WeakHashMap具有什么特性?

    1. 没有实现Clone和Serializable接口,所以不具有克隆和序列化的特性。
    2. 内部的key会存储为弱引用,当jvm gc的时候,如果这些key没有强引用存在的话,会被gc回收掉,下一次当我们操作map的时候会把对应的Entry整个删除掉,基于这种特性,WeakHashMap特别适用于缓存处理。
  3. WeakHashMap通常用来做什么?

    WeakHashMap特别适用于缓存处理

  4. WeakHashMap使用String作为key是需要注意些什么?为什么?

    使用String作为key时,一定要使用new String()这样的方式声明key,才会失效,其它的基本类型的包装类型是一样的;

List接口

  1. ArrayList和LinkedList有什么区别?

    数据结构不同,ArrayList是数组,LinkedList是双链表结构;

    LinkedList不仅可以作为list,还可以作为双端队列以及栈;

    ArrayList支持随机访问,LinkedList不支持随机访问;

    ArrayList查询效率较高,LinkedList插入删除数据效率较高;

  2. ArrayList是怎么扩容的?

    1. 检查是否需要扩容;
    2. 如果数组对象等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA则初始化为默认容量10;
    3. 扩容到原始容量的1.5倍(oldCapacity + (oldCapacity >> 1)),扩容后的容量还是小于实际需要容量,则以实际需要容量为准;
    4. 创建新容量的数组,并把内容拷贝到新数组;
  3. ArrayList插入、删除、查询元素的时间复杂度各是多少?

    添加元素到末尾,时间复杂度O(1);

    添加到指定位置,时间复杂度O(n);

    查询指定位置元素,时间复杂度O(1);

    删除指定位置元素/指定元素,时间复杂度为O(n);

  4. 怎么求两个集合的并集、交集、差集?

    求并集: addAll()

    求交集: retainAll()

    单方向差集: removeAll()

  5. ArrayList是怎么实现序列化和反序列化的?

    实现Serializable接口

  6. 集合的方法toArray()有什么问题?

    容易出现类型转换异常ClassCastException

    解决方案,使用toArray(T[] a)

  7. 什么是fail-fast?

    java集合的一种异常检测机制。当多个线程对部分集合进行结构上的改变的操作时,有可能会产生fail-fast机制。这个时候会抛出ConcurrentModificationException

    异常重现:使用增强for循环遍历list时,在循环体中调用add()或者remove()方法时就会出现该异常。

    核心代码:

    final void checkForComodification() {
        if (modCount != expectedModCount)
            //modCount在集合类中,随着集合改变次数而增加
            //expectedModCount,集合内部的类中的属性,只有使用迭代器修改集合该值才会改变
            throw new ConcurrentModificationException();
    }
    

    原因:

    modCount在集合类中,随着集合改变次数而增加;

    expectedModCount,集合内部的类中的属性,只有使用迭代器修改集合该值才会改变,在开始遍历前该值等于modCount。

    增强for循环的本质是迭代器,在循环体中使用add()或者remove()方法时会改变modCount的值,导致modCount和expectedModCount不相等,这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改!

    所以,在使用Java的集合类的时候,如果发生CMException,优先考虑fail-fast有关的情况,实际上这里并没有真的发生并发,只是Iterator使用了fail-fast的保护机制,只要他发现有某一次修改是未经过自己进行的,那么就会抛出异常。

  8. LinkedList是单链表还是双链表实现的?

    双向链表

  9. LinkedList除了作为List还有什么用处?

    实现了Queue和Deque接口,可以作为双端队列来使用(无界队列),也可以当作栈来使用。

  10. LinkedList插入、删除、查询元素的时间复杂度各是多少?

    在队列头部或者尾部添加/删除元素,时间复杂度为O(1);

    在队列中间添加/删除元素,时间复杂度是O(n);

  11. 什么是随机访问?

    实现RandomAccess接口,支持快速随机访问策略;

    实现了该接口的集合,使用for循环遍历优于使用迭代器遍历。

  12. 哪些集合支持随机访问?他们都有哪些共性?

    ArrayList

    CopyOnWriteArrayList

    共性:实现了RandomAccess接口,提供随机访问能力

  13. CopyOnWriteArrayList是怎么保证并发安全的?

    使用可重入锁ReentrentLock

  14. CopyOnWriteArrayList的实现采用了什么思想?

    读写分离的思想,写数据时都复制一个新的数组进行操作,操作完成后再替换旧数组

  15. CopyOnWriteArrayList是不是强一致性的?

    CopyOnWriteArrayList只支持最终一致性,但是不能保证实时一致性;

  16. CopyOnWriteArrayList适用于什么样的场景?

    由于写的时候占用内存比较多(复制数据),空间复杂度是O(n),但是读操作支持随机访问,时间复杂度是O(1),适合读多写少的场景;

  17. CopyOnWriteArrayList插入、删除、查询元素的时间复杂度各是多少?

    查询的时间复杂度是O(1);

    添加元素到末尾,时间复杂度O(1);

    添加到指定位置,时间复杂度O(n);

    删除指定位置元素,时间复杂度为O(n);

  18. CopyOnWriteArrayList为什么没有size属性?

    因为底层是拷贝数据,直接使用数组长度即可获取到集合中元素数量,因此不需要size属性

  19. CopyOnWriteArrayList和Vector 比较?

    • 都是线程安全的
    • 实现原理不同,Vector 使用Synchronized加锁,CopyOnWriteArrayList使用ReentrentLock加锁;
    • 并发性能CopyOnWriteArrayList高于Vector
  20. 如何比较两个list完全相等?

    private static <T> boolean eq(List<T> list1, List<T> list2) {
            if (list1.size() != list2.size()) {
                return false;
            }
    
            // 标记某个元素是否找到过,防止重复
            boolean matched[] = new boolean[list2.size()];
    
            outer: for (T t : list1) {
                for (int i = 0; i < list2.size(); i++) {
                    // i这个位置没找到过才比较大小
                    if (!matched[i] && list2.get(i).equals(t)) {
                        matched[i] = true;
                        continue outer;
                    }
                }
                return false;
            }
    
            return true;
        }
    

    设定一个标记数组,标记某个位置的元素是否找到过

Set接口

  1. HashSet怎么保证添加元素不重复?

    HashSet的值作为底层HashMap的key,因此需要计算hashcode值来判断在桶的位置,同时与其他的值的hashcode值进行比较。

    如果没有发现相同hashcode值,HashSet会假设对象没有重复出现;

    如果发现hashcode值有重复的,调用equals放比较对象是否真的相同,如果两者相同,那么就是重复的,加入失败,如果两者不相同,那么就是不重复的,可以加入。

  2. HashSet是有序的吗?

    无序的,底层实现为HashMap,HashMap的key值是无序的

  3. HashSet是否允许null元素?

    允许,HashMap的key值允许是null

  4. Set是否有get()方法?

    没有,只有一个contains()方法

  5. LinkedHashSet底层使用什么存储数据?

    底层使用LinkedHashMap存储数据

  6. LinkedHashSet与HashSet有什么不同?

    LinkedHashSet是有序的,HashSet是无序的;底层存储数据的结构不同,LinkedHashSet是LinkedHashMap,HashSet是HashMap。

  7. LinkedHashSet是有序的吗?怎么个有序法?

    有序的,按照插入顺序排序

  8. LinkedHashSet支持按元素访问顺序排序吗?

    不支持,LinkedHashSet构造方法中没有提供修改accessOrder值的方法,默认是false,因此是按照插入顺序排序;

  9. TreeSet真的是使用TreeMap来存储元素的吗?

    源码中使用的是NavigableMap<E,Object> m,不一定是个TreeMap;

    默认构造方法是使用TreeMap实现,代码如下:

    public TreeSet() {
            this(new TreeMap<E,Object>());
    }
    
  10. TreeSet是有序的吗?怎么个有序法?

    是有序的,实现了SortedSet接口,它的有序性主要依赖于NavigableMap的有序性,而NavigableMap又继承自SortedMap,这个接口的有序性是指按键的自然排序保证的有序性,而键的自然排序又有两种实现方式,一种是密钥实现Comparable接口,一种是构造方法放置Comparator比较器

  11. TreeSet和LinkedHashSet有何不同?

    LinkedHashSet并没有实现SortedSet接口,它的有序性主要依赖于LinkedHashMap的有序性,所以它的有序性是指按照插入顺序保证的有序性;

    而TreeSet实现了SortedSet接口,它的有序性主要依赖于NavigableMap的有序性,而NavigableMap又继承自SortedMap,这个接口的有序性是指按键的自然排序保证的有序性,而键的自然排序又有两种实现方式,一种是密钥实现Comparable接口,一种是构造方法放置Comparator比较器。

  12. TreeSet和SortedSet有什么区别和联系?

    TreeSet实现了NavigableSet接口,NavigableSet接口继承了SortedSet接口,因此可以说TreeSet实现了SortedSet接口。

  13. CopyOnWriteArraySet是用Map实现的吗?

    不是,使用的CopyOnWriteArrayList实现的,添加时调用CopyOnWriteArrayList的addIfAbsent()方法,只有元素不存在时才添加

  14. CopyOnWriteArraySet是有序的吗?怎么个有序法?

    有序的,底层是CopyOnWriteArrayList,数组是有序的

  15. CopyOnWriteArraySet怎么保证并发安全?

    底层是CopyOnWriteArrayList,使用ReentrantLock加锁保证并发安全,且是读写分离的;

  16. CopyOnWriteArraySet以何种方式保证元素不重复?

    使用的CopyOnWriteArrayList实现的,添加时调用CopyOnWriteArrayList的addIfAbsent()方法,只有元素不存在时才添加

  17. 如何比较两个Set中的元素是否完全一致?

    因为Set中的元素并不重复,所以只要先比较两个Set的元素个数是否相等,再作一次两层循环就可以了

     private static <T> boolean eq(Set<T> set1, Set<T> set2) {
            if (set1.size() != set2.size()) {
                return false;
            }
    
            for (T t : set1) {
                // contains相当于一层for循环
                if (!set2.contains(t)) {
                    return false;
                }
            }
    
            return true;
        }
    

Queue

// TODO

Deque

// TODO

posted @ 2021-03-17 17:55  一步一年  阅读(146)  评论(0编辑  收藏  举报