面试常问笔记整理
MySQL
一、MySQL 操作的四大特性通常指的是ACID。
1. 原子性(Atomicity)
原子性要求事务中的所有操作 “要么全部执行,要么全部不执行”,不存在部分完成的中间状态。
- 例如,转账操作中 “扣款” 和 “收款” 两个步骤必须同时成功或同时失败。
- 若过程中发生错误(如网络中断),数据库会回滚(Rollback)到事务开始前的状态,避免出现 “钱扣了但没到账” 的情况。
2. 一致性(Consistency)
一致性确保事务执行前后,数据库的完整性约束(如主键唯一、字段非空、外键关联)不被破坏,数据始终处于合法状态。
- 例如,一个字段定义为 “年龄” 且只能是正整数,事务中不能将其更新为负数或字符串。
- 即使事务执行失败并回滚,数据也会保持符合规则的初始一致状态。
3. 隔离性(Isolation)
隔离性控制多个并发事务之间的相互影响,防止一个事务的操作结果被其他未完成的事务干扰。
- MySQL 通过 “隔离级别” 实现隔离性,从低到高包括:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read,MySQL 默认)、串行化(Serializable)。
- 例如,在 “读已提交” 级别下,事务 A 看不到事务 B 未提交的修改,避免出现 “脏读”(读取到未最终确认的数据)。
4. 持久性(Durability)
持久性保证一旦事务执行成功并提交(Commit),其对数据的修改会被永久保存,即使后续发生数据库崩溃、服务器断电等故障也不会丢失。
- MySQL 通过 “重做日志(Redo Log)” 实现持久性,事务提交时会先将修改记录到重做日志,再写入磁盘数据文件。
- 即使数据文件写入未完成时服务器宕机,重启后数据库也能通过重做日志恢复已提交的事务。
二. MySQL 四大事务隔离级别
- 读未提交(Read Uncommitted):最低隔离级别,允许读取其他事务未提交的修改,可能导致 “脏读”“不可重复读”“幻读”。
- 读已提交(Read Committed):只能读取其他事务已提交的修改,避免 “脏读”,但仍可能出现 “不可重复读” 和 “幻读”,是许多数据库的默认级别。
- 可重复读(Repeatable Read):MySQL 默认级别,确保同一事务内多次读取同一数据的结果一致,避免 “脏读” 和 “不可重复读”,通过 Next-Key Lock 机制减少 “幻读”。
- 串行化(Serializable):最高隔离级别,强制事务串行执行(不并发),完全避免所有并发问题,但性能极低,仅用于数据一致性要求极高的特殊场景。
三、MySQL 四大常用索引类型
- 主键索引(Primary Key):唯一标识表中每条记录,一个表只能有一个主键索引,且索引列值不允许为 NULL。
- 唯一索引(Unique Index):确保索引列的值在表中唯一,但允许为 NULL(多个 NULL 值不冲突),常用于手机号、邮箱等唯一标识字段。
- 普通索引(Normal Index):最基础的索引,仅用于加速查询,不限制索引列的值是否唯一或为 NULL,适用于频繁作为查询条件的字段。
- 联合索引(Composite Index):基于多个列创建的索引,遵循 “最左前缀原则”,适用于多列组合查询的场景(如 “用户名 + 手机号” 联合查询)。
HashMap
一、HashMap的put方法
- HashMap存储的是键值对;
- 首先计算Key的哈希值,减少哈希冲突;
- 再通过哈希值去计算他的索引位置;
- 得到索引位置会出现两种情况,索引位置为空,或者不为空,不为空就是存在单节点,链表或者红黑树;
- 我们先说为空的情况,为空的话直接创建节点插入即可;
- 如果不为空,是单节点的话我们判断一下该节点的Key值和我们的Key是否相同,如果相同直接覆盖就可以了;
- 不相同就是链表或者红黑树;
- 是链表我们就遍历链表,看看有没有重复Key,有就覆盖,没有就从链表尾部插入;
- 这个时候需要注意链表长度是否超过8,超过的话需要将链表转化为红黑树;
- 如果是红黑树的花我们也是如此,有相同key就覆盖,没有就用树的插入方法插入新节点。
- 最后有一个扩容判断吧,当插入后如果元素数量超过负载因子*数组长度,就会对当前集合进行扩容。
- 长度翻倍,哈希值和索引位置重新计算。
二、核心方法
1、核心操作方法(增删改查)
1. V put(K key, V value)
- 功能:插入键值对,若 key 已存在则覆盖旧 value,返回旧值(无旧值则返回 null)。
- 底层逻辑:
- 计算 key 的哈希值,确定数组索引。
- 处理哈希冲突(链表 / 红黑树插入),必要时触发扩容(
resize())。
- 注意:key 可为 null(固定存放在索引 0 位置)。
2. V get(Object key)
- 功能:根据 key 获取对应的 value,若 key 不存在则返回 null。
- 底层逻辑:
- 计算 key 的哈希值和索引,遍历对应位置的链表 / 红黑树,匹配 key(
hash相等且equals为 true)。
- 计算 key 的哈希值和索引,遍历对应位置的链表 / 红黑树,匹配 key(
3. V remove(Object key)
- 功能:删除指定 key 的键值对,返回被删除的 value(无则返回 null)。
- 底层逻辑:
- 定位 key 所在节点,删除后若为红黑树且节点数少于 6,会退化为链表;若为链表则直接调整指针。
4. boolean containsKey(Object key)
- 功能:判断是否包含指定 key。
- 逻辑:类似
get,找到匹配的 key 则返回 true。
5. boolean containsValue(Object value)
- 功能:判断是否包含指定 value。
- 逻辑:遍历整个哈希表(数组 + 链表 / 红黑树),逐个比较 value(效率低,时间复杂度 O (n))。
2、批量操作方法
1. void putAll(Map<? extends K, ? extends V> m)
- 功能:将另一个 Map 中的所有键值对插入当前 HashMap。
- 逻辑:遍历参数 Map,逐个调用
put方法。
2. void clear()
- 功能:清空所有键值对。
- 逻辑:遍历数组,将所有节点置为 null,
size设为 0(不缩容,数组长度不变)。
3、集合视图方法(返回视图,而非拷贝)
1. Set<K> keySet()
- 功能:返回所有 key 的集合视图(
Set)。 - 特性:视图的修改会同步到原 HashMap(如
keySet().remove(key)会删除原 Map 中的键值对)。
2. Collection<V> values()
- 功能:返回所有 value 的集合视图(
Collection)。 - 特性:同上,修改会同步到原 Map。
3. Set<Map.Entry<K, V>> entrySet()
- 功能:返回所有键值对(
Entry)的集合视图(Set)。 - 用途:高效遍历(
for (Entry<K,V> entry : map.entrySet())),可通过entry.setValue(value)修改 value。
4、容量与大小相关方法
1. int size()
- 功能:返回当前键值对数量。
2. boolean isEmpty()
- 功能:判断是否为空(
size() == 0)。
3. int capacity()(JDK 1.8+ 新增,默认包访问权限)
- 功能:返回当前数组容量(
table.length),外部无法直接调用,需通过反射获取。
4. float loadFactor()
- 功能:返回负载因子(默认 0.75),用于计算扩容阈值(
threshold = capacity * loadFactor)。
5、其他重要方法
1. void resize()(核心内部方法)
- 功能:数组扩容(默认翻倍),重新计算所有节点的索引并迁移,是
put方法中保证性能的关键。 - 触发时机:
size超过阈值时。
2. final int hash(Object key)(内部方法)
- 功能:计算 key 的哈希值,通过高 16 位与低 16 位异或(
(h = key.hashCode()) ^ (h >>> 16))减少哈希冲突。
3. Object clone()
- 功能:返回浅拷贝(键值对本身是新对象,但 key 和 value 仍为原引用)。
关键特性总结
- 线程不安全:多线程并发修改(如
put和resize同时进行)可能导致数据异常(如链表环化)。 - 无序性:迭代顺序与插入顺序无关(JDK 1.8 中红黑树可能影响顺序),需有序性可使用
LinkedHashMap。 - 性能:理想情况下
put/get时间复杂度为 O (1),哈希冲突严重时退化至 O (n)(链表)或 O (log n)(红黑树)。
三、哈希冲突
哈希冲突(Hash Collision)是哈希表(如 HashMap)中常见的现象,指不同的键(key)经过哈希函数计算后得到相同的哈希值,导致它们需要存储在哈希表的同一位置。理解哈希冲突的产生原因、解决方法及影响,对设计和使用哈希表至关重要。
1、为什么会产生哈希冲突?
哈希表的核心是通过哈希函数将键映射到数组的索引位置(index = hash(key) % 数组长度)。由于:
- 键的范围远大于数组长度:例如,字符串、对象等键的可能取值是无限的,而哈希表数组长度是有限的(即使动态扩容,也不可能覆盖所有可能的键)。
- 哈希函数的局限性:任何哈希函数都无法完全避免不同的键映射到同一索引(数学上 “鸽巢原理”:n 个鸽子放进 m 个鸽巢,n > m 时必然有鸽巢包含多个鸽子)。
因此,哈希冲突是不可避免的,只能通过优化减少冲突概率或高效处理冲突。
2、解决哈希冲突的常见方法
哈希表通过特定机制处理冲突,确保即使哈希值相同,键值对也能正确存储和查找。主流方法有两种:
1. 链地址法(Chaining)
- 原理:哈希表数组的每个位置(桶,Bucket)不仅存储单个键值对,还存储一个链表(或红黑树),所有哈希冲突的键值对都按顺序存入该链表 / 树中。
- 流程:
- 当新键值对的哈希值对应索引已存在数据时,直接将其添加到该位置的链表尾部(或红黑树中)。
- 查询时,先通过哈希值定位到索引,再遍历链表 / 树匹配 key。
- 优点:
- 处理简单,不易产生聚集效应(一个冲突不会影响其他位置)。
- 动态扩容时迁移数据方便。
- 应用:Java 的
HashMap(JDK 1.8 中,当链表长度 > 8 时转为红黑树,提升查询效率)、Python 的dict等。
2. 开放地址法(Open Addressing)
- 原理:当哈希冲突发生时,通过某种规则(探查序列)在数组中寻找下一个空闲位置存储键值对,而不是使用链表。
- 常见探查方式:
- 线性探查:冲突时依次检查下一个位置(
index+1, index+2, ...),直到找到空闲位置。 - 二次探查:冲突时按平方数偏移(
index+1², index-1², index+2², ...),减少聚集效应。 - 双重哈希:使用第二个哈希函数计算偏移量,降低探查序列的规律性。
- 线性探查:冲突时依次检查下一个位置(
- 优点:数据存储在数组中,访问速度快(无需遍历链表)。
- 缺点:
- 容易产生 “聚集效应”(连续多个位置被占用,导致新插入的键需要探查更远的位置)。
- 删除数据复杂(不能直接置空,否则会断裂探查序列,需标记为 “已删除”)。
- 应用:Java 的
ThreadLocal(底层ThreadLocalMap使用开放地址法)、某些数据库索引。
3、如何减少哈希冲突的概率?
虽然冲突不可避免,但可通过以下方式降低冲突频率:
- 优化哈希函数:
- 让哈希值分布更均匀,减少 “热点索引”(某一位置聚集大量键值对)。例如,JDK 1.8 中
HashMap的哈希函数通过(h = key.hashCode()) ^ (h >>> 16)将高 16 位与低 16 位异或,增强随机性。
- 让哈希值分布更均匀,减少 “热点索引”(某一位置聚集大量键值对)。例如,JDK 1.8 中
- 合理设置哈希表容量:
- 容量通常设为 2 的幂(如
HashMap初始容量 16),配合index = (n-1) & hash计算索引,确保哈希值的低位能均匀分布到数组中。
- 容量通常设为 2 的幂(如
- 动态扩容:
- 当哈希表中键值对数量(
size)超过阈值(容量 × 负载因子,默认 0.75)时,触发扩容(容量翻倍),降低负载,减少冲突。
- 当哈希表中键值对数量(
4、哈希冲突对性能的影响
- 链地址法:若冲突较少,链表 / 红黑树的查询时间接近 O (1);若冲突严重(如哈希函数设计差),链表会过长,查询时间退化至 O (n)(红黑树可优化至 O (log n))。
- 开放地址法:冲突越多,探查次数越多,插入和查询效率越低,极端情况下可能接近 O (n)。
因此,良好的哈希函数设计和扩容策略是保证哈希表高性能的关键。
总结
哈希冲突是哈希表的固有问题,核心解决思路是链地址法(主流)和开放地址法。实际开发中,我们无需手动处理冲突(如 HashMap 已封装实现),但理解其原理有助于更好地使用哈希表(如避免使用哈希值分布差的 key,合理设置初始容量等)。
MySQL优化
RBAC在项目里的应用
SPI 类加载
双亲委托机制以及为什么要用
JVM
- 栈和堆的异常了解哪些
- 内存溢出是怎么导致的
- CPU飙升一般是什么导致的
- fullGC
线程池的几个参数
SQL执行计划
事务隔离级别,你平时常用哪个级别
连接大量字符串一般是怎么做的
单例模式
泛型了解多少
基础类型可以用泛型吗为什么
泛型擦除
深拷贝浅拷贝
1.MySQL优化
- 索引优化:
- 为查询频繁的字段(where、join、order by)建索引,避免索引失效(如模糊查询 % 开头、函数操作索引列、类型隐式转换)。
- 控制索引数量(过多会拖慢写入),优先用联合索引(遵循最左前缀原则)。
- SQL 优化:
- 避免 select *,只查需要的字段;避免子查询,改用 join;分页优化(limit 大偏移量时用索引定位)。
- 禁止在 where 子句中使用!=、not in、or,可能导致全表扫描。
- 表结构优化:
- 选择合适字段类型(如用 int 代替 varchar 存手机号),避免大字段(text/blob),必要时分表(水平 / 垂直分表)。
- 配置优化:
- 调整连接池参数(max_connections)、缓存大小(innodb_buffer_pool_size)、日志刷盘策略(innodb_flush_log_at_trx_commit)。
2.RBAC在项目里的应用
RBAC(基于角色的访问控制)是权限管理的主流模型,核心是 “用户 - 角色 - 权限” 三层关系:
- 用户:系统操作者(如 admin、user1)。
- 角色:一组权限的集合(如管理员、普通用户),用户可关联多个角色。
- 权限:具体操作(如查询、删除)或资源(如菜单、接口)。
- 应用场景:
- 数据库设计:通常需 5 张表(用户表、角色表、权限表、用户 - 角色关联表、角色 - 权限关联表)。
- 实现逻辑:用户登录后,查询其关联的角色→获取角色对应的权限→校验接口 / 按钮权限(如 Spring Security 结合 RBAC)。
3. SPI 类加载
- 定义:Java 提供的一种服务发现机制,允许第三方为接口提供实现,无需硬编码依赖。
- 流程:
- 定义接口(如
com.example.Plugin)。 - 第三方实现接口(如
com.example.impl.PluginImpl)。 - 在
META-INF/services/下创建以接口全类名为名的文件,内容为实现类全类名。 - 程序通过
ServiceLoader.load(Plugin.class)加载所有实现类。
- 定义接口(如
- 应用:JDBC 驱动加载(
java.sql.Driver)、日志框架适配、Spring 的 SPI 扩展(如SpringFactoriesLoader)。
4.双亲委托机制以及为什么要用
- 类加载过程:加载(读 class 文件到内存)→验证(校验字节码合法性)→准备(静态变量赋默认值)→解析(符号引用转直接引用)→初始化(执行静态代码块、赋初始值)。
- 双亲委托机制:
- 类加载器加载类时,先委托给父加载器,父加载器无法加载时才自己加载。
- 加载器层级:Bootstrap(核心类,如 java.lang.String)→Extension→Application→自定义加载器。
- 为什么用:
- 避免类重复加载(如
java.lang.String不会被自定义加载器篡改)。 - 保证核心类的安全性(防止恶意替换核心类)。
- 避免类重复加载(如
5.JVM
- 栈和堆的异常:
- 栈异常:
StackOverflowError:栈深度超过虚拟机限制(如递归调用无终止条件)。OutOfMemoryError: StackOverflowError:栈动态扩展时内存不足(极少发生,通常与线程数过多有关)。
- 堆异常:
OutOfMemoryError: Java heap space:堆内存不足(对象过多未回收,如内存泄漏)。
- 栈异常:
- 内存溢出(OOM)原因:
- 堆:创建大量对象且长期被引用(如静态集合缓存未清理)。
- 方法区(元空间):加载类过多(如动态生成大量类,CGLib 代理滥用)。
- 直接内存:NIO 的 DirectByteBuffer 分配过大,且未释放。
- CPU 飙升原因:
- 死循环(如代码逻辑错误导致无限循环)。
- 频繁 GC(对象创建 / 回收过于频繁,导致 GC 线程占用大量 CPU)。
- 锁竞争激烈(大量线程争夺同一把锁,导致上下文切换频繁)。
- Full GC:
- 触发:老年代空间不足、元空间不足、调用
System.gc()(仅建议,不保证执行)。 - 影响:STW(Stop The World)时间长,频繁 Full GC 会导致应用卡顿,需优化(如减少大对象创建、调整老年代大小)。
- 触发:老年代空间不足、元空间不足、调用
6.线程池的几个参数
ThreadPoolExecutor的核心参数:
corePoolSize:核心线程数(长期存活,即使空闲也不销毁)。maximumPoolSize:最大线程数(核心线程 + 临时线程的上限)。keepAliveTime:临时线程空闲超时时间(超过则销毁)。workQueue:任务等待队列(核心线程满时,新任务入队)。threadFactory:线程创建工厂(自定义线程名、优先级等)。handler:拒绝策略(队列满且线程达最大值时,处理新任务的方式,如 AbortPolicy 直接抛异常)。
7.SQL执行计划
通过EXPLAIN + SQL语句查看,关键字段:
type:访问类型(性能从优到差:system > const > eq_ref > ref > range > index > ALL),目标是达到range或更好。key:实际使用的索引(为 NULL 则未用索引)。rows:预估扫描行数(越小越好)。extra:额外信息(如Using index表示覆盖索引,Using filesort表示文件排序,需优化)。
8.事务隔离级别(常用)
MySQL 默认是可重复读(Repeatable Read),日常开发中最常用:
- 避免脏读(读未提交数据)和不可重复读(同一事务内多次读结果不一致)。
- 多数业务(如订单、支付)只需保证这两点,串行化(Serializable)性能太差,读已提交(Read Committed)可能有不可重复读问题。
9.连接大量字符串的做法
保证一个类只有一个实例,常用实现:
- 饿汉式:类加载时初始化(线程安全,可能浪费内存)。
- 懒汉式(双重校验锁):延迟初始化,加锁保证线程安全(
volatile防止指令重排)。
public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}11. 泛型
- 定义:参数化类型,编译时检查类型安全,避免强制转换(如
List<String>只能存字符串)。 - 基础类型不能用泛型:泛型擦除后会转为 Object,而基础类型(int、char 等)不是 Object 子类,需用包装类(Integer、Character)。
- 泛型擦除:编译后泛型信息被擦除(如
List<String>变为List),目的是兼容 JDK1.5 之前的版本,可能导致运行时类型转换问题(需用Class.cast()处理)。
12. 深拷贝 & 浅拷贝
- 浅拷贝:复制对象时,仅复制基本类型字段,引用类型字段仍指向原对象(如
Object.clone()默认浅拷贝)。 - 深拷贝:复制对象及所有引用类型字段(递归拷贝),两者完全独立。
- 实现:
- 浅拷贝:实现
Cloneable接口,重写clone()。 - 深拷贝:序列化(对象及引用对象都实现
Serializable)、递归调用clone()。
- 浅拷贝:实现
