1. 什么是软件测试, 谈谈你对软件测试的了解
软件测试就是验证产品特性是否符合用户需求, 软件测试贯穿于软件的整个生命周期.
>>>
那软件测试具体是什么呢 ?
就拿生活中的例子来说, 比如说我们去商场买衣服, 会有以下几个步骤 :
第一步: 我们会走进门店, 看看衣服的外观好不好看, 进行外观测试;
第二步 : 我们会摸一下衣服的材质如何, 看看衣服是纯棉的, 还是涤纶的, 进行材质测试;
第三步 : 我们会将衣服拿到试衣间试穿一下, 看是否合身, 进行试穿测试;
第四步 : 我们会询问服务人员衣服的价格如何, 看看是否符合自己的预期;
如果上述步骤都符合自己的需求, 那么才会有后续的交易完成.
>>>
其实软件测试也是类似的流程, 是需要站在用户的角度, 了解用户的需求, 再针对产品进行一系列的软件测试, 看看产品的功能, 性能, 界面, 兼容性, 易用性是否符合用户需求.
以上就是我对软甲测试的一个了解.
2. 我看你简历上有写了解常见的开发模型和测试模型, 那你跟我讲一下敏捷模型
敏捷模型中最熟悉的就是敏捷宣言, 它的内容包括四点 :
1. 个体与交互重于过程和工具;
2. 可用的软件重于完备的文档;
3. 客户协作重于合同谈判;
4. 响应变化重于遵循计划.
总结来说, 敏捷模型的特点就是 : 轻流程, 轻文档, 重目标, 重产出.
>>>
另外呢, 敏捷模型中最典型的就是 scrum 模型.
scrum 模型主要包括三个重要角色和五个重要会议.
三个重要角色 : 分别是产品经理, 项目经理和研发团队;
而五个重要会议呢 :
首先会有一个需求池, 里面放着一个个的用户需求
1. 然后会议 1 是需求发布会议, 根据需求池中的需求确定本次迭代要实现的需求有哪些.
2. 会议 2 是迭代计划会议, 该会议将需求拆分成一个一个的任务, 明确每个任务对应的负责人, 初步评估工时.
3. 会议 3 是每日会议, 会议中每个研发团队成员需要回答三个问题 : 第一个问题是昨天做了什么, 这个问题可以及时的, 实时的知道研发团队的工作进度; 第二个问题是今天要做什么, 这个问题对应了敏捷宣言里的重目标; 第三个问题是遇到了什么问题, 可以让研发团队针对你这个问题给出一些合理的建议, 保证尽快的解决问题.
4. 会议 4 是演示会议, 演示会议的产物是用户的需求, 然后将这些需求继续放入需求池中, 为下一个周期提供新的需求.
5. 会议 5 是回顾会议, 简单来说就是复盘.
以上就是我对敏捷模型的一些了解.
3. 我看你简历上还写了挺多开发技能的, 那你给我讲讲哈希表的实现流程
在 jdk1.7 的时候, 它是数组 + 链表的数据结构实现方式, 在 jdk1.8 的时候, 它是数组 + 链表或者红黑树的数据结构实现方式.
哈希表的实现流程主要就是两个方法 - put 和 get, put 方法呢, 主要就是用于存储对象, 它在存储对象的时候呢, 会先调用参数 key 的 hashcode() 方法, 得到一个哈希值, 然后再使用这个哈希值去模上数组的长度得到一个下标, 而这个下标就表示当前键值对是需要存储在数组中哪一个哈希桶里, 然后再遍历当前哈希桶下的链表, 查找链表 :
1. 是否为空, 如果为空, 就直接插入;
2. 如果已经包含该 key, 那么就进行 value 覆盖;
3. 如果当前是红黑树, 就直接插入该键值对;
4.如果当前数组长度 >= 64, 链表长度 >= 8 时, 就 先将链表转成红黑树, 然后再进行插入.
5. 其他情况就是不包含该 key, 然后也不需要转成红黑树, 就进行链表的尾插, 在 jdk1.7 的时候, 是进行链表的头插, 但是存在链表成环问题, 所以 jdk1.8 做出了优化.
>>>
每当我们向哈希表中插入一个键值对后, 都需要检查当前负载因子是否超过默认负载因子, 当前负载因子是用数组中元素的个数除以数组的长度得到的, 如果超过了, 就需要进行重新哈希, 重新哈希需要遍历数组中每一个哈希桶下链表中的每一个结点, 然后重新哈希到新的哈希表中.
>>>
然后 get 方法呢, 主要就是传入一个 key, 获取对应的 value, 首先同样也是先调用 key 的hashcode() 方法, 得到一个哈希值, 然后再模上数组的长度得到一个下标, 找到对应的哈希桶, 然后遍历当前哈希桶下的链表, 找到对应的 key, 返回 value 即可.
>>>
当然哈希表源码中, 不是通过哈希值 % 数组长度去找到对应的哈希桶, 而是使用了
(数组长度 - 1) & hash, 去找到对应的哈希桶, 因为 JDK 规定哈希表的数组长度必须是 2 的某个次幂, 而且当数组的长度是 2 的某个次幂时, 这两种方式找到的哈希桶是相同的, 而且位运更高效.
例如 : 哈希值(hash)为 22, 数组长度(n)为 8 时, 通过 hash % n 找到的是下标为 6 的哈希桶, 通过 hash & (n - 1) 找到的也是下标为 6 的哈希桶.
>>>
最后就是 hashmap 的容量和扩容机制 :
当我们写出 HashMap<key, value> map = new HashMap<>() 这样一行代码时, 它此时的容量为 0, 当我们第一次 put 元素的时候, 它的大小就扩容为了 16. 如果我们手动指定 hashmap 的大小为 19, 那么它的真实容量其实是 32, 因为 JDK 规定数组的长度必须是 2 的某个次幂, 然后 2^4 是 16, 不够19, 就需要向上取整, 也就是 2^5 = 32.
扩展问题(根据实际情况回答) :
3.1 两个 key 调用 hashcode() 得到的结果相同, 调用 equals() 得到的结果一定相同吗 ?
答案 : 不一定相同.
因为 hashcode 找到的是当前键值对要存放的哈希桶是哪一个, 而 equals 比较的是同一个哈希桶下链表中的结点是否相同.
3.2 两个 key 调用 equals() 得到的结果相同, 调用 hashcode() 得到的结果一定相同吗 ?
答案 : 一定相同.
因为 equals() 找到的是对应哈希桶下的链表中的结点, 如果 equals() 都相同了, 那么肯定在同一个哈希桶下.
4. 谈一谈什么是线程安全问题, 如何解决
线程安全问题的万恶之源, 就是因为操作系统的随机调度, 抢占式执行这个过程. 在随机调度的情况下, 多线程程序执行的时候, 有无数种排列方式, 在这些排列方式中, 有一些排列方式的额逻辑是正确的, 而有一些排列方式, 可能会引起程序 bug, 对于多线程并发时, 会使程序出现 bug 的代码, 称作线程不安全的代码, 这个就是线程安全问题. 为什么排列方式的不同可能会导致线程安全问题呢 :
最好举例证明: 共享屏幕举例说明, 例如多个线程针对同一个变量进行 + 1 操作时, 此过程类似于计算 1 + 1 = 2.
线程 1 执行 LOAD,ADD,SAVE :
线程 2 的执行 LOAD,ADD,SAVE :
两个线程针对同一变量 + 1, count 的结果为 2 , 此种排列方式逻辑正确.
错误的排列方式下, 线程 1 线程 2 的执行流程 :
线程 1 执行 LOAD :
线程 2 执行 LOAD,ADD,SAVE :
线程 1 执行 ADD,SAVE :
上述排列方式就导致了 1 + 1 = 1, 这种排列方式就是一种 bug, 所以说多线程程序排列方式的不同, 就可能会导致线程安全问题.
>>>
线程安全问题主要有五大因素 :
1. 第一点也是最重要的一点, 就是刚才的操作系统的随机调度, 抢占式执行导致的.
2. 第二个原因是因为多个线程同时修改同一个变量, 也就是刚才举的例子.
3. 第三个原因是因为有些修改操作不是原子性的. 比如赋值 "=" 操作符对应的就是一条机器指令, 而自增或者自减对应的就是三条机器指令, 也就是上述例子中的 (LOAD,ADD,SAVE), 多线程环境下, 如果不保证原子性, 当一个线程正在对一个变量操作时, 其他线程中途插入进来打断当前线程的操作时, 就可能会导致结果结果出错.
4. 第四个原因是内存可见性, 引起的线程安全问题. 内存可见性问题主要是一个线程修改, 一个线程读的场景. 当线程 1 反复读取内存中的数据, 然后判断 CPU 寄存器中的数据是否符合预期时, 线程 2 中途突然针对 CPU 寄存器中的值进行了修改, 然后写回内存, 而编译器看到的就是线程 1 在不断的读内存, 然后判断, 而且每次读内存读的都是同一个变量, 并且该变量似乎也没有发现变化, 于是就做出了 "编译器优化", 将不断的读内存, 判断操作, 优化成了读一次内存, 然后一直判断, 就如下图场景 (面试中可以画图举例)
此场景对应的代码例子如下 :
public class Main {
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while(counter.flag == 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 结束");
});
t1.start();
Thread t2 = new Thread(() -> {
//让用户输入一个数字,赋值给 flag
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数: ");
counter.flag = scanner.nextInt();
});
t2.start();
}
}
下次写。。。
5. 既然你选择走测试, 为什么还要学这么多的开发知识
首先我个人是对开发比较感兴趣, 所以在校期间学了很多开发方面的专业知识. (目的是为了凸显自己爱学习)
其次呢, 测试它不仅仅包含白盒测试, 黑盒测试, 它也是需要具备扎实的开发能力来提高个人的项目测试质量. 作为测试人员, 如果我们具备扎实的开发能力, 当我们给开发人员提 bug 的时候, 开发人员对我们所提出来的 bug 的可信度也是会比较高的. 并且 我们测试人员也是需要开发效能工具来提高测试效率的. (目的是为了凸显自己对软件测试工作的了解)
下次再补充......