第六节 面试问题-初级版

亮子 2025-09-05 10:22:24 3823 0 1 0

1、Java的基本数据类型有哪些?

8种,分四类:

  • 整数型:byte、short、int、long,范围从小到大
  • 浮点型:float、double,精度从低到高
  • 字符型:char
  • 布尔型:boolean

日常开发里 int、long、boolean 用得最多,float 一般不建议直接用于金额计算,精度会丢。

2、Java的引用数据类型有哪些?

主要是类(class)、接口(interface)、数组(array)、枚举(enum)、注解(annotation)。它们存的都是对象的引用地址,和基本类型最大的区别就是它们指向堆里的对象,基本类型直接存值。

3、Java的包装类有什么作用?

三个核心作用:

第一,让基本类型具备对象特性,能放进泛型和集合,比如 List,不能写 List。

第二,提供了类型转换和常量这些工具方法,比如 Integer.parseInt()、Integer.MAX_VALUE。

第三,在需要对象的场景——像反射、序列化——基本类型没法直接用,必须用包装类。

4、什么叫装箱拆箱?

装箱就是把基本类型转成包装类对象,比如 int 赋值给 Integer。拆箱反过来,Integer 转成 int。JDK5 以后有自动装箱拆箱,编译器帮我们做了,开发时基本无感,但要注意 NPE——比如把一个 null 的 Integer 自动拆箱会直接空指针。

5、面向对象编程有哪些特点?

四个核心特性:

封装:把数据和操作封装在类里,对外只暴露必要的接口,保护数据安全。

继承:子类复用父类的属性和方法,减少重复代码,同时可以扩展自己的功能。

多态:同一个行为不同表现,靠父类引用指向子类对象来实现,是框架里最常用的扩展机制。

抽象:提取核心特征、忽略细节,通过接口或抽象类定义规范,具体实现交给子类。

6、== 和 equals 的区别是什么?

== 是运算符:基本类型比的是值,引用类型比的是内存地址,看是不是同一个对象。

equals 是 Object 的方法:默认实现也是比地址,但 String、包装类这些都重写了 equals,比的是内容是否相等。所以日常比较字符串和对象内容,一律用 equals。

7、说说你对 hashCode 的理解?

hashCode 是对象的哈希值,int 类型。核心作用是在哈希表——比如 HashMap、HashSet——里快速定位桶的位置。

有个必须遵守的约定:两个对象 equals 相等,hashCode 必须相等;反过来不成立,hashCode 相等不代表 equals。所以重写 equals 必须同时重写 hashCode,不然 HashMap 里会出 bug。

8、Java的容器(集合)有哪些?

分两大体系:

Collection 接口:存单个元素。下面又分 List(有序可重复,ArrayList、LinkedList)、Set(无序不重复,HashSet、TreeSet)、Queue(队列,LinkedList、PriorityQueue)。

Map 接口:存键值对。HashMap 最常用,TreeMap 有序,LinkedHashMap 保持插入顺序,Hashtable 是老版本的线程安全 Map,现在一般用 ConcurrentHashMap。

9、请说一下 ArrayList 的扩容机制

无参构造时初始容量默认是 10。添加元素时如果容量不够,触发扩容,新容量 = 旧容量 + 旧容量 >> 1,也就是 1.5 倍。如果 1.5 倍还不够,直接扩到所需容量。底层用的是 Arrays.copyOf(),本质就是创建新数组、复制元素,所以有性能开销——如果能预估数据量,最好在构造时指定容量。

10、请说一下 HashMap 的扩容机制

以 JDK8 为例,核心是数组+链表+红黑树的结构。

默认初始容量 16,负载因子 0.75,当元素数量超过 16×0.75=12 时就触发扩容。扩容后容量翻倍,原来的元素要重新 hash 到新位置——因为容量是 2 的幂,rehash 其实就是看高位多出来的那一位是 0 还是 1。

还有一个特殊规则:链表长度到了 8 但数组容量小于 64 时,优先扩容而不是转红黑树,因为扩容可能直接缩短链表。

11、单例模式有哪些实现方式?

说几种最常用的:

饿汉式:类加载时就创建实例,线程安全但可能浪费内存。

双重检查锁:两次判空 + volatile 修饰实例,兼顾线程安全和延迟加载,推荐。

