Java集合(五)Map接口
在现实生活中,每个人都有唯一的身份证号,通过身份证号可以查询到这个人的信息,这两者是一对一的关系。在应用程序中,如果想存储这种具有对应关系的数据,则需要使用
Java 中提供的 Map 接口。
Map 接口是一种双列集合,它的每个元素都包含一个键对象 Key 和值对象 Value,键和值对象之间存在一种对应关系,称为映射。Map 中的映射关系是一对一的,所以“键对象”不能重复。一个键对象Key 对应唯一一个值对象 Value,其中键对象Key 和值对象Value 可以是任意数据类型,并且键对象 Key 不允许重复,这样在访问 Map 集合中的元素时,只要指定了 Key,就能找到对应的Value。
Map接口实现类有HashMap,TreeMap,HashTable,Properties等
Map接口定义的常用方法:
HashMap集合
HashMap 集合是Map 接口的一个实现类,它用于存储键值映射关系,该集合的键和值允许为空,但键不能重复,且集合中的元素是无序的。HashMap 底层是由哈希表结构组成的,其实就是“数组十链表”的组合体,数组是 HashMap 的主体结构,链表则主要是为了解决哈希值冲突而存在的分支结构。正因为这样特殊的存储结构,HashMap 集合对于元素的增、删、改、查操作表现出的效率都比较高。接下来,通过一张图来展示 HashMap 集合底层实现,如下图所示在图所示结构中,水平方向以数组结构为主体并在竖直方向以链表结构进行结合的就是 HashMap 中的哈希表结构。在哈希表结构中,水平方向数组的长度称为 HashMap集合的容量(capacity),竖直方向每个元素位置对应的链表结构称为一个桶(bucket),每个桶的位置在集合中都有对应的桶值,用于快速定位集合元素添加、查找时的位置。图中,在展示 HashMap 集合内部哈希表结构的基础上,也展示了存储元素的原理。
当向 HashMap 集合添加元素时,首先会调用键对象k的hash(k)方法,快速定位并寻上到该元素在集合中要存储的位置。在定位到存储元素键对象k的哈希值所对应桶位置后,会出现两种情况:第一种情况,键对象k的hash 值所在桶位置为空,则可以直接向该桶位置插人元素对象;第二种情况,键对象k的 hash 值所在桶位置不为空,则还需要继续通过键对象上的equals(k)方法比较新插人的元素键对象k和已存在的元素键对象上是否相同,如果键对象k相同,就会对原有元素的值对象v进行替换并返回原来的旧值,否则会在该桶的链表结构头部新增一个节点来插入新的元素对象。
举例如下:
下面我们自己定义了一个employee类,往map中添加,构造器,get,set方法,toString方法:
在上面介绍HashMap 集合时,已经说明HashMap 集合并不保证集合元素存入和取出的顺序。如果想让这两个顺序一致,可以使用 Java 中提供的 LinkedHashMap 类,它是HashMap的子类,和 LinkedList一样也使用双向链表来维护内部元素的关系,使LinkedHashMap 元素选代的顺序与存入的顺序一致,接下来通过一个案例来学习下LinkedHashMap 的用法,如下图
TreeMap集合
TreeMap也是Map接口的实现类,它也是用来存储键值映射关系的,并不允许出现重复的键。TreeMap是红黑二叉树的典型实现,来保证键的唯一性,这与TreeSet集合存储原理一样,因此TreeMap中所有的键都是按照某种顺序排列的。TreeMap和HashMap实现了同样的接口Map,因此,用法对于调用来说没有区别。HashMap效率高于TreeMap,在需要排序时才选TreeMap。
举例如下 :
可以看到,我们按220,3,6的顺序put值进入tm1,但是取出就是按照键递增的方式输出,这是因为添加的元素中键对象是Integer类型,Integer类型实现了Comparable接口,因此默认会按照自然顺序对元素排序。
如果我的键是自定义的,那怎么排序呢,我们可以自己继承Comparable接口实现自定义排序:
举例如下,我们自定义一个类实现comparable接口:
我们先通过薪水salary比较,如果薪水一样,我们再通过id比较,具体方式和输出结果如上图。
当然我们也可以自定义比较器Comparatble进行排序。
HashTable集合
HashTable类和HashMap用法几乎一样,底层实现几乎一样,只不过HashTable的方法添加了synchronized关键字确保线程同步检查,效率较低。
HashMap与HashTable区别:
1.HashMap:线程不安全,效率高。允许key或value为null
2.HashTable:线程不安全,效率低。不允许key或value为null。
Properties集合
。。。。
========================================================================
JDK7与JDK8的HashMap区别:
- jdk8中添加了红黑树,当链表长度大于等于8的时候链表会变成红黑树
- 链表新节点插入链表的顺序不同(jdk7是插入头结点,jdk8因为要把链表变为红 黑树所以采用插入尾节点)
- hash算法简化 ( jdk8 )
- resize的逻辑修改(jdk7会出现死循环,jdk8不会
HashMap的容量与扩容机制:
1.HashMap的默认负载因子
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
*默认的负载因子是0.75f,也就是75% 负载因子的作用就是计算扩容阈值用,比如说使用
*无参构造方法创建的HashMap 对象,他初始长度默认是16 阈值 = 当前长度 * 0.75 就
*能算出阈值,当当前长度大于等于阈值的时候HashMap就会进行自动扩容
*/
为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?
- 阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子x容量的结果是一个整数,这个值是0.75(4/3)比较合理,因为这个数和任何2的次幂乘积结果都是整数。
- 理论上来讲,负载因子越大,导致哈希冲突的概率也就越大,负载因子越小,费的空间也就越大,这是一个无法避免的利弊关系,所以通过一个简单的数学推理,可以测算出这个数值在0.75左右是比较合理的
2.HashMap的扩容机制
阈值(threshold) = 负载因子(loadFactor) x 容量(capacity)
当HashMap中table数组(也称为桶)长度 >= 阈值(threshold) 就会自动进行扩容。
扩容的规则是这样的,因为table数组长度必须是2的次方数,扩容其实每次都是按照上一次tableSize位运算得到的就是做一次左移1位运算,
假设当前tableSize是16的话 16转为二进制再向左移一位就得到了32 即 16 << 1 == 32 即扩容后的容量,也就是说扩容后的容量是当前
容量的两倍,但记住HashMap的扩容是采用当前容量向左位移一位(newtableSize = tableSize << 1),得到的扩容后容量,而不是当前容量x2
为什么计算扩容后容量要采用位移运算呢,怎么不直接乘以2呢?
因为cpu毕竟它不支持乘法运算,所有的乘法运算它最终都是再指令层面转化为了加法实现的,这样效率很低,如果用位运算的话对cpu来说就非常的简洁高效。
3.HashMap中散列表数组初始长度
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* HashMap中散列表数组初始长度为 16 (1 << 4)
* 创建HashMap的时候可以设置初始化容量和设置负载因子,
* 但HashMap会自动优化设置的初始化容量参数,确保初始化
* 容量始终为2的幂
*/
为啥HashMap中初始化大小为什么是16呢?
HashMap的源码中当新put一个数据时会进行计算位于table数组(也称为桶)中的下标:
int index =key.hashCode()&(length-1);
HashMap每次扩容都是以 2的整数次幂进行扩容
因为是将二进制进行按位于,(16-1) 是 1111,末位是1,这样也能保证计算后的index既可以是奇数也可以是偶数,并且只要传进来的key足够分散,均匀那么按位于的时候获得的index就会减少重复,这样也就减少了hash的碰撞以及hashMap的查询效率。
那么到了这里你也许会问? 那么就然16可以,是不是只要是2的整数次幂就可以呢?
答案是肯定的。那为什么不是8,4呢? 因为是8或者4的话很容易导致map扩容影响性能,如果分配的太大的话又会浪费资源,所以就使用16作为初始大小。
四、HashMap的结构
JDK7与JDK8及以后的HashMap结构与存储原理有所不同:
Jdk1.7:数组 + 链表 ( 当数组下标相同,则会在该下标下使用链表)
Jdk1.8:数组 + 链表 + 红黑树 (预值为8 如果链表长度<=8则会把链表变成红黑树 )
Jdk1.7中链表新元素添加到链表的头结点,先加到链表的头节点,再移到数组下标位置
Jdk1.8中链表新元素添加到链表的尾结点
(数组通过下标索引查询,所以查询效率非常高,链表只能挨个遍历,效率非常低。jdk1.8及以
上版本引入了红黑树,当链表的长度大于或等于8的时候则会把链表变成红黑树,以提高查询效率)
五、HashMap存储原理与存储流程
1.HashMap存储原理
- 获取到传过来的key,调用hash算法获取到hash值
- 获取到hash值之后调用indexFor方法,通过获取到的hash值以及数组的长度算
出数组的下标 (把哈希值和数组容量转换为二进,再在数组容量范围内与哈希值
进行一次与运算,同为1则1,不然则为0,得出数组的下标值,这样可以保证计算出的数组下标不会大于当前数组容量) - 把传过来的key和value存到该数组下标当中。
- 如该数组下标下以及有值了,则使用链表,jdk7是把新增元素添加到头部节点 jdk8则添加到尾部节点。
2.HashMap存储流程
前面寻址算法都是一样的,根据key的hashcode经过高低位异或之后的值,再按位与 &(table.lingth - 1),得到一个数组下标,然后根据这个数组下标内的状况,状况不同,然后情况也不同,大概分为了4种状态:
( 1.)第一种就是数组下标下内容为空:
这种情况没什么好说的,为空据直接占有这个slot槽位就好了,然后把当前.put方法传进来的key和value包装成一个node对象,放到这个slot中就好了。
( 2.)第二种情况就是数组下标下内容不为空,但它引用的node还没有链化:
这种情况下先要对比一下这个node对象的key与当前put对象的key是否完全.相等,如果完全相等的情况下,就行进行replace操作,把之前的槽位中node.下的value替换成新的value就可以了,否则的话这个put操作就是一个正儿.八经的hash冲突,这种情况在slot槽位后面追加一个node就可以了,用尾插法 ( 前面讲过,jdk7是把新增元素添加到头部节点,而jdk8则添加到尾部节点)。
( 3.)第三种就是该数组下标下内容已经被链化了:
这种情况和第二种情况处理很相似,首先也是迭代查找node,看看链表上中元素的key,与当前传过来的key是否完全一致,如果完全一致的话还是repleace操作,用put过来的新value替换掉之前node中的value,否则的话就是一致迭代到链表尾节点也没有匹配到完全一致的node,就和之前的一样,把put进来数据包装成node追加到链表的尾部,再检查一下当前链表的长度,有没有达到树化阈值,如果达到了阈值就调用一个树化方法,树化操作都是在这个方法里完成的。
( 4.)第四种情况就是冲突很严重的情况下,这个链表已经转化成红黑树了:
红黑树就比较复杂 要将清楚这个红黑树还得从TreeNode说起 TreeNode继承了Node结构,在Node基础上加了几个字段,分别是指向父节点parent字段,指向左子节点left字段,指向右子节点right字段,还有一个表示颜色的red字段,这就是TreeNode的基本结构,然后红黑树的插入操作,首先找到一个合适的插入点,就是找到插入节点的父节点,然后红黑树它又满足二叉树的所有特性,所以找这个父节点的操作和二叉树排序是完全一致的,然后说一下这个二叉树排序,其实就是二分查找算法映射出来的结构,就是一个倒立的二叉树,然后每个节点都可以有自己的子节点,本且左节点小于但前节点,右节点大于当前节点,然后每次向下查找一层就能那个排除掉一半的数据,查找效率非常的高效,当查找的过程中也是分情况的。
- 首先第一种情况就是一直向下探测,直到查询到左子树或者右子树位null,说明整个树中,并没有发现node链表中的key与当前put key一致的TreeNode,那此时探测节点就是插入父节点的所在了,然后就是判断插入节点的hash值和父节点的hash值大小决定插入到父节点的左子树还是右子树。当然插入会打破平衡,还需要一个红黑树的平衡算法保持平衡。
- 其次第二种情况就是根节点在向下探测过程中发现TreeNode中key与当前put的key完全一致,然后就也是一次repleace操作,替换value。
六、jdk8中HashMap为什么要引入红黑树?
其实主要就是为了解决jdk1.8以前hash冲突所导致的链化严重的问题,因为链表结构的查询效率是非常低的,他不像数组,能通过索引快速找到想要的值,链表只能挨个遍历,当hash冲突非常严重的时候,链表过长的情况下,就会严重影响查询性能,本身散列列表最理想的查询效率为O(1),当时链化后链化特别严重,他就会导致查询退化为O(n)为了解决这个问题所以jdk8中的HashMap添加了红黑树来解决这个问题,当链表长度>=8的时候链表就会变成红黑树,红黑树其实就是一颗特殊的二叉排序树嘛,这个时间复杂…反正就是要比列表强很多