Java面试题之Java集合面试题 50道(带答案)

简介: 这篇文章提供了50道Java集合框架的面试题及其答案,涵盖了集合的基础知识、底层数据结构、不同集合类的特点和用法,以及一些高级主题如并发集合的使用。

导言

一、集合容器概述(10个问题)

1. 什么是集合

集合就是一个放数据的容器,准确的说是放数据对象引用的容器 集合类存放的都是对象的引用,而不是对象的本身
集合类型主要有3种:set(集)、list(列表)和map(映射)。

2. 集合的特点

集合的特点主要有如下两点:
集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。
和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小

3. 集合和数组的区别

  1. 数组是固定长度的;集合可变长度的。
  2. 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
  3. 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

4. 使用集合框架的好处

  1. 容量自增长;
  2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
  3. 可以方便地扩展或改写集合,提高代码复用性和可操作性。
  4. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。

5. 常用的集合类有哪些?

Map接口和Collection接口是所有集合框架的父接口:

  1. Collection接口的子接口包括:Set接口和List接口
  2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
  3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

6. List,Set,Map三者的区别?

Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接 口。

我们比较常用的是Set、List,Map接口不是collection的子接口。

Collection集合主要有List和Set两大接口

  • List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多 个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

  • Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一 个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

  • Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重 复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应 的值对象。
    Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、 ConcurrentHashMap

7. 集合框架底层数据结构

a、Collection

  • List
    1. Arraylist: Object数组
    2. Vector: Object数组
    3. LinkedList: 双向循环链表
  • Set
    1. HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
    2. LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来 实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不 过还是有一点点区别的。
    3. TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

b、Map

  • HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是 主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较 大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间

  • LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是 基于拉链式 散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加 了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。

  • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突 而存在的

  • TreeMap: 红黑树(自平衡的排序二叉树)

8. 哪些集合类是线程安全的?

  • Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使 用。

  • hashTable:就比hashMap多了个synchronized (线程安全),不建议使用。

  • ConcurrentHashMap:是Java5中 支持高并发、高吞吐量的线程安全HashMap实现。它由 Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的 角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组, Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry 数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元 素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用)

9. Java集合的快速失败机制 “fail-fast”?

  • 是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast 机制。

  • 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时 候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这 个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

  • 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集 合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next() 遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍 历;否则抛出异常,终止遍历。

  • 解决办法:

    1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
    2. 使用CopyOnWriteArrayList来替换ArrayList 10.

10. 怎么确保一个集合不能被修改?

可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变 集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。

示例代码如下:

List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. 

unmodifiableCollection(list); 
clist. add("y");  // 运行时此行报错
System. out. println(list. size());

二、Collection接口(个问题)

1. List接口

11、迭代器 Iterator 是什么?

  • Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来 获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程 中移除元素。
  • 因为所有Collection接继承了Iterator迭代器
    在这里插入图片描述

12. Iterator 怎么使用?有什么特点?

Iterator 使用代码如下:

List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
  String obj = it. next();
  System. out. println(obj);
}

Iterator 的特点是 只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改 的时候,就会抛出 ConcurrentModificationException 异常。

13. 如何边遍历边移除 Collection 中的元素?

  • 边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:

    Iterator<Integer> it = list.iterator();
    while(it.hasNext()){
       *// do something*
       it.remove();
    }
    
  • 一种最常见的错误代码如下:

    for(Integer i : list){
       list.remove(i)
    }
    

    运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它

14. Iterator 和 ListIterator 有什么区别?

  • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
  • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
  • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元 素、获取前面或后面元素的索引位置。

15. 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

  • 遍历方式有以下几种:

    1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素, 当读取到最后一个元素后停止。
    2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
    3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过 程中操作数据集合,例如删除、替换
  • 最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。

    • 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均 时间复杂度为 O(1),如ArrayList。
    • 如果没有实现该接口,表示不支持 Random Access,如LinkedList。
    • 推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历

16. 说一下 ArrayList 的优缺点

  • ArrayList的优点如下:

    • ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口, 因此查找的时候非常快
    • ArrayList 在顺序添加一个元素的时候非常方便。
  • ArrayList 的缺点如下:

    • 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
    • 插入元素的时候,也需要做一次元素复制操作,缺点同上。
  • ArrayList 比较适合顺序添加、随机访问的场景。

