从源码角度来谈谈 HashMap

2024-01-09

HashMap的知识点可以说在面试中经常被问到,是Java中比较常见的一种数据结构。所以这一篇就通过源码来深入理解下HashMap。

1 HashMap的底层是如何实现的?(基于JDK8)

1.1 HashMap的类结构和成员

/**
HashMap继承AbstractMap,而AbstractMap又实现了Map的接口
*/ 
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
    

从上面源码可以看出HashMap支持序列化和反序列化,而且实现了cloneable接口,能支持clone()方法复制一个对象。

1.1.1 HashMap源码中的几个成员属性
//最小容量为16,且一定是2的幂次
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

 //最大容量为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//当某节点的链表长度大于8并且hash数组的容量达到64时,链表将会转换成红黑树
static final int TREEIFY_THRESHOLD = 8;

//当链表长度小于6时,红黑树将转换成链表
static final int UNTREEIFY_THRESHOLD = 6;

//链表变成红黑树的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;

从上面的源码可以看出,JDK1.8的HashMap实际上是由数组+链表+红黑树组成,在一定条件下链表会转换成红黑树。这里要谈一下默认加载因子为什么为0.75(3/4),加载因子也叫扩容因子,用来判断HashMap什么时候进行扩容。**选择0.75的原因是为了平衡容量与查找性能:扩容因子越大,造成hash冲突的几率就越大,查找性能就会越低,反之扩容因子越小,所占容量就会越大。**于此同时,负载因子为3/4的话,和capacity的乘积结果就可以是一个整数。

下面再看看hash数组中的元素

1.1.2 HashMap中的数组节点

​ hash数组一般称为哈希桶(bucket),结点在JDK1.7中叫Entry,在JDK1.8中叫Node。

//1.8中Node实现entry的接口
static class Node<K,V> implements Map.Entry<K,V> {
    //每个节点都会包含四个字段:hash、key、value、next
        final int hash;
        final K key;
        V value;
        Node<K,V> next;//指向下一个节点

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
    
