面试官都扯不过你系列之集合框架类总结

文章目录

前言

这段时间也一直在补习一些基础性的知识问题,这篇文章也算是查漏补缺之文了,看了很多的公众号和牛客别人的面经,发现对于大厂来说对于基础的集合框架问的还是蛮多的,更加深入就会询问HashMap的更深层次的每个函数的具体实现,及扩容时候的位置以及为何会出现环,再者就是一些集合类的比较。
想来一下系统的记录,也给大家进行一个总结。不用再遇到问题时候找不到合适的答案。

借用放翁的两句浅案:

   ---纸上得来终觉浅,绝知此事要躬行。---

框架概述

说一说集合类有什么特点及与数组的比较

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

  • 对象封装数据,对象多了也需要存储。集合用于存储对象。
  • 对象的个数确定可以使用数组,对象的个数不确定的可以用集合。因为集合是可变长度的。

集合和数组的区别

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

对于集合容器,有很多种。因为每一个容器的自身特点不同,其实原理在于每个容器的内部数据结构不同。
集合容器在不断向上抽取过程中,出现了集合体系。在使用一个体系的原则:参阅顶层内容。建立底层对象

说一说集合类之间的主要关系

先来看一张图。

在这里插入图片描述

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
    解释:
  1. Collection:Collection 是集合 List、Set、Queue 的最基本的接口。
  2. Iterator:迭代器,可以通过迭代器遍历集合中的数据
  3. Map:是映射表的基础接口

List、Map、Set 三个接口,存取元素时,各有什么特点?

  • 首先,List 与 Set 具有相似性,它们都是单列元素的集合,所以,它们有一个功共同的父接口,叫 Collection。Set 里面不允许有重复的元素,所谓重复,即不能有两个相等(注意,不是仅仅是相同)的对象 ,即假设 Set 集合中有了一个 A 对象,现在我要向 Set 集合再存入一个 B 对象,但 B 对象与 A 对象equals 相等,则 B 对象存储不进去,所以,Set 集合的 add 方法有一个 boolean 的返回值,当集合中没有某个元素,此时 add 方法可成功加入该元素时,则返回 true,当集合含有与某个元素 equals 相等的元素时,此时 add 方法无法加入该元素,返回结果为 false。Set 取元素时,没法说取第几个,只能以 Iterator 接口取得所有的元素,再逐一遍历各个元素。

  • List 表示有先后顺序的集合, 注意,不是那种按年龄、按大小、按价格之类的排序。当我们多次调用 add(Object) 方法时,每次加入的对象就像火车站买票有排队顺序一样,按先来后到的顺序排序。有时候,也可以插队,即调用 add(int index,Obj e) 方法,就可以指定当前对象在集合中的存放位置。一个对象可以被反复存储进 List 中,每调用一次 add 方法,这个对象就被插入进集合中一次,其实,并不是把这个对象本身存储进了集合中,而是在集合中用一个索引变量指向这个对象,当这个对象被 add 多次时,即相当于集合中有多个索引指向了这个对象,如图 x 所示。List 除了可以以 Iterator 接口取得所有的元素,再逐一遍历各个元素之外,还可以调用 get(index i) 来明确说明取第几个。

  • Map 与 List 和 Set 不同,它是双列的集合,其中有 put 方法,定义如下:put(obj key,obj value),每次存储时,要存储一对 key/value,不能存储重复的 key,这个重复的规则也是按 equals 比较相等。取则可以根据 key 获得相应的 value,即 get(Object key) 返回值为 key 所对应的 value。另外,也可以获得所有的 key 的结合,还可以获得所有的 value 的结合,还可以获得 key 和 value 组合成的 Map.Entry 对象的集合。
    List 以特定次序来持有元素,可有重复元素。Set无法拥有重复元素, 内部排序。Map 保存 key-value 值,value 可多值。 HashSet 按照 hashcode 值的某种运算方式进行存储,而不是直接按 hashCode 值的大小进行存储。例如,“abc” —> 78,“def” —> 62,“xyz” —> 65 在 hashSet 中的存储顺序不是 62,65,78,

集合类全息图

