Java集合容器面试题(2020最新版),深入理解linux内核百度网盘

  • ArrayList 和 Vector 的区别是什么?

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

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

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

  • List 和 Set 的区别

  • Set接口

    • 说一下 HashSet 的实现原理?
  • HashSet如何检查重复?HashSet是如何保证数据不可重复的?

  • HashSet与HashMap的区别

  • Queue

    • BlockingQueue是什么?
  • 在 Queue 中 poll()和 remove()有什么区别?

  • Map接口

    • 说一下 HashMap 的实现原理?
  • HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现

    • JDK1.8之前
  • JDK1.8之后

  • JDK1.7 VS JDK1.8 比较

  • HashMap的put方法的具体流程?

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

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

    • 什么是哈希?
  • 什么是哈希冲突?

  • HashMap的数据结构

  • hash()函数

  • JDK1.8新增红黑树

  • 总结

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

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

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

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

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

  • HashMap 与 HashTable 有什么区别?

  • 如何决定使用 HashMap 还是 TreeMap?

  • HashMap 和 ConcurrentHashMap 的区别

  • ConcurrentHashMap 和 Hashtable 的区别?

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

  • 辅助工具类

    • Array 和 ArrayList 有何区别?
  • 如何实现 Array 和 List 之间的转换?

  • comparable 和 comparator的区别?

  • Collection 和 Collections 有什么区别?

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

Java面试总结汇总,整理了包括Java基础知识,集合容器,并发编程,JVM,常用开源框架Spring,MyBatis,数据库,中间件等,包含了作为一个Java工程师在面试中需要用到或者可能用到的绝大部分知识。欢迎大家阅读,本人见识有限,写的博客难免有错误或者疏忽的地方,还望各位大佬指点,在此表示感激不尽。文章持续更新中…

| 序号 | 内容 | 链接地址 |

| — | — | — |

| 1 | Java基础知识面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104390612 |

| 2 | Java集合容器面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104588551 |

| 3 | Java异常面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104390689 |

| 4 | 并发编程面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104863992 |

| 5 | JVM面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104390752 |

| 6 | Spring面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397516 |

| 7 | Spring MVC面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397427 |

| 8 | Spring Boot面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397299 |

| 9 | Spring Cloud面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397367 |

| 10 | MyBatis面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/101292950 |

| 11 | Redis面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/103522351 |

| 12 | MySQL数据库面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104778621 |

| 13 | 消息中间件MQ与RabbitMQ面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104588612 |

| 14 | Dubbo面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104390006 |

| 15 | Linux面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104588679 |

| 16 | Tomcat面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397665 |

| 17 | ZooKeeper面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397719 |

| 18 | Netty面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104391081 |

| 19 | 架构设计&分布式&数据结构与算法面试题(2020最新版) | https://thinkwon.blog.csdn.net/article/details/105870730 |

集合容器概述


什么是集合

集合框架:用于存储数据的容器。

集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。

任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。

接口:表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现,从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。

实现:集合接口的具体实现,是重用性很高的数据结构。

算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。这些算法通常是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现。事实上,算法是可复用的函数。

它减少了程序设计的辛劳。

集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部分上,而不是为了让程序能正常运转而将注意力于低层设计上。

通过这些在无关API之间的简易的互用性,使你免除了为改编对象或转换代码以便联合这些API而去写大量的代码。 它提高了程序速度和质量。

集合的特点

集合的特点主要有如下两点:

  • 对象封装数据,对象多了也需要存储。集合用于存储对象。

  • 对象的个数确定可以使用数组,对象的个数不确定的可以用集合。因为集合是可变长度的。

集合和数组的区别

  • 数组是固定长度的;集合可变长度的。

  • 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。

  • 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

数据结构:就是容器中存储数据的方式。

对于集合容器,有很多种。因为每一个容器的自身特点不同,其实原理在于每个容器的内部数据结构不同。