		//hash值是由key和value的hashcode异或得到
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            //判断o对象是否为Map.Entry的实例
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                //再判断两者的key和value值是否相同
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
}
//这个是扰动函数,减少hash碰撞
static final int hash(Object key) {
        int h;
    	//将key的高16位与低16位异或(int是2个字节,32位)
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

1.2 HashMap中的方法

1.2.1 查询方法
public V get(Object key) {
        Node<K,V> e;
    	//将key值扰动后传入getNode函数查询节点
        return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    	//判断哈希表是否为空,第一个节点是否为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //从第一个节点开始查询,如果hash值和key值相等,则查询成功,返回该节点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //查询下一个节点
            if ((e = first.next) != null) {
                //若该节点存在红黑树,则从红黑树中查找节点
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //若该节点存在链表,循着链表查找节点
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
}
1.2.2 新增方法

向哈希表中插入一个节点

public V put(K key, V value) {
    	//将扰动的hash值传入,调用putVal函数
        return putVal(hash(key), key, value, false, true);
    }
//当参数onlyIfAbsent为true时,不会覆盖相同key的值value;当evict是false时,表示是在初始化时调用
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //若哈希表为空,直接对哈希表进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //若当前节点为空,则直接在该处新建节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {//若当前节点非空,则说明发生哈希碰撞,再考虑是链表或者红黑树
        Node<K,V> e; K k;
        //如果与该节点的hash值和key值都相等,将节点引用赋给e
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //如果p是树节点的实例,调用红黑树方法新增一个树节点e
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //若该节点后是链表
        else {
            for (int binCount = 0; ; ++binCount) {
                //遍历到链表末尾插入新节点
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //若插入节点后,链表节点数大于转变成红黑树的临界值(>=8)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //将链表转换成红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                //遍历过程中发现了key和hash值相同的节点,用e覆盖该节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //对e节点进行处理
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //节点插入成功,修改modCount值
    ++modCount;
    //如果达到扩容条件,直接扩容
    if (++size > threshold)
        resize();
    
    afterNodeInsertion(evict);
    return null;
}

1.2.3 扩容方法(非常重要)
final Node<K,V>[] resize() {
    //当前的数组
    Node<K,V>[] oldTab = table;
    //当前的数组大小和阈值
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    //对新数组大小和阈值初始化
    int newCap, newThr = 0;
    //若当前数组非空
    if (oldCap > 0) {
        //若当前数组超过容量最大值,返回原数组不扩容
        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
    }
    //数组为空,且大于最小容量(数组初始化过)
    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);
    }
    //数组为空,且新的阈值为0
    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;
                //若循环到该节点是最后一个非空节点,直接赋值
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //若发现该节点是树节点
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //若该节点后是链表
                else { // preserve order
                    //定义现有数组的位置low,扩容后的位置high;high = low + oldCap
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        /*通过(e.hash & oldCap)来确定元素是否需要移动,
                          e.hash & oldCap大于0,说明位置需要作相应的调整。
                          反之等于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;
                    }
                    //处于高位位置要改变为j + oldCap
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

​ HashMap实际上是线程不安全的,在JDK1.7中,链表的插入方式为头插法,在多线程下插入可能会导致死循环。因此在JDK1.8中替换成尾插法(其实想要线程安全大可用ConcurrentHashMap、Hashtable)

//JDK1.7源码
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;
            if(rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, new Capacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

假如HashMap的容量为2,其中在数组中有一个元素a(此时已经到达扩容的临界点)。创建两个线程t1、t2分别插入b、c,因为没有锁,两个线程都进行到扩容这一步,那么其中有节点位子因为扩容必然会发生变化(以前的容量不够),这个时候假设t1线程成功运行,插入成功。但是由于t2线程的合并,加上节点位置的挪动,就会造成链表成环。最后读取失败

1.2.4 删除方法
//通过key值删除该节点,并返回value
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
//删除某个节点
//若matchValue为true时,需要key和value都要相等才能删除;若movable为false时,删除节点时不移动其他节点
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为删除点
        Node<K,V> node = null, e; K k; V v;
        //查到头节点为所要删除的点,直接赋于node
        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);
            }
        }
        //对取回的node节点进行处理,当matchValue为false,或者value相等时
        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
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

2.一些面试题

2.1 JDK1.8 HashMap扩容时做了哪些优化

  1. 新元素下标方面,1.8通过高位运算 (e.hash & oldCap) == 0 分类处理表中的元素:低位不变,高位原下标+原数组长度;而不是像1.7中计算每一个元素下标。

  2. 在resize()函数中,1.8将1.7中的头插逆序变成尾插顺序。但是仍然建议在多线程下不要用HashMap。

2.2 HashMap与Hashtable的区别

  1. 线程安全:Hashtable是线程安全的,不允许key,value为null。
  2. 继承父类:Hashtable是Dictionary类的子类(Dictionary类已经被废弃),两者都实现了Map接口。
  3. 扩容:Hashtable默认容量为11,扩容为原来的容量2倍+1,所以Hashtable获取下标直接用模运算符%。
  4. 存储方式:Hashtable中出现冲突后,只有用链表方式存储。

2.3 HashMap线程不安全,那么有哪些Map可以实现线程安全

  1. Hashtable: 直接在方法上加synchronized关键字,锁住整个哈希桶
  2. ConcurrentHashMap:使用分段锁,相比于Hashtable性能更高
  3. Collectons.synchronizedMap:是使用Collections集合工具的内部类,通过传入Map封装一个SynchronizedMap对象,内部定义一个对象锁,方法通过对象锁实现。

参考博文:

HashMap 底层实现原理是什么?JDK8 做了哪些优化?

一个HashMap跟面试官扯了半个小时

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从源码角度来谈谈 HashMap 的相关文章

  • Java 正则表达式 String.matches 工作不一致

    我有一个正则表达式来检查字符串是否是数字 该格式的千位分隔符是空格 小数分隔符是点 小数点后部分是可选的 问题是 在某些时候 String matches 函数会停止按预期工作 以前有效的方法现在不再有效了 例如 JUnit 代码 impo
  • 编码java Cookie值

    应如何对 Java Cookie 对象的实际值进行编码 我无法传递 等字符或 US ASCII 之外的任何字符 Br 乔因斯 如何并不重要 但通常Base64 http en wikipedia org wiki Base64应该可以正常工
  • Spring AOP 是否进行编译时编织或加载时编织?

    我开始在一个项目中使用 Spring AOP 但我对编织有点困惑 我知道 Spring AOP 依赖于 AspectJweaver jar 但正如文档所说 这不是为了编织 而只是它使用了这个 jar 中的一些类 但我的问题是 如果不使用As
  • Android ArrayList 的 IndexOutOfBoundsException [重复]

    这个问题在这里已经有答案了 我遇到了一个非常烦人的问题 一些代码抛出 IndexOutOfBoundsException 我真的不明白为什么 logcat 指向以下代码的 addTimetableItem 我们将对此进行更多解释 if so
  • Hibernate OneToMany 列表中的重复结果

    我已将 1 N 关系与 OneToMany 列表映射 但当我访问该列表时 由于 OUTER JOIN 结果会重复 映射如下所示 Entity public class Programmer ElementCollection fetch F
  • 检查手机是否可以发送短信

    我已经读过一些相关的问题 但大多数都是针对呼叫 而不是短信 到目前为止我发现的是 TelephonyManager manager TelephonyManager context getSystemService Context TELE
  • Apache HttpClient 摘要式身份验证

    基本上我需要做的是执行摘要身份验证 我尝试的第一件事是可用的官方示例here http svn apache org repos asf httpcomponents httpclient tags 4 0 1 httpclient src
  • Java 工具创建的 WSDL 文件的 WCF 序列化问题

    我的团队的任务是让几个内部开发的 NET 客户端应用程序连接到一些新的 Java Web 服务 Java Web 服务是第三方 供应商提供的 WSDL 文件 我们的团队修改 控制的能力有限 这意味着我们可能有权要求我们的供应商对 WSDL
  • 序列化的 lambda 且没有serialVersionUID?

    我正在尝试了解 Java 及其最新版本的序列化如何工作 我正在尝试像这样序列化 lambda Runnable r Runnable Serializable gt System out println This is a test 但我注
  • Java如何使用私钥文件而不是PEM来解密?

    使用 Java 和 Bouncy Castle 1 52 我可以使用以下代码通过 PEM 证书加载私钥 我还有一个相同的 PKCS8 格式的 private key 文件 直接使用private key文件而不是PEM的代码是什么 Stri
  • 如何将 Netbeans 项目导入 Eclipse

    我想将我的 NetBeans 项目转移到 Eclipse 这是一个网络应用程序项目 我将 war 文件导入到 Eclipse 中 但无法获取 Java 文件 并且 war 文件给了我很多错误 导入整个项目的最佳方式是什么 另一种简单的方法如
  • 使用 Java 谓词和 Lambda

    为什么下面的代码会返回Predicate
  • 是否有办法排除从父 POM 继承的工件?

    可以通过声明排除依赖项中的工件
  • 如何更改操作栏上标题文本的大小?

    有一个ActionBar在每个 Android 4 0 应用程序上 它都有一个标题 我需要缩小这个尺寸 但我不明白我该怎么做 因为ActionBar不为其提供公共方法 Remark 我不想使用自定义视图 实际上 您可以对ActionBar
  • 无法运行正在访问 GlassFish v3 上的 EJB 的应用程序客户端

    环境 GlassFish 3 0 1 NetBeans 6 9 JDK 6u21 Problem 无法运行正在访问 EJB 的应用程序客户端 错误报告可以在下面找到http netbeans org bugzilla show bug cg
  • 破译Streamreduce函数

    为什么两者都是c1 and c2不被视为两个字符串 而是一个String和一个Integer Arrays asList duck chicken flamingo pelican stream reduce 0 c1 c2 gt c1 l
  • 如何从 InputStream 读取一行而不缓冲输入? [复制]

    这个问题在这里已经有答案了 我有一个输入流 其中包含一行字符串 然后是二进制数据 如果我使用读取该行new BufferedReader new InputStreamReader inputStream 二进制数据也正在被读取并且不能被重
  • OSGI Felix 容器正在初始化模拟私有字段

    我试图模拟我的类中的一个私有字段 该字段由运行我的应用程序的 OSGI 容器初始化 我放了一个示例代码供参考 请提供任何线索 import org apache felix scr annotations Component name My
  • Java多线程和安全发布[关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 看完之后 Java并发实践 http jcip net and OSGI 实践 http neilbartlett name blog osgi
  • 在哪里可以找到所有 HQL 关键字的列表?

    在哪里可以找到所有 HQL 关键字的列表 在完整的 Hibernate 源代码下载中 有一个grammar hql g文件 这是ANTLR http www antlr org 语言定义 您可以从官方GitHub源码仓库查看该文件的最新版本

随机推荐

  • 在线智能抠图软件有哪些?证件照背景颜色更换不求人

    2024年上半年全国计算机等级考试 NCRE 报名开始啦 不出意外 这次报名仍然是需要提交证件照 具体要求如下 本人近期正面免冠 白色背景 彩色证件照 不得使用生活照 美颜照 最小像素高宽为192x144 最小成像区高宽为48mmx33mm
  • 大模型笔记【2】 LLM in Flash

    Apple最近发表了一篇文章 可以在iphone MAC 上运行大模型 LLM in a flash Efficient Large Language Model Inference with Limited Memory 主要解决的问题是
  • ros2 基础学习 15- URDF:机器人建模方法

    URDF 机器人建模方法 ROS是机器人操作系统 当然要给机器人使用啦 不过在使用之前 还得让ROS认识下我们使用的机器人 如何把一个机器人介绍给ROS呢 为此 ROS专门提供了一种机器人建模方法 URDF Unified Robot De
  • 解锁数据之门Roxlabs全球住宅IP赋能海外爬虫与学术研究

    11 20云账房测试一面凉经 华为开奖啦 关于邮储的一些情况 维信金科一面 二面 hr面 我的导师太好了 上海维信金科 技术面试一 Java后端开发岗记录贴 维信金科正式批面经 软件技术领域就业大纲 1 公司分类 你裁掉这个应届生用了多长时
  • MySQL 8.0中新增的功能(五)

    改进哈希连接性能 MySQL 8 0 23重新实现了用于哈希连接的哈希表 从而改进了哈希连接的性能 这项工作包括修复了一个问题 Bug 31516149 Bug 99933 在这个问题中 用于连接缓冲区 join buffer size 的
  • 【设计模式之美】 SOLID 原则之五:依赖反转原则:将代码执行流程交给框架

    文章目录 一 控制反转 IOC 二 依赖注入 DI 三 依赖注入框架 DI Framework 四 依赖反转原则 DIP 一 控制反转 IOC 通过一个例子来看一下 什么是控制反转 public class UserServiceTest
  • MySQL 8.0中新增的功能(六)

    配置 MySQL中主机名的最大允许长度已经从之前的60个字符提高到255个ASCII字符 这适用于数据字典中与主机名相关的列 mysql系统模式 性能模式 INFORMATION SCHEMA和sys模式 CHANGE MASTER TO语
  • 论文查重部分算不算重复率呢

    大家好 今天来聊聊论文查重部分算不算重复率呢 希望能给大家提供一点参考 以下是针对论文重复率高的情况 提供一些修改建议和技巧 可以借助此类工具 论文查重部分算不算重复率呢 在论文查重过程中 查重部分是否计入重复率是一个关键问题 本文将从七个
  • python安装教程(2020最新),python安装视频教程

    大家好 小编来为大家解答以下问题 python安装教程 2020最新 python安装视频教程 现在让我们一起来看看吧 python安装是学习pyhon第一步 很多刚入门小白不清楚如何安装python 今天我来带大家完成python安装与配
  • 请求各位大佬帮助,请问qt项目能调用卷积神经网络进行训练模型嘛?

    qt项目想调MobileNetV3网络进行训练模型 得到权重文件 能实现嘛
  • ros2 基础学习14-- Launch:多节点启动与配置脚本

    到目前为止 每当我们运行一个ROS节点 都需要打开一个新的终端运行一个命令 机器人系统中节点很多 每次都这样启动好麻烦呀 有没有一种方式可以一次性启动所有节点呢 答案当然是肯定的 那就是Launch启动文件 它是ROS系统中多节点启动与配置
  • 【数位dp】【动态规划】C++算法:233.数字 1 的个数

    作者推荐 动态规划 C 算法312 戳气球 本文涉及的基础知识点 动态规划 数位dp LeetCode 233数字 1 的个数 给定一个整数 n 计算所有小于等于 n 的非负整数中数字 1 出现的个数 示例 1 输入 n 13 输出 6 示
  • 软件测试开发/全日制/测试管理丨测试左移体系

    对于 测试左移 这一术语 通常是指在软件开发过程中 测试活动的介入时间往项目生命周期的早期移动 这个概念强调在需求 设计和编码阶段就开始考虑测试 以提高软件的质量和减少后期修复缺陷的成本 以下是测试左移体系的一些关键方面 1 早期测试活动
  • Qt Creator 常用快捷键

    Qt Creator 常用快捷键一览表 功能描述 快捷键 多行注释模式 Ctrl 激活欢迎模式 Ctrl 1 激活编辑模式 Ctrl 2 激活调试模式 Ctrl 3 激活项目模式 Ctrl 4
  • MySQL 8.0中新增的功能(七)

    EXPLAIN ANALYZE 语句 在MySQL 8 0 18中引入了一种新形式的EXPLAIN语句 即EXPLAIN ANALYZE 它提供了关于SELECT语句执行的扩展信息 以TREE格式显示查询过程中每个迭代器的执行计划 并可以比
  • 双系统安装win7出现grub怎么解决

    我们在重装系统时 特别苹果装双系统时 会出现安装过程或者安装后的问题 发现系统开机显示grub 导致不能正常进入系统 让人很是着急 其实出现这种情况是因为系统找不到主引记录所导致 只要重建主引记录mbr即可解决 下面小编就教教大家win7系
  • 实时获取建材网商品数据:API实现详解与代码示例

    一 引言 随着电子商务的快速发展 实时获取商品数据对于企业决策 市场分析以及数据驱动的营销策略至关重要 建材网作为国内知名的建材信息平台 提供了API接口 使得第三方开发者可以方便地获取商品数据 本文将详细介绍如何使用 建材网的API接口
  • 代码随想录算法训练营Day18 | 二叉搜索树中的插入操作、二叉搜索树的最近公共祖先、删除二叉搜索树的节点、修剪二叉搜索树、二叉搜索树转换成累加树、将有序数组转换成二叉搜索树

    LeetCode 701 二叉搜索树的插入操作 本题思路 既然是二叉搜索树 那么插入就变得比较简单了 因为二叉搜索树的左树节点值都小于根节点值 右树节点值都大于根节点值 直接将要插入的节点值和根节点进行对比 如果比它小 就往左孩子走 比他大
  • go测试覆盖率

    go test cover ok mytest cached coverage 42 9 of statements go test 增加 cover 参数可以查看测试用例的覆盖率 go test coverprofile size cov
  • 从源码角度来谈谈 HashMap

    HashMap的知识点可以说在面试中经常被问到 是Java中比较常见的一种数据结构 所以这一篇就通过源码来深入理解下HashMap 1 HashMap的底层是如何实现的 基于JDK8 1 1 HashMap的类结构和成员 HashMap继承