软件使用:幕布
会员免费领取地址
在这里插入图片描述

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

  • Vector:就比Arraylist多了个同步化机制(线程安全)。
  • Hashtable:使用了synchronized关键字使其变得线程安全,但是却很少使用到因为效率过于低下。
  • ConcurrentHashMap:是一种高效但是线程安全的集合。
  • Stack:栈,也是线程安全的,继承于Vector。

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

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

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

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

解决办法:
在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
使用CopyOnWriteArrayList来替换ArrayList

什么是迭代器

Iterator 接口中提供了很多对集合元素迭代的方法。每个集合中都有可以返回迭代器对象的方法iterator()。迭代器在迭代的过程中还可以删除底层的元素。

iterator和ListIterator的区别

  • Iterator可以用来遍历SetList,但是ListIterator只能用来遍历List.
  • lterator 只能向前遍历 (next) 。而ListIterator可以向前遍历,也可以向后遍历(previous())
    *ListIterator 实现了Iterator接口

Collection和Collections的区别

  • Collection是集合类的上级接口,继承他的接口主要有Set和List
  • Collections 仅是针对集合封装类的一个类,提供一系列的静态方法对各种几何的搜索,排序,线程安全化等操作。在java.util 包下面。

Comparable和Compartor接口是干什么,列出区别

Java 提供了只包含一个conpareTo方法的Comparable接口,这个方法可以给两个对象排序,用来返回 负数,0 和正数,来表示 小于 等于 大于。
对于 Compartor提供了compare() 和 equals() 两个方法。compare和compareTo的使用大概相同,传进去两个参数。equals() 需要一个对象作为参数,用来判断输入的参数是否和compartor相等,只有两者相等时候 返回true;

heap 和stack 有什么区别

Java 的内存分为两类,一类是堆内存,一类是栈内存。栈内存是指程序进入到一个方法时候,会为这个方法单独分配一块私属空间,用于存储这个方法内部的局部变量,当这个方法结束的时候,分配给这个方法的栈会释放,这个栈中的变量也会随之释放。
堆与栈作用于不同的内存,一般用于存放的是不在当前方法栈中的那些数据,例如使用new 创建的对象都是存放在堆上,所以说 不会随着方法的结束而消失。并且在方法中的局部变量使用到了final修饰后,放在堆中(此处使用到了jvm的相关知识)。

如何确保一个集合不会被修改

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

List<String> list = new ArrayList<>();
list. add("A");
Collection<String> unmlist = Collections. unmodifiableCollection(list);
unmlist. add("B"); // 运行时此行报错
System. out. println(list.size());

Collection

List

