Skip to content

java ​

== 和 eques() 有什么区别? ​

== 对基本类型比较的是值,对引用类型比较的是对象地址是否相同。

equals() 是方法,默认也是比较地址,但很多类会重写它来比较对象内容,比如 String。

equals() 和 hashCode() 有什么关系。 ​

equals() 用来判断两个值是否相等,hashCode() 用来计算对象的哈希值。

如果两个对象的 equals() 相等,那么他们的 hashCode() 必须相等。

但反过来,hashCode相等,两个值不一定相等。

为什么重写 equals() 的时候通常也要重写 hashCode()? ​

因为 HashMap、HashSet 等集合会先通过 hashCode() 定位,再通过 equals() 判断是否相等。

如果只重写 equals() 不重写 hashCode(),两个逻辑上相等的对象可能得到不同的 hashCode,导致集合无法正确判断

String 为什么是不可变的? ​

String 内部的数据创建后不能修改,任何修改操作都会创建新的 String 对象。

它本质上是被 final 修饰的 Char 数组

不可变使它:

  • 线程安全
  • 可以使用字符串常量池
  • hashCode 可以缓存
  • 安全性,例如作为类加载、网络连接等参数时不能被随意修改

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

String 不可变,每次修改都会创建新对象。

StringBuilder 和 StringBuffer 都是可变字符串,适合频繁拼接。

StringBuilder 不保证线程安全,性能更好;StringBuffer 的方法大多使用 synchronized,线程安全但性能相对低。

String s = "hello" 和 new String("hello") 有什么区别? ​

java
String s1 = "hello";
String s2 = "hello";

String s3 = new String("hello");

"hello" 是字符串字面量,会优先使用字符串常量池中的对象。

new String("hello") 会在堆中创建一个新的 String 对象。

java
s1 == s2       // true
s1 == s3       // false
s1.equals(s3)  // true

Java的值传递和引用传递怎么理解 ​

Java 只有值传递,没有引用传递。传递的是对象引用的副本。

java
void test(User user) {
    user.name = "Tom";
}

可以修改原对象,因为复制出来的引用仍然指向同一个对象。

但是:

java
void test(User user) {
    user = new User();
}

不会改变外部变量的指向,因为修改的是引用的副本。

Java 中 final 有什么用? ​

final 表示“不可改变”,具体作用取决于修饰对象。

  • 变量: 不能重新赋值
  • 方法:不能被子类重写
  • 类:不能被继承

static 有什么作用? ​

static 用来定义属于类本身而不是某个对象的成员。

static 方法为什么不能直接访问非 static 成员? ​

因为 static 方法属于类,可以在没有创建对象的情况下调用;而非 static 成员属于具体对象。

在没有对象的情况下,static 方法不知道应该访问哪个对象的成员。

Java 的访问修饰符 ​

  • public
  • protected
  • default
  • private
修饰符同类同包子类其他包
public✅✅✅✅
protected✅✅✅❌
default✅✅❌❌
private✅❌❌❌

抽象类和接口有什么区别 ​

抽象类接口
继承单继承多实现
成员变量可以有普通变量默认是 public static final
构造方法有没有
普通方法可以Java 8 后可以有 default/static
使用场景代码复用、抽象公共行为定义规范和能力

ArrayList 和 LinkedList 有什么区别? ​

ArrayList 底层是动态数组,随机访问速度快,查询适合;

LinkedList 底层是双向链表,中间插入删除在已经找到节点的情况下效率较高,但随机访问慢。

ArrayListLinkedList
底层动态数组双向链表
随机访问快慢
查找快慢
中间插入删除一般节点定位后较快
内存占用较低较高

ArrayList 底层是什么? ​

ArrayList 底层是一个动态数组,本质上是 Object[]。

ArrayList 扩容机制是什么? ​

当元素数量超过当前数组容量时,ArrayList 会创建一个更大的数组,然后把原来的元素复制过去。

默认扩容大约为原容量的 1.5 倍。

HashMap 底层数据结构是什么? ​