集合容器在不断向上抽取过程中,出现了集合体系。在使用一个体系的原则:参阅顶层内容。建立底层对象。

使用集合框架的好处

  1. 容量自增长;

  2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;

  3. 允许不同 API 之间的互操作,API之间可以来回传递集合;

  4. 可以方便地扩展或改写集合,提高代码复用性和可操作性。

  5. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。

常用的集合类有哪些?

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

  1. Collection接口的子接口包括:Set接口和List接口

  2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等

  3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等

  4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

List,Set,Map三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各有什么特点?

img

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

集合框架底层数据结构

Collection

  1. List
  • Arraylist: Object数组

  • Vector: Object数组

  • LinkedList: 双向循环链表

  1. Set
  • HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素

  • LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。

  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

Map

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

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

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

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

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

  • vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。

  • statck:堆栈类,先进后出。

  • hashtable:就比hashmap多了个线程安全。

  • enumeration:枚举,相当于迭代器。

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

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

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

示例代码如下:

List list = new ArrayList<>();

list. add(“x”);

Collection clist = Collections. unmodifiableCollection(list);

clist. add(“y”); // 运行时此行报错

System. out. println(list. size());

Collection接口


List接口

迭代器 Iterator 是什么?

Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。

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

Iterator 使用代码如下:

List list = new ArrayList<>();

Iterator it = list. iterator();

while(it. hasNext()){

String obj = it. next();

System. out. println(obj);

}

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

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

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

Iterator 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 时另一个线程修改它。

Iterator 和 ListIterator 有什么区别?

  • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。

  • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。

  • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

遍历一个 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 遍历。

说一下 ArrayList 的优缺点

ArrayList的优点如下:

  • ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。

  • ArrayList 在顺序添加一个元素的时候非常方便。

ArrayList 的缺点如下:

  • 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。

  • 插入元素的时候,也需要做一次元素复制操作,缺点同上。

ArrayList 比较适合顺序添加、随机访问的场景。

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

  • 数组转 List:使用 Arrays. asList(array) 进行转换。

  • List 转数组:使用 List 自带的 toArray() 方法。

代码示例:

// list to array

List list = new ArrayList();

list.add(“123”);

list.add(“456”);

list.toArray();

// array to list

String[] array = new String[]{“123”,“456”};

Arrays.asList(array);

ArrayList 和 LinkedList 的区别是什么?

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

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

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

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

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

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

补充:数据结构基础之双向链表

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

ArrayList 和 Vector 的区别是什么?

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

  • 线程安全:Vector 使用了 Synchroni

《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》

【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享

zed 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。

  • 性能:ArrayList 在性能方面要优于 Vector。

  • 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。

Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。

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

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

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

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

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

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

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

List synchronizedList = Collections.synchronizedList(list);

synchronizedList.add(“aaa”);

synchronizedList.add(“bbb”);

for (int i = 0; i < synchronizedList.size(); i++) {

System.out.println(synchronizedList.get(i));

}

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

ArrayList 中的数组定义如下:

private transient Object[] elementData;

再看一下 ArrayList 的定义:

public class ArrayList extends AbstractList

implements List, 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

s.writeObject(elementData[i]);

if (modCount != expectedModCount) {

throw new ConcurrentModificationException();

}

每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。

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接口

说一下 HashSet 的实现原理?

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

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 map;

public HashSet() {

map = new HashMap<>();

}

public boolean add(E e) {

// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值

return map.put(e, PRESENT)==null;

}

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

  1. 如果两个对象相等,则hashcode一定也是相同的

  2. 两个对象相等,对两个equals方法返回true

  3. 两个对象有相同的hashcode值,它们也不一定是相等的

  4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖

  5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

==与equals的区别

  1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同

  2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较3.==指引用是否相同 equals()指的是值是否相同

HashSet与HashMap的区别

| HashMap | HashSet |

| — | — |

| 实现了Map接口 | 实现Set接口 |

| 存储键值对 | 仅存储对象 |

