java集合面试题总结(1~20题)

1.  HashMap与HashTable的区别?

        1.HashMap是非线程安全的,HashTable是线程安全的。

        2.HashMap的键和值都允许有null值存在,而HashTable则不行。

        3.因为线程安全的问题,HashMap效率比HashTable的要高。

        4.默认容量不同 (HashMap:16  HashTable:11)

2.  HashMap,ConcurrentHashMap与LinkedHashMap的区别?

        ConcurrentHashMap是使用了锁分段技术技术来保证线程安全的,锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问

        ConcurrentHashMap 是在每个段(segment)中线程安全的

        LinkedHashMap维护一个双链表,可以将里面的数据按写入的顺序读出

3. ConcurrentHashMap应用场景?

        1:ConcurrentHashMap的应用场景是高并发,但是并不能保证线程安全,而同步的HashMap和HashMap的是锁住整个容器,而加锁之后ConcurrentHashMap不需要锁住整个容器,只需要锁住对应的Segment就好了,所以可以保证高并发同步访问,提升了效率。

        2:可以多线程写。

4.  ConcurrentHashMap把HashMap分成若干个Segmenet?

        1.get时,不加锁,先定位到segment然后在找到头结点进行读取操作。而value是volatile变量,所以可以保证在竞争条件时保证读取最新的值,如果读到的value是null,则可能正在修改,那么就调用ReadValueUnderLock函数,加锁保证读到的数据是正确的。

        2.Put时会加锁,一律添加到hash链的头部。

        3.Remove时也会加锁,由于next是final类型不可改变,所以必须把删除的节点之前的节点都复制一遍。

        4.ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对Hash表的不同Segment进行的修改。

        ConcurrentHashMap能够保证每一次调用都是原子操作,但是并不保证多次调用之间也是原子操作。

5.  Java中Vector和ArrayList的区别?

       首先看这两类都实现List接口,而List接口一共有三个实现类,分别是ArrayList、Vector和LinkedList。List用于存放多个元素,能够维护元素的次序,并且允许元素的重复。3个具体实现类的相关区别如下:

        ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要讲已经有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

        Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。

        LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

6.  HashMap、HashTable、LinkedHashMap和TreeMap用法和区别?

        Java为数据结构中的映射定义了一个接口java.util.Map,它有四个实现类,分别是HashMap、HashTable、LinkedHashMap和TreeMap。本节实例主要介绍这4中实例的用法和区别。

关键技术剖析:

Map用于存储键值对,根据键得到值,因此不允许键重复,值可以重复。

        (1)HashMap是一个最常用的Map,它根据键的hashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为null,不允许多条记录的值为null。HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要同步,可以用Collections.synchronizedMap(HashMap map)方法使HashMap具有同步的能力。

        (2)Hashtable与HashMap类似,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,然而,这也导致了Hashtable在写入时会比较慢。

        (3)LinkedHashMap像HashMap一样允许null key,内部通过维护一个双向链表,当迭代输出时可以以插入顺序(通常情况下是插入顺序,还可以是访问顺序)输出,因此性能稍微比HashMap低一点。当重新插入一条数据(也就是put一个已经存在的key)时不会影响插入顺序。LinkedHashMap让用户从未指定的、混乱顺序的Map实现HashMap和HashTable中解脱出来。非同步的。并且与TreeMap相比,没有增加插入代价。

        (4)TreeMap能够把它保存的记录根据键排序,默认是按升序排序,也可以指定排序的比较器。当用Iteraor遍历TreeMap时,得到的记录是排过序的。TreeMap的键和值都不能为空。非同步的。

        TreeMap 的底层就是一颗红黑树,它的 containsKey , get , put and remove 方法的时间复杂度是 log(n) ,并且它是按照 key 的自然顺序(或者指定排序)排列,与 LinkedHashMap 不同, LinkedHashMap 保证了元素是按照插入的顺序排列。

        TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。

        LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同

7.  为什么用HashMap?

        HashMap 是一个散列桶(数组和链表),它存储的内容是键值对 key-value 映射

        HashMap 采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改

        HashMap 是非 synchronized,所以 HashMap 很快

        HashMap 可以接受 null 键和值,而 Hashtable 则不能(原因就是 equlas() 方法需要对象,因为 HashMap 是后出的 API 经过处理才可以)