静态内部类:利用类加载机制实现懒加载和线程安全,代码比双重检查锁更简洁。

枚举:最简单,天然线程安全,还能防反射和反序列化破坏单例,是 Effective Java 推荐的写法。

12、JDK 1.8 有哪些新特性?

主要有这几个:

第一,Lambda 表达式,函数式编程支持,简化匿名内部类。

第二,Stream API,用链式操作处理集合,过滤、映射、聚合一行写完,代码可读性高很多。

第三,接口的 default 和 static 方法,接口可以有默认实现,向后兼容。

第四,新的时间 API——LocalDate、LocalDateTime 这些,线程安全、API 设计合理,彻底替代 Date。

第五,Optional 类,优雅地处理 null,减少空指针。

第六,方法引用 ::,进一步简化 Lambda。

13、字节流和字符流有什么区别?

字节流以 byte 为单位,能处理所有文件类型——图片、视频、文本都行,是通用的。

字符流以 char 为单位,专门处理文本,内置了字符编码处理,比如读中文不会乱码。

如果用字符流读二进制文件,编码转换会破坏数据。所以一句话:字节流通用,字符流只用于文本。

14、JDK、JRE、JVM 之间有什么区别?

包含关系:JDK ⊇ JRE ⊇ JVM。

JVM 是虚拟机,负责执行字节码,是跨平台的核心。

JRE 包含 JVM 加核心类库,能运行 Java 程序但不能编译。

JDK 包含 JRE 加开发工具——javac 编译器、调试器这些。开发装 JDK,服务器上跑服务装 JRE 就够了。

15、String、StringBuffer、StringBuilder 有什么区别?

三个维度来区分:

可变性:String 不可变,每次修改都是创建新对象;StringBuffer 和 StringBuilder 可变,在原对象上改。

线程安全:StringBuffer 的方法都加了 synchronized,线程安全但慢;StringBuilder 没加,线程不安全但快。

所以场景选择很明确:单线程拼字符串用 StringBuilder,多线程用 StringBuffer,少量拼接用 String 就行。

16、线程的创建有哪几种方式?

四种:

继承 Thread 类,重写 run()。

实现 Runnable 接口,重写 run(),比继承灵活,因为 Java 单继承。

实现 Callable 接口,重写 call(),有返回值还能抛异常,配合 Future 使用。

线程池——Executors 或直接 new ThreadPoolExecutor。实际项目里推荐用线程池,方便管理和复用。

17、线程池的参数有哪些?

以 ThreadPoolExecutor 为例,七个参数:

corePoolSize:核心线程数,常驻的。

maximumPoolSize:最大线程数,核心满了 + 队列满了才扩容到这个数。

keepAliveTime 和 unit:非核心线程空闲多长时间后销毁。

workQueue:任务队列,常用 LinkedBlockingQueue(无界)和 SynchronousQueue(直接移交)。

threadFactory:线程工厂,一般用来给线程起名字,方便排查问题。

rejectedExecutionHandler:拒绝策略,四个内置策略——抛异常、交给调用者执行、丢弃最老任务、直接丢弃。

18、线程池都有哪些类型?

通过 Executors 工具类创建的主要有四种:

FixedThreadPool:固定线程数,核心=最大,队列无界,适合负载稳定的场景。

SingleThreadExecutor:只有一个线程,保证任务顺序执行。

CachedThreadPool:线程数动态伸缩,核心为 0,最大是 Integer.MAX_VALUE,60 秒空闲回收,适合大量短任务。

ScheduledThreadPool:支持定时和周期性任务,用于延迟执行或定时调度。

实际开发中很少直接用 Executors,因为前两个队列无界可能 OOM,一般是手动 new ThreadPoolExecutor 来控制。

19、请说说线程池的工作原理?

任务提交后的处理流程分步走:

第一步,判断核心线程满了没,没满就创建核心线程执行。

第二步,核心线程满了,任务进工作队列排队。

第三步,队列也满了,判断有没有达到最大线程数,没到就创建非核心线程。

第四步,最大线程数也到了,执行拒绝策略。

第五步,非核心线程空闲超过 keepAliveTime 就回收,核心线程默认不回收。

20、线程的状态都有哪些?

六种状态,按生命周期:新建(New)→ 就绪/运行(Runnable)→ 阻塞(Blocked)/等待(Waiting)/超时等待(Timed Waiting)→ 终止(Terminated)。

关键区分:Blocked 是等锁,Waiting 是无时限等(wait、join),Timed Waiting 是有时限的(sleep、wait(timeout)),场景不同,桩因也不同。

21、synchronized 与 Lock 有什么区别?

四个区别:

层级:synchronized 是 JVM 层面的关键字,Lock 是 JUC 包里的接口。

释放:synchronized 自动加锁释放锁,代码块结束或异常都自动放;Lock 必须手动 unlock(),一般写在 finally 里。

功能:Lock 支持公平锁、可中断获取、尝试获取超时、多个条件变量,synchronized 都不支持。

性能:JDK6 以后 synchronized 做了大量优化,低竞争场景差距不大,高竞争场景 Lock 还是好一些。

22、线程的 start() 和 run() 有什么区别?

start():启动一个新线程,线程进入就绪状态等 CPU 调度,由 JVM 调用该线程的 run()。一个线程只能 start 一次,重复会抛异常。

run():就是个普通方法,直接调用不会启动新线程,在当前线程里执行。

核心区别:start 启新线程,run 跑在当前线程。

23、wait() 和 sleep() 有什么区别?

最大的区别是锁:

wait() 是 Object 的方法,调用时必须持有该对象的锁,调用后会释放锁,等别人 notify 才醒。

sleep() 是 Thread 静态方法,不会释放锁,时间到了自动醒。

使用场景也不一样:wait 用于线程间通信——比如生产者消费者,sleep 就是让当前线程暂停一会儿。

24、悲观锁和乐观锁有什么区别?

核心是对冲突的假设不同:

悲观锁假设冲突一定会发生,所以操作前先加锁,阻止别人动数据。数据库的行锁、Java 的 synchronized 都是这种思路。适合写多读少的场景。

乐观锁假设冲突很少发生,不锁数据,只在提交时检查版本号是不是变了,变了就重试。CAS 就是乐观锁的实现。适合读多写少的场景——比如查商品详情。

25、什么是锁?

锁是并发控制机制,用来保证同一时刻只有一个线程能访问共享资源,解决多线程环境下的数据不一致、脏读、竞态条件这些问题。

关键特性三个:排他性(同一时间只有一个线程持有)、可见性(锁释放后修改对后续线程可见)、有序性(控制执行顺序、防止指令重排)。

26、什么是死锁?

多个线程互相持有对方需要的锁,谁都不放,全都卡住。

产生需要四个条件:互斥、持有并等待、不可剥夺、循环等待——缺一个都不会死锁。

排查的话,开发中可以 jstack dump 线程,看有没有 found 1 deadlock。解决思路就是破坏其中一个条件,比如统一加锁顺序,或者用 tryLock 加超时。

27、Java 都有哪些锁?

从不同维度来分:

按锁升级路径:偏向锁 → 轻量级锁 → 重量级锁,这是 synchronized 的优化机制,竞争越激烈升级越高。

按特性:可重入锁(synchronized、ReentrantLock)、公平/非公平锁、独占锁/共享锁。

按实现:隐式锁(synchronized,JVM 管)和显式锁(Lock 接口,手动管)。

还有一个常用的:读写锁 ReentrantReadWriteLock,读读共享,读写互斥,适合读多写少的场景。

28、数据库设计三大范式是什么?

第一范式:列不可再分,字段要原子性。比如地址要拆成省市街道。

第二范式:非主键列完全依赖主键,消除部分依赖。主要是针对复合主键的场景。

第三范式:非主键列不依赖其他非主键列,消除传递依赖。比如学号→系名→系主任,要拆成两张表。

实际开发中不会死扣范式,有时候为了查询性能做适当的冗余是合理的。

29、数据库的事务有哪些特性?

ACID 四个:

原子性:事务是不可分割的最小单位,要么全成功要么全回滚。

一致性:事务前后数据状态都满足约束规则。

隔离性:并发事务之间互不干扰。

持久性:事务提交后数据永久保存,断电也不丢。

MySQL InnoDB 通过 redo log 保证持久性,undo log 保证原子性,锁和 MVCC 保证隔离性。

30、MySQL 的事务隔离级别有哪些?

