HashMap

HashMap是一个哈希表结构,以键值对形式存取数据,允许为null值,key重复则覆盖,非同步,线程不安全,而且不保证顺序,如要保证顺序可使用LinkedHashMap,要保证安全可使用ConcurrentHashMap

1.默认容量

HashMap默认容量为16,但是是一个懒加载的机制,第一次put时才会申请内存空间,负载因子为0.75,初始化时也可自己设定默认容量,但是必须是2的次幂,不然会被自动调成2的次幂;

坑: 准备用HashMap存1w条数据,构造时传10000还会触发扩容吗?

不会扩容,因为如果构造时传1W,1W不是2的次幂,会自动转为2的14次幂,即16384,乘以负载因子0.75=12288,存入1W条数据绰绰有余。

为什么必须是2的n次方

如果用取模运算会让数据分布比较均匀,但是消耗比较大,所以hashMap是用hash&(n - 1) 来计算下标的,其中n是Map数组的长度,而在length是2的n次幂时hash&(n - 1)等价于hash%length,效率会更高 只有它的长度是2的n次方的时候,我们对它进行-1操作才能拿到二进制位数全部是1的值,这样对他进行按位与(&)才能够非常快速的用位运算的方式拿到数组的下标,并且分布还是均匀的。

2. 计算hash值的方法

ini 复制代码
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

向右移16位且按位异或的操作,为了降低哈希碰撞的几率

JDK1.7 的时候是一堆异或操作和右移操作

3. JDK1.7 与 1.8 的区别

(1)JDK1.7采用头插法,JDK 1.8 采用尾插法。而改用尾插法的原因本人理解有两点:一是头插法会造成循环链表,二是需要在链表长度大于 8 时进行树化判断,也不能把链表的长度存在 map 中,那样会增加开支,所以需要遍历一遍来判断链表的长度是否达到 8 。

头插法就是插入新的节点,是从头部插入,原本的都往后排,createEntry方法

less 复制代码
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash,key,value,e)  //新节点的下一个节点等于原本的第一个节点
size++

所以多线程情况下容易变成环形链表,会导致查询一个不存在key,最终导CPU100%

原因为在JDK1.7的hashMap的resize()方法里有个transfer()方法

ini 复制代码
/**
 * Transfers all entries from current table to newTable.
 */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) { 
            // 这行也是重点
            Entry<K,V> next = e.next;		--------------(1)
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //计算出重新存放的位置
            int i = indexFor(e.hash, newCapacity);
            // 重点这三行
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

这个过程为,本来有链表 1------>2,然后线程一和线程二同时走到transfe方法的while循环里的(1)处, 此e = 1,e.next=2,然后线程二挂起,线程一把循环执行完之后并把线程一生成的新table放在内存里,此时链表为2------>1,接着线程二执行,但是现在线程二还是e = 1,e.next=2,然后执行重点三行代码会出现1.next=2,就变成了1------>2------>1,循环链表就形成了

还有一种情况:通过精心构造的而已请求引发Dos,比如可以通过精心构造的http请求,就是一些hash值相同的key,让tomcat内部产生大量的链表,消耗大量CPU发生Dos(拒绝服务攻击)

(2)出现哈希冲突时,1.7把数据存放在链表,1.8是先放在链表,链表长度达到8并且数组的长度不小于64时 就转成红黑树,低于6重新转换为链表

为什么超过8会变成红黑树?

红黑树占用空间是链表的两倍,空间损耗比较大,所以没有直接使用红黑树,源码里说是符合泊松分布,哈希冲突八次的几率较小,转为红黑树是因为要保证在冲突比较多的时候的查询效率

(3) 1.7的addEntry()方法里的扩容条件是map的元素大于阈值且存在哈希冲突,1.8扩容条件是

HashMap中的是元素个数大于阈值 或 数组中的链表长度达到8且Node数组长度小于64时


(4)1.7添加新元素是先扩容并重新计算下标,再进行插入,1.8后是先进行插入,如果 ++size > threshold再按照扩容后的规律统一计算。扩容后的统一规律:

(5)new HashMap()之后并不会在堆区申请内存空间,因为构造方法里没有 new 那个数组 ,JDK1.7中 ,put 方法中的 inflateTable() 方法里如果是 EMPTY_TABLE 时才会进行 new Entry,而1.8中是在 put() 方法里如果数组的长度是0的时候调用 resize() 方法,在 resize() 中进行 new Node[] 的。

4. 树化的条件

链表长度达到8的时候执行treeifyBin() 方法,但还会进行判断,tab.length >= 64,的时候才会树化,不然只会进行扩容

遍历HashMap的方式

vbnet 复制代码
 public static void main(String[] args) {
        HashMap<Integer,Integer> hashMap = new HashMap();
        for (int i = 0; i < 10; i++) {
            hashMap.put(i,i);
        }

//        Set set = hashMap.keySet();
//        for (Object o : set) {
//            System.out.println("Key: "+o+" Value: "+hashMap.get(o));
//        }
        Set<Map.Entry<Integer, Integer>> entries = hashMap.entrySet();
        for(Map.Entry<Integer, Integer> entry : entries){
            System.out.println("key: "+ entry.getKey() + "; value: " + entry.getValue());
        }
    }

LinkedHashMap怎么,保持有序的?

LinkedHashMap有一个head变量,就是头结点,tail,尾节点,还定义了一个Entry类,该类继承了HashMap.Node类,添加了一个before,after,就和linkedList相似,指向他的前一个Entry和后一个Entry

LinkedHashMap 重写了 HashMap 的 newNode() 方法,在new Node() 时,调用了 linkNodeLast()。

将新元素的 before 指向 tail 节点,并把 tail 节点变为最新的节点,实现了双向链表功能,保证有序。

LinkedHashMap 比 HashMap 多了一个 accessOrder ,accessOrder 为 true ,那么除了 new Node() 时会调整链表结构,新增修改和查看等操作都会通过 被重写的 afterNodeAccess() 方法修改链表结构。

TreeMap怎么保持有序?

TreeMap是基于红黑树实现的,保证二楼键的有序性,迭代时根据键的大小排序

红黑树性质

1.每个节点都有两个子节点,但是子节点可能为空

2.每个节点不是黑色就是红色,根节点一定是黑色

3.每个红色节点的两个子节点都是黑色,不可能有连在一起的红色节点

每次插入和删除的时候,如果打破红黑树的平衡,都会自动做旋转和颜色变化,以达到保持平衡,新插入的节点的位置是根据key的大小来判断的

相关推荐
chuanauc5 分钟前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴22 分钟前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao28 分钟前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc78732 分钟前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
云泽野6 小时前
【Java|集合类】list遍历的6种方式
java·python·list
二进制person7 小时前
Java SE--方法的使用
java·开发语言·算法
小阳拱白菜8 小时前
java异常学习
java