8.  有什么方法可以减少碰撞?

        扰动函数可以减少碰撞

        原理是如果两个不相等的对象返回不同的 hashcode 的话,那么碰撞的几率就会小些。这就意味着存链表结构减小,这样取值的话就不会频繁调用 equal 方法,从而提高 HashMap 的性能(扰动即 Hash 方法内部的算法实现,目的是让不同对象返回不同hashcode)。

        使用不可变的、声明作 final 对象,并且采用合适的 equals() 和 hashCode() 方法,将会减少碰撞的发生

        不可变性使得能够缓存不同键的 hashcode,这将提高整个获取对象的速度,使用 String、Integer 这样的 wrapper 类作为键是非常好的选择。

        为什么 String、Integer 这样的 wrapper 类适合作为键?

        因为 String 是 final,而且已经重写了 equals() 和 hashCode() 方法了。不可变性是必要的,因为为了要计算 hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的 hashcode 的话,那么就不能从 HashMap 中找到你想要的对象。

9.  HashMap 中 hash 函数怎么是实现的?

        在 hashmap 中要找到某个元素,需要根据 key 的 hash 值来求得对应数组中的位置。如何计算这个位置就是 hash 算法。

        hashmap 的数据结构是数组和链表的结合,所以我们当然希望这个 hashmap 里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个。那么当我们用 hash 算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。 所以,我们首先想到的就是把 hashcode 对数组长度取模运算。这样一来,元素的分布相对来说是比较均匀的。

       但是“模”运算的消耗还是比较大的,能不能找一种更快速、消耗更小的方式?我们来看看 JDK1.8 源码是怎么做的

      右移16位,之后按位异或

       高16 bit 不变,低16 bit 和高16 bit 做了一个异或(得到的 hashcode 转化为32位二进制,前16位和后16位低16 bit和高16 bit做了一个异或)

       (n·1) & hash = -> 得到下标

10. 拉链法导致的链表过深,为什么不用二叉查找树代替而选择红黑树?为什么不一直使用红黑树?

        之所以选择红黑树是为了解决二叉查找树的缺陷:二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成层次很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋、右旋、变色这些操作来保持平衡。引入红黑树就是为了查找数据快,解决链表查询深度的问题。我们知道红黑树属于平衡二叉树,为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少。所以当长度大于8的时候,会使用红黑树;如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

11. 说说你对红黑树的见解?

红黑树

     每个节点非红即黑

     根节点总是黑色的

     如果节点是红色的,则它的子节点必须是黑色的(反之不一定)

    每个叶子节点都是黑色的空节点(NIL节点)

    从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

12. 如果 HashMap(jdk8) 的大小超过了负载因子(load factor)定义的容量怎么办?

        HashMap 默认的负载因子大小为0.75。也就是说,当一个 Map 填满了75%的 bucket 时候,和其它集合类一样(如 ArrayList 等),将会创建原来 HashMap 大小的两倍的 bucket 数组来重新调整 Map 大小,并将原来的对象放入新的 bucket 数组中。这个过程叫作 rehashing

        因为它调用 hash 方法找到新的 bucket 位置。这个值只可能在两个地方,一个是原下标的位置,另一种是在下标为 <原下标+原容量> 的位置。

13. 重新调整 HashMap 大小存在什么问题吗?

        重新调整 HashMap 大小的时候,确实存在条件竞争。

        因为如果两个线程都发现 HashMap 需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来。因为移动到新的 bucket 位置的时候,HashMap 并不会将元素放在链表的尾部,而是放在头部。这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。多线程的环境下不使用 HashMap。

        为什么多线程会导致死循环,它是怎么发生的?

        HashMap 的容量是有限的。当经过多次元素插入,使得 HashMap 达到一定饱和度时,Key 映射位置发生冲突的几率会逐渐提高。这时候, HashMap 需要扩展它的长度,也就是进行Resize。

        扩容:创建一个新的 Entry 空数组,长度是原数组的2倍

rehash:遍历原 Entry 数组,把所有的 Entry 重新 Hash 到新数组

      (友情链接:HashMap扩容全过程)

14、简单聊一下HashTable?

       HashTable的主要方法的源码实现逻辑,与HashMap中非常相似,有一点重大区别就是所有的操作都是通过synchronized锁保护的。只有获得了对应的锁,才能进行后续的读写等操作。

        数组 + 链表方式存储

        默认容量:11(质数为宜)

        put操作:首先进行索引计算 (key.hashCode() & 0x7FFFFFFF)% table.length;若在链表中找到了,则替换旧值,若未找到则继续;当总元素个数超过 容量 * 加载因子 时,扩容为原来 2 倍并重新散列;将新元素加到链表头部

        对修改 Hashtable 内部共享数据的方法添加了 synchronized,保证线程安全