| 调用put()向map中添加元素 | 调用add()方法向Set中添加元素 |

| HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false |

| HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 |

Queue

BlockingQueue是什么?

Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

在 Queue 中 poll()和 remove()有什么区别?

  • 相同点:都是返回第一个元素,并在队列中删除返回的对象。

  • 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。

代码示例:

Queue queue = new LinkedList();

queue. offer(“string”); // add

System. out. println(queue. poll());

System. out. println(queue. remove());

System. out. println(queue. size());

Map接口


说一下 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)

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

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

JDK1.8之前

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

jdk1.7中HashMap数据结构

JDK1.8之后

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

jdk1.8中HashMap数据结构

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 原位置 + 旧容量) |

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方法执行流程图

putVal方法执行流程图

public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

//实现Map.put和相关方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

boolean evict) {

Node[] tab; Node p; int n, i;

// 步骤①:tab为空则创建

// table未初始化或者长度为0,进行扩容

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

// 步骤②:计算index,并对null做处理

// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)

if ((p = tab[i = (n - 1) & hash]) == null)

tab[i] = newNode(hash, key, value, null);

// 桶中已经存在元素

else {

Node e; K k;

// 步骤③:节点key存在,直接覆盖value

// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

// 将第一个元素赋值给e,用e来记录

e = p;

// 步骤④:判断该链为红黑树

// hash值不相等,即key不相等;为红黑树结点

// 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null

else if (p instanceof TreeNode)

// 放入树中

e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

// 步骤⑤:该链为链表

// 为链表结点

else {

// 在链表最末插入结点

for (int binCount = 0; ; ++binCount) {

// 到达链表的尾部

//判断该链表尾部指针是不是空的

if ((e = p.next) == null) {

// 在尾部插入新结点

p.next = newNode(hash, key, value, null);

//判断链表的长度是否达到转化红黑树的临界值,临界值为8

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

//链表结构转树形结构

treeifyBin(tab, hash);

// 跳出循环

break;

}

// 判断链表中结点的key值与插入的元素的key值是否相等

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

// 相等,跳出循环

break;

// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表

p = e;

}

}

//判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值

if (e != null) {

// 记录e的value

V oldValue = e.value;

// onlyIfAbsent为false或者旧值为null

if (!onlyIfAbsent || oldValue == null)

//用新值替换旧值

e.value = value;

// 访问后回调

afterNodeAccess(e);

// 返回旧值

return oldValue;

}

}

// 结构性修改

++modCount;

// 步骤⑥:超过最大容量就扩容

// 实际大小大于阈值则扩容

if (++size > threshold)

resize();

// 插入后回调

afterNodeInsertion(evict);

return null;

}

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

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

①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;

②.每次扩展的时候,都是扩展2倍;

③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

final Node[] resize() {

Node[] oldTab = table;//oldTab指向hash桶数组

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)

newThr = oldThr << 1; // double threshold 双倍扩容阀值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”})

Node[] newTab = (Node[])new Node[newCap];//新建hash桶数组

table = newTab;//将新数组的值复制给旧的hash桶数组

// 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散

if (oldTab != null) {

// 遍历新数组的所有桶下标

for (int j = 0; j < oldCap; ++j) {

Node e;

if ((e = oldTab[j]) != null) {

// 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收

oldTab[j] = null;

// 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树

if (e.next == null)

// 用同样的hash映射算法把该元素加入新的数组

newTab[e.hash & (newCap - 1)] = e;

// 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排

else if (e instanceof TreeNode)

((TreeNode)e).split(this, newTab, j, oldCap);

// e是链表的头并且e.next!=null,那么处理链表中元素重排

else { // preserve order

// loHead,loTail 代表扩容后不用变换下标,见注1

Node loHead = null, loTail = null;

// hiHead,hiTail 代表扩容后变换下标,见注1

Node hiHead = null, hiTail = null;

Node next;

// 遍历链表

do {

你可能感兴趣的:(程序员,面试,java,后端)