JDK 8 中,HashMap 底层主要是数组 + 链表 + 红黑树。

HashMap 为什么使用红黑树? ​

当大量 Hash 冲突导致链表过长时,查询效率会从 O(1) 接近 O(n)。

转换成红黑树后,查询可以降低到 O(log n),提高极端情况下的查询效率。

HashMap 的 put() 流程是什么? ​

首先计算 key 的 hash,然后根据 hash 定位数组位置;如果位置为空,直接插入;如果发生冲突,则通过 equals() 判断 key 是否相同,相同则覆盖 value,不同则加入链表或者红黑树。超过阈值后会扩容。

HashMap 的 get() 流程是什么? ​

先根据 key 计算 hash,定位数组下标;然后比较节点的 hash 和 key,找到对应节点后返回 value。如果存在哈希冲突,则继续遍历链表或者红黑树。

HashMap 的 key 可以是 null 吗? ​

可以。

HashMap、TreeMap、Hashtable、ConcurrentHashMap 有什么区别? ​

HashMapTreeMapHashtableConcurrentHashMap
线程安全❌❌✅✅
顺序无序按 key 排序无序无保证
null key✅通常不允许❌❌
null value✅通常允许❌❌
典型场景普通 Map需要排序老代码并发场景

普通场景用 HashMap,需要排序用 TreeMap,多线程并发场景一般使用 ConcurrentHashMap,Hashtable 属于比较老的线程安全实现,现在使用较少。

List、Set、Map 有什么区别? ​

  • List:有序、允许重复,通过索引访问。
  • Set:通常用于存储不重复的数据,不保证元素一定有序。
  • Map:保存 key-value 键值对,key 通常不能重复。

Error 和 Exception 有什么区别? ​

Error 表示 JVM 或系统层面的严重错误,通常程序无法恢复,比如 OutOfMemoryError。

Exception 表示程序运行过程中可以处理的异常,比如 IO 异常、空指针异常等。

Error 不应该由业务代码主动处理,Exception 可以根据业务进行处理。

RuntimeException 和普通 Exception 有什么区别? ​

RuntimeException 是运行时异常,属于非受检异常,编译器不强制要求处理。

普通 Exception 通常指 Checked Exception,编译器要求必须处理或者声明抛出。

运行时异常:

  • RuntimeException
  • NullPointerException
  • IndexOutOfBoundsException
  • ...

Checked Exception:

  • IOException
  • SQLException

throw 和 throws 有什么区别? ​

throw 用来主动抛出一个异常对象;

java
throw new RuntimeException("error");

throws 用来声明此方法可能抛出的异常。

java
public void test() throws IOException {
}

Lambda 表达式是什么? ​

Lambda 是 Java 8 引入的一种简洁表示函数式接口实现的语法,可以把一个函数作为参数传递。

java
list.forEach(item -> System.out.println(item));

什么是函数式接口? ​

只包含一个抽象方法的接口叫函数式接口,可以使用 Lambda 实现。

Stream 是什么? ​

Stream 是 Java 8 提供的一种处理集合数据的 API,可以通过声明式的方式进行过滤、转换、排序、聚合等操作。

Stream 本身不会直接修改原集合,除非你在 Lambda 中主动修改外部对象。

Stream 中间操作是惰性的,只有遇到终止操作时才会真正执行。

synchronized 是什么? ​

synchronized 是 Java 提供的内置锁机制,用于保证多个线程访问共享资源时的线程安全,同时能够保证锁范围内操作的可见性和原子性。

简单来说:同一时刻只能有一个线程执行被同一把锁保护的代码。

synchronized 可以修饰什么? ​

主要有三种:

  • 修饰实例方法
  • 修饰静态方法
  • 修饰代码块

修饰实例方法

public synchronized void test() {
}

锁的是:当前对象的 this

修饰静态方法

java
public static synchronized void test() {
}

锁的是:当前 Class 对象

修饰代码块

java
synchronized (obj) {
    // ...
}

锁的是:obj

