From 781e6e2b4e2095b70c8dc4da6b882954204f876a Mon Sep 17 00:00:00 2001 From: yazhouasu <2570044368@qq.com> Date: Sat, 23 May 2020 21:56:50 +0800 Subject: [PATCH] =?UTF-8?q?update=20Java=E9=9B=86=E5=90=88=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=E5=B8=B8=E8=A7=81=E9=9D=A2=E8=AF=95=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Java集合框架常见面试题.md | 501 +++++++++--------- 1 file changed, 257 insertions(+), 244 deletions(-) diff --git a/docs/java/collection/Java集合框架常见面试题.md b/docs/java/collection/Java集合框架常见面试题.md index 7af02610..0a852934 100644 --- a/docs/java/collection/Java集合框架常见面试题.md +++ b/docs/java/collection/Java集合框架常见面试题.md @@ -1,47 +1,81 @@ 点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - -- [剖析面试最常见问题之Java集合框架](#剖析面试最常见问题之java集合框架) - - [说说List,Set,Map三者的区别?](#说说listsetmap三者的区别) - - [Arraylist 与 LinkedList 区别?](#arraylist-与-linkedlist-区别) - - [补充内容:RandomAccess接口](#补充内容randomaccess接口) - - [补充内容:双向链表和双向循环链表](#补充内容双向链表和双向循环链表) - - [ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?](#arraylist-与-vector-区别呢为什么要用arraylist取代vector呢) - - [说一说 ArrayList 的扩容机制吧](#说一说-arraylist-的扩容机制吧) - - [HashMap 和 Hashtable 的区别](#hashmap-和-hashtable-的区别) - - [HashMap 和 HashSet区别](#hashmap-和-hashset区别) - - [HashSet如何检查重复](#hashset如何检查重复) - - [HashMap的底层实现](#hashmap的底层实现) - - [JDK1.8之前](#jdk18之前) - - [JDK1.8之后](#jdk18之后) - - [HashMap 的长度为什么是2的幂次方](#hashmap-的长度为什么是2的幂次方) - - [HashMap 多线程操作导致死循环问题](#hashmap-多线程操作导致死循环问题) - - [ConcurrentHashMap 和 Hashtable 的区别](#concurrenthashmap-和-hashtable-的区别) - - [ConcurrentHashMap线程安全的具体实现方式/底层具体实现](#concurrenthashmap线程安全的具体实现方式底层具体实现) - - [JDK1.7(上面有示意图)](#jdk17上面有示意图) - - [JDK1.8 (上面有示意图)](#jdk18-上面有示意图) - - [comparable 和 Comparator的区别](#comparable-和-comparator的区别) - - [Comparator定制排序](#comparator定制排序) - - [重写compareTo方法实现按年龄来排序](#重写compareto方法实现按年龄来排序) - - [集合框架底层数据结构总结](#集合框架底层数据结构总结) - - [Collection](#collection) - - [1. List](#1-list) - - [2. Set](#2-set) - - [Map](#map) - - [如何选用集合?](#如何选用集合) +* [1.1 集合概述](#) + * [1.1.1 说说List,Set,Map三者的区别?](#ListSetMap) + * [1.1.2 集合框架底层数据结构总结](#-1) + * [Collection](#Collection) + * [Map](#Map) + * [1.1.3 如何选用集合?](#-1) +* [1.2 Iterator迭代器接口](#Iterator) +* [1.3 Collection子接口之List](#CollectionList) + * [ 1.3.1 Arraylist 与 LinkedList 区别?](#ArraylistLinkedList) + * [**补充内容:RandomAccess接口**](#:RandomAccess) + * [补充内容:双向链表和双向循环链表](#:) + * [1.3.2 ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?](#ArrayListVectorArraylistVector) + * [1.3.3 说一说 ArrayList 的扩容机制吧](#ArrayList) +* [1.4 Collection子接口之Set](#CollectionSet) + * [1.4.1 comparable 和 Comparator的区别](#comparableComparator) + * [Comparator定制排序](#Comparator) + * [重写compareTo方法实现按年龄来排序](#compareTo) +* [1.5 Map接口](#Map-1) + * [1.5.1 HashMap 和 Hashtable 的区别](#HashMapHashtable) + * [1.5.2 HashMap 和 HashSet区别](#HashMapHashSet) + * [1.5.3 HashSet如何检查重复](#HashSet) + * [1.5.4 HashMap的底层实现](#HashMap) + * [JDK1.8之前](#JDK1.8) + * [JDK1.8之后](#JDK1.8-1) + * [1.5.5 HashMap 的长度为什么是2的幂次方](#HashMap2) + * [ 1.5.6 HashMap 多线程操作导致死循环问题](#HashMap-1) + * [1.5.7 ConcurrentHashMap 和 Hashtable 的区别](#ConcurrentHashMapHashtable) + * [1.5.8 ConcurrentHashMap线程安全的具体实现方式/底层具体实现](#ConcurrentHashMap) + * [JDK1.7(上面有示意图)](#JDK1.7) + * [JDK1.8 (上面有示意图)](#JDK1.8-1) +* [1.6 Collections工具类](#Collections) +* [公众号](#-1) # 剖析面试最常见问题之Java集合框架 -## 说说List,Set,Map三者的区别? +## 1.1 集合概述 +### 1.1.1 说说List,Set,Map三者的区别? - **List(对付顺序的好帮手):** List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 - **Set(注重独一无二的性质):** 不允许重复的集合。不会有多个元素引用相同的对象。 - **Map(用Key来搜索的专家):** 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。 -## Arraylist 与 LinkedList 区别? +### 1.1.2 集合框架底层数据结构总结 + +#### Collection + +##### List + +- **Arraylist:** Object数组 +- **Vector:** Object数组 +- **LinkedList:** 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) + +##### Set + +- **HashSet(无序,唯一):** 基于 HashMap 实现的,底层采用 HashMap 来保存元素 +- **LinkedHashSet:** LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的 +- **TreeSet(有序,唯一):** 红黑树(自平衡的排序二叉树) + +#### Map + +- **HashMap:** JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间 +- **LinkedHashMap:** LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931) +- **Hashtable:** 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 +- **TreeMap:** 红黑树(自平衡的排序二叉树) + +### 1.1.3如何选用集合? + +主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。 + +## 1.2 Iterator迭代器接口 + +## 1.3 Collection子接口之List +### 1.3.1 Arraylist 与 LinkedList 区别? - **1. 是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; @@ -53,7 +87,7 @@ - **5. 内存空间占用:** ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。 -### **补充内容:RandomAccess接口** +#### **补充内容:RandomAccess接口** ```java public interface RandomAccess { @@ -81,7 +115,7 @@ public interface RandomAccess { - 实现了 `RandomAccess` 接口的list,优先选择普通 for 循环 ,其次 foreach, - 未实现 `RandomAccess`接口的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大size的数据,千万不要使用普通for循环 -### 补充内容:双向链表和双向循环链表 +#### 补充内容:双向链表和双向循环链表 **双向链表:** 包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。 @@ -93,211 +127,25 @@ public interface RandomAccess { ![双向循环链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/双向循环链表.png) -## ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢? +### 1.3.2 ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢? `Vector`类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。 `Arraylist`不是同步的,所以在不需要保证线程安全时建议使用Arraylist。 -## 说一说 ArrayList 的扩容机制吧 +### 1.3.3 说一说 ArrayList 的扩容机制吧 详见笔主的这篇文章:[通过源码一步一步分析ArrayList 扩容机制](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md) -## HashMap 和 Hashtable 的区别 - -1. **线程是否安全:** HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); -2. **效率:** 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它; -3. **对Null key 和Null value的支持:** HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。 -4. **初始容量大小和每次扩充容量大小的不同 :** ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 -5. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 - -**HashMap 中带有初始容量的构造函数:** - -```java - public HashMap(int initialCapacity, float loadFactor) { - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal initial capacity: " + - initialCapacity); - if (initialCapacity > MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + - loadFactor); - this.loadFactor = loadFactor; - this.threshold = tableSizeFor(initialCapacity); - } - public HashMap(int initialCapacity) { - this(initialCapacity, DEFAULT_LOAD_FACTOR); - } -``` - -下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。 - -```java - /** - * Returns a power of two size for the given target capacity. - */ - static final int tableSizeFor(int cap) { - int n = cap - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; - } -``` - -## HashMap 和 HashSet区别 - -如果你看过 `HashSet` 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 `clone() `、`writeObject()`、`readObject()`是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。 - -| HashMap | HashSet | -| :------------------------------: | :----------------------------------------------------------: | -| 实现了Map接口 | 实现Set接口 | -| 存储键值对 | 仅存储对象 | -| 调用 `put()`向map中添加元素 | 调用 `add()`方法向Set中添加元素 | -| HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性, | - -## HashSet如何检查重复 - -当你把对象加入`HashSet`时,HashSet会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用`equals()`方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。(摘自我的Java启蒙书《Head fist java》第二版) - -**hashCode()与equals()的相关规定:** - -1. 如果两个对象相等,则hashcode一定也是相同的 -2. 两个对象相等,对两个equals方法返回true -3. 两个对象有相同的hashcode值,它们也不一定是相等的 -4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 - -**==与equals的区别** - -1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 -2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较 -3. ==指引用是否相同 equals()指的是值是否相同 - -## HashMap的底层实现 - -### JDK1.8之前 - -JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** - -**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** - -**JDK 1.8 HashMap 的 hash 方法源码:** - -JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 - -```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^ :按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } -``` - -对比一下 JDK1.7的 HashMap 的 hash 方法源码. - -```java -static int hash(int h) { - // This function ensures that hashCodes that differ only by - // constant multiples at each bit position have a bounded - // number of collisions (approximately 8 at default load factor). - - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); -} -``` - -相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 - -所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 - -![jdk1.8之前的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/jdk1.8之前的内部结构-HashMap.jpg) - -### JDK1.8之后 - -相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 - -![jdk1.8之后的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8之后的HashMap底层数据结构.jpg) - -> TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 - -**推荐阅读:** - -- 《Java 8系列之重新认识HashMap》 : - -## HashMap 的长度为什么是2的幂次方 - -为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。 - -**这个算法应该如何设计呢?** - -我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。** - -## HashMap 多线程操作导致死循环问题 - -主要原因在于并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。 - -详情请查看: - -## ConcurrentHashMap 和 Hashtable 的区别 - -ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 - -- **底层数据结构:** JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; -- **实现线程安全的方式(重要):** ① **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 - -**两者的对比图:** - -图片来源: - -**HashTable:** - -![HashTable全表锁](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/HashTable全表锁.png) - -**JDK1.7的ConcurrentHashMap:** - -![JDK1.7的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ConcurrentHashMap分段锁.jpg) - -**JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):** - -![JDK1.8的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8-ConcurrentHashMap-Structure.jpg) - -## ConcurrentHashMap线程安全的具体实现方式/底层具体实现 - -### JDK1.7(上面有示意图) - -首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 - -**ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成**。 - -Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。 - -```java -static class Segment extends ReentrantLock implements Serializable { -} -``` - -一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 - -### JDK1.8 (上面有示意图) - -ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N))) - -synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。 - -## comparable 和 Comparator的区别 +## 1.4 Collection子接口之Set +### 1.4.1 comparable 和 Comparator的区别 - comparable接口实际上是出自java.lang包 它有一个 `compareTo(Object obj)`方法用来排序 - comparator接口实际上是出自 java.util 包它有一个`compare(Object obj1, Object obj2)`方法用来排序 一般我们需要对一个集合使用自定义排序时,我们就要重写`compareTo()`方法或`compare()`方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写`compareTo()`方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 `Collections.sort()`. -### Comparator定制排序 +#### Comparator定制排序 ```java ArrayList arrayList = new ArrayList(); @@ -345,7 +193,7 @@ Collections.sort(arrayList): [7, 4, 3, 3, -1, -5, -7, -9] ``` -### 重写compareTo方法实现按年龄来排序 +#### 重写compareTo方法实现按年龄来排序 ```java // person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列 @@ -421,34 +269,199 @@ Output: 30-张三 ``` -## 集合框架底层数据结构总结 +## 1.5 Map接口 +### 1.5.1 HashMap 和 Hashtable 的区别 -### Collection +1. **线程是否安全:** HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); +2. **效率:** 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它; +3. **对Null key 和Null value的支持:** HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。 +4. **初始容量大小和每次扩充容量大小的不同 :** ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 +5. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 -#### 1. List +**HashMap 中带有初始容量的构造函数:** -- **Arraylist:** Object数组 -- **Vector:** Object数组 -- **LinkedList:** 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) +```java + public HashMap(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal initial capacity: " + + initialCapacity); + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + + loadFactor); + this.loadFactor = loadFactor; + this.threshold = tableSizeFor(initialCapacity); + } + public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } +``` -#### 2. Set +下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。 -- **HashSet(无序,唯一):** 基于 HashMap 实现的,底层采用 HashMap 来保存元素 -- **LinkedHashSet:** LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的 -- **TreeSet(有序,唯一):** 红黑树(自平衡的排序二叉树) +```java + /** + * Returns a power of two size for the given target capacity. + */ + static final int tableSizeFor(int cap) { + int n = cap - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; + } +``` -### Map +### 1.5.2 HashMap 和 HashSet区别 -- **HashMap:** JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间 -- **LinkedHashMap:** LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931) -- **Hashtable:** 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 -- **TreeMap:** 红黑树(自平衡的排序二叉树) +如果你看过 `HashSet` 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 `clone() `、`writeObject()`、`readObject()`是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。 -## 如何选用集合? +| HashMap | HashSet | +| :------------------------------: | :----------------------------------------------------------: | +| 实现了Map接口 | 实现Set接口 | +| 存储键值对 | 仅存储对象 | +| 调用 `put()`向map中添加元素 | 调用 `add()`方法向Set中添加元素 | +| HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性, | -主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。 +### 1.5.3 HashSet如何检查重复 -## 公众号 +当你把对象加入`HashSet`时,HashSet会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用`equals()`方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。(摘自我的Java启蒙书《Head fist java》第二版) + +**hashCode()与equals()的相关规定:** + +1. 如果两个对象相等,则hashcode一定也是相同的 +2. 两个对象相等,对两个equals方法返回true +3. 两个对象有相同的hashcode值,它们也不一定是相等的 +4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 +5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 + +**==与equals的区别** + +1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 +2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较 +3. ==指引用是否相同 equals()指的是值是否相同 + +### 1.5.4 HashMap的底层实现 + +#### JDK1.8之前 + +JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** + +**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** + +**JDK 1.8 HashMap 的 hash 方法源码:** + +JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 + +```java + static final int hash(Object key) { + int h; + // key.hashCode():返回散列值也就是hashcode + // ^ :按位异或 + // >>>:无符号右移,忽略符号位,空位都以0补齐 + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); + } +``` + +对比一下 JDK1.7的 HashMap 的 hash 方法源码. + +```java +static int hash(int h) { + // This function ensures that hashCodes that differ only by + // constant multiples at each bit position have a bounded + // number of collisions (approximately 8 at default load factor). + + h ^= (h >>> 20) ^ (h >>> 12); + return h ^ (h >>> 7) ^ (h >>> 4); +} +``` + +相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 + +所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 + +![jdk1.8之前的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/jdk1.8之前的内部结构-HashMap.jpg) + +#### 5.4.2. JDK1.8之后 + +相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 + +![jdk1.8之后的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8之后的HashMap底层数据结构.jpg) + +> TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 + +**推荐阅读:** + +- 《Java 8系列之重新认识HashMap》 : + +### 1.5.5 HashMap 的长度为什么是2的幂次方 + +为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。 + +**这个算法应该如何设计呢?** + +我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。** + +### 1.5.6 HashMap 多线程操作导致死循环问题 + +主要原因在于并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。 + +详情请查看: + +### 1.5.7 ConcurrentHashMap 和 Hashtable 的区别 + +ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 + +- **底层数据结构:** JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; +- **实现线程安全的方式(重要):** ① **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 + +**两者的对比图:** + +图片来源: + +**HashTable:** + +![HashTable全表锁](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/HashTable全表锁.png) + +**JDK1.7的ConcurrentHashMap:** + +![JDK1.7的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ConcurrentHashMap分段锁.jpg) + +**JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):** + +![JDK1.8的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8-ConcurrentHashMap-Structure.jpg) + +### 1.5.8 ConcurrentHashMap线程安全的具体实现方式/底层具体实现 + +#### JDK1.7(上面有示意图) + +首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 + +**ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成**。 + +Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。 + +```java +static class Segment extends ReentrantLock implements Serializable { +} +``` + +一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 + +#### JDK1.8 (上面有示意图) + +ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N))) + +synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。 + +## 1.6 Collections工具类 + + + +## 公众号 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 @@ -456,4 +469,4 @@ Output: **Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) +![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) \ No newline at end of file