底层结构
HashMap的底层结构是由数组+链表构成的。
数组的元素是每个链表的头结点,每个节点存放key,value和next下一个节点
put和get方法
put()方法大概过程如下:
如果添加的key值为null,那么将该键值对添加到数组索引为0的链表中,不一定是链表的首节点。
如果添加的key不为null,则根据key计算数组索引的位置:
数组索引处存在链表,则遍历该链表,如果发现key已经存在,那么将新的value值替换旧的value值
数组索引处不存在链表,将该key-value添加到此处,成为头节点
get()方法的大概过程:
1. 如果key为null,那么在数组索引table[0]处的链表中遍历查找key为null的value
2. 如果key不为null,根据key找到数组索引位置处的链表,遍历查找key的value,找到返回value,若没找到则返回null
扩容机制
先看一个例子,创建一个HashMap,初始容量默认为16,负载因子默认为0.75,那么什么时候它会扩容呢?
来看以下公式:
实际容量 = 初始容量 × 负载因子
计算可知,16×0.75=12,也就是当实际容量超过12时,这个HashMap就会扩容。
负载因子
负载因子是哈希数组在其容量自动增加之前可以达到多满的一种尺度。(时间与空间的折衷) 当哈希数组中的条目数超出了加载因子与初始容量的乘积时,则要对该哈希数组进行扩容操作(即resize)。
特点:
1*16=16
0.75*16=12 0.5*16=8 负载因子越小, 容易扩容—容易扩容的时候 产生新的空数组位置
负载因子越小,容易扩容,浪费空间,但查找效率高 链表特别短 减少hash冲突
负载因子越大,不易扩容,对空间的利用更加充分,查找效率低(链表拉长)hash冲突比较多,链表比较长
扩容过程
HashMap在扩容时,新数组的容量将是原来的2倍,由于容量发生变化,原有的每个元素需要重新计算数组索引Index,再存放到新数组中去,这就是所谓的rehash。
eqauls方法和hashCode方法
1 如果两个对象相同,那么它们的hashCode值一定要相同。也告诉我们重写equals方法,一定要重写hashCode方法,也就是说hashCode值要和类中的成员变量挂上钩,对象相同–>成员变量相同—->hashCode值一定相同。
2 如果两个对象的hashCode相同,它们并不一定相同,这里的对象相同指的是用eqauls方法比较。
代码简单实现hashMap
public interface MapDemo<K,V> {
//定义节点
public interface Entry<K,V>{
//得到key
K getKey();
//得到value
V getValue();
//存放value
V setValue(V value);
}
//向集合中插入元素
public V put(K key,V value);
//根据key向集合中查询元素
public V get(K key);
//获得集合元素个数
public int size();
}
/**
* 1.定义节点,存放集合的key,value和下一个节点
* 2.定义table 存放HasMap 数组元素 默认是没有初始化容器 懒加载
* 3.实现put方法
* 3.1考虑扩容问题
* 4.实现get方法
* @param <K>
* @param <V>
*/
public class HashMapDemo<K,V> implements MapDemo<K,V>{
//1.定义table 存放HasMap 数组元素 默认是没有初始化容器 懒加载
Node<K,V>[] table = null;
//2.定义数组实际存放元素的大小
int size;
//3.HashMap默认负载因子,负载因子越小,hash冲突机率越低, 根据每个链表的个数
float DEFAULT_LOAD_FACTOR = 0.75f;
// 4.table默认初始大小 16
int DEFAULT_INITIAL_CAPACITY = 16;
/**
* 向集合中插入元素
* 1.判断集合是否为空(懒加载)
* 2.判断是否需要扩容
* 3.计算hash值指定下标
* 4.判断是否发生hash冲突
* 4.1如果没有发生hash冲突 new新的节点
* 4.2如果发生hash冲突 遍历解决hash冲突
*
* @param key
* @param value
* @return
*/
@Override
public V put(K key, V value) {
//1.判断集合是否为空(懒加载)
if(table == null){
table = new Node[DEFAULT_INITIAL_CAPACITY];
}
//2.判断是否需要扩容
//3.计算hash值指定下标
int index = getIndex(key, DEFAULT_INITIAL_CAPACITY);
//4.判断是否发生hash冲突
Node<K,V> node = table[index];
//4.1如果没有发生hash冲突
if(node == null){
//此时下一个节点为空
node = new Node<K,V>(key,value,null);
//实际存放元素个数+1
size++;
}else {
//4.2如果发生hash冲突
Node<K,V> currentNode = node;
//从前往后遍历节点
//目的:遍历每个节点,如果已存在key,则覆盖value
while(currentNode != null){
//如果key相同,直接覆盖原来的值
if(currentNode.getKey().equals(key) || currentNode.getKey() == key){
return currentNode.setValue(value);
}else {
//已经发生hash冲突问题key 直接添加(冲突node)到前面了 不是往后面加
//将新的Node放在第一位,遍历的当前节点设为下一个节点
//确保遍历完所有节点,保证新的节点只添加一次
if(currentNode.nextNode ==null){
node = new Node<K,V>(key, value, node);
//实际存放元素个数+1
size++;
}
}
currentNode = currentNode.nextNode;
}
}
table[index] = node;
return null;
}
//根据key和table容量计算下标值
public int getIndex(K key,int length){
int hashCode = key.hashCode();
int index = hashCode % length;
return index;
}
/**
* 扩容技术
* 1.生成新的table,容量扩充两倍
* 2.重新计算index索引,存放在新的table里面
* 3.将扩容后的table赋值给原table
*/
private void resize(){
//1.生成新的table,容量扩充两倍
Node<K,V>[] newTable = new Node[DEFAULT_INITIAL_CAPACITY << 1];
//2.重新计算index索引,存放在新的table里面
for(int i = 0;i < table.length;i++){
//获得原理table[i]的节点
Node<K,V> oldNode = table[i];
while(oldNode != null){
//将之前的node删除
table[i] = null;
//得到原来节点的key
K oldKey = oldNode.getKey();
//重新根据key计算下标值
int index = getIndex(oldKey, newTable.length);
//将目前存放的节点赋给旧节点下一个节点
oldNode.nextNode = newTable[index];
//将旧节点赋给table[index]下的第一个节点
newTable[index] = oldNode;
//继续遍历
oldNode = oldNode.nextNode;
}
//3.将扩容后的table赋值给原table
table = newTable;
DEFAULT_INITIAL_CAPACITY =newTable.length;
newTable = null;
}
}
//定义节点
class Node<K,V> implements Entry<K,V>{
//存放集合key
private K key;
//存放集合value
private V value;
//下一个节点
private Node<K,V> nextNode;
public Node(K key, V value, Node<K, V> nextNode) {
this.key = key;
this.value = value;
this.nextNode = nextNode;
}
@Override
public K getKey() {
return this.key;
}
@Override
public V getValue() {
return this.value;
}
@Override
public V setValue(V value) {
//返回原来元素的值
V oldValue = this.value;
this.value = value;
return oldValue;
}
}
@Override
public V get(K key) {
//1.计算下标值index
int index = getIndex(key, DEFAULT_INITIAL_CAPACITY);
//2.获取节点
Node<K,V> node = table[index];
//遍历节点
while(node != null){
if(node.getKey().equals(key)){
return node.getValue();
}
node = node.nextNode;
}
return null;
}
@Override
public int size() {
return size;
}
// 测试方法.打印所有的链表元素
void print() {
for (int i = 0; i < table.length; i++) {
Node<K, V> node = table[i];
System.out.print("下标位置[" + i + "]");
while (node != null) {
System.out.print("[ key:" + node.getKey() + ",value:" + node.getValue() + "]");
node = node.nextNode;
}
System.out.println();
}
}
}