Array与ArrayList有什么区别

  • Array可以包含基本类型的对象类型,但是ArrayLsit只能包含对象类型的数据类型。
  • Array的大小值是固定的,ArrayList的动态变化的
  • ArrayList 还提供了Array不具有的方法,addAll()removeAll()iterator()
  • 对于基本数据类型,集合有采用自动封箱来减少代码量。但是对于处理固定大小的数据类型时候,处理起来就会比较慢

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

  • List转数组:toArray(arraylist.size()方法

  • 数组转List:Arrays的asList(a)方法

List<String> arrayList = new ArrayList<String>();
arrayList.add("s");
arrayList.add("e");
arrayList.add("n");
/**
 * ArrayList转数组
 */
int size=arrayList.size();
String[] a = arrayList.toArray(new String[size]);
//输出第二个元素
System.out.println(a[1]);//结果:e
//输出整个数组
System.out.println(Arrays.toString(a));//结果:[s, e, n]
/**
 * 数组转list
 */
List<String> list=Arrays.asList(a);
/**
* list转Arraylist
*/
List<String> arrayList2 = new ArrayList<String>();
arrayList2.addAll(list);
System.out.println(list);

ArrayList 和LinkedList 的区别

  • 是否保证线程安全: 都不是同步的,也就是说都不能够保证线程的安全性。
  • 底层的数据结构: ArrayList底层使用的是 Object数组; LinkedList 底层使用的是 双向链表来实现
  • 插入元素与删除元素时候是否会受到位置的影响:如前面所说的 Arr底层使用的是数组,所以在查找的效率会比较高,但是对于插入和删除而言,会动用到很多其他的元素。对于LinkedList而言底层采用链表结构 对于查找不是很有利,但是进行插入和删除会比较快。
  • 是否支持随机访问: LinkedList 底层使用链表,不支持随机访问。ArrayList 底层使用Object数组 可以随机访问
  • 空间占用: ArrayList 的空间浪费主要体现在List列表的结尾会预留一定量的空间容量,而LinkedList的空间发费体现在她的每一个元素消耗的空间都比Arr要多(因为对于一个链表的节点,不仅要存取其值,还要存取其前驱节点和后继节点信息。)

如何边遍历边移除 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 时另一个线程修改它。

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

  • 如果两个对象相等,则hashcode一定也是相同的
  • 两个对象相等,对两个equals方法返回true
  • 两个对象有相同的hashcode值,它们也不一定是相等的
    综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
    hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

ArrayList 和Vector 有什么区别

都是集成了List 接口 (List接口继承了Collection接口)都是有序的集合,相当于一种动态的数组可以按照位置索引号取出某个元素,并且运行元素是重复的,这是与HashSet最大不同的地方。主要有两个方面的区别:

  • 同步性
    对于 Vertor是线程安全的,也就是说她的方法是线程同步的,而ArrayList是线程不安全的。如果有一个线程访问集合,最好使用ArrayList,不用考虑线程安全的情况下,效率会更高。对于有多个线程访问时候,使用到Vertor 因为不需要我们自己去考虑编写线程安全的代码。
  • 数据增长:
    对于ArrayList 与 Vertor都有一个初始的容量大小,当数据增长到存储的容量时候,就需要增加空间, 他们的存储空间,每次都会增加一定数量的存储空间,对于Vertor增长的是原来的两倍,而在Arr中源码表示的是增长 1.5倍。Arr与Vertor都可以设置初始的空间大小。Ver还可以设置增长的空间大小,但是Arr没有提供增长空间的方法。

ArrayList,Vertor,LinkedList的存储特性

ArrayListVertor都是使用数组进行存储,相对之下,查找较快,但是插入和删除比较慢,LinkedList使用双向链表与之相反,关于 为什么Vertor的效率较Arr慢,是因为使用了synchronized(线程安全) 所以性能就会较差。

Set

HashSet如何检查是否重复

往HashSet中存放一个元素时候 HashSet首先会根据 对象的hashCode值判断当前集合中此hashCode对应的位置有没有值,如果没有就直接添加,若是有就使用equals 来判断两个对象是否相同,相同就不再存储(保证了Set集合的不可重复性)否则就散列到其他位置存储。

HashMap 与 HashSet的区别

HashMap HashSet
实现了Map接口 实现了Set接口
储存键值对 仅仅储存对象
使用了Key值一系列的计算 来计算hashCode 使用成员对象来计算 hashCode 对于两个对象来说,hashcode可能是相同的,所以使用 equals来判断对象的相等性

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

TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。
Collections 工具类的 sort 方法有两种重载的形式,
第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;
第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

Queue

什么是组塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法

  1. 支持组塞的插入方法: 意思是 当队列满的时候,队列会组塞插入元素的线程,直到队列不满。
  2. 支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
    组塞队列常用于生产者和消费者的场景,生产者是向队列里面添加元素的线程,消费者是从队列中取元素的线程

不可用时候的处理

❑ 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException (“Queuefull”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
❑ 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
❑ 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
❑ 超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。

注意: 如果是无界组塞队列,队列不可能会出现满的情况,所以使用 put和offer方法永远不会被阻塞,而且使用 offer方法的时候,该方法永远返回的都是true

阻塞队列的类型

提供了七个组塞队列:

❑ ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序,默认情况下不保证线程公平访问队列

❑ LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。

❑ PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。

❑ DelayQueue:一个使用优先级队列实现的无界阻塞队列。
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素
DelayQueue非常有用,可以将DelayQueue运用在以下应用场景。

  1. 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
  2. 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的。

❑ SynchronousQueue:一个不存储元素的阻塞队列。
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。它支持公平访问队列。默认情况下线程采用非公平性策略访问队列
❑ LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。

  1. transfer方法
    如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。transfer方法的关键代码如下。
Node pred =tryAppend(s,haveData);
return awaitMatch(s,pred,e,(how==TIMED),nanos);

第一行代码是试图把存放当前元素的s节点作为tail节点。第二行代码是让CPU自旋等待消费者消费元素。因为自旋会消耗CPU,所以自旋一定的次数后使用Thread.yield()方法来暂停当前正在执行的线程,并执行其他线程。
2. tryTransfer方法
tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,以First单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是JDK的bug,使用时还是用带有First和Last后缀的方法更清楚。

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

  • 相同点:都是返回第一个元素,并在队列中删除返回的对象。
  • 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。

Map

HashMap 和 TreeMap有什么区别:

是否线程安全 都不是线程安全的
是否有序 对于 hashmap来说 继承的是 AbstractMap 类 是通过 hashcode()对内容进行快速查找的 所以是无序的 但是 TreeMap继承了SortMap 保证了键的有序性。
对于 hashmap来说 底层使用到了 链表加数组加红黑树 能够 对equals 方法进行了重写 并且提供了优化处理 可以进行调优初始容量与负载因子。
对于 tree 底层使用到的是 红黑树 没有调优的功能 因为对于 红黑树来说 总是处于平衡的状态。
hashmap适用于 插入 删除 对元素的定位 判断是否存在
tree来说 适用于 按自然顺序 或自定义顺序遍历键值。

LinkedhashMap的实现原理

也是基于HashMap实现的 不同的是它定义了一个Entry header ,这个 header不是存在方Table里面,是额外独立出来的。LinkedHashMap 通过继承 hashMap 中的 Entry, 并添加两个属性 Entry
before,after, 和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序LinkedHashMap 定义了排序模式 accessOrder,该属性为 boolean 型变量,对于访问顺序,为 true;对于插入顺序,则为 false。一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。

HashMap 和HashTable 的区别

两者都实现了Map接口 主要区别在 安全性 同步性,和速度上。

  1. 是否线程安全: HashMap是线程不安全,对于 Hashtable 内部都经过了synchronized的修饰,线程同步安全。
    2.效率: 由于synchronize的修饰 效率会交HashMap差一点。
  2. 对于 null key 与 null value的支持:对于HashMap 支持null key但是只能有一个key的值为null。对于 value可以有多个的value值为null。
    而在HashTable中 key值是不可以为null的。
  3. 底层数据结构的处理: 对于HashMap 在JDK1.8中增加了红黑树的处理,对于一个数组中的链表长度大于阙值(8)时候 会将链表自动转换为红黑树,以增加效率,但是在 HashTable中 没有这样的处理。

HashMap的实现原理及具体问题

这里就不再展示出来写在另外一篇博客里面呀
HashMap详解

为什么不一开始就使用红黑树,不是效率很高吗?

因为红⿊树需要进⾏左旋,右旋,变⾊这些操作来保持平衡,⽽单链表不需要。
当元素⼩于8个当时候,此时做查询操作,链表结构已经能保证查询性能。
当元素⼤于8个的时候,此时需要红⿊树来加快查 询速度,但是新增节点的效率变慢了。
因此,如果⼀开始就⽤红⿊树结构,元素太少,新增效率⼜⽐较慢,⽆疑这是浪费性能的。

什么时候红黑树退化为链表

为6的时候退转为链表。中间有个差值7可以防⽌链表和树之间频繁的转换。
假设⼀下,如果设计成链表个数超过8则链表转 换成树结构,链表个数⼩于8则树结构转换成链表,
如果⼀个HashMap不停的插⼊、删除元素,链表个数在8左右徘徊,就会 频繁的发⽣树转链表、链表转树,效率会很低。

HashMap多线程下会出现什么问题

(1)多线程扩容,引起的死循环问题
(2)多线程put的时候可能导致元素丢失
(3)put⾮null元素后get出来的却是null

不安全性的解决方案

  1. 在之前使用hashtable。 在每一个函数前面都加上了synchronized 但是 效率太低 我们现在不常用了。
  2. 使用 ConcurrentHashmap函数,对于这个函数而言 我们可以每几个元素共用一把锁。用于提高效率。

你一般用什么作为HashMap的key值

  • key可以是null吗,value可以是null吗
  • 一般用什么作为key值
  • 用可变类当Hashmap1的Key会有什么问题
  • 让你实现一个自定义的class作为HashMap的Key该如何实现

key可以是null吗,value可以是null吗

当然都是可以的,但是对于 key来说只能运行出现一个key值为null,但是可以出现多个value值为null

一般用什么作为key值

⼀般⽤Integer、String这种不可变类当HashMap当key,⽽且String最为常⽤。
(1)因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。 这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。 这就是HashMap中的键往往都使⽤字符串。
(2)因为获取对象的时候要⽤到equals()和hashCode()⽅法,那么键对象正确的重写这两个⽅法是⾮常重要的,这些类已 经很规范的覆写了hashCode()以及equals()⽅法。

用可变类当Hashmap1的Key会有什么问题

hashcode可能会发生变化,导致put进行的值,无法get出来,如下代码所示:

 HashMap<List<String>,Object> map=new HashMap<>();
        List<String> list=new ArrayList<>();
        list.add("hello");
        Object object=new Object();
        map.put(list,object);
        System.out.println(map.get(list));
        list.add("hello world");
        System.out.println(map.get(list));

输出值如下:

java.lang.Object@1b6d3586
null

实现一个自定义的class作为Hashmap的key该如何实现

对于这个问题考查到了下面的两个知识点

  • 重写hashcode和equals方法需要注意什么?
  • 如何设计一个不变的类。
    针对问题⼀,记住下⾯四个原则即可
    (1)两个对象相等,hashcode⼀定相等
    (2)两个对象不等,hashcode不⼀定不等
    (3)hashcode相等,两个对象不⼀定相等
    (4)hashcode不等,两个对象⼀定不等
    针对问题⼆,记住如何写⼀个不可变类
    (1)类添加final修饰符,保证类不被继承。 如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖⽗类的⽅法并且继承类可以改变成员变量值,那么⼀旦⼦类 以⽗类的形式出现时,不能保证当前类是否可变。
    (2)保证所有成员变量必须私有,并且加上final修饰 通过这种⽅式保证成员变量不可改变。但只做到这⼀步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第4 点弥补这个不⾜。
    (3)不提供改变成员变量的⽅法,包括setter 避免通过其他接⼝改变成员变量的值,破坏不可变特性。
    (4)通过构造器初始化所有成员,进⾏深拷⻉(deep copy)
    (5) 在getter⽅法中,不要直接返回对象本⾝,⽽是克隆对象,并返回对象的拷⻉ 这种做法也是防⽌对象外泄,防⽌通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发⽣改变

如何决定使用 HashMap 还是 TreeMap?

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

HashMap 和 ConcurrentHashMap 的区别

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

ConcurrentHashMap 和 Hashtable 的区别?

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

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

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

首先需要明白对于concurrentHashMap同hashmap一样对于jdk1.8之后也是做了改动,先来看看原始版本:

JDK1.7

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

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

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

在这里插入图片描述
总的来说,ConcurrentHashMap的高效并发机制是通过以下三方面来保证的:

  • 通过锁分段技术保证并发环境下的写操作;

  • 通过 HashEntry的不变性、Volatile变量的内存可见性和加锁重读机制保证高效、安全的读操作;

  • 通过不加锁和加锁两种方案控制跨段操作的的安全性。

详解

:这也只是简单进行一个解释,表示都是重要的知识点,面试官就会问你关于两者(1.7和1.8)的区别.后期会出一篇关于其的详解,这里就不再进行具体的详解。
ConcurrentHashMap类中包含两个静态内部类 HashEntrySegment,其中 HashEntry 用来封装具体的K/V对,是个典型的四元组;Segment 用来充当锁的角色,每个 Segment 对象守护整个ConcurrentHashMap的若干个桶 (可以把Segment看作是一个小型的哈希表),其中每个桶是由若干个 HashEntry 对象链接起来的链表。
总的来说,一个ConcurrentHashMap实例中包含由若干个Segment实例组成的数组,而一个Segment实例又包含由若干个桶,每个桶中都包含一条由若干个 HashEntry 对象链接起来的链表。特别地,ConcurrentHashMap 在默认并发级别下会创建16个Segment对象的数组,如果键能均匀散列,每个 Segment 大约守护整个散列表中桶总数的 1/16。

JDK1.8

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

后记

就多看源码,多理解,多实践,集合类这一块还算是基础部分。一般也都是一面二面较多,但是若是知道的部分尽力去回答到最好,增加印象分,当然不是让你去背题目,而是理解,运用,就酱。

发布了60 篇原创文章 · 获赞 38 · 访问量 4467

猜你喜欢

转载自blog.csdn.net/weixin_44015043/article/details/105381907