synchronized 和 Lock 有什么区别? ​

synchronized 是 Java 的内置锁,使用简单,自动加锁和释放锁;

Lock 是 Java 并发包提供的显式锁,需要手动 lock() 和 unlock(),但提供了更多功能。

什么是Ioc ​

Inversion of Control,控制反转。

是一种设计思想,把对象的创建和依赖关系管理交给 Spring 容器,而不是由程序自己 new 对象。

什么是DI ​

Dependency Injection,依赖注入。

Spring 容器负责把一个对象所需要的依赖对象注入进去。

IOC和DI的关系 ​

IoC 是思想,DI 是 IoC 的一种具体实现方式。

IoC
 ↓
控制反转
 ↓
对象不自己管理依赖
 ↓
DI
 ↓
Spring把依赖注入进来

Spring Bean是什么 ​

Bean 是由 Spring IoC 容器负责创建、管理和维护生命周期的对象。

Spring Bean的生命周期是什么 ​

实例化
 ↓
属性注入
 ↓
Aware接口
 ↓
BeanPostProcessor 前置处理
 ↓
初始化
 ↓
BeanPostProcessor 后置处理
 ↓
Bean正常使用
 ↓
销毁

可以简单记:

创建
 ↓
注入
 ↓
初始化
 ↓
使用
 ↓
销毁

Spring 如何创建 Bean ​

Spring 启动时会扫描配置和组件,找到需要管理的 Bean 定义,然后通过反射等方式实例化 Bean,进行依赖注入,再执行初始化流程,最后放入 IoC 容器中供其他对象使用。

@Component、@Service、@Repository、@Controller 有什么区别? ​

本质上都是 Spring 管理 Bean 的组件注解。

区别主要是语义不同:

@Component
通用组件

@Service
业务逻辑层

@Repository
数据访问层

@Controller
Web控制层

@Autowired 和 @Resource 有什么区别? ​

@Autowired 是 Spring 提供的,默认按照类型注入;

@Resource 是 Java 标准注解体系中的注解,通常优先按照名称查找,也可以结合类型。

Spring 为什么推荐构造器注入? ​

  • 依赖关系明确
  • 可以保证依赖不为空
  • 方便单元测试
  • 可以使用 final
  • 有助于发现循环依赖问题

构造器注入能够明确表达 Bean 的必需依赖,并且更利于不可变设计和测试。

什么是 Bean 的单例? ​

单例 Bean 指的是在同一个 Spring IoC 容器中,一个 Bean 定义通常只创建一个 Bean 实例。

Spring 默认 Bean 是单例的吗? ​

是。

一个 Bean 定义在一个 Spring IoC 容器中通常只有一个实例。

什么是 AOP? ​

Aspect-Oriented Programming,面向切面编程。

它主要用于把一些与核心业务无关、但很多地方都需要的功能统一抽取出来。

如:

业务代码
 +
日志
 +
事务
 +
权限
 +
监控

可以理解成:

核心业务
    ↑
    │
 ┌──┴───────┐
 │   AOP    │
 └──────────┘
日志 / 事务 / 权限

AOP 用于将横切关注点从业务代码中抽离出来,实现统一处理。

AOP有哪些应用场景? ​

  • 事务管理
  • 日志记录
  • 权限校验
  • 接口监控
  • 性能统计
  • 异常处理
  • 审计

Spring AOP 是如何实现的 ​

主要有两种:

  • JDK动态代理
  • CGLIB代理

简单理解:

调用目标方法
     ↓
代理对象
     ↓
执行前置逻辑
     ↓
目标方法
     ↓
执行后置逻辑

例如:

Controller
   ↓
Proxy
   ↓
事务
   ↓
Service

什么是代理对象? ​

代理对象是目标对象的一个代理,可以在不修改目标对象代码的情况下,在目标方法执行前后增加额外逻辑。

例如:

原来的:

Service
 ↓
method()

使用代理:

Proxy
 ↓
权限检查
 ↓
事务
 ↓
Service.method()
 ↓