17. 如何实现数组和 List 之间的转换?

  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法。
  • 代码示例:

     // list to array
    List<String> list = new ArrayList<String>();
    list.add("123");
    list.add("456");
    list.toArray();
    
    // array to list
    String[] array = new String[]{"123","456"};
    Arrays.asList(array);
    

18. ArrayList 和 LinkedList 的区别是什么?

  1. 数据结构实现: ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现

  2. 随机访问效率: ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数 据存储方式,所以需要移动指针从前往后依次查找。

  3. 增加和删除效率: 在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

  4. 内存空间占用: LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储 了两个引用,一个指向前一个元素,一个指向后一个元素。

  5. 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

  • 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多 时,更推荐使用 LinkedList。

  • LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向 直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结 点和后继结点。

19. ArrayList 和 Vector 的区别是什么?

  • 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合

    1. 线程安全: Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非 线程安全的。
    2. 性能: ArrayList 在性能方面要优于 Vector。
    3. 扩容: ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次 会增加 1 倍,而 ArrayList 只会增加 50%。
  • Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问 Vector的话代码要在同步操作上耗费大量的时间。

  • Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。

20. 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述ArrayList、Vector、LinkedList 的存储性能和特性?

  • ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便 增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操 作,所以索引数据快而插入数据慢

  • Vector 中的方法 由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较 ArrayList差

  • LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需 要记录当前项的前后项即可,所以 LinkedList 插入速度较快

21. 多线程场景下如何使用 ArrayList?

ArrayList 不是线程安全的,如果遇到多线程场景,可以 通过 Collections 的 synchronizedList 方法 将其转换成线程安全的容器后再使用。例如像下面这样:

List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++) {
    System.out.println(synchronizedList.get(i));
}

22. 为什么 ArrayList 的 elementData 加上 transient 修饰?

  • ArrayList 中的数组定义如下:
    在这里插入图片描述

  • 再看一下 ArrayList 的定义:

    public class ArrayList<E> extends AbstractList<E>
         implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    
  • 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现:

    private void writeObject(java.io.ObjectOutputStream s) throws
    java.io.IOException{
          *// Write out element count, and any hidden stuff*
              int expectedModCount = modCount;
          s.defaultWriteObject();
          *// Write out array length*
              s.writeInt(elementData.length);
          *// Write out all elements in the proper order.*
              for (int i=0; i<size; i++)
                  s.writeObject(elementData[i]);
          if (modCount != expectedModCount) {
              throw new ConcurrentModificationException();
    }
    
  • 每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后 遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后 的文件大小。

23. List 和 Set 的区别

  • List , Set 都是继承自Collection 接口

  • List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多 个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

  • Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一 个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

  • 另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是 set只能用迭代,因为他无序,无法用下标来取得想要的值

  • Set和List对比
    Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。 List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起 其他元素位置改变

Set接口

24. 说一下 HashSet 的实现原理?

HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统 一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值

25. HashSet如何检查重复?HashSet是如何保证数据不可重复的?

  • 向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。

  • HashSet 中的add ()方法会使用HashMap 的put()方法。

  • HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key, 并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。

  • 以下是HashSet 部分源码:

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

hashCode()与equals()的相关规定:

  1. 如果两个对象相等,则hashcode一定也是相同的
    • hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值
  2. 两个对象相等,对两个equals方法返回true
  3. 两个对象有相同的hashcode值,它们也不一定是相等的
  4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
  5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个
    对象无论如何都不会相等(即使这两个对象指向相同的数据)。

==与equals的区别

  1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存 空间的值是不是相同
  2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较

26. HashSet与HashMap的区别

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

三、Map接口

27. 什么是Hash算法

哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈
希值。

28. 什么是链表

  • 链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能。

  • 链表大致分为单链表和双向链表

    1. 单链表:每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指 针
      在这里插入图片描述
    2. 双向链表:除了包含单链表的部分,还增加的pre前一个节点的指针
      在这里插入图片描述
  • 链表的优点

    • 插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加 删除元素)
    • 内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大 小),并且在需要空间的时候才创建空间)
    • 大小没有固定,拓展很灵活。
  • 链表的缺点

    • 不能随机查找,必须从第一个开始遍历,查找效率低
  1. 说一下HashMap的实现原理?
  • HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

  • HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模 拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。 HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

  • HashMap 基于 Hash 算法实现的

    1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数 组中的下标
    2. 存储时,如果出现hash值相同的key,此时有两种情况。
      (1)如果key相同,则覆盖原始值;
      (2)如果key不同(出现冲突),则将当前的key-value放入链表中
    3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
    4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
  • 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转 为红黑树来提高查询效率,从原来的O(n)到O(logn)

30. HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现

  • 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和 删除困难;链表的特点是:寻址困难,但插入和删除容易; 所以我们将数组和链表结合在一起,发 挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。

a、HashMap JDK1.8之前

  • JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组 中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
    在这里插入图片描述

b、HashMap JDK1.8之后

相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
在这里插入图片描述

c、JDK1.7 VS JDK1.8 比较

  • JDK1.8主要解决或优化了一下问题:
    1. resize 扩容优化
    2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
    3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
不同 JDK 1.7 JDK 1.8
存储结构 数组 + 链表 数组 + 链表 + 红黑树
初始化方式 单独函数: inflateTable() 直接集成到了扩容函数 resize() 中
hash值计算方式 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 扰动处理 = 2次扰动 = 1次位运算 + 1次异 或运算
存放数据的规则 无冲突时,存放数组;冲突时,存放链表 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8: 树化并存放红黑树
插入数据方式 头插法(先讲原位置的数据移 到后1位,再插入数据到该位 置) 尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式 全部按照原来方法进行计算 (即hashCode ->> 扰动函数 - >> (h&length-1)) 按照扩容后的规律计算(即扩容后的位置 =原位置 or 原位置 + 旧容量)

31. 什么是红黑树

a、说道红黑树先讲什么是二叉树

二叉树简单来说就是 每一个节上可以关联俩个子节点

大概就是这样子:
                  a
                /  \ 
              b        c
             /\        /\ 
            d  e   f  g
           /\  /\ /\ /\ 
          h i j  kl mn o

b、红黑树

【Nil:表示无值,任何变量在没有被赋值之前的值都为nil。Null:是在计算中具有保留的值。】

  • 红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红 (Red)或黑(Black)。

  • 红黑树的每个结点是黑色或者红色。当是不管怎么样他的 根结点是黑色每个叶子结点(叶子结点 代表终结、结尾的节点)也是黑色 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]。

  • 如果一个结点是红色的,则它的子结点必须是黑色的。

  • 每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩 倍,所以红黑树是相对接近平衡的二叉树的!]

  • 红黑树的基本操作是 添加、删除 。在对红黑树进行添加或删除之后,都会用到 旋转方法 。为什么 呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面 三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新 成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。

32. HashMap的put方法的具体流程?

