HashMap
一、简介
无序
键值都可以为空,但是只能有一个键为空
1.1、jdk1.8版本前后
1、在jdk1.8之前, 主要存储的方式是数组+链表的结构 ,数组作为主体。链表主要解决hash值冲突问题(两个对象在调用hashcode()方法计算出一个相同的hash值,导致数组的索引一样,这时就要用到链表存储)
2、在调用构造方法时就会创建一个16长度的数组,Entry[]table 存储键值对数据
在jdk1.8之后 ,引入了红黑树存储 (当链表长度超过阈值【或者红黑树的边界值:默认为8】,并且数组长度超过64时,数组索引的位置将会变成黑红树存储)【如果超过阈值但是长度没有超过64就会扩容存储】
2、创建数据长度是在第一次调用put方法是创建一个Node[]table数组
- 数组没有超过64,进行扩容,主要是数组长度小于64还是数组搜索快一些
- 使用红黑树效率会变低,因为红黑树为了平衡会 左旋 ,右旋等操作
1.2、存储数据的原理
1、存储第一个数据(eg:轻松 24)时先调用String重写后的hashcode()方法,计算出hash值,再根据Node数组的长度采用某种算法计算出向数组中存放位置的索引值(假若是“2”)
2、存入第二个数据(eg:星星 “25”)如果计算的索引值也是(“2”),就会比较这两个hash值,如果不一样就会划出一个节点,以链表的方式来存储第二个数据
3、存入第三个数据(eg:“轻松 33”)这时索引值也是(‘2’),所以判断和之前存的两对象key 的hash值,如果相等,就是发生hash碰撞, 再调用equels ,判断两个key是否相等如果不相等就会继续在链表下继续存储,相等就覆盖vlue值
1.3、集合容量
A:初始容量(必须是二的n次幂)
默认开始扩容为1左移4位=》 16 也就是1*2的4次方
是二的n的n次幂
按位与运算(hash &(数组长度-1))
这样能跟好的均匀的分配数组空间
不是二的n次幂
非常容易使得索引相同,和哈希碰撞,使得存储的不均匀
B:如果传的长度不是2的n次幂
回走这个方法,找到大于传的数的最小2的幂次方
通过右移再或运算得到
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
(eg:传10)计算如下:
第一次计算(将1001 相邻的0变为1 1101)
n=9;
第二次计算 (再次将 1001 高位的0变为1 1111 )
第三次计算 (基本没有变化了 )
1.4、红黑树和链表之间的转化
开始使用链表的原因:因为红黑树的节点比较大,而且红黑树的空间是链表的两倍 所以开始使用链表效率会高一些
当节点高于8的时候,会自动转为红黑树,小于6时,会自动变为链表
主要是泊松分布原理
1.5、加载因子
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
不能太小,太小会导致空间使用率低,会扩容多(扩容很不好,会开辟一个新的数组,然后将旧的数组里面的数据放入新的数组里面 ,使得效率降低)
也不能太大,太大会导致产生更多的链表,红黑树会很多,导致查询等效率大大降低
1.5、扩容机制
扩容之后的索引的位置要么是原来的索引位置,要么是原来索引位置+就数组的容器
1.6、删除元素
实际是调用removeNode()方法
1、先判断是否是数组结构,如果是就直接按照数组删除
2、判断是否有节点 如果有节点,再判断是红黑树还是链表结构
3、如果是红黑树,就按照红黑树方式删除,链表就按链表结构删除
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
1.6、遍历元素
第一种
//先假设有一个HashMap集合 ->map ,有值
Map<String,String> map=new HashMap<String,String>
//先得到所有的key值
String keys=map.keySet();//然后遍历得值
//得到所有的Vule值
Collection<String> values=map.values();//然后再便利得值
第二种
HashMap<String,String> map=new HashMap<String, String>();
//先得到一个set集合
Set<Map.Entry<String, String>> entries = map.entrySet();
//再用迭代器,迭代集合
for (Iterator<Map.Entry<String, String>> it=entries.iterator(); it.hasNext()) {
Map.Entry<String, String> next = it.next();
System.out.println(next.getKey()+next.getValue());
}
第三种(不建议使用)迭代了两次
HashMap<String,String> map=new HashMap<String, String>();
//得到所有的key keys
Set<String> keys = map.keySet();
for (String key : keys) {
//遍历keys 通过key找到value
String value = map.get(key);
System.out.println(key + value);
}
第四种 JDK8之后默认方法
HashMap<String,String> map=new HashMap<String, String>(10);
//JDK8 新特性Lambda表达式
map.forEach((key,value)->{
System.out.println(key+"-----"+value);
});
1.7、知道map的容量,合理设计map集合
也就是初始化容器,尽量避免扩容
容量 initialCapacity=(需要存储的元素个数 / 负载因子)+1
一般建议使用第二种构造方法,设计好初始容量
1、如果使用第一种,默认为第一次调用put扩容16,如果数据很多,就会多次扩容,扩容很消耗性能
2、一般加载因子不建议修改,原因如上面解释
容量设计正解: 容量 initialCapacity=(需要存储的元素个数 / 负载因子)+1
》
》
eg:要存7个数据,如果容器刚好设计为7,因为通过jdk处理,会被设计成8,因为HashMap元素达到80.75=6就会扩容
当按照公式算得为10 因为通过jdk会自动升为16 ,而0.75=12(*负载因子)刚好合适 ,所以不能直接按照数据大小设计,