日志

什么是切点 Pointcut? ​

Pointcut(切点)用于定义哪些方法需要被 AOP 增强。

什么是通知 Advice? ​

Advice(通知)就是具体要执行的增强逻辑。

什么是连接点 JoinPoint? ​

什么是连接点 JoinPoint?

@Transactional 是怎么实现的? ​

@Transactional 本质上是通过 Spring AOP + 动态代理 实现的。

调用被 @Transactional 修饰的方法时:

调用 Service 方法
      ↓
代理对象
      ↓
开启事务
      ↓
执行目标方法
      ↓
正常 → 提交事务
异常 → 回滚事务

Spring 通过 AOP 创建代理对象,在目标方法执行前开启事务,执行结束后根据结果提交或回滚。

Spring 事务传播机制有哪些? ​

事务传播机制主要是定义:一个事务方法调用另一个事务方法时,两个方法之间如何处理事务关系。

Spring 有 7 种:

  • REQUIRED
  • REQUIRES_NEW
  • SUPPORTS
  • NOT_SUPPORTED
  • MANDATORY
  • NEVER
  • NESTED
传播行为含义
REQUIRED*有事务就加入,没有就创建
REQUIRES_NEW*不管有没有,都创建新事务
SUPPORTS有事务就加入,没有就非事务执行
NOT_SUPPORTED有事务就挂起,以非事务执行
MANDATORY必须存在事务,否则异常
NEVER必须不存在事务,否则异常
NESTED*有事务则创建嵌套事务,没有则创建事务

REQUIRED 和 REQUIRES_NEW 有什么区别? ​

REQUIRED:如果当前已经存在事务,就加入当前事务;如果不存在,就创建一个新事务。

A()
 ↓
事务A
 ↓
B()
 ↓
加入事务A

如果 B 出异常:事务A → 一起回滚

REQUIRES_NEW:无论当前有没有事务,都创建一个新的事务;如果外层存在事务,会先将外层事务挂起。

A()
 ↓
事务A
 ↓
B()
 ↓
挂起事务A
 ↓
创建事务B

如果 B 回滚:

事务B → 回滚

事务A → 可以继续执行

所以最重要的区别:

REQUIRED
→ 加入原事务

REQUIRES_NEW
→ 创建新事务

@Transactional 为什么有时候会失效? ​

常见原因:

  • 方法不是 public:Spring AOP 默认无法正常通过这种方式进行事务代理。
  • 同一个类内部调用:没有经过 Spring 生成的代理对象,所以 AOP 不会生效。
  • 异常被自己捕获了:Spring 没有感知到异常,就可能正常提交事务。
  • 异常类型不匹配:Spring 对 RuntimeException 和 Error 回滚,对受检异常 Exception 默认不回滚。
  • 方法不是 Spring Bean 管理的

Spring Boot 自动配置是什么? ​

Spring Boot 自动配置就是根据项目的依赖、配置和环境,自动创建和配置相应的 Spring Bean,减少手动配置。

自动配置的核心目的就是根据当前项目环境自动完成常用配置。

@SpringBootApplication 包含哪些注解? ​

@SpringBootApplication 是一个组合注解,主要包含 @SpringBootConfiguration、@EnableAutoConfiguration 和 @ComponentScan。

Spring Boot 为什么不需要大量 XML 配置? ​

因为 Spring Boot 大量使用了注解配置、Java Config 和自动配置机制,可以根据项目依赖自动完成大量配置。

Spring Boot 并不是完全不需要配置,而是把大量重复、固定的配置自动化了。

Spring Boot 启动过程是什么? ​

main()
 ↓
SpringApplication.run()
 ↓
创建 SpringApplication
 ↓
准备 Environment
 ↓
创建 ApplicationContext
 ↓
加载 BeanDefinition
 ↓
自动配置
 ↓
扫描并创建 Bean
 ↓
刷新 ApplicationContext
 ↓
启动完成