15、可以使用 CocurrentHashMap 来代替 Hashtable 吗?

        我们知道 Hashtable 是 synchronized 的,但是 ConcurrentHashMap 同步性能更好,因为它仅仅根据同步级别对 map 的一部分进行上锁

        ConcurrentHashMap 当然可以代替 HashTable,但是 HashTable 提供更强的线程安全性

        它们都可以用于多线程的环境,但是当 Hashtable 的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。由于 ConcurrentHashMap 引入了分割(segmentation),不论它变得多么大,仅仅需要锁定 Map 的某个部分,其它的线程不需要等到迭代完成才能访问 Map。简而言之,在迭代的过程中,ConcurrentHashMap 仅仅锁定 Map 的某个部分,而 Hashtable 则会锁定整个 Map

16. ConcurrentHashMap 底层逻辑(JDK 1.7)?

        ConcurrentHashMap 是由 Segment 数组和 HashEntry 数组和链表组成

        Segment 是基于重入锁(ReentrantLock):一个数据段竞争锁。每个 HashEntry 一个链表结构的元素,利用 Hash 算法得到索引确定归属的数据段,也就是对应到在修改时需要竞争获取的锁。ConcurrentHashMap 支持 CurrencyLevel(Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment

        核心数据如 value,以及链表都是 volatile 修饰的,保证了获取时的可见性

        首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put 操作如下:

        将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。

        遍历该 HashEntry,如果不为空则判断传入的  key 和当前遍历的 key 是否相等,相等则覆盖旧的 value

        不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容

        最后会解除在 1 中所获取当前 Segment 的锁。

        虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理

        首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

        尝试自旋获取锁

        如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。最后解除当前 Segment 的锁

17. ConcurrentHashMap底层逻辑(JDK 1.8)?

        CocurrentHashMap 抛弃了原有的 Segment 分段锁,采用了CAS + synchronized来保证并发安全性。其中的val next 都用了 volatile 修饰,保证了可见性。

        最大特点是引入了 CAS

        借助 Unsafe 来实现 native code。CAS有3个操作数,内存值 V、旧的预期值 A、要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值V修改为 B,否则什么都不做。Unsafe 借助 CPU 指令 cmpxchg 来实现。

put 过程

根据 key 计算出 hashcode

判断是否需要进行初始化

通过 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功

如果当前位置的 hashcode == MOVED == -1,则需要进行扩容

如果都不满足,则利用 synchronized 锁写入数据

如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树

get 过程

根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值

如果是红黑树那就按照树的方式获取值

就不满足那就按照链表的方式遍历获取值

18. hashMap(1.7) 扩容的条件是什么?

      数据结构:数组 + 链表 

        扩容必须满足两个条件:

        1、 存放新值的时候当前已有元素的个数必须大于等于阈值

        2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)

     最少12个元素,最多27个元素   12+15个

19. hashMap(1.8) 扩容的条件是什么?

       数据结构:数组 + ( 链表 or 红黑树 ) 

       扩容必须满足条件:

       1、 存放新值的时候当前已有元素的个数必须大于阈值

     当第13个元素进来时,发生扩容

20. hashMap(1.8)源码字段分析 ?

//默认table数组buckets的数目,必须是2的平方,默认值是16  

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   

//默认table最大的长度约10亿多(1073741824)最大的buckets数目  

static final int MAXIMUM_CAPACITY = 1 << 30;  

//默认负载因子  

static final float DEFAULT_LOAD_FACTOR = 0.75f;  

//当单个链表的个数超过8个节点就转化为红黑树存储  

static final int TREEIFY_THRESHOLD = 8;  

//如果原来是红黑树,后来被删除一些节点后,只剩小于等于6个,会被重新转成链表存储  

static final int UNTREEIFY_THRESHOLD = 6;  

//当数组的长度(注意不是map的size而是table.length)大于64的时候,  

//会对单个桶里大于8的链表进行树化  

static final int MIN_TREEIFY_CAPACITY = 64;  

//用来实现遍历map的set,依次遍历table中所有桶中的node或者treeNode  

transient Set> entrySet;  

//当前存储的实际数据量=map.size而不是table.length  

transient int size;  

//修改次数,用来判断是否该map同时被多个线程操作,  

//多线程环境下会抛出异常ConcurrentModificationException  

transient int modCount;  

//当前数组中的阈值 table.length * loadFactor,如果超 过这个阈值,就要进行扩容 (resize)  

int threshold;  

//负载因子  

final float loadFactor;  

你可能感兴趣的:(java集合面试题总结(1~20题))