从低到高四种:

读未提交:能读到别人未提交的数据,三种问题都有,基本不用。

读已提交:只能读已提交的,解决了脏读。Oracle 的默认级别。

可重复读:同一事务内多次读结果一致,解决了不可重复读。MySQL InnoDB 的默认级别,通过 MVCC 实际上也避免了幻读。

串行化:事务串行执行,没问题但性能最差,一般不选。

31、每种隔离级别可能会出现什么问题?

读未提交:脏读、不可重复读、幻读全有。

读已提交:解决了脏读,还有不可重复读和幻读。

可重复读:解决了脏读和不可重复读,理论上还有幻读——但 InnoDB 通过间隙锁实际上也避免了。

串行化:三种问题全解决了。

总结就是级别越高越安全,但并发性能越差。

32、什么是脏读、不可重复读、幻读?

脏读:读到了别的事务还没提交的数据,如果对方回滚,你读到的数据是无效的。

不可重复读:同一个事务里两次读同一条数据,结果不一样——因为别的事务中间提交了修改。针对的是 UPDATE。

幻读:同一个事务里两次查询,结果集行数不一样——因为别的事务插入了新数据。针对的是 INSERT。

33、数据库事务底层是如何实现的?

三大机制:

Redo Log:记录数据被改成什么样了,崩溃恢复时重放,保证持久性。

Undo Log:记录修改前的数据,回滚时用,也支持 MVCC 读历史版本。

锁 + MVCC:锁控制并发访问,MVCC 通过保留数据多版本实现读写不互斥,读不加锁。

34、数据库锁有哪些?

按粒度分:行锁(锁单行,并发高)、表锁(锁整表,并发低)、页锁(中间粒度)。

按功能分:共享锁(S 锁,读锁,可多个事务同时持有)、排他锁(X 锁,写锁,独占)。

还有几个特殊的:间隙锁(锁索引记录间的间隙,防幻读)、临键锁(行锁+间隙锁的组合,InnoDB 可重复读下的默认锁)、意向锁(表级,标识事务意图)。

35、MySQL 的存储引擎有哪些?

最核心的两个:

InnoDB:默认引擎,支持事务、行级锁、外键、MVCC,OLTP 场景首选。

MyISAM:老引擎,不支持事务和外键,表锁,查询性能好但写并发差,适合读多写少的分析场景。

其他的:Memory(数据放内存,重启丢失,适合临时表)、Archive(高压缩,适合日志归档)。

36、SQL 查询中哪些情况会导致索引失效?

常见的有:

对索引列用函数或者表达式,比如 WHERE YEAR(create_time)=2024。

LIKE 左边加通配符,‘%abc’ 这样就不走索引了。

隐式类型转换,比如字符串列你用数字去查,索引会失效。

用 !=、<>、NOT IN 这些否定操作。

组合索引没满足最左匹配。

OR 连接的条件里包含非索引列。

最后一种:优化器判断全表扫描更快,主动不走索引——可以用 EXPLAIN 验证。

37、数据库索引的结构有哪些?

最常用的是 B+树,InnoDB 和 MyISAM 都用它。特点是数据只存在叶子节点,叶子节点之间有链表连接,范围查询很快。

其他几类:哈希索引(只支持等值查询)、全文索引(文本搜索)、R 树(空间数据)。

38、B 树和 B+ 树有什么区别?

三点核心区别:

第一,B 树每个节点都存数据,查到中间节点就能返回;B+ 树只有叶子节点存数据,非叶子只存索引,所以同样深度的 B+ 树能存更多索引。

第二,B+ 树叶子节点有链表相连,范围查询时直接遍历链表,效率高很多。

第三,B+ 树查询永远要到叶子节点,性能稳定;B 树可能在中间就返回了。

数据库索引基本都选 B+ 树,就是因为范围查询和磁盘 IO 的适配好。

39、哪些字段适合建索引?

适合建索引的字段特征:

频繁出现在 WHERE、JOIN、ORDER BY、GROUP BY 中的字段。

区分度高、基数大的字段,比如身份证号。性别这种只有两三个值的,建了也没用。

长度短的字段,int 比长 varchar 好。

主键和外键——主键 InnoDB 自动建聚簇索引,外键一般也要加索引,不然关联查询走全表。