SpringApplication.run() 启动 Spring Boot,创建并准备 ApplicationContext,然后加载配置、扫描 Bean、执行自动配置并创建 Bean,最后启动应用。

application.yml 和 application.properties 有什么区别? ​

两者都是 Spring Boot 的配置文件,功能基本相同,主要区别是配置格式不同。

什么是索引? ​

索引是数据库为了提高查询效率而建立的一种数据结构,可以快速定位表中的数据。

可以理解成书的目录,没有时一页一页查,有了就可以快速定位数据位置。

为什么索引可以提高查询速度? ​

因为索引建立了额外的数据结构,可以避免每次查询都扫描整张表,而是通过索引快速定位数据。

本质上是通过额外的存储空间,换取查询速度。

最左匹配原则是什么? ​

最左匹配原则是针对联合索引而言的,查询条件会从联合索引的最左列开始匹配,不能跳过最左边的列。

例如建立:

sql
CREATE INDEX idx_user
ON user(name, age, gender);

联合索引:

(name, age, gender)
   ↑     ↑     ↑
  最左

可以使用索引

sql
WHERE name = 'Tom'

-- 或

WHERE name = 'Tom'
AND age = 20

-- 或

WHERE name = 'Tom'
AND age = 20
AND gender = 1

也可以

sql
WHERE name = 'Tom'
AND gender = 1

这种情况下通常可以利用 name,但 gender 不能直接跳过 age 后完整利用索引。

不能充分利用这个联合索引

sql
WHERE age = 20

因为跳过了最左边的:name

联合索引遵循从最左列开始匹配的原则,不能直接跳过最左边的列。

数据库隔离级别有哪些? ​

数据库事务有 4 种标准隔离级别:

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

从低到高:

隔离性越来越强
      ↑
READ UNCOMMITTED
      ↓
READ COMMITTED
      ↓
REPEATABLE READ
      ↓
SERIALIZABLE
隔离级别脏读不可重复读幻读
READ UNCOMMITTED❌❌❌
READ COMMITTED✅❌❌
REPEATABLE READ✅✅❌
SERIALIZABLE✅✅✅
  • ❌ = 可能发生
  • ✅ = 可以避免

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

  • 脏读:一个事务读取到了另一个事务尚未提交的数据。
  • 不可重复读:同一个事务中,两次读取同一条数据,结果不一样。
  • 幻读:同一个事务中,两次按照相同条件查询,第二次查询发现了第一次不存在的新数据。

快速区分:

脏读
↓
读到别人没提交的数据

不可重复读
↓
同一条数据
前后两次读取结果不同

幻读
↓
同一个查询条件
前后两次查询
记录数量发生变化

Redis 为什么快? ​

Redis 快主要是因为它是基于内存操作,避免了大量磁盘 IO;

同时 Redis 的数据结构简单高效,大部分操作时间复杂度较低,并且采用单线程执行核心命令,避免了线程切换和锁竞争。

四个重要点:

内存操作
+
高效数据结构
+
单线程执行命令
+
IO 多路复用

Redis 有哪些数据类型? ​

常见类型:

类型常见用途
String缓存、计数器
List消息队列、列表
Set去重、集合运算
Hash对象存储
ZSet排行榜、按分数排序

Redis 常见应用场景? ​

常见场景:

  • 缓存
  • Session
  • 分布式锁
  • 计数器
  • 排行榜
  • 限流
  • 消息队列
  • 分布式 ID

Redis 最常见的是作为缓存,也可以用于分布式锁、Session、计数器、排行榜和限流等场景。

Redis 缓存穿透是什么? ​

缓存穿透是查询一个缓存和数据库中都不存在的数据,导致请求每次都绕过 Redis 直接访问数据库。

解决办法:缓存穿透是查不存在的数据导致请求直接打到数据库,可以通过缓存空值、布隆过滤器等方式解决。

Redis 缓存击穿是什么? ​

缓存击穿是某一个热点数据突然过期,在大量并发请求同时访问这个数据时,所有请求都去访问数据库。

解决办法:互斥锁 / 逻辑过期 / 热点数据不过期

