面试复盘复习
List、Set、Map区别
list适合顺序存储,存储的元素是按照我们存入的顺序的,list中元素可以重复。
set适合去重,存储的元素是无序的,不可以重复的。
map适合键值对存储,通过键索引值,key是不可以重复的,值是可以重复的。
如何选用集合
根据我们所需要的不同的场景选取不同的数据结构即可。
如果我们需要快速通过键定位值,那么可以选用map,如果是并发场景,考虑concurrenthashmap,需要排序时选择treemap。
如果我们只是想按照插入顺序访问元素,只想找一个存储元素的容器,并且想要通过索引来快速查找,可以选用list,涉及频繁插入删除使用linkedlist。如果想去重元素可以使用set,需要排序选用treeset。
各个集合对应的线程安全类
- concurrenthashmap
- copyonwriteArrayList
- concurrentlinkedlist:高并发阻塞队列
- blockingqueue:接口,阻塞队列
- concurrentskiplistmap:跳表
list之间的区别
arraylist和vector的区别
arraylist:是list的主要实现类,底层采用object数组,适合频繁的查找工作,线程不安全
vector:list的古老实现类,底层和arraylist相同,线程安全,不过不推荐使用
arraylist和linkedlist区别
- 是否保证线程安全:都不保证线程安全
- 底层数据结构:
- arraylist是object数组
- linkedlist是双向链表
- 插入和删除是否受到元素位置的影响:
- arraylist对插入删除在尾部的元素影响不大,直接追加,或者删除即可。但是如果在中间进行追加和删除就需要移动大量元素来保证数组的连续性
- linkedlist插入也是o1的时间复杂度。但是插入和删除指定位置元素的话,需要先移动到指定位置,再去删除元素,也是on的时间复杂度。
- 是否支持随机访问:
- arraylist支持通过下标索引进行随机访问
- linkedlist不支持通过下标索引随机访问
- 内存空间占用:
- arraylist主要浪费空间在于每次扩容需要生成新数组,而且结尾会预留一定空间。
- linkedlist浪费空间主要在于每个结点都需要存储其前后结点间的关系
maven作用
- 统一维护包
- 项目结构相同
- Java代码都放到src/main/java
- 测试代码都放到src/test/java
- 使用pom描述用到的依赖
- 自动解决依赖,如果我们需要的一个包依赖另一个包,maven可以自动帮我们引入
maven包冲突
如果A->D1.5,B->C->D1.6,那么可能在实际使用的时候由于版本不同,有些新增方法没有,就会报错。
解决原则
- 最短路径优先:最快可以找到冲突的依赖的优先
- 最先声明优先:先声明的优先
解决方法
- 排除依赖:使用exclusion标签排除依赖
- 版本锁定:定义一个父pom,把公共依赖放入父pom中,升级时方便升级。也不会造成冲突。
git作用
可以用来做版本控制以及协同开发
git命令
本次
- git init:初始化git仓库
- 从服务器初始化一个git仓库:git clone [url] name
- 查看当前文件状态:git status
- 把更改添加到暂存区:git add
- 提交更新:git commit -m “本次更新注释”
- 从暂存区移除文件:git remove
- 对文件重命名:git mv
- 查看提交历史:git log
远程
- 关联远程仓库:git remote add origin […]
- 提交到远程:git push original master
- 远程仓库文件重命名:git remote rename
- 删除远程仓库:git remote rm
动态规划是什么?介绍一下
- 使求解决策过程最优化
- 通常用于求解具有某种最优性质的问题。
- 最优子结构性质:将待求解问题转换为若干子问题,先求解子问题,再从子问题的解,得到原问题的解。
- 重复子问题:有大量子问题被重复计算。
- 无后效性某状态以后的过程不会影响以前的状态,只与当前状态有关
- 可以使用表记录之前子问题的结果。以空间换时间
- 求解最值,求解可行不可行,求解方案总数
贪心算法是什么?
每次从局部取最优解,然后再从下一个的局部取最优解,最后能获得整体的较优解。
- 最优子结构性质
- 贪心选择性质:问题的最优解可以通过局部的最优解选择达到
抽象类和接口的区别是什么?
-
逻辑上:
- 抽象类是模板设计模式的体现,主要是复用父类代码,父类把几乎相同的代码实现后提供给子类复用,不同的方法标记为抽象方法,让子类自己实现。比如:AbstarctList就是提供了很多基本的集合方法的公共实现,保证了子类即使不实现本方法也可以调用这个功能。子类也可以根据自己的需求重写某些方法。对于abstract的方法,需要子类根据自己不同的实现去实现该方法,比如get,父类只是限定了子类需要提供这个方法,但是对于如何实现提供了宽泛的要求。
- 接口更像是对于行为的描述,或者是对于功能的描述。比如:iterator就是表示实现了这个接口就具有可以迭代的功能。
-
具体上:
方法:
- 抽象类可以有抽象方法,也可以没有,抽象方法必须实现,抽象方法可以是public、protected、default但不能是private,因为抽象方法就是需要子类去实现的,如果是private则与初衷不同。
- 接口的方法默认是public的。实现接口,就必须实现接口的所有方法。jdk1.8之后,接口可以有静态方法,1.9之后,接口可以有默认方法。(新的扩充都需要接口提供默认实现)
-
变量:
接口的变量默认是public static final的
抽象类的变量没有这个限制
-
语言特性:
Java支持单继承和接口的多实现。所以最多可以继承一个抽象类,但是可以实现多个接口。
ArrayList扩容机制
懒加载
- 第一次添加元素,默认容量为10,那么会初始化一个长度为10大小的数组。
- 其他时间扩容的话,是当前数组的长度不足以存储下一个元素,那么会扩容。具体是扩容为原来的1.5倍,然后创建新数组,把就数组的内容复制过去,采用的是Arrays.copyOf(),Arrays.copyOf()底层采用的是System.arraycopy()。
扩容时机:
- 普通的add
- add到指定位置
HashMap底层数据结构
数组+链表+红黑树
数组是保证元素可以通过键值扰动之后散列到数组中的具体位置,之后可以快速通过键值扰动后的值得到该键值对应的值存储的位置,保证了O1的时间复杂度。
链表是解决哈希冲突的。如果有元素哈希之后在数组中的位置不为null,表示此处已经存在值了。那么hashmap的解决方式就是在其数组该索引位置上链接上链表,以后查询的时候,如果发现是这个位置的,可以遍历链表找到相同的值再返回。
(键可以为null,null键值hashmap提供的哈希扰动结果是0。值也可以为null,null值直接存储,因为hashmap存储的都是Node节点,所以null存储的node节点的值就是null。)
红黑树是为了解决大数据量下的查找缓慢问题,因为传统的链表查找的时间复杂度为On,红黑树是弱平衡二叉树,可以保证在大数据量的前提下仍然有Olgn的插入,删除,查询复杂度。
HashMap变成红黑树的条件
-
必须当前位置元素数量大于等于8。
-
看数组长度是否大于64,如果小于,证明数据量不大,那么扩容显然是更好的选择。
否则就会扩容。
HashMap从红黑树退化为链表的条件
- 红黑树的元素个数小于等于6。
HashMap为什么要选择红黑树而不是二叉树?
- 传统的二叉树无序,寻找的话是On的时间复杂度。
- 搜索二叉树是左节点小于根节点,右节点大于根节点的一种有序树,具有Olgn的时间复杂度,但是有可能造成树向一边生长的情况,比如插入123456,树就会向左生长,造成的结果是失去了Olgn的时间复杂度,退化为了链表
- AVL树:平衡二叉查找树。满足左右子树的高度差不大于1。(计算高度差,左节点为-1,右节点为1,从底向上结算。)通过旋转来实现平衡的。
- 由于AVL树较为严格,实际并不需要太过于严格的平衡,只要能基本保证Olgn的时间复杂,又不会在旋转上耗费大量的时间,找到性能的平衡点即可。
- 红黑树是弱平衡二叉树,保证了从根节点出发不会有任何一条路径比其他路径长两倍。通过红黑树的五条性质以及变色和旋转来实现的。
红黑树的五条性质是什么?如何保证平衡的?
五条性质:
- 结点非黑即红
- 叶子结点都是黑色的
- 根节点是黑色
- 不能有两个连续的红结点
- 从根节点出发,到各个叶子结点的路径中黑结点的数目相同
保证平衡:
- 变色
- 旋转
HashMap什么时候会扩容?扩容机制是什么?
会在插入新键值对时候会扩容。
扩容机制是:
懒加载,第一次加入元素的时候,会检查数组是否为空,为空则创建数组,默认容量是16。
其他时间的话,在我们加入的元素数量大于等于 加载因子 * 数组容量 时,会进行扩容。
扩容为原来的两倍。
这里面有两个值得关注的点。
-
为什么需要加载因子 * 数组容量呢?ArrayList也没见到什么加载因子啊?
我们需要理解哈希表的本质,保证元素可以尽量散列在表中的各个位置,减少发生冲突的概率。
那么加载因子就是干这个事的。
加载因子可以保证我们放入元素冲突的可能性更小,因为如果等到元素满之后再去扩容,那么大概率就已经有链表的存在了,就是说有很多冲突了。
那么加载因子可以保证我们在合适的时候进行扩容,数组的容量大了,那么可供散列的位置就多了,那么发生冲突的概率就会变小。
默认的加载因子是0.75,表示如果在默认容量为16的情况下,元素数量如果大于12那么就会扩容。
-
为什么扩容为原来的两倍?
这得从hashmap的计算哈希位置的方法讲起。
因为传统的哈希函数有如下几种:
- 线性变换:对于哈希码进行一个线性的比如ax + b的变换
- 观察规律,找到几个区别大的部分
- 对于哈希码进行平方再取中间几位
- 对于哈希码高位和低位进行相加
- 直接取余哈希表的长度
这里hashmap选用的是e + d的组合。
选用d + e,但是由于取余的效率不如位运算,所以,此处考虑使用位运算代替取余操作。
如果选用位运算的话,那么此时,如果是二的次方,减一之后低位都是1,可以保证散列的均匀。如果不是二的次方,可能会有一些位不能被计算到,比如:10 -》1010,减一之后是1001,无论用原数还是减一之后的数都不能保证散列的均匀。如果需要保证散列均匀,与操作的话,可以保证有50的概率在这个位置。
如果要保证每次都是2的次方,必须保证初始的值是2的次方,并且扩容为原来的两倍。
HashMap是线程安全的吗?线程安全的Map是什么?
HashMap不是线程安全的,没有采用同步机制。
线程安全的Map有HashTable、concurrentHashMap。
HashTable是历史遗留类,全部操作使用sync对于类的class对象进行加锁,同一时刻只能有一个线程操作HashTable。其不允许把null作为键或值。HashTable默认容量是11,每次扩容为原来的2n + 1。
concurrenthashmap是官方推荐使用的线程安全类。(也不能把null作为键和值)
- (1.7之前,可以不谈)此时的concurrenthashmap采用的是分段锁的思想,初始化的时候会初始化几个segment数组,每个segment数组可以看做小型的hashmap,每次操作的时候先定位到需要操作的segment数组,只对该数组进行加锁即可,其他segment位置的元素可以同时操作,提高了并发的效率。
- (1.8之后)采用CAS + sync的思路实现。(由于sync已经被优化,所以可以使用sync,加上CAS,效率也没有下降多少)。sync只会锁出现冲突的链表首节点或者红黑树对象,只要不产生hash冲突,就不会产生并发。(CAS在把指定位置数组为null的元素更新为值的时候回用到,别的我暂时没看)
为什么HashTable没有采用2的幂次方作为初始容量,而且每次也扩容不是原来的两倍,而是2n+1倍?
因为HashTable采用了 取余% 操作符计算元素应该存储的位置,所以不需要2的幂次方作为初始容量。
CAS是什么?
CAS是硬件原语:比较并交换。
在Java中提供了调用底层CAS的方法。
CAS所做的操作是:我们想要更新一个值,比较预期值和原来的值是否相同,相同则交换,不同则什么也不做。(获取原来的值是获取其内存地址对应的值,预期值是本次操作想要更新时,记录的值是多少。)
比如AtomicInteger中,我们使用了cas来保证单次增长的原子性。
Java1.8之后的lambda表达式用过吗?举例子。
Runnalbe,之前可以通过一个类实现Runnable接口或者直接创建的时候使用匿名内部类实现Runnable接口。
现在可以通过() -> {}的方式实现。
又比如:比较器Comparator可以通过lambda接口实现,比如(o1, o2) -> {return o2 - o1};
幂等性是什么?
一次和多次请求某一个调用方法的返回结果应该是相同的。
比如:
- 用户多次点击提交,后台应该只产生一次订单。
- 向支付宝发起支付请求,由于网络或者别的问题重发几遍,支付宝应该只扣一次钱。
外部调用者可能存在多次调用的情况,为了防止外部多次调用对系统数据状态发生多次改变,需要将服务或者方法设计成幂等的
泛型是什么?用过吗?
泛型,即“参数化类型”。就是将类型由原来的具体的类型参数化,然后在使用/调用时传入具体的类型(类型实参)。
1,适用于多种数据类型执行相同的代码(代码复用)
2, 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
可以实现泛型类或者泛型方法或者泛型接口。
泛型的约束和局限性:
- 不能实例化泛型类,不能实例化泛型数组
- 基本类型不能作为泛型类,因为最后会擦除为Object
泛型的有效时期
泛型截止到编译前有效。编译之后所有的类型会被擦除为Object。
带来的问题:
父类的问题:
如果父类有一个泛型参数的方法,子类实现的时候实现为具体的类型,那么在子类中,就会出现两个该方法,一个是从父类继承的Object参数的方法,一个是自己覆写后的方法。
例如:
public class F<T> {
public T v;
public T getV() {
return v;
}
public void setV(T v) {
this.v = v;
}
}
class S extends F<String> {
@Override
public String getV() {
return super.getV();
}
@Override
public void setV(String v) {
super.setV(v);
}
}
那么类型擦除后实际会变成如下的形式
class S extends F<String> {
@Override
public String getV() {
return super.getV();
}
@Override
public void setV(String v) {
super.setV(v);
}
public Object getV() {
// 调用返回值为String的getV方法
}
public void setV(Object v) {
// 调用参数为String的setV方法
}
}
我们可以看到,此时相当于进行了方法的重载而不是重写。
Java底层帮我们进行桥接,对于从父类继承的方法,会去调用我们本身的方法。
MySql索引了解吗?
索引是一种数据结构,可以帮助我们快速地查找到对应的数据。
常见的索引类型有两种:
- B+树索引
- 哈希索引
哈希索引底层采用散列表,由于按照索引散列在整个散列表中,存储元素顺序是无序的,故单条记录查询快,不适合需要排序、分组和范围查找的场景。
B+树索引是一种多路平衡树,根据索引值的大小进行排序,所以适合通过B+树索引进行范围查找和分组以及排序的场景。
为什么要选择B+树?优势在哪里?
因为B+树是多路平衡树,m阶B+树除了根结点的非叶子结点可以最多有m个子树。
每个结点存储的数据比之前多,这样树每一层容纳的结点数量更多,那么树的高度比较低。
每次可以把更多的索引读到内存中,减少IO的次数,在内存中进行索引的检索比较快。
(普通的二叉平衡树会导致频繁的IO,所以比较慢)
说一下聚簇索引和非聚簇索引。
非聚簇索引是叶子结点保存的是数据记录的地址。(对于MyIsam是这样的)
非聚簇索引的叶子结点也可能保存的是主键的值(对于InnoDB是这样的)
聚簇索引的叶子结点存放的是整条记录的内容。
聚簇索引和非聚簇索引查询数据的时候有区别吗?
有区别,聚簇索引在查询数据的时候更快。
非聚簇索引如果不是进行覆盖索引查找的话,需要先获取主键的值,再去主键对应的B+树去搜索对应的值。
如果是覆盖索引查找的话,(即索引包含了所要查找的值,比如我们要通过一个别的索引获取主键id的值,就可以直接在叶子结点获取到对应的值,无需回表查询。还比如如果是联合索引的话,按照最左匹配原则的索引查找其他的索引或者主键也称作覆盖索引。重点在于,如果叶子结点包含了需要获取的值,那么无需回表,这就是覆盖索引)
刚才提到了最左前缀原则,你能说一下吗?
最左前缀法则是我们在进行索引匹配的时候用到的原则,按照建立索引的顺序进行查询,从左向右中间不缺失索引字段,可以不包括右边的某些字段,但是整体的顺序必须是从左到右,而且中间无缺失.或者包含整个联合索引位置任意。
比如(age, region, sex)
那么因为我们建立索引的时候是按照这个顺序指定的,那么MySql就是按照这个顺序建立的索引,所以如下的查询可以使用到该索引。
-- 1.
select * from user where age = 18;
-- 2.
select * from user where age = 18 and region = '北京市';
-- 3.
select * from user where region = '北京市' and age = 18 and sex = '男';
MySQL索引什么时候会失效呢?
-
没有使用最左匹配原则
-
使用模糊查询时,未遵循最左前缀,即%放在了前面。
-
搜索一个索引而在另一个索引上排序。
例如(如果region和age是索引)
select * from user where region = '北京市' order by age;
只能使用region上的索引。
-
使用or在不同索引上。
-
在索引上使用函数,或者需要运算的,not in以及!=。
Mysql事务。
事务的四大特性
原子性、隔离性、一致性、持久性
事务隔离级别。
-
读未提交
一个事务可以看到别的事务未提交的修改。(脏读)
可能出现的问题:(脏读、不可重复读、幻读)
-
读已提交
一个事务可以看到别的事务已经提交的内容。未提交的事务看不到。
存在的问题:不可重复读。即读取单条记录时,第一次和第二次读取该记录的某些值可能不同。
可能出现的问题:(不可重复读、幻读)
-
可重复读
一个事务在其执行期间多次读取同样的记录返回的结果是相同的。
存在的问题:幻读。(按照范围索引获取记录时,可能每次获取的记录数量不同)
但是Mysql通过MVCC解决了这个问题。
-
串行化
强制事务串行执行,可以解决所有问题,但是效率比较低。
MVCC可以简单介绍一下吗?
一种用来 解决读写冲突的无锁并发控制 。为事务分配单向增长的版本。
主要通过三种机制来实现。
-
每条记录之后的2列隐式字段
-
事务id
记录创建或者插入的事务id
-
回滚指针
指向这条记录的上一个版本
-
删除标记字段
标记当前记录是否被删除,等到purge线程来清理
-
标记删除的事务id
-
-
undo log日志
-
插入回滚日志:插入时创建,只在事务回滚时需要,事务提交或者回滚完毕后被删除。
-
更新回滚日志:事务更新或者删除时的日志,不仅在事务回滚时需要,也在快照读时需要,故只有在快照读和事务回滚不涉及该日志时,会被purge线程回收
快照读:读取的是记录数据的可见版本。(第一次select时候记录当前表照片)
undo log的链首就是最新的旧记录,链尾就是最早的旧记录
-
-
read view
事务进行快照读的时候产生的视图。主要用来作可见性的判断
事务进行快照读的时候,就会生成数据库系统当前的快照,并维护系统当前活跃事务的ID。
在读已提交情况下,每次快照读会产生read view。
在可重复读情况下,同一事务中,只有第一次挥生成read view,其余都是使用第一次的read view。
增删改查的处理:
-
insert:插入新纪录,记录当前事务的id
-
delete:赋值记录后面隐藏字段的删除事务id,更新删除标记为1。用X锁锁定当前行。
-
update:用X锁锁定当前行。再执行一次delete,再执行一次insert。
相当于
update user set u_id = 2 where id = 2; ==> 1. delete from user where id = 2; 2. insert into user (u_id) values (2);
X锁只有在当前事务提交后才会释放。
-
select。
存在的问题:可能会丢失更新
事务的四个特性如何实现的呢?
- 持久性:redo log:重做日志:在数据库崩溃后,重启系统之后还可以按照redo log中记录的信息进行数据的持久化。(因为redo log已经持久化了)
- 原子性:undo log:撤销日志:用于事务的回滚。
- 隔离性:MVCC + 锁:
- 一致性:一起实现的。
给你一个查询,怎么判断这个查询是不是慢查询。
可以使用explain查看本次查询用到的索引情况,结合SQL判断慢查询。
常见的type类型: ALL、index、range、 ref、eq_ref、const、system、NULL
Springboot核心注解?
启动类注解:
@SpringBootApplication