HashMap介绍
底层结构
JDK1.7 HashMap的底层结构是由数组+链表构成的。
JDK1.8 HashMap的底层结构是由数组+链表+红黑树构成的。
put和get方法
put()方法大概过程如下:
- 如果添加的key值为null,那么将该键值对添加到数组索引为0的链表中,不一定是链表的首节点。
- 如果添加的key不为null,则根据key计算数组索引的位置:
数组索引处存在链表,则遍历该链表,如果发现key已经存在,那么将新的value值替换旧的value值
数组索引处不存在链表,将该key-value添加到此处,成为头节点
get()方法的大概过程:
- 如果key为null,那么在数组索引table[0]处的链表中遍历查找key为null的value
- 如果key不为null,根据key找到数组索引位置处的链表,遍历查找key的value,找到返回value,若没找到则返回null
扩容机制
先看一个例子,创建一个HashMap,初始容量默认为16,负载因子默认为0.75,那么什么时候它会扩容呢?
来看以下公式:
实际容量 = 初始容量 × 负载因子
计算可知,16×0.75=12,也就是当实际容量超过12时,这个HashMap就会扩容。
初始容量
当构造一个hashmap时,初始容量设为不小于指定容量的2的次方的一个数(new HashMap(5), 指定容量为5,那么实际初始容量为8,2^3=8>5),且最大值不能超过2的30次方。
负载因子
负载因子是哈希数组在其容量自动增加之前可以达到多满的一种尺度。(时间与空间的折衷) 当哈希数组中的条目数超出了加载因子与初始容量的乘积时,则要对该哈希数组进行扩容操作(即resize)。
特点:
负载因子越小,容易扩容,浪费空间,但查找效率高
负载因子越大,不易扩容,对空间的利用更加充分,查找效率低(链表拉长)
扩容过程
HashMap在扩容时,新数组的容量将是原来的2倍,由于容量发生变化,原有的每个元素需要重新计算数组索引Index,再存放到新数组中去,这就是所谓的rehash。
手写 Map 接口
package com.xiaoming.day25.hashmap;
/**
* 手写 Map 接口
*/
public interface ExtMap<K, V> {
/**
* 向集合中插入数据
* @param k
* @param v
* @return
*/
public V put(K k, V v);
/**
* 根据 k 从 map 集合中查询元素
* @param k
* @return
*/
public V get(K k);
/**
* 获取集合元素的个数
* @return
*/
public int size();
/**
* Entry 的作用 == Node 节点
* @param <K>
* @param <V>
*/
interface Entry<K, V>{
K getKey();
V getValue();
V setValue(V value);
}
}
实现
/**
* 手写hashMap
* 1.定义节点
* 2.定义table 存放HashMap 数组元素 默认是没有初始化容器 懒加载
* 3.定义实际用到 table 存储容量大小
* 4.定义负载因子 0.75 扩容的时候才会用到 负载因子越小,他的 hash 冲突越少
* 5.定义 HashMap 默认初始大小 16
*
* @param <K>
* @param <V>
*/
public class ExtHashMap<K, V> implements ExtMap<K, V> {
/**
* 定义table 存放HashMap 数组元素 默认是没有初始化容器 懒加载
*/
private Node<K, V>[] table = null;
/**
* 实际用到 table 存储容量大小
*/
int size;
/**
* 负载因子 0.75 扩容的时候才会用到 负载因子越小,他的 hash 冲突越少
*/
static float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* HashMap 默认初始大小 16
*/
static int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 添加元素
* 1.判断table 是否为空?为空,进行初始化
* 2.判断数组是否需要扩容
* 3.判断key是否为空?为空,获取位置位 0 的节点
* 4.不为空 计算 hash 值指定下标位置, 获取指定下标位置的节点 node
* 5.判断是否发生 hash 冲突
* 没有发生,创建第一个节点,next 为空
* 发生,遍历链表上的 node,
* 6.判断 key equals 是否相同
* 相同 修改值
* 不相同,往单链表首部添加节点 next 为当前 node
* <p>
* hashMap 的 key 为 null, 插入到 0 个位置
*
* @param key
* @param value
* @return
*/
@Override
public V put(K key, V value) {
//判断table 是否为空?为空,进行初始化
if (table == null) {
table = new Node[DEFAULT_INITIAL_CAPACITY];
}
//判断数组是否需要扩容 hashMap 中是从什么时候开始扩容
//实际存储大小 = 负载因子 * 初始容量 = DEFAULT_LOAD_FACTOR 0.75 * DEFAULT_INITIAL_CAPACITY 16 = 12
//如果 size >= 12 的时候就需要开始扩容数组,扩容数组大小是之前的两倍
if(size >= DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY){
//需要开始对 table 进行数组扩容
resize();
}
Node<K, V> node = null;
//hashMap 的 key 为 null, 插入到 0 个位置
if(key == null){
node = table[0];
} else {
//计算 hash 值指定下标位置
int index = getIndex(key, DEFAULT_INITIAL_CAPACITY);
node = table[index];
}
if (node == null) {
//没有发生hash冲突
node = new Node<K, V>(key, value, null);
size++;
} else {
//已经发生 hash 冲突
Node<K, V> newNode = node;
while (newNode != null) {
// == 用来判断值类型 比如 a == 95
if (newNode.getKey().equals(key) || node.getKey() == key) {
// hashcode 和 equals 相同,存放的是同一个对象 修改值
V oldValue = newNode.setValue(value);
return oldValue;
} else {
//往单链表首部添加节点, hashCode 取模余数相同 index 存放在链表或者 hashCode 相同但是对象不同
if (newNode.next == null) {
//说明遍历到最后一个 node,添加 node
node = new Node<K, V>(key, value, node);
size++;
}
}
newNode = newNode.next;
}
}
table[index] = node;
return node.getValue();
}
/**
* 测试方法,打印所有的链表的元素
*/
public void print() {
for (int i = 0; i < table.length; i++) {
Node<K, V> node = table[i];
System.out.print("下标位置[" + i + "]" + "\t");
while (node != null) {
System.out.print("key:" + node.getKey() + ",value:" + node.getValue() + "\t");
node = node.next;
/*if (node.next != null){
node = node.next;
}else{
//结束循环
node = null;
}*/
}
System.out.println();
}
}
/**
* 计算 hash 值指定下标位置
*
* @param k
* @param length table 的长度
* @return
*/
public int getIndex(K k, int length) {
if(k == null){
return 0;
}
int hashCode = k.hashCode();
int hash = hashCode % length;
return hash;
}
/**
* 1.使用取模算法,定位数组链表
*
* @param k
* @return
*/
@Override
public V get(K k) {
//使用取模算法,定位数组链表
int index = getIndex(k, DEFAULT_INITIAL_CAPACITY);
Node<K, V> node = getNode(table[index], k);
return node == null ? null : node.getValue();
}
public Node<K, V> getNode(Node<K, V> node, K k) {
while (node != null) {
if (node.getKey().equals(k)) {
return node;
}
node = node.next;
}
return null;
}
@Override
public int size() {
return size;
}
/**
* 对 table 进行扩容
* 1.生成新的 table 是之前的两倍的大小
* 2.重新计算 index 索引,存放在新的 table 里面
* 3.将新的 newTable 赋值给老的 table
*/
public void resize(){
//生成新的 table 是之前的两倍的大小
Node<K,V >[] newTable = new Node[DEFAULT_INITIAL_CAPACITY << 1];
//重新计算 index 索引,存放在新的 table 里面
for (int i = 0; i < table.length; i++) {
//存放之前的 table 原来的 node
Node<K, V> oldNode = table[i];
while (oldNode != null){
table[i] = null;//为了垃圾回收机制能够回收 将之前的 node 删除
//存放之前的 table 原来的 node key
K oldKey = oldNode.getKey();
//重新计算 index
int index = getIndex(oldKey, newTable.length);
//存放之前的 table 原来的 node next
Node<K, V> oldNext = oldNode.next;
//将节点存放在新 table 链表的表头 如果 index 下表在新 newTable 发生相同的时候,以链表进行存储 原来的 node 的下一个是最新的(原来的node存放在新的 node 下一个)
oldNode.next = newTable[index];
//将之前的 node 赋值给 newTable[index]
newTable[index] = oldNode;
oldNode = oldNext;
}
}
//将新的 newTable 赋值给老的 table
table = newTable;
DEFAULT_INITIAL_CAPACITY = newTable.length;
newTable = null;//为了垃圾回收机制能够回收
}
/**
* 定义节点 单链表
*
* @param <K>
* @param <V>
*/
class Node<K, V> implements Entry<K, V> {
/**
* 存放Map 结合 key
*/
private K key;
/**
* 存放Map 结合 value
*/
private V value;
/**
* 下一个节点
*/
private Node<K, V> next;
public Node(K key, V value, Node<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
@Override
public K getKey() {
return this.key;
}
@Override
public V getValue() {
return this.value;
}
/**
* 设置新值返回老的值
*
* @param value 新的值
* @return 老的值
*/
@Override
public V setValue(V value) {
//老的值
V oldValue = this.value;
this.value = value;
return oldValue;
}
}
}