Redis 缓存雪崩是什么? ​

缓存雪崩是大量缓存 Key 在同一时间失效,导致大量请求同时访问数据库,从而造成数据库压力过大。

解决办法:设置不同的过期时间(随机过期时间) / 缓存预热 / 热点数据不过期 / 限流 / 熔断降级 / 高可用 Redis

Redis 如何实现分布式锁? ​

最简单的方式是使用:

SET key value NX EX

例如:

SET lock:order 123456 NX EX 30

含义:

NX
↓
只有Key不存在时才能设置

EX 30
↓
30秒自动过期

Redis 为什么需要持久化? ​

因为 Redis 数据主要存储在内存中,如果 Redis 进程宕机或服务器断电,内存中的数据可能丢失,所以需要通过持久化将数据保存到磁盘。

Redis 常见两种持久化方式:

  • RDB: 定期把某个时间点的内存数据生成快照保存到磁盘。
  • AOF: 把 Redis 执行过的写命令记录下来,重启时重新执行这些命令恢复数据。

为什么需要消息队列? ​

消息队列主要用于异步处理、解耦、削峰填谷,让系统之间不需要直接同步调用。

用户下单
   ↓
订单服务
   ↓
消息队列
   ↓
┌─────────┬─────────┐
↓         ↓         ↓
库存      积分      通知

RabbitMQ 和 Kafka 有什么区别? ​

RabbitMQ 更偏向于传统消息队列和业务消息处理,Kafka 更偏向于高吞吐量的消息流和日志处理。

RabbitMQKafka
定位消息队列分布式消息流平台
吞吐量较高非常高
消息模型Exchange + QueueTopic + Partition
消费方式Broker 推送为主Consumer 主动拉取
消息顺序队列内可保证Partition 内保证
典型场景业务异步、任务队列日志、数据流、大数据
消息回溯能力相对弱支持按 offset 回溯

什么是消息重复消费? ​

同一条消息被消费者处理了多次。

如何保证消息不重复处理? ​

严格来说,很难保证消息“绝对不重复投递”,实际系统通常采用“至少一次投递 + 消费端幂等”来保证业务结果不重复。

常见方法:

  • 唯一业务 ID:消息携带唯一的 messageId
  • 数据库唯一约束
  • Redis / 数据库记录消费状态

消息队列通常无法保证绝对不重复消费,因此一般通过消息 ID、幂等接口、数据库唯一约束等方式保证消费端幂等。

什么是消息丢失? ​

消息已经产生,但最终没有被消费者正确处理,导致消息数据丢失。

一般可能发生在三个阶段:

生产者
  ↓
① MQ
  ↓
② MQ存储
  ↓
③ 消费者

生产者 → MQ 丢失

Producer
 ↓
MQ

发送失败,但生产者不知道。

MQ → 消费者丢失

消费者拿到消息:

消费消息
 ↓
还没处理成功
 ↓
直接确认 ACK
 ↓
消费者宕机

消息已经被确认,MQ 不会再次发送。

MQ 自身丢失

消息还在内存
 ↓
MQ服务器突然宕机
 ↓
消息没有持久化
 ↓
消息丢失

如何保证消息不丢失? ​

核心就是保证三个环节:

生产者
 ↓
可靠发送
 ↓
MQ
 ↓
持久化
 ↓
消费者
 ↓
处理成功后 ACK

① 生产者

使用:

发送确认机制

确保消息真正到达 MQ。

② MQ

开启:

消息持久化

防止 MQ 宕机导致消息丢失。

③ 消费者

业务处理成功之后再 ACK。

保证消息不丢失,需要保证生产端可靠发送、MQ 持久化,以及消费者处理成功后再确认消息。

什么是消息积压? ​

消息生产速度长期超过消费者处理速度,导致大量消息堆积在消息队列中。

常见解决方法:

  • 增加消费者数量
  • 提高消费者处理速度
  • 增加分区/队列
  • 优化业务逻辑
  • 临时扩容

Last updated: