HashMap的介绍
HashMap的实现原理
从底层结构、put和get方法、hash数组索引、扩容机制等几个方面来分析HashMap的实现原理:
底层结构
HashMap的底层结构是由数组+链表构成的。
数组(紫色):hash数组(桶),数组元素是每个链表的头节点
链表(绿色):解决hash冲突,不同的key映射到了数组的同一索引处,则形成链表。
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,那么什么时候它会扩容呢?
来看以下公式:
实际容量 = 初始容量 × 负载因子
1
计算可知,16×0.75=12,也就是当实际容量超过12时,这个HashMap就会扩容。
初始容量
当构造一个hashmap时,初始容量设为不小于指定容量的2的次方的一个数(new HashMap(5), 指定容量为5,那么实际初始容量为8,2^3=8>5),且最大值不能超过2的30次方。
负载因子
负载因子是哈希数组在其容量自动增加之前可以达到多满的一种尺度。(时间与空间的折衷) 当哈希数组中的条目数超出了加载因子与初始容量的乘积时,则要对该哈希数组进行扩容操作(即resize)。
特点:
1*16=16
0.75*16=12 0.5*16=8 负载因子越小, 容易扩容—容易扩容的时候 产生新的空数组位置
三次扩容
负载因子1 负载因子0.75 负载因子0.5
1.16*2=32 16 1.16*2=32 12 16*2=32 8
2.32*2=64 32 2.32*2=64 24 32*2=64 16
2.64*2=128 64 3.64*2=128 48 64*2=128 32
存放数据10000万个 >13毫秒 |
存放数据10000万个 13毫秒 |
存放数据10000万个 21毫秒 |
存放数据10000万个 76左右毫秒 |
存放数据10000万个 77左右毫秒 |
负载因子越小,容易扩容,浪费空间,但查找效率高 链表特别短 减少hash冲突
负载因子越大,不易扩容,对空间的利用更加充分,查找效率低(链表拉长)hash冲突比较多,链表比较长
扩容过程
HashMap在扩容时,新数组的容量将是原来的2倍,由于容量发生变化,原有的每个元素需要重新计算数组索引Index,再存放到新数组中去,这就是所谓的rehash。
eqauls方法和hashCode方法
1 如果两个对象相同,那么它们的hashCode值一定要相同。也告诉我们重写equals方法,一定要重写hashCode方法,也就是说hashCode值要和类中的成员变量挂上钩,对象相同–>成员变量相同—->hashCode值一定相同。
2 如果两个对象的hashCode相同,它们并不一定相同,这里的对象相同指的是用eqauls方法比较。
基于Linkedlist实现Map
代码实现方式
/**
* 1.纯手写HasMap集合 完全采用Arraylist实现 缺点效率低 <br>
*/
public class ExtArrayListMap<Key, Value> {
List<Entry<Key, Value>> tables = new ArrayList<Entry<Key, Value>>();
public void put(Key key, Value value) {
// 判断key是否已经重复
Entry existEntry = getEntry(key);
if (existEntry != null) {
existEntry.value = value;
return;
}
Entry entry = new Entry<Key, Value>(key, value);
tables.add(entry);
}
public Value get(String key) {
for (Entry<Key, Value> entry : tables) {
if (entry.key.equals(key)) {
return entry.value;
}
}
return null;
}
public void remove(Key key) {
Entry existEntry = getEntry(key);
if (existEntry != null) {
tables.remove(existEntry);
}
}
public Entry<Key, Value> getEntry(Key key) {
for (Entry<Key, Value> entry : tables) {
if (entry.key.equals(key)) {
return entry;
}
}
return null;
}
public static void main(String[] args) {
ExtArrayListMap<String, String> extArrayListMap = new ExtArrayListMap<String, String>();
extArrayListMap.put("a", "aaaaa");
extArrayListMap.put("b", "bbb");
extArrayListMap.put("a", "aa");
// extArrayListMap.remove("b");
System.out.println(extArrayListMap.get("b"));
}
}
class Entry<Key, Value> {
public Entry(Key key, Value value) {
this.key = key;
this.value = value;
}
Key key;
Value value;
}
/**
* 纯手写HasMap集合(采用 LinkedList)集合实现<br>
*/
public class LinkedListMap<Key, Value> {
// 实际存放Map元素
LinkedList<Entry>[] tables = new LinkedList[998];
// 实际Map大小
private int size;
public void put(Object key, Object value) {
// 创建entry;
Entry newEntry = new Entry(key, value);
// hashCode
int hash = getHash(key);
// 判断是否已经在数组重存在
LinkedList<Entry> entrylinkedList = tables[hash];
if (entrylinkedList == null) {
// 数组中没有存放元素
LinkedList<Entry> linkedList = new LinkedList<>();
linkedList.add(newEntry);
tables[hash] = linkedList;
} else {
// hashCode 相同情况下 存放在链表后面
for (Entry entry : entrylinkedList) {
if (entry.key.equals(key)) {
// hashCode相同 对象也相同
entry.value = value;
} else {
// hashCode 相同,但是对象不同。
entrylinkedList.add(newEntry);
}
}
}
size++;
}
public int getHash(Object key) {
int hashCode = key.hashCode();
int hash = hashCode % tables.length;
return hash;
}
public Value get(Object key) {
return (Value) getEntry(key).value;
}
public Entry getEntry(Object key) {
// hashCode
int hash = getHash(key);
LinkedList<Entry> listEntry = tables[hash];
if (listEntry != null) {
for (Entry entry : listEntry) {
if (entry.key.equals(key)) {
return entry;
}
}
}
return null;
}
public static void main(String[] args) {
LinkedListMap linkedListMap = new LinkedListMap<String, String>();
linkedListMap.put("a", "644");
linkedListMap.put("b", "123456");
linkedListMap.put("b", "123");
System.out.println(linkedListMap.get("b"));
}
}
class Entry {
public Entry(Object key, Object value) {
this.key = key;
this.value = value;
}
Object key;
Object value;
}
基于JDK1.7版本实现HashMap
创建Map接口
/**
* 纯手写Map集合<br>
*/
public interface ExtMap<K, V> {
// 向集合中插入数据
public V put(K k, V v);
// 根据k 从Map集合中查询元素
public V get(K k);
// 获取集合元素个数
public int size();
interface Entry<K, V> {
K getKey();
V getValue();
V setValue(V value);
}
}
创建HashMap实现
/**
* 纯手写HashMap集合<br>
*/
public class ExtHashMap<K, V> implements ExtMap<K, V> {
// 1.定义table 存放HasMap 数组元素 默认是没有初始化容器 懒加载
Node<K, V>[] table = null;
// 2. 实际用到table 存储容量 大小
int size;
// 3.HashMap默认负载因子,负载因子越小,hash冲突机率越低, 根据每个链表的个数
float DEFAULT_LOAD_FACTOR = 0.75f;
// 4.table默认初始大小 16
static int DEFAULT_INITIAL_CAPACITY = 16; // 16
public V put(K key, V value) {
// 1.判断table 数组大小是否为空(如果为空的情况下 ,做初始化操作)
if (table == null) {
table = new Node[DEFAULT_INITIAL_CAPACITY];
}
// 2.判断数组是否需要扩容 实际存储容量=负载因子0.75*初始容量16 =12 相当于如果长度大于12的时候就需要开始数组扩容
if (size >= (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)) {
System.out.println("开始扩容啦!!!!");
resize();
}
// 3.计算hash值指定下标位置
int index = getIndex(key, DEFAULT_INITIAL_CAPACITY);
Node<K, V> node = table[index];
if (node == null) {
// 没有发生hash冲突问题
node = new Node<K, V>(key, value, null);
size++;
} else {
Node<K, V> newNode = node;
while (newNode != null) {
// // 已经发生hash冲突问题key 直接添加(冲突node)到前面了 不是往后面加
if (newNode.getKey().equals(key) || newNode.getKey() == key) {
// hashCodoe 相同,而且equals 相等情况 说明是同一个对象 修改值
// node.value = value;
return newNode.setValue(value);
} else {
// 继续添加,排在前面 hascode 取模余数相同 index 存放在链表 或者hashCode 相同但是对象不同
// 新的node 的next 原来的node
if (newNode.next == null) {
node = new Node<K, V>(key, value, node);
size++;
}
}
newNode = newNode.next;
}
}
table[index] = node;
return null;
}
// hashMap数组扩容机制
private void resize() {
// 1.定义新的数组元素
Node[] newTables = new Node[DEFAULT_INITIAL_CAPACITY << 1];
// 2. 将老的table的key index重新计算下标
for (int i = 0; i < table.length; i++) {
// 老的Node节点
Node<K, V> oldNode = table[i];
while (oldNode != null) {
table[i] = null;
// 重新计算index 索引下标位置
K oldKey = oldNode.key;
int index = getIndex(oldKey, newTables.length);
// 老的next
Node oldnext = oldNode.next;
// 判断newTables数组中,是否存在下标相同,如果下标相同则存放在原来的.next
oldNode.next = newTables[index];
// 将原来的node赋值给新的node
newTables[index] = oldNode;
// 获取下一个节点,判断是否继续循环
oldNode = oldnext;
}
}
// 3.将newTable赋值给table;
table = newTables;
DEFAULT_INITIAL_CAPACITY = newTables.length;
newTables = null;// 将 对象变为不可达对象
}
// hasMap数组扩容
// private void resize() {
//
// // 1.定于新数组() 按照两倍进行扩容 // 1.使用取模算法定位数组链表 DEFAULT_INITIAL_CAPACITY)*2
// Node[] newTables = new Node[DEFAULT_INITIAL_CAPACITY << 1];
// // 2.hashCode不会发生改变.但是index会发生改变,重新计算index 重新取模
// for (int i = 0; i < table.length; i++) {
// Node<K, V> node = table[i];
// while (node != null) {
// table[i] = null;
// int index = getIndex(node.key, newTables.length);
// // 获取下一个节点
// ExtHashMap<K, V>.Node<K, V> next = node.next;
// // 判断是否有下一个节点
// node.next = newTables[index];
// // 将之前node 计算的inde下标存放在 newTables
// newTables[index] = node;
// // 判断是否继续循环
// node = next;
// }
//
// }
// // 将新的tables 赋值改老的table
// table = newTables;
// DEFAULT_INITIAL_CAPACITY = newTables.length;
// newTables = null;// 变为不可达对象
// }
// 测试方法.打印所有的链表元素
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.next;
// if (node.next != null) {
// node = node.next;
// } else {
// // 结束循环
// node = null;
// }
}
System.out.println();
}
}
public int getIndex(K k, int length) {
// System.out.println("k:" + k + ",hashCode=" + hashCode);
int index = k == null ? 0 : k.hashCode() % length;
return index;
}
public V get(K k) {
Node<K, V> node = getNode(table[getIndex(k, DEFAULT_INITIAL_CAPACITY)], k);
return node == null ? null : node.value;
}
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;
}
public int size() {
return size;
}
// 定义节点
class Node<K, V> implements Entry<K, V> {
// 存放Map 集合 key
private K key;
// 存放Map 集合 value
private V value;
// 下一个节点Node
private Node<K, V> next;
public Node(K key, V value, Node<K, V> next) {
super();
this.key = key;
this.value = value;
this.next = next;
}
public K getKey() {
return this.key;
}
public V getValue() {
return this.value;
}
public V setValue(V value) {
// 设置新值的返回老的 值
V oldValue = this.value;
this.value = value;
return oldValue;
}
}
}
HashMap 核心源码部分的分析
1. HashMap只允许一个为null的key。
2. HashMap的扩容:当前table数组的两倍
3. HashMap实际能存储的元素个数: capacity * loadFactor
4. HashMap在扩容的时候,会重新计算hash值,并对hash的位置进行重新排列, 因此,为了效率,尽量给HashMap指定合适的容量,避免多次扩容