40、哪些情况不适合建索引?

反过来:

数据量很小的表,几百行,全表扫描比走索引还快。

频繁更新的字段,索引维护开销大,可能得不偿失。

区分度极低的字段,比如性别、是否删除这种。

很少出现在查询条件里的字段,建了也用不上。

大字段——TEXT、BLOB 不能直接建索引,只能建前缀索引。

41、说说你对复合索引最左匹配原则的理解

复合索引像楼梯,必须从第一级开始走。比如建了 (a, b, c) 的复合索引:

WHERE a=1 能走索引,WHERE a=1 AND b=2 也能走。但 WHERE b=2 或者 WHERE a=1 AND c=3 跳过了 b,索引就不生效了。

原理很简单:索引是按 (a, b, c) 的顺序排序的,a 排好了 b 才有序,跳过了 a 或者 b 就不再有序了,没法用。

42、说说你对索引回表的理解

回表发生在非聚簇索引查询时。非聚簇索引叶子节点只存索引列和主键,如果 SELECT 的字段不在索引里,就需要拿主键再去聚簇索引里查一遍,这个过程就是回表。

打个比方:非聚簇索引像书的目录,目录告诉你第几页,你要看内容还得翻到那一页——翻页就是回表。

43、如何避免索引回表?

最直接的办法就是覆盖索引——把查询需要的所有字段都加到索引里,索引自己就包含了全部数据,不用回聚簇索引。

但也不是字段越多越好,索引太大写入和维护成本高。所以要根据查询场景权衡,高频查询才考虑覆盖索引。

44、Redis 常用的数据类型有哪些?

五种:String、List、Hash、Set、Sorted Set。

String 最通用,缓存单值、计数、分布式锁都用它。Hash 适合存对象字段。List 做队列和时间线。Set 做标签、去重、交并差集。Sorted Set 做排行榜,靠 score 排序。

45、Redis 的持久化方式有几种,项目中怎么选择?

两种:RDB 和 AOF。

RDB 是定期快照,二进制存,恢复快,但最后一次快照之后的数据可能丢。

AOF 记录每条写命令,数据安全性更高,但文件大、恢复慢。

实际项目里,对数据一致性要求高的开 AOF + everysec 刷盘策略,对性能要求高的只用 RDB。两者也能同时开,官方推荐。

46、想在 Redis 里存一个 Java 对象,能存吗?

可以。先序列化——转成 JSON 字符串或者用 JDK 序列化转字节数组——存进去,读的时候反序列化回来。通常用 JSON,可读性好,跨语言也方便。

47、如何保证 Redis 中存放的都是热点数据?

核心靠内存淘汰策略。最常用的是 allkeys-lru,内存满了自动淘汰最近最少用的 key,自然留下的就是热点数据。

另外还可以主动预热——提前把热点数据加载进去;对非热点数据设短一点的过期时间,让它尽快被淘汰。

48、什么是缓存击穿?如何解决?

一个热点 key 突然过期了,那一瞬间大量请求直接打到数据库,数据库压力骤增。

解决思路:

热点 key 不设过期时间,或者后台定时续期。

加互斥锁,同一个 key 同时只让一个请求去查库、更新缓存,其他的等着。

热点数据提前预热,延长过期时间。

49、什么是缓存雪崩?如何解决?

大量缓存 key 在同一时间过期,或者 Redis 宕机,所有请求直接打数据库,数据库可能扛不住。

解决办法:

第一,过期时间加随机值,比如原本 1 小时,加个 1-5 分钟的随机偏移,避免同时过期。

第二,服务层面做熔断降级限流,保护数据库。

第三,Redis 集群化部署,避免单点故障。

第四,核心热点数据永不过期。

50、什么是缓存穿透?如何解决?

请求一个根本不存在的数据——缓存里没有,数据库里也没有——每次都穿透到数据库,白白浪费资源。恶意攻击常用这种方式。

解决方法:

缓存空值,不存在的 key 也存一个空结果,设短一点的过期时间。

布隆过滤器,提前判断 key 是否存在,不存在的直接拦截。

接口层做参数校验,把明显非法的请求挡在最外面。

51、Redis 是单线程的为什么还这么快?

原因几个:

第一,纯内存操作,没有磁盘 IO 瓶颈。

第二,单线程没有上下文切换和锁竞争的开销。

第三,IO 多路复用,一个线程能同时处理大量连接。

第四,数据结构设计得极致简单高效,比如跳表、压缩列表、SDS 这些。

52、Redis 是单进程单线程的吗?

核心命令处理一直是单线程的,这是 Redis 简单高性能的基础。但从 4.0 开始,后台删除大 key 用了多线程;6.0 以后网络 IO 读写也可以配置多线程,提升多核 CPU 的利用率。所以现在不完全是单线程了,但命令执行逻辑始终是单线程的。

53、Redis 内存淘汰策略有哪些?

8种,分三类来记:

不淘汰:noeviction,默认策略,内存满了直接报错。

LRU(最近最少使用):allkeys-lru、volatile-lru。

LFU(最不经常使用):allkeys-lfu、volatile-lfu,4.0 引入的。

随机:allkeys-random、volatile-random。

TTL 最短优先:volatile-ttl。

带 volatile 的只淘汰设了过期时间的 key,带 allkeys 的对所有 key 生效。生产环境一般用 allkeys-lru。

54、Redis 过期的键是如何删除的?

三种策略配合:

惰性删除:访问 key 的时候才检查是否过期,过期就删。省 CPU 但可能占内存。

定期删除:后台每隔一段时间随机抽一批 key 检查,过期了就删。平衡 CPU 和内存。

内存淘汰:内存到 maxmemory 了,按淘汰策略清理。兜底机制。

三种互补,单独用哪一种都不够。

55、如何保证 MySQL 和 Redis 的数据一致性?

没有银弹,看业务能接受多强的一致性。

方案一:先更新 MySQL,再更新 Redis。简单但有短暂不一致窗口。

方案二:延时双删——更新 MySQL 前后各删一次缓存,第二次延迟几百毫秒,消除并发脏读。能解决大部分场景。

方案三:用 Canal 监听 MySQL binlog,异步同步到 Redis。最终一致性,对业务侵入小,适合读多写少。

56、请说说延时双删的方案

分三步:

第一步,删 Redis 缓存。

第二步,更新 MySQL。

第三步,延迟几百毫秒后再删一次缓存。

为什么要延迟再删一次?因为更新 MySQL 的过程中,可能有其他请求读到旧数据写回了缓存,第二次删除就是为了把这个脏缓存清掉。适合一致性要求较高、但对短暂不一致能接受的场景。

57、JVM 内存区域都有哪些?

五个区域:

程序计数器:线程私有,记录当前线程执行到哪一行字节码。

虚拟机栈:线程私有,每个方法执行时对应一个栈帧,存局部变量、操作数栈这些。

本地方法栈:和虚拟机栈类似,但是给 Native 方法用的。

堆:线程共享,存对象实例,GC 的主要战场。

方法区:线程共享,存类信息、常量、静态变量。JDK8 以后改为元空间,用的是本地内存,不再有永久代的 OOM 问题。

58、Java 的类加载器有哪些?

四种,按层级关系:

启动类加载器(Bootstrap):C++ 实现,加载核心类库,rt.jar 这些。

扩展类加载器(Extension):加载 ext 目录下的类,JDK9 以后被平台类加载器替代。

应用程序类加载器(App):加载 classpath 下的类,也是我们写的代码。

自定义类加载器:继承 ClassLoader,比如隔离不同版本的 jar。

59、什么是双亲委派模型?它的好处是什么?

核心就是一句话:一个类加载请求,先往上找父加载器加载,父能加载就让父来,父加载不了才自己加载。

好处两个:一是避免类的重复加载,二是保护核心类安全——比如你自己写个 java.lang.String 是加载不进来的,因为启动类加载器已经加载过了。沙箱安全机制就靠这个。

60、常见的垃圾收集算法有哪些?

标记-清除:标记垃圾然后统一清除。简单但会产生内存碎片。

标记-复制:把内存分两块,只用一块,满了就把活着的对象复制到另一块,原来那块全清。没碎片但浪费一半空间。新生代用这个。

标记-整理:先标记垃圾,把存活对象往一端移动,然后清理边界外的。没碎片,适合老年代。

分代收集:新生代对象朝生夕死用复制算法,老年代存活率高用标记-清除或标记-整理。现代 GC 都用的这个思路。