1.HashMap特点
1.HashMap可以接受null键值和值
2.HashMap是非synchronized的
3.存储的是键值对。
4.提一句HashMap和HashTab特点不同 。hashTable是线程安全的且不接受null的键值对
2. 概念
hashing(散列法或哈希法)
散列法(hashing):是将字符串转换为固定长度数值或索引值的方法
散列表特点:由于通过更短的哈希表比原始的值进行搜索更快所以用在数据库的比较多
3 .HashMap概念和底层结构
HashMap是一个底层为“链表散列”的数据结构,即数组+链表(从jdk1.8开始添加了红黑树)。
数组特点:存储区间连续,占用内存严重,查询容易,插入删除难。
链表特点:存储区间离散,占用内存宽松,查询困难,插入删除简单。
HashMap实现了两种数据结构,查询简单,插入删除也简单
HashMap的结构示意图(从网上整一个):
可以看到hashmap的是数组 数据的头都维护这一个链表,Entry后面的点是next 指向下一个Entry
实现原理:
1 首先每一个元素都是链表的数组,当添加一个元素(key-value)时, 就首先计算元素key的hash值,以此确定插入数组的位置,但是可能存在同一hash值的元素已经被放到数组的同一位置,这是就添加到同一hash值的元素的后面,他们在数组的同一位置形成链表,同一链表上的Hash值是相同的,所以说数组存放的是链表,而当链表长度太长时,链表就转换为红黑树这样大大提高了查找效率。
当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍,把原链表数组的元素搬移到新的数组中
3构造方法+源码
有四个构造方法
initialCapacity:初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
loadFactor:装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍(扩容后面讲)。
从上到下这四个构造的方法作用是:
1、构造一个带指定初始容量和加载因子的空 HashMap。
2、构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
3、构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
4、构造一个映射关系与指定 Map 相同的 HashMap。
其实看源码可以发现其他的构造方法都是在调用第一个构造方发即
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;
this.threshold = tableSizeFor(initialCapacity);
}
因为初始容量必须是二的幂 所有tableSizeFo()的方法主要功能是返回一个比给定整数大且最接近的2的幂次方整数,如给定10,返回2的4次方16.
我们接着看这个方法putMapEntries(m, false);
首先判断一下传进来的Map的大小是不是大于0(等于0就没必要初始化了),由于上图是通过构造器新建一个HashMap,所以table是null(table是HashMap的Node数组public
使用了forEach循环遍历,将传入HashMap构造器的形参m中的所有key-value全部放到当前HashMap对象中(this)。
我们先看一下table==null中的这个table
该代码是table数组的定义。table是一个Node类型的数组,,transient修饰符的作用是使该变量在序列化的时候不会被储存。
然后我们看一下Node类型是什么情况
在这里我们看到Node实现了Map.Entry<K,V> 。看源码我们可以通过node对象获取键值对。Node指向下一对键值对
现在我们可以看一下putVal方法了
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//如果table为空或者长度为0,则resize()
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//确定插入table的位置,算法是(n - 1) & hash,在n为2的幂时,相当于取摸操作。
//找到key值对应的槽并且是第一个,直接加入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//在table的i位置发生碰撞,有两种情况,1、key值是一样的,替换value值,
//2、key值不一样的有两种处理方式:2.1、存储在i位置的链表;2.2、存储在红黑树中
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//2.2存储在红黑树中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//2.1 存储在i位置的链表
else {
for (int binCount = 0; ; ++binCount) {
///链表的尾端也没有找到key值相同的节点,则生成一个新的Node,
//并且判断链表的节点个数是不是到达转换成红黑树的上界达到,则转换成红黑树
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不为空就替换旧的oldValue值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
大概主要流程就是这样子,现在有几个比较核心的方法:
1.resize()扩容
final Node<K,V>[] resize() {
// 保存当前table
Node<K,V>[] oldTab = table;
// 保存当前table的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 保存当前阈值
int oldThr = threshold;
// 初始化新的table容量和阈值
int newCap, newThr = 0;
/*
1. resize()函数在size > threshold时被调用。oldCap大于 0 代表原来的 table 表非空,
oldCap 为原表的大小,oldThr(threshold) 为 oldCap × load_factor
*/
if (oldCap > 0) {
// 若旧table容量已超过最大容量,更新阈值为Integer.MAX_VALUE(最大整形值),
//这样以后就不会自动扩容了。
if (oldCap >= MAXIMUM_CAPACITY) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 容量翻倍,使用左移,效率更高
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
/*
2. resize()函数在table为空被调用。oldCap 小于等于 0 且 oldThr 大于0,代表用户创建了一个 HashMap,但是使用的构造函数为
HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity)
或 HashMap(Map<? extends K, ? extends V> m),导致 oldTab 为 null,oldCap 为0, oldThr 为用户指定的 HashMap的初始容量。
*/
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 若节点是单个节点,直接在 newTab 中进行重定位
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 若节点是 TreeNode 节点,要进行 红黑树的 rehash 操作
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 若是链表,进行链表的 rehash 操作
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash
do {
next = e.next;
// 根据算法 e.hash & oldCap 判断节点位置rehash 后是否发生改变
//最高位==0,这是索引不变的链表
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
具体内部实现的逻辑想知道的同学最好去看看源码跟着走走,
2putTreeVal方法,这个方法是在TreeNod下面
/**
当存在hash碰撞的时候,且元素数量大于8个时候,就会以红黑树的方式将这些元素组织起来
* map 当前节点所在的HashMap对象
* tab 当前HashMap对象的元素数组
* h 指定key的hash值
* k 指定key
* v 指定key上要写入的值
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;// 定义k的Class对象
boolean searched = false;//标识是否已经遍历过一次树,未必是从根节点遍历的,但是遍历路径上一定已经包含了后续需要比对的所有节点
TreeNode<K,V> root = (parent != null) ? root() : this; 父节点不为空那么查找根节点,为空那么自身就是根节点
for (TreeNode<K,V> p = root;;) {// // 从根节点开始遍历,
int dir, ph; K pk;// // 声明方向、当前节点hash值、当前节点的键对象
if ((ph = p.hash) > h) 如果当前节点hash 大于 指定key的hash值
dir = -1; 要添加的元素应该放置在当前节点的左侧
else if (ph < h)// 如果当前节点hash 小于 指定key的hash值
dir = 1;// 要添加的元素应该放置在当前节点的右侧
else if ((pk = p.key) == k || (k != null && k.equals(pk))) // 如果当前节点的键对象 和 指定key对象相同
return p;// 那么就返回当前节点对象
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) { // 走到这一步说明 当前节点的hash值 和 指定key的hash值 是相等的,但是equals不等
/*
* searched 标识是否已经对比过当前节点的左右子节点了
* 如果还没有遍历过,那么就递归遍历对比,看是否能够得到那个键对象equals相等的的节点
* 如果得到了键的equals相等的的节点就返回
* 如果还是没有键的equals相等的节点,那说明应该创建一个新节点了!
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
/*
* 如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就可以把要添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较
* 如果dir大于等于0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素作为当前节点的右节点,如果不为空,还需要下一轮继续比较
* 如果以上两条当中有一个子节点不为空,这个if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
这是大概的putTreeVal方法的原理。也是jdk7和jdk8在hashmao中做的改进。