map不是地图吗?为什么map变成了映射?
小学就学过,map这个单词的含义是地图。但是,大多数人第一次接触集合时候可能一脸懵逼,为何map的意思变成了映射?别急,且听我细细道来。
其实map的本意就是映射。所谓地图就是把地球上的一条路映射到了一张纸上,把地球上的一个房子映射到了一张纸上,地图的本质不就是将现实世界的某些东西映射到了一张纸上吗,然后给这张纸起名叫:地图。
所以map的本意其实就是映射,而地图其实算是map的引申义。
扯了map映射的来历,接下来要聊一聊HashMap了。
HashMap 和 Hashtable 的区别
- 线程是否安全:
HashMap是非线程安全的,Hashtable是线程安全的,因为Hashtable内部的方法基本都经过synchronized修饰。 - 效率: 因为线程安全的问题,
HashMap要比Hashtable效率高一点。另外,Hashtable基本被淘汰,因为性能太差了; - 对 Null key 和 Null value 的支持:
HashMap可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出NullPointerException。 - 初始容量大小和每次扩充容量大小的不同 :
- 创建时如果不指定容量初始值,
Hashtable默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。 - 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而
HashMap会将其扩充为 2 的幂次方大小(HashMap中的tableSizeFor()方法保证,下面给出了源代码)。也就是说HashMap总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
- 创建时如果不指定容量初始值,
- 底层数据结构:
- JDK1.8 以后的
HashMap在链表长度大于8且数组的长度大于64时,会将链表转化为红黑树,以减少搜索时间 - Hashtable 没有这样的机制
- JDK1.8 以后的
ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
- 底层数据结构:
- JDK1.7 的
ConcurrentHashMap底层采用 分段的数组+链表 实现 - JDK1.8 采用的数据结构跟
HashMap1.8的结构一样,数组+链表/红黑树 Hashtable和 JDK1.8 之前的HashMap的底层数据结构类似都是采用 数组+链表 的形式
- JDK1.7 的
- 实现线程安全的方式:
- 在 JDK1.7 的时候,
ConcurrentHashMap对整个桶数组进行了分段,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 - 在 JDK1.8 的时候,而是直接用
Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和 CAS 来操作,每次只锁头结点,并发度很高。 Hashtable: 使用synchronized来保证线程安全,也就是说整个HashTable只使用一把锁,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,会进入阻塞。
- 在 JDK1.7 的时候,
HashMap底层数据结构, 1.7和1.8有何不同?
- 1.7的HashMap是哈希表, 数组 + 链表.
- 1.8的HashMap是数组 + (链表 或 红黑树)
HashMap什么时候进行扩容
HashMap默认的初始化大小为16, 默认负载因子为0.75.- 当hashmap中的元素个数超过
数组大小*负载因子(loadFactor)时,就会进行数组扩容. - 之后每次扩充,容量变为原来的 2 倍。
HashMap为何使用红黑树
- 最开始使用链表是 用哈希表的拉链法来解决Hash冲突问题. 当某个位置, Hash冲突严重, 则链表的长度会很长. 那么查找的时候依次比较, 效率会很低. 将链表转为红黑树, 因为红黑树左子树全都小于根, 右子树全都大于根, 搜索效率很高. 所以使用红黑树就提高了查找效率.
- 红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。
- 平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
HashMap的链表什么时候树化
- 链表长度超过树化阈值8
- 数组长度大于等于64
- 当链表长度超过8时, 若数组长度小于64, 则会对数组进行扩容, 然后二次哈希的值就会变, 此时链表的部分值就会重新分配到数组其他位置
为何一开始不树化
- 刚开始链表的长度可能只有三四个, 如果此时树化, 那么树化后查询的效率和短链表差不多. 所以, 在短链表的情况下, 树化意义不大.
- 而且链表的节点是Node, 红黑树的节点为TreeNode. TreeNode的内存占用大于Node.
为何树化阈值为8
- 红黑树用来避免DoS攻击,防止链表超长时性能下降,树化应当是偶然情况.
- hash表的查找, 更新的时间复杂度是O(1). 而红黑树的查找, 更新的时间复杂度是O(log2n), TreeNode占用空间也比普通Node的大,如非必要,尽量还是使用链表。
- hash值如果足够随机,则在hash表内按泊松分布,在负载因子0.75的情况下,长度超过8的链表出现概率是0.00000006. 长度超过8的链表出现几率非常小, 选择8就是为了让树化几率足够小
何时红黑树退化为链表
- 退化情况1: 当元素增加, 数组扩容时, 如果树被拆分, 当树节点<=6时会退化为链表.
- 退化情况2: 删除节点时, 若root, root.left, root.right, root.left.left有一个为null(移除前检查), 也会退化为链表.
索引计算方式
- 对任何一个对象调用其hashCode()方法会获得其原始hash值. 对原始hash值再调用HashMap的hash方法进行二次hash, 获取到二次hash值. 二次hash值对数组容量进行取余操作获取到存放的数组下标.
- 取余操作可以用
hash值 & (数组容量 - 1)的方式得到. 这种按位与数组容量-1的方式取余的效率更高.
- 取余操作可以用
- 注: 按位与数组容量-1的方式需要数组容量为2^n才可以.
为何需要二次hash?
- 二次hash是为了让hash值分布更加均匀, 减少hash冲突, 从而使链表更短, 因此也就提升了查找效率.
- 1.8中二次hash的算法: 二次hash值 = 原hash值 ^ (原hash值>>>16)
- 原hashCode为32位, 右移16位就获取到了其高16位. 用原hashCode和其高16位异或的到二次hash值.
数组容量为何是2^n?
- 计算索引时,如果是2的n次暴可以使用位与运算代替取模运算, 效率更高
- 扩容时
二次hash值&旧数组容量==0的元素留在原来位置,不等于0则新位置=旧位置+旧容量 - 数组容量为质数会使hash值分布均匀, 但是2^n计算索引的效率更高.
HashMap的put()方法流程
1.8的put流程
- HashMap 是懒惰创建数组的,首次使用才创建数组
- 计算索引(桶下标)
- 如果桶下标还没人占用,创建Node放入数据后返回
- 如果桶下标已经有人占用
- 已经是TreeNode走红黑树的添加或更新逻辑
- 是普通Node,走链表的添加或更新逻辑. 如果链表长度超过树化阈值,走树化逻辑
- 返回前检查容量是否超过阈值, 一旦超过进行扩容.
- 扩容时, 先将新的数据放进数组, 然后创建新的数组, 再将旧数组元素迁移到新数组
1.8和1.7的put流程不同之处
- 链表插入节点时, 1.7是头插法, 1.8是尾插法
- 1.7是大于等于阈值且待插入的桶中已经有数据的时候才扩容. 1.8是只要大于阈值就扩容
- 1.8有判断链表长度, 树化的逻辑. 1.7没有.
负载因子为何是0.75
- 0.75是在占用空间和查询时间中取得了比较好的平衡. (扩容阈值=数组大小*负载因子)
- 大于0.75, 冲突增加了, 数组空间就节省了, 但是链表就会比较长, 影响性能.
- 若负载因子为1, 则
16*1 = 16, 只有当存满16个元素之后, 才会扩容. 节省空间, 影响性能.
- 若负载因子为1, 则
- 小于0.75, 冲突减少了, 链表会比较短, 数组扩容会很频繁, 空间占用增多.
- 若负载因子为0.25, 则
16*0.25=4, 当元素个数大于4个, 就会扩容, 浪费空间, 性能提升
- 若负载因子为0.25, 则
多线程操作HashMap会出现什么问题
- 扩容死链 (1.7)
- 扩容的时候线程切换, 两个线程都要进行扩容.
- 因为1.7是头插法, 进行数组扩容的时候, 需要链表迁移. 在并发环境下, 会出现循环链表, 造成扩容死链问题.
- 数据错乱 (1.7, 1.8)
- 两个线程都要放入一个新数据 (两个数据索引一致, 且该索引下无链表). 此时就会出现数据错乱问题
key 能否为 null,作为key的对象有什么要求?
- HashMap的key可以为null,但Map的其他实现则不然
- 作为key的对象,必须实现hashCode和equals,并且key的内容不能修改(不可变)
- 若key为一个对象, put进hashMap中后, 修改了对象属性值. 那么再get只会得到null
String对象的hashCode()如何设计的,为啥每次乘的是31
- 目标是达到较为均匀的散列效果,每个字符串的hashCode足够独特