hashmap是我们常用的容器类,但是为什么会有这种数据结构出现呢?它是怎么操作的呢?本文会带你领略大神的编程。
我们已经有了数组,ArrayList和LinkedList,为什么要有HashMap? 因为在之前的数据结构中,最好的搜索方法是有序数组的二分查找和AVL树搜索。它们的最坏情况所搜时间都是O(lgn)。是否有更快的算法?散列表数据结构提供了这样的保证——O(1)的平均搜索时间。
本文的内容结构为
1、HashMap 存储结构
2、HashMap 各常量、成员变量、内部类作用
3、HashMap 几种构造方法
4、HashMap put 及其相关方法
5、HashMap get 及其相关方法
6、HashMap remove 及其相关方法
7、HashMap 扩容方法resize()
先来看一下 HashMap 的继承图:
HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。HashMap 最多只允许一条记录的键为 null ,允许多条记录的值为 null 。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。
我们再来看一下HashMap的实现方式
HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示:
常量
//创建 HashMap 时未指定初始容量情况下的默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//HashMap 的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//HashMap 默认的装载因子,当 HashMap 中元素数量超过 容量*装载因子 时,进行 resize() 操作
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//用来确定何时将解决 hash 冲突的链表转变为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 用来确定何时将解决 hash 冲突的红黑树转变为链表
static final int UNTREEIFY_THRESHOLD = 6;
//当需要将解决 hash 冲突的链表转变为红黑树时,需要判断下此时数组容量,若是由于数组容量太小(小于 MIN_TREEIFY_CAPACITY )导致的 hash 冲突太多,
//则不进行链表转变为红黑树操作,转为利用 resize() 函数对 hashMap 扩容
static final int MIN_TREEIFY_CAPACITY = 64;
变量
//保存Node<K,V>节点的数组
transient Node<K,V>[] table;
//由 hashMap 中 Node<K,V> 节点构成的 set
transient Set<Map.Entry<K,V>> entrySet;
//记录 hashMap 当前存储的元素的数量
transient int size;
//记录 hashMap 发生结构性变化的次数(注意 value 的覆盖不属于结构性变化)
transient int modCount;
//threshold的值应等于 table.length * loadFactor, size 超过这个值时进行 resize()扩容
int threshold;
//记录 hashMap 装载因子
final float loadFactor;
内部类
// Node<K,V> 类用来实现数组及链表的数据结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //保存节点的 hash 值
final K key; //保存节点的 key 值
V value; //保存节点的 value 值
Node<K,V> next; //指向链表结构下的当前节点的 next 节点,红黑树 TreeNode 节点中也有用到
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
// TreeNode<K,V> 继承 LinkedHashMap.Entry<K,V>,用来实现红黑树相关的存储结构
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 存储当前节点的父节点
TreeNode<K,V> left; //存储当前节点的左孩子
TreeNode<K,V> right; //存储当前节点的右孩子
TreeNode<K,V> prev; // 存储当前节点的前一个节点
boolean red; // 存储当前节点的颜色(红、黑)
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
//构造方法1,指定初始容量及装载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
/** tableSizeFor(initialCapacity) 方法返回的值是最接近 initialCapacity 的2的幂,
* 若指定初始容量为9,则实际 hashMap 容量为16
*/
this.threshold = tableSizeFor(initialCapacity);
}
//构造方法2,仅指定初始容量,装载因子的值采用默认的 0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//构造方法3,所有参数均采用默认值
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
重要说明 此方法意为传一个期望值返回一个最接近期望值的2的幂。
//tableSizeFor(initialCapacity) 方法返回的值是最接近 initialCapacity 的2的幂
static final int tableSizeFor(int cap) {
int n = cap - 1;
/**
* 以下表达式为 n的二进制与 n的二进制数右移后x位的结果进行异或
* 目的是保证高位1后的数据均为1,普及一下如果高位1后面全是1的话,
* 即 0000 0000 0000 0000 0000 0000 0000 1111
* 这样的二进制表示为 15,可以推导出 为2的幂-1。
* 至于为何 右移 1 + 2 + 4 + 8 + 16 = 31位
* 是因为一个 Integer 类型占 4 字节,一个字节占 8 位二进制码,
* 因此一个 Integer 总共占 32 位二进制码。去除第一位的符号位,
* 剩下 31 位来表示数值。所以右移31位,以此保证高位1后面的数据均为1。
*/
n |= n >>> 1;// >>> 代表无符号右移
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
/**
* 当n小于 最大值时
* 返回 n+1,由上面我们知道 如果高位1后面全是1的话,
* 用十进制表示为 2的n次方-1,所以返回的n+1 则为
* 2的n次方,这就是为什么hashmap的容量必须会是2的n次方
*/
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
内容太多,更多精彩请期待