当我们put的时候,首先计算 keyhash 值,这里调用了 hash 方法, hash 方法实际是让 key.hashCode()key.hashCode() >>> 16 进行异或操作,高16bit补0,一个数和0异或不变, 所以 hash 函数大概的作用就是: 高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标 index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中 用了复杂度 O(logn)的树结构来提升碰撞下的性能。

  • putVal方法执行流程图
    在这里插入图片描述
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab 哈希数组, p: 该哈希桶的首节点, n: hashmap 的长度, i:计算出的数组下标
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //  步骤1:tab为空则创建
    // 先判断,然后获取长度 并 进行扩容,使用的是懒加载,table 一开始是没有加载的,等put后才开始加载
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /**
    计算插入存储的数组索引i:根据键值key计算的hash值得到 i
        此处的数组下标方式 i=(n - 1) & hash, 同JDK 1.7 中的 indexFor()一致,JDK8变成了一句话
    插入时,需判断是否存在Hash冲突:
        若不存在(即当前tab[i]==null),则直接在该数组位置新建节点,插入完毕
    **/
    // 步骤2:计算index,并对null做处理
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 插入新节点
        tab[i] = newNode(hash, key, value, null);
    // 否则,代表存在Hash冲突,即当前存储位置已存在节点,则依次往下判断:
    else {
        // e: 临时节点的作用, k 存放该当前节点的key
        Node<K,V> e; K k;
        // 第一种:当前位置的key是否与需插入的key相同,若相同 则 直接用 新value 覆盖 旧value
        // 步骤3:节点key存在,直接覆盖value
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 第二种:判断需插入的数据结构是否为 红黑树 or 链表发生哈希冲突的几种情况
        // 步骤4:判断该链为红黑树
        else if (p instanceof TreeNode)
            /**
            如果是红黑树的节点,向红黑树插入 or 更新数据(键值对)
            过程:遍历红黑树 判断该节点的key 是否与需插入的key相同:
                a、若相同,则新value 覆盖 旧value
                b、若不相同,则插入
            **/
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 第三种:hash值 不等于首节点,不为红黑树的节点,则为链表的节点
        // 步骤5:该链为链表
        else {
            // 遍历该链表
            for (int binCount = 0; ; ++binCount) {
                // 如果找到尾部,则表明添加的 key-value 没有重复,在尾部进行添加
                if ((e = p.next) == null) {
                    // 与JDK7区别,JDK8是在链表尾部添加,JDK7在头部添加
                    p.next = newNode(hash, key, value, null);
                    // 判断是否要转换为红黑树结构 TREEIFY_THRESHOLD值为8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果 链表中有重复的 key ,e 则为当前重复的节点,结束循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 有重复的 key,则用待插入值进行覆盖,返回旧值。
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 到了此步骤,则表明待插入的key-value 是没有key 的重复,因为插入成功e节点的值为null
    // 修改次数 +1
    ++modCount;
    // 实际长度 +1,判断是否大于临界值,大于则扩容
    // 步骤6:超过最大容量就扩容
    if (++size > threshold)
        resize();
    // 调用 LinkedHashMap的实现方法,其实什么也没做,因为这里 evict=true
    afterNodeInsertion(evict);
    // 添加成功
    return null;
}
  1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向 步骤6,如果table[i]不为空,转向 步骤3;
  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向 步骤4,这里的相同指的是hashCode以及equals;
  4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向 步骤5;
  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

33. HashMap的扩容操作是怎么实现的?

  1. 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进 行扩容;
  2. 每次扩展的时候,都是扩展2倍;
  3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
  • 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一 次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩 容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7 中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据 在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置 要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
final Node<K,V>[] resize() {
    //oldTab指向hash桶数组
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        //如果oldCap不为空的话,就是hash桶数组不为空
        if (oldCap >= MAXIMUM_CAPACITY) {
            //如果大于最大容量了,就赋值为整数最大的阀值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // double threshold 双倍扩容阀值threshold
            newThr = oldThr << 1; // double threshold
    }
    // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化 成最小2的n次幂
    // 直接将该值赋给新的容量
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 新的threshold = 新的cap * 0.75
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    // 计算出新的数组长度后赋给当前成员变量table
    @SuppressWarnings({"rawtypes","unchecked"})
    //新建hash桶数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 将新数组的值复制给旧的 hash 桶数组
    table = newTab;
    // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使 其均匀的分散
    if (oldTab != null) {
        // 遍历新数组的所有桶下标
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
                oldTab[j] = null;
                // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树 if (e.next == null)
                if (e.next == null)
                    // 用同样的hash映射算法把该元素加入新的数组
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // e是链表的头并且e.next!=null,那么处理链表中元素重排
                else { // preserve order
                    // loHead,loTail 代表扩容后不用变换下标,见注1
                    Node<K,V> loHead = null, loTail = null;
                    // hiHead,hiTail 代表扩容后变换下标,见注1
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍历链表
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后 loHead 
                                // 代表下标保持不变的链表的头元素
                                loHead = e;
                            else
                                // loTail.next指向当前e
                                loTail.next = e;
                            // loTail指向当前的元素e
                            // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next 指向下一个元素时,
                            // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
                            // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

34. HashMap是怎么解决哈希冲突的?

答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;

a、什么是哈希?

Hash,一般翻译为“散列”,也有直接音译为“哈希”的, Hash就是指使用哈希算法是指把任意长度 的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。

b、什么是哈希冲突?

当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

c、HashMap的数据结构

  • 在Java中,保存数据有两种比较简单的数据结构:数组和链表。

    • 数组的特点是:寻址容易,插入和删除困难;
    • 链表的特点是:寻址困难,但插入和删除容易;
  • 所以我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式: 链地址法和开放地址法 可以解决哈希冲突:
    在这里插入图片描述

  • 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;

  • 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽 位。

  • 但相比于hashCode返回的int类型,我们HashMap初始的容量大小 DEFAULT_INITAL_CAPACITY = 1 << 4 (即2的四次方16)要远小于int类型的范围,所以我们 如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化

d、hash()函数

上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的 低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算, 进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的 hash()函数如下:

static final int hash(Object key) {
    int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己 右移16位进行异或运算(高低位异或)
}
  • 这比在 JDK 1.7 中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只 进行了1次位运算和1次异或运算(2次扰动);

e、总结

  • 简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
    • 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
    • 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用 的槽位。

35. 能否使用任何类作为 Map 的 key?

可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:

  • 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。

  • 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。

  • 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。

  • 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的 性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关 的问题了。

36. 为什么HashMap中String、Integer这样的包装类适合作为K?

  • 答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
    • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
    • 内部已重写了 equals() 、 hashCode() 等方法,遵守了HashMap内部的规范(不清楚可以 去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

37. 如果使用Object作为HashMap的Key,应该怎么办呢?

答:重写 hashCode()equals() 方法

  1. 重写 hashCode() 是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中 排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
  2. 重写 equals() 方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;

38. HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

  • 答: hashCode() 方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空 间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最 大值的,并且设备上也难以提供这么多的存储空间,从而导致通过 hashCode() 计算出的哈希值可 能不在数组大小范围内,进而无法匹配存储位置;

  • 那怎么解决呢?

    1. HashMap自己实现了自己的 hash() 方法,通过两次扰动使得它自己的哈希值高低位自行进 行异或运算,降低哈希碰撞概率也使得数据分布更平均;
    2. 在保证数组长度为2的幂次方的时候,使用 hash() 运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大 小范围不匹配”的问题;

39. HashMap 的长度为什么是2的幂次方

  • 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

  • 这个算法应该如何设计呢?
    我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的 前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效 率,这就解释了 HashMap 的长度为什么是2的幂次方。

  • 那为什么是两次扰动呢?
    答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置 的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的 目的;

40. HashMap 与 HashTable 有什么区别?

  1. 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本 都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap);
  2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap);
  3. 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
  4. `初始容量大小和每次扩充容量大小的不同 ;
  5. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
  6. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其 扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
  7. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制
  8. 推荐使用 : 在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要 多线程使用则用 ConcurrentHashMap 替代

41. 什么是TreeMap

TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。 TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据 创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。 TreeMap是线程非同步的。

42. 如何决定使用 HashMap 还是 TreeMap?

对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一 个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向 HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

43. HashMap 和 ConcurrentHashMap 的区别

  1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁 进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而 HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的 方式实现,利用CAS算法。)
  2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

44. ConcurrentHashMap 和 Hashtable 的区别?

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

    • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主 体,链表则是主要为了解决哈希冲突而存在的;
    • 实现线程安全的方式:
      1. 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段
        (Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就 不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16 倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑 树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
      2. Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程 访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加 元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
  • 两者的对比图:

    1. HashTable:
      在这里插入图片描述
    2. JDK1.7的ConcurrentHashMap:
      在这里插入图片描述
    3. JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
      在这里插入图片描述
  • 答: ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同 步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行 时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的

45. ConcurrentHashMap 底层具体实现知道吗?实现原理是什么

a、JDK1.7

  • 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段 数据时,其他段的数据也能被其他线程访问。

  • 在JDK1.7中,ConcurrentHashMap采用 Segment + HashEntry 的方式进行实现,结构如下:

  • 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一 种数组和链表结构,一个Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构 的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修 改时,必须首先获得对应的Segment的锁。
    在这里插入图片描述

  1. 该类包含 两个静态内部类 HashEntry 和 Segment ; 前者用来封装映射表的键值对,后者用来充当 锁的角色;
  2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元 素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

b、JDK1.8

  • 在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用 Node + CAS + Synchronized 来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲 突,就不会产生并发,效率又提升N倍

  • 结构如下:
    在这里插入图片描述

  • 附加源码,有需要的可以看看

  • 插入元素过程(建议去看看源码):

  • 如果相应位置的Node还没有初始化,则调用CAS插入相应的数据;(put方法中一段代码)

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null,
                 new Node<K,V>(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
}
  • 如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如 果该节点的hash不小于0,则遍历链表更新节点或插入新节点;
if (fh >= 0) {
    binCount = 1;
    for (Node<K,V> e = f;; ++binCount) {
        K ek;
        if (e.hash == hash &&
            ((ek = e.key) == key ||
             (ek != null && key.equals(ek)))) {
            oldVal = e.val;
            if (!onlyIfAbsent)
                e.val = value;
            break;
        }
        Node<K,V> pred = e;
        if ((e = e.next) == null) {
            pred.next = new Node<K,V>(hash, key,
                                      value, null);
            break;
        }
    }
}
  1. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节 点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通 过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生 影响,则直接返回旧值;

  2. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;

46. HashMap、Hashtable、ConcurrentHashMap、LinkedHashMap之间的区别

1. HashMap

  1. 底层实现: 数组+链表(哈希表) ,即元素是链表的数组

    • HashMap主干是一个Entry数组,Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对
    • HashMap 是基于散列表,并且用拉链法来解决哈希冲突的。
    • 不过当链表的元素个数超过一个阀值( static final int TREEIFY_THRESHOLD = 8; )的时候,会将链表转换为红黑树,所以如果哈希冲突多的话,数组的元素将会是红黑树。
  2. 是无序的、效率高允许null key和null value、线程不同步且不安全(无synchronized)、父类是AbstractMap、时间复杂度O(1)

  3. HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null。

  4. 基于哈希表(散列表)的 Map 接口实现
    哈希表的主干就是数组

  5. 默认初始容量为16,加载因子为0.75,扩容增量:原容量的1倍

2. TreeMap

  1. 底层实现:红黑树
  2. 排序顺序、不允许null key,但允许null value、线程不安全、时间复杂度O(log(n))
  3. TreeMap实现SortMap接口,基于红黑二叉树的NavigableMap的实现
    存入TreeMap的元素应当实现Comparable接口或者实现Comparator接口

3. HashTable

  1. 底层实现: 数组+链表(哈希表)

  2. 无序、效率低、不允许null key和null value、线程同步且安全(synchronized)、父类是Dictionary

  3. 默认初始容量为11,加载因子为0.75,扩容增量:原容量的1倍 +1

  4. 相同点:都实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口

  • 细节
    1. Hashtable与 HashMap类似,它继承自Dictionary类,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了 Hashtable在写入时会比较慢。
    2. Hashtable的实现原理与HashMap基本一致。区别在于Hashtable的方法是同步的,Hashtable是线程安全的;HashMap的方法不是同步的,HashMap不是线程安全的。
    3. 除构造函数外,Hashtable的所有 public 方法声明中都有 synchronized关键字,效率较低。

4. ConcurrentHashMap

  1. 底层实现:
    (1.7)sagment数组(重入锁:ReentrentLock) + HashEntry数组
    (1.8)Node数组+链表+红黑树

  2. 无序、不允许null key和null value、线程安全

  3. ConcurrentHashMap和Hashtable的区别:
    HashTable是锁住整个hash表,而concurrentHashMap引入了分割(锁分离技术),锁的是每一个sagment数组元素

  4. 线程同步,比HashTable效率高

  • 细节:
    1. ConcurrentHashMap是使用了锁分段技术技术来保证线程安全的,锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
    2. ConcurrentHashMap 是在每个段(segment)中线程安全的。
    3. ConcurrentHashMap的应用场景是高并发,但是并不能保证线程安全,而同步的HashMap和HashMap的是锁住整个容器,而加锁之后ConcurrentHashMap不需要锁住整个容器,只需要锁住对应的Segment就好了,所以可以保证高并发同步访问,提升了效率。
    4. 可以多线程写
    5. ConcurrentHashMap把HashMap分成若干个Segmenet
      ConcurrentHashMap能够保证每一次调用都是原子操作,但是并不保证多次调用之间也是原子操作。
    6. get时不加锁,put时加锁,remove时也会加锁
    7. 线程同步的HashMap,比Hashtable效率高。

5. LinkedHashMap

  1. 是HashMap的子类
  2. 迭代 - 插入顺序、允许null key和null value、线程不安全、有下标、时间复杂度O(1)
  3. 拥有 HashMap 的所有特性,它比 HashMap 多维护了一个双向链表,所以 它的内存相比而言要比 HashMap 大,并且性能会差一些
  • 细节
    1. LinkedHashMap 是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。
    2. 在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

6. 相互关系

在这里插入图片描述

四、辅助工具类

46. Array 和 ArrayList 有何区别?

  • Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。

  • Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。

  • Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

47. 如何实现 Array 和 List 之间的转换?

  • Array 转 List: Arrays. asList(array) ;
  • List 转 Array:List 的 toArray() 方法。

48. comparable 和 comparator的区别?

  • comparable接口实际上是出自 java.lang包,它有一个 compareTo(Object obj) 方法用来排序

  • comparator接口实际上是出自 java.util 包,它有一个 compare(Object obj1, Object obj2)方法用 来排序

  • 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当 我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排 序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个 Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort().

49. Collection 和 Collections 有什么区别?

  • java.util.Collection 是一个 集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操 作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为 各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set

  • Collections则是集合类的一个 工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进 行排序、搜索以及线程安全等各种操作。

50. TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工 具类中的 sort()方法如何比较元素?

  • TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映 射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。

  • Collections 工具类的 sort 方法有两种重载的形式,

  • 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;

  • comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序

  • comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序

  • 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当 我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排 序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个 Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort().

五、区别

51

52. HashSet、LinkedHashSet、TreeSet区别

  1. 如果你需要一个访问快速的Set,你应该使用HashSet;当你需要一个排序的Set,你应该使用TreeSet;当你需要记录下插入时的顺序时,你应该使用LinedHashSet。

  2. HashSet是采用 hash表 来实现的。其中的元素没有按顺序排列,add()、remove()以及contains()等方法都是复杂度为O(1)的方法。

  3. TreeSet是采用 树结构 实现(红黑树算法)。元素是按顺序进行排列,但是add()、remove()以及contains()等方法都是复杂度为O(log (n))的方法。它还提供了一些方法来处理排序的set,如first(), last(), headSet(), tailSet()等等。

  4. LinkedHashSet介于HashSet和TreeSet之间。它也是一个hash表,但是同时维护了一个双链表 来记录插入的顺序。基本方法的复杂度为O(1)。

1. HashSet

  1. 不能保证元素的排列顺序,顺序有可能发生变化
  2. 不是同步的,非线程安全
  3. 集合元素可以是null,但只能放入一个null
  4. 当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。
  5. 简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值相等
  6. 注意,如果要把一个对象放入HashSet中,重写该对象对应类的equals方法,也应该重写其hashCode()方法。其规则是如果两个对象通过equals方法比较返回true时,其hashCode也应该相同。另外,对象中用作equals比较标准的属性,都应该用来计算hashCode的值。

2. LinkedHashSet

nkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起 来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。

  1. LinkedHashSet中不能有相同元素,可以有一个Null元素,元素严格按照放入的顺序排列。

  2. LinkedHashSet如何保证有序和唯一性?

    1).底层数据结构由哈希表和链表组成。

    2).链表保证了元素的有序即存储和取出一致,哈希表保证了元素的唯一性。

  3. 添加、删除操作时间复杂度都是O(1)。

  4. 非线程安全

3. TreeSet

TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。
TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0

自然排序
自然排序使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列。

定制排序
自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator接口,实现 int compare(T o1,T o2)方法

  1. TreeSet是中不能有相同元素,不可以有Null元素,根据元素的自然顺序进行排序。

  2. TreeSet如何保证元素的排序和唯一性?
    底层的数据结构是红黑树(一种自平衡二叉查找树)

  3. 添加、删除操作时间复杂度都是O(log(n))

  4. 非线程安全

相关文章
|
4天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
9天前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
2天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
9 2
|
2天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
7天前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
5天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
22 4
|
6天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
37 4
|
7天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
7天前
|
Java 开发者
|
17天前
|
存储 Java
[Java]面试官:你对异常处理了解多少,例如,finally中可以有return吗?
本文介绍了Java中`try...catch...finally`语句的使用细节及返回值问题,并探讨了JDK1.7引入的`try...with...resources`新特性,强调了异常处理机制及资源自动关闭的优势。
18 1