zoukankan      html  css  js  c++  java
  • 面试题七:集合框架

    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

  • 相关阅读:
    线程
    链表
    String 练习
    23种设计模式
    静态方法
    ASCII码及UTF-8概义
    java语法基础
    二进制、八进制、十进制、十六进制之间转换
    数组概意
    java程序安装 配置环境变量
  • 原文地址:https://www.cnblogs.com/ybyn/p/14550900.html
Copyright © 2011-2022 走看看