2025年Java最全八股文速通

模块核心考点考察频率百分比复习优先级
Java基础String区别、equals与==、异常处理、泛型、序列化、BIO/NIO/AIO20%⭐⭐⭐⭐⭐
集合HashMap原理/扩容/线程安全、ConcurrentHashMap实现、ArrayList vs LinkedList18%⭐⭐⭐⭐⭐
JUC线程状态、synchronized原理、锁升级、volatile、线程池参数、ThreadLocal、死锁条件16%⭐⭐⭐⭐⭐
JVM内存结构、GC算法、分代回收、G1回收器、双亲委派、类加载过程、OOM排查15%⭐⭐⭐⭐⭐
MySQL索引结构(B+树)、聚簇索引、事务ACID/隔离级别、锁机制、慢SQL优化、redo/undo log14%⭐⭐⭐⭐⭐
SpringIOC/AOP原理、循环依赖解决、事务失效场景、Bean生命周期、SpringBoot自动配置8%⭐⭐⭐⭐
Redis缓存穿透/击穿/雪崩、持久化、集群方案、分布式锁、双写一致性5%⭐⭐⭐⭐
计算机网络TCP三次握手/四次挥手、HTTPS加密流程、HTTP状态码、浏览器URL访问过程3%⭐⭐⭐
设计模式单例模式(DCL/枚举)、动态代理(JDK/CGLIB)、工厂模式在Spring的应用1%⭐⭐
消息中间件消息丢失/重复消费解决方案、Kafka顺序消费/分区再平衡、RabbitMQ死信队列/延迟队列0%

一、Java基础

1.1、Java语言基础

1、JVM vs JRE vsJDK⭐⭐⭐⭐

  • JVM是Java程序运行的虚拟机,负责将字节码转换成机器码并执行。
  • JRE是Java程序的运行环境,包含了JVM和必要的类库,用于运行已经编译好的Java程序。
  • JDK是Java开发工具包,包含了JRE和开发工具,用于开发Java程序。

这三者之间的关系是层层嵌套的:JDK > JRE > JVM。在开发Java程序时,需要安装JDK;而在运行Java程序时,只需要安装JRE即可。

2、Java 中的几种基本数据类型⭐⭐⭐⭐⭐

  • 4 种整数型:byte(1字节)、short(2字节)、int(4字节)、long(8字节)
  • 2 种浮点型:float(4字节)、double(8字节)
  • 1 种字符类型:char(2字节)
  • 1 种布尔型:boolean(特殊,1字节,但具体实现可能依赖于虚拟机)

3、基本类型和包装类型的区别? ⭐⭐⭐

  • 用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。
  • 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
  • 占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
  • 默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
  • 比较方式:对于基本数据类型来说,======比较的是值。对于包装数据类型来说,====== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法。

4、包装类型的缓存机制 ⭐⭐
java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。

  • Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据
  • Character 创建了数值在 [0,127] 范围的缓存数据
  • Boolean 直接返回 True or False
  • 两种浮点数类型的包装类 Float,Double 并没有实现缓存机制

如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。

5、自动装箱与拆箱是什么及其原理? ⭐⭐

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。因此,Integer i = 10 (自动装箱)等价于 Integer i = Integer.valueOf(10)。int n = i(自动拆箱) 等价于 int n = i.intValue();

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案【点击此处即可/免费获取】https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho

6、为什么浮点数运算的时候会有精度丢失的风险?如何解决? ⭐⭐⭐
为什么:这和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
解决:BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的
浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断,这都是因为会出现精度问题。所以浮点数比较有两种比较方法。
第一种方法指定一个误差范围,如Math.abs(a-b)<1e-6F。
第二种方法是用BigDecimal的compareTo去比较,compareTo() 方法比较的时候会忽略精度。

7、静态方法和实例方法有何不同? ⭐⭐⭐

  • 调用方式:在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。
    不过,需要注意的是一般不建议使用 对象.方法名 的方式来调用静态方法。这种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类。因此,一般建议使用 类名.方法名 的方式来调用静态方法。
  • 访问类成员是否存在限制:静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。

8、成员变量与局部变量的区别? ⭐⭐⭐

  • 定义位置:成员变量是在类中定义的,但在方法外部。它们是类的一部分,并且可以被类的所有方法访问。局部变量是在方法、构造函数或代码块内部定义的。它们的作用范围仅限于定义它们的代码块内
  • 生命周期:成员变量生命周期与对象的生命周期相同。它们在对象创建时被分配内存,直到对象被垃圾回收时才释放内存。局部变量生命周期仅限于定义它们的代码块执行期间。当代码块执行完毕后,局部变量的内存会被释放。
  • 默认值:成员变量有默认值。如果没有显式初始化,它们会被自动初始化为默认值。局部变量没有默认值。在使用之前必须显式初始化,否则编译器会报错。
  • 访问权限:成员变量可以使用访问修饰符(如 public、private、protected、默认)来控制访问权限。局部变量没有访问修饰符。它们的作用范围完全受限于定义它们的代码块。

9、为什么 Java 只有值传递? ⭐⭐⭐
基本数据类型(如 int、float、char 等)在方法调用时传递的是值的副本。这意味着方法内部对参数的修改不会影响方法外部的原始变量。
对于对象,传递的是对象引用的副本,而不是对象本身。这意味着虽然引用(指针)的副本被传递到方法中,但引用指向的实际对象是共享的

  • 传递的是引用的副本:在方法内对对象属性的修改会影响原始对象,因为引用指向的是同一个对象。

  • 但引用本身是值传递:即方法内对引用的重新赋值不会影响原始引用的值。

public class Example {
    public void modify(MyObject obj) {
        obj.value = 10; // 在方法内对对象属性的修改会影响原始对象
        obj = new MyObject(); // 对引用的重新赋值不会影响原始引用的值。
    }
    public static void main(String[] args) {
        MyObject original = new MyObject();
        modify(original);
    }
}

AI生成项目java运行

这种设计简化了参数传递的模型,并保证了 Java 程序的安全性和一致性。

1.2、面向对象编程

1、重载和重写有什么区别? ⭐⭐⭐⭐⭐

  • 定义:重载指在同一个类中定义多个方法,它们的名称相同,但参数列表不同(参数的数量、类型或顺序不同),返回类型可以相同也可以不同,但仅凭返回类型不能区分重载的方法。重写指子类重新定义从父类继承的方法,方法名称、参数列表、返回类型都必须与父类中的方法完全相同。
  • 目的:重载主要用于增加方法的灵活性,使得一个方法名称可以用于不同的参数类型和数量。可以使代码更具可读性和简洁性。重写主要用于在子类中提供父类方法的特定实现。允许子类根据需要修改或扩展父类的行为,从而实现多态性。
  • 访问修饰符:重载访问修饰符可以不同。即使方法具有相同的名称和不同的参数,访问修饰符的不同也不会影响重载。重写访问修饰符不能减少父类方法的访问权限。即子类重写的方法访问修饰符必须与父类中相应的方法的访问修饰符相同或更宽松
  • 异常处理:在重载方法中,异常类型和数量可以不同。重载方法的异常处理不受限制。在重写方法中,子类方法可以抛出比父类方法更少或相同的异常,但不能抛出更多或不同的异常。这是为了保持父类和子类的一致性。
  • 发生阶段:重载方法发生在编译期,重写方法发生在运行期。

2、面向对象和面向过程的区别 ⭐⭐

  • 面向过程编程(POP):将问题分解为一系列步骤(函数或过程),代码围绕 “怎么做” 展开。数据和操作数据的函数是分离的。
  • 面向对象编程(OOP):数据(属性)和操作数据的函数(方法)封装到 “对象” 中,代码围绕 “谁来做” 展开。对象通过消息传递(方法调用)交互。

相比较于 POP,OOP 开发的程序一般具有下面这些优点:

  • 易维护:由于良好的结构和封装性,OOP 程序通常更容易维护。
  • 易复用:通过继承和多态,OOP 设计使得代码更具复用性,方便扩展功能
  • 易扩展:模块化设计使得系统扩展变得更加容易和灵活。

3、Java 创建对象有几种方式 ⭐⭐⭐

  • new关键字:这是最常见的创建对象的方式。通过调用类的构造函数,使用new关键字可以在内存中分配一个新的对象。
Person person=new Person();

AI生成项目java运行

  • 1
  • 反射:Java的反射机制允许在运行时动态地创建对象。通过获取类的Class对象,并调用其构造函数,可以实现对象的创建。
Class clazz=Class.forName("Person");
Person person=(Person)clazz.getDeclaredConstructor().newInstance();

AI生成项目java运行

  • 1
  • 2
  • clone()方法:如果类实现了Cloneable接口,并重写 clone() 方法:就可以使用clone()方法创建对象的副本。
Person person2 = (Person) person1.clone();

AI生成项目java运行

  • 1
  • 对象的反序列化:通过将对象序列化到一个字节流中,然后再进行反序列化,可以创建对象的副本,。
FileInputStream fileInputStream = new FileInputStream("person.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Person person = (Person) objectInputStream.readObject();

AI生成项目java运行

  • 1
  • 2
  • 3

其中,使用new关键字是最常见和推荐的创建对象的方式。其他方式通常在特定场景下使用,如需要动态创建对象或创建对象的副本等情况。

4、构造方法有哪些特点?是否可被 override? ⭐⭐

  • 名称与类名相同:构造方法的名称必须与类名完全一致。
  • 没有返回值:构造方法没有返回类型,且不能使用 void 声明。
  • 自动执行:在生成类的对象时,构造方法会自动执行,无需显式调用。

构造方法不能被重写(override),但可以被重载(overload)。因此,一个类中可以有多个构造方法,这些构造方法可以具有不同的参数列表,以提供不同的对象初始化方式。

5、面向对象三大特征⭐⭐⭐⭐

  • 封装是指将对象的状态(属性)和行为(方法)结合在一起,并将这些实现细节隐藏在对象内部,只暴露出接口给外部使用。通过封装,对象的内部状态可以被保护,不被外部直接访问,从而减少系统复杂性,提高安全性和灵活性。
  • 继承是指一个类(子类)可以继承另一个类(父类)的属性和方法,从而实现代码重用。子类可以继承父类的公共和保护成员,并可以对其进行扩展和修改。
  • 多态是指不同的对象对同一消息做出不同的响应。多态有两种主要形式:方法重载(编译时多态)和方法重写(运行时多态)。

6、接口和抽象类有什么共同点和区别? ⭐⭐⭐⭐⭐

维度抽象类(Abstract Class)接口(Interface)
定义方式使用 abstract class 声明,可包含抽象方法和非抽象方法使用 interface 声明,方法默认是 public abstract,字段默认是 public static final
实现关键字子类通过 extends 继承抽象类,只能单继承类通过 implements 实现接口,可同时实现多个接口
方法实现可以包含具体方法的实现(非抽象方法)方法不能有实现(JDK8+ 允许默认方法和静态方法实现)
字段属性可以有普通成员变量,也可以有静态变量字段必须是 public static final(默认省略修饰符),不可修改
构造方法可以有构造方法(供子类调用初始化)没有构造方法,无法实例化
设计目的用于抽取同类事物的公共属性和行为,体现 “is-a” 关系(继承)用于定义行为规范,体现 “can-do” 关系(实现),强调功能拓展
多态支持抽象类的多态通过继承实现接口的多态通过不同类实现同一接口实现

7、深拷贝和浅拷贝区别了解吗?什么是引用拷贝? ⭐⭐⭐

  • 浅拷贝:浅拷贝创建一个新的对象,但并不复制对象所引用的内部对象。浅拷贝只复制对象的基本属性值和对象的引用。这意味着,如果对象包含对其他对象的引用,那么这些引用仍然指向原来的对象,而不是创建新的副本。
  • 深拷贝:深拷贝创建一个新的对象,并递归地复制对象内部所引用的所有对象。这样,新的对象和原始对象完全独立,没有共享的引用。深拷贝确保所有的内部对象也被复制,避免了共享引用的问题。
  • 引用拷贝 引用拷贝并不真正复制对象,而是创建一个新的引用指向原来的对象。换句话说,两个引用变量指向同一个对象。引用拷贝在内存中不会创建新的对象实例,只是复制了对象的引用。
    在这里插入图片描述

8、Object 类的常见方法有哪些? ⭐⭐⭐⭐

//native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
public final native Class<?> getClass()

 //native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
public native int hashCode()

//用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
public boolean equals(Object obj)

//native 方法,用于创建并返回当前对象的一份拷贝。
protected native Object clone() throws CloneNotSupportedException

//返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
public String toString()

//native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notify()

//native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void notifyAll()

//native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间
public final void wait() throws InterruptedException

//实例被垃圾回收器回收的时候触发的操作
protected void finalize() throws Throwable { }

AI生成项目java运行

9、equals与== 区别⭐⭐⭐⭐⭐
在Java中,======是一个比较操作符,用于比较两个变量的值是否相等。而"equals()"是Object类中定义的方法,用于比较两个对象是否相等
具体区别如下:

  • ======用于比较基本数据类型和引用类型变量的地址值是否相等。对于基本数据类型,比较的是它们的实际值;对于引用类型,比较的是它们所引用的对象的地址值。
  • equals()方法用于比较两个对象的内容是否相等。默认情况下,它与======的作用相同,比较的是对象的地址值。但是,可以根据具体的类重写该方法,以实现自定义的比较逻辑,String已经重写了equals方法。

10、hashCode()有什么用⭐⭐⭐

  • 核心功能 返回对象的哈希码(整数),用于确定对象在哈希表(如 HashMap、HashSet、Hashtable)中的存储位置(索引)。

  • 在哈希表中的使用场景(以HashSet为例)
    步骤1:添加对象时,先计算 hashCode(),根据哈希码定位对象在哈希表中的存储位置。
    步骤2:若该位置无其他对象,直接存入(无需调用equals())。
    步骤3:若该位置已有对象,则调用 equals() 比较两者是否相等: 相等 → 不存入(避免重复)。 不相等 →通过哈希冲突解决机制(如链表/红黑树)存入其他位置。

  • 性能优势:通过 hashCode() 快速定位数据,减少 equals() 调用次数,从而提升哈希表操作效率(如查询、去重)

11、为什么重写 equals() 时必须重写 hashCode() 方法? ⭐⭐⭐⭐
hashCode() 和 equals() 方法之间有一个重要的契约(约定)。这个契约规定了,如果两个对象通过 equals() 方法比较认为是相等的(即 equals() 返回 true),那么这两个对象的 hashCode() 方法必须返回相同的哈希码。这个契约是确保哈希表(如 HashMap、HashSet)等哈希数据结构正常工作的基础。

1.3、字符串处理

1、String、StringBuffer、StringBuilder 的区别? ⭐⭐⭐⭐⭐

  • String:不可变,线程安全,适合不需要修改的字符串。
  • StringBuffer:可变,线程安全,适合多线程环境中频繁修改的字符串。
  • StringBuilder:可变,不线程安全,适合单线程环境中频繁修改的字符串。

2、String 为什么是不可变的? ⭐⭐⭐
不可变原理:

  • final 类:String 类被声明为 final,这意味着它不能被继承,从而防止了通过继承改变其行为。

  • final 字段:String 类中的 value 字段是 final 的,这保证了 String 对象在创建后其内部字符数组不会被改变。

  • 没有提供修改方法:String 类没有提供任何可以修改其内容的方法。所有对 String 的操作(如拼接、替换等)都会生成一个新的String 对象。

为什么设计成不可变

  • 线程安全:在多线程环境下,不可变对象可以被安全共享,无需额外同步机制。
  • 缓存哈希值: String 的哈希值在创建时被缓存(hash 字段),避免重复计算。
  • 字符串常量池优化:JVM 维护字符串常量池(String Pool),相同字面量的字符串共享同一实例。

3、字符串拼接用“+” 还是 StringBuilder? ⭐⭐

  • “+”运算符的底层机制:在单行代码中通过“+”拼接字符串(例如 String s = a+ b + c;),编译器会自动优化为一个 StringBuilder,调用其 append() 方法完成拼接,最终通过 toString() 生成新字符串。这种方式在简单场景下高效且代码简洁。
  • 循环中使用“+”的缺陷:在循环内使用“+”拼接字符串时,编译器无法优化为单个 StringBuilder,每次循环都会创建新的 StringBuilder 对象。
  • 显式使用 StringBuilder 的优势:直接在循环中使用 StringBuilder,可以复用同一个对象,避免内存浪费

4、字符串常量池的作用 ⭐⭐⭐
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

AI生成项目java运行

5、String s1 = new String(“abc”);这句话创建了几个字符串对象? ⭐⭐⭐

  • 如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中
  • 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”

1.4、异常处理

1、Exception 和 Error 有什么区别? ⭐⭐⭐⭐⭐
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获,Exception 又可以分为 Checked Exception 和 Unchecked Exception 。Error:属于程序无法处理的错误 ,不建议通过catch捕获

  • Error:表示系统级不可恢复的故障。例如 栈溢出(StackOverflowError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

  • Checked Exception 预期可能发生的异常(如文件不存在) ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws关键字处理的话,就没办法通过编译。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常。
    常见的受检查异常有:IO 相关的异常、ClassNotFoundException、SQLException、IOException。

  • Unchecked Exception 编程错误(如空指针),Java 代码在编译过程中,我们即使不处理不受检查异常也可以正常通过编译。RuntimeException及其子类都统称为非受检查异常,常见的有(NullPointerException(空指针错误)、IllegalArgumentException(参数错误比如方法入参类型错误)、ArrayIndexOutOfBoundsException(数组越界错误)、ClassCastException(类型转换错误)

2、try-catch-finally 如何使用? ⭐⭐⭐⭐⭐

  • try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch块:用于处理 try 捕获到的异常。
  • finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return语句时,finally 语句块将在方法返回之前被执行。

注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。

3、finally 中的代码一定会执行吗? ⭐⭐⭐⭐
在某些情况下,finally 中的代码不会被执行:

  • finally 之前虚拟机被终止运行。System.exit(0); 强制终止 JVM

  • 线程中断:当线程在执行 try 或 finally 代码块时被中断(interrupt()),

  • JVM 崩溃:若在执行 finally 前 JVM 发生崩溃(如内存溢出且无法处理),finally 无法执行。

5、如何使用 try-with-resources 代替try-catch-finally? ⭐⭐⭐

在 Java 中,try-with-resources 语句是处理资源管理的一种简洁方法。它可以自动关闭资源,避免了显式的 finally 块和手动关闭资源的复杂性。try-with-resources 语句的引入主要是为了简化资源管理,特别是在处理如文件、流、数据库连接等需要显式关闭的资源时。
基本用法:
try-with-resources 语句需要在 try 关键字后面定义一个或多个实现了 AutoCloseable 接口的资源。资源将在 try 块执行完毕后自动关闭,无论 try 块中是否抛出了异常。这使得资源管理变得更加简单和安全。

语法结构:

try (ResourceType resource = new ResourceType()) {
    // 使用资源的代码
} catch (Exception e) {
    // 异常处理代码
}
// 资源会在 try 块结束后自动调用 close() 关闭,无需 finally 块

AI生成项目java运行

6、异常使用有哪些需要注意的地方? ⭐⭐⭐

  • 不要定义静态异常变量:静态异常会共享堆栈信息,掩盖真实错误位置。
  • 不要复用异常对象:每次抛出异常时必须 new 新对象,否则堆栈信息会指向首次创建位置,导致调试困难。
  • 异常信息应具体且有意义:包含上下文信息(如参数值、操作类型),避免模糊描述。建议抛出更加具体的异常而不是其父类
  • 避免重复记录日志:若已在捕获处记录完整日志,再次抛出时无需重复记录。
  • finally块注意:避免在finally中抛出异常会覆盖原始异常。,避免在 finally 中 return会覆盖 try/catch 的返回值。

1.5、其他高级特征

1、什么是泛型?有什么作用? ⭐⭐⭐⭐
泛型是 Java 中的一种机制,它允许在类、接口和方法中使用类型参数,从而提高代码的重用性和类型安全性。泛型是在 Java 5 中引入的,它使得代码更加灵活和安全,同时也能减少强制类型转换的需求。

  • 类型安全:泛型提供了编译时的类型检查,防止了类型转换错误。例如,在泛型集合中添加非预期类型的元素会在编译时被检测到,从而避免了运行时的ClassCastException。
  • 代码重用:使用泛型可以编写通用的算法和数据结构,而不必为每种类型编写重复的代码。例如,Java 标准库中的 List, Map, Set等集合类都使用了泛型,使得它们可以存储不同类型的对象。
  • 消除强制类型转换:在没有泛型的情况下,你可能需要使用强制类型转换来将对象从 Object类型转换为特定类型。泛型避免了这种强制转换,使得代码更清晰、安全。

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案【点击此处即可/免费获取】https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho

2、什么是反射及其优缺点? ⭐⭐⭐⭐
反射允许程序在运行时动态获取类的信息(如字段、方法、注解),并操作对象的属性和行为。
反射的优点:

  • 灵活性:允许在运行时动态操作类、方法和字段,使得代码能够适应不断变化的需求。例如,可以在不知道具体类的情况下操作它们。
  • 简化框架的实现:许多框架和库(如 Spring 和 Mybatis)使用反射来实现配置和动态行为,从而使得框架的使用更加灵活和简洁。
  • 动态创建对象:在一些动态语言或需要动态配置的系统中,反射可以用来创建对象、调用方法,而无需在编译时知道所有信息。
  • 测试和调试:反射可以用来访问和测试私有字段和方法,这对于单元测试和调试很有帮助。

反射的缺点:

  • 性能开销:反射涉及到大量的动态检查和操作,这会比正常的静态类型操作要慢。频繁使用反射可能会导致性能下降。
  • 安全问题:反射可以访问私有字段和方法,这可能导致安全问题。如果不加以控制,可能会泄露敏感信息或破坏对象的封装性。
  • 编译时检查失效:使用反射时,很多错误(如方法不存在、字段不匹配等)只能在运行时发现。这意味着程序的类型安全性会降低,可能导致难以发现和调试的问题。
  • 代码复杂性:过度使用反射可能导致代码变得复杂和难以维护。反射代码通常较难理解,因为它绕过了静态类型检查和编译时验证。

3、什么是注解,注解的解析方法有哪几种? ⭐⭐
注解是 Java 1.5 引入的一种元数据(Metadata) 机制,用于为代码添加描述性信息。它不直接影响程序逻辑,但可被编译器、框架或运行时环境读取。实现以下功能:编译时检查(如 @Override 确保方法正确重写);代码生成(如 Lombok 自动生成 getter/setter);框架配置(如 Spring 的 @Component 标记组件);文档生成(如 @Deprecated 标记废弃接口)。

常见的解析方法有三种:

  • 编译期解析:编译器在编译 Java 代码的时候扫描对应的注解并处理,如使用@Override
    注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
  • 运行期解析:在程序运行时,通过反射 API 动态获取对象 / 方法 / 字段的注解,执行对应逻辑。如Spring 通过 @Autowired 实现依赖注入;
  • 类加载期解析:例如:Spring AOP 通过 @Aspect生成代理类

4、序列化和反序列化 ⭐⭐⭐⭐⭐
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。

  • 序列化:将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
  • 综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
  • 对于不想进行序列化的变量,使用 transient 关键字修饰。

5、为什么不推荐使用 JDK 自带的序列化? ⭐⭐⭐

  • 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。

  • 性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。

  • 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

6、IO流为什么要分为字节流和字符流 ⭐⭐⭐

  • 数据类型的针对性:字节流处理二进制数据,字符流处理文本数据。

  • 编码处理的自动化:字符流自动处理字符编码,避免手动转换的复杂性和错误。

  • 性能优化:字符流针对文本操作进行了优化(如缓冲、按行读取)。

7、BIO,NIO,AIO区别 ⭐⭐⭐⭐⭐

  • BIO(Blocking I/O):即传统的阻塞式 I/O。在 BIO 中,当线程执行 I/O操作时,如读取文件或网络数据,线程会被阻塞,直到操作完成。这意味着在 I/O 操作进行期间,线程无法执行其他任务,会一直处于等待状态。
  • NIO(Non - Blocking I/O):也叫新 I/O 或非阻塞式 I/O。NIO 允许线程在执行 I/O操作时不会被阻塞。线程可以在 I/O 操作未完成时继续执行其他任务,通过轮询或事件通知的方式来获取 I/O 操作的结果。
  • AIO(Asynchronous I/O):即异步 I/O。AIO 与 NIO 的非阻塞不同,它是基于事件和回调机制实现的。当发起一个I/O 操作后,线程会继续执行其他任务,I/O 操作完成后会通过回调函数来通知线程,线程不需要主动去查询 I/O 操作的状态。

二、Java集合

Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:List、Set 、 Queue。

在这里插入图片描述

2.1、集合概述

1、集合框架底层数据结构 ⭐⭐⭐⭐⭐
List(有序、可重复)

  • ArrayList:Object[] 数组。线程不安全,支持快速随机访问。
  • Vector:Object[] 数组。线程安全(方法用synchronized修饰),但性能差,已淘汰。
  • LinkedList:双向链表。线程不安全,插入删除高效,随机访问慢。
  • CopyOnWriteArrayList:Object[] 数组。线程安全,写操作复制新数组,适合读多写少场景。

Set(无序,唯一)

  • HashSet:基于HashMap实现,元素作为HashMap的key存储(value为固定PRESENT对象)。线程不安全。
  • LinkedHashSet:继承HashSet,内部通过LinkedHashMap实现,维护插入顺序。线程不安全。
  • TreeSet:基于TreeMap(红黑树)实现,元素按自然顺序或Comparator排序。线程不安全。

Queue(有序,可重复)

  • PriorityQueue:Object[] 数组实现小顶堆。线程不安全,元素按自然顺序或Comparator排序。
  • ArrayDeque:可扩容动态双向数组。线程不安全,高效实现栈和队列操作。

Map(key唯一,value可重复)

  • HashMap:JDK8+为数组+链表/红黑树,链表长度≥8且数组长度≥64时树化。线程不安全。
  • LinkedHashMap:继承HashMap,通过双向链表维护插入顺序或访问顺序。线程不安全。
  • ConcurrentHashMap:JDK8+采用数组+链表/红黑树,CAS+synchronized实现线程安全,锁粒度更细。
  • Hashtable:数组+链表,全表锁,线程安全但已淘汰,建议用ConcurrentHashMap替代。
  • TreeMap:红黑树实现,key按自然顺序或Comparator排序。线程不安全。

2、Comparable 和 Comparator 的区别 ⭐⭐⭐
Comparable 是类内部实现的自然排序,Comparator 是外部定义的自定义排序。
Comparable 接口

  • 定义: Comparable 接口用于定义对象的自然排序。实现此接口的类需要重写 compareTo 方法。
  • 比较方式: compareTo 方法只允许以当前对象与另一个对象进行比较。一般来说,该对象的排序顺序在类的实现中是固定的。
  • 实现类: 通常在类中实现此接口,例如,如果你想让 Person 类根据年龄进行排序,你可以在 Person 类中实现 Comparable
    接口。
public class Person implements Comparable<Person> {
    private int age;

    public Person(int age) {
        this.age = age;
    }

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

AI生成项目java运行

Comparator

  • 定义: Comparator 接口用于定义一个比较器,可以在外部实现以定义多个不同的排序方式。实现此接口的类需要重写 compare方法。
  • 比较方式: compare方法可以接受两个对象进行比较,允许你在多个比较器之间选择不同的排序规则,也就是说,你可以为同一类型的对象定义多种排序方式。
  • 实现类: 通常在一个单独的类中实现,例如,你可以创建一个 AgeComparator 类来根据年龄排序,一个 NameComparator类来根据名字排序。
import java.util.Comparator;

public class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        return Integer.compare(p1.getAge(), p2.getAge());
    }
}

AI生成项目java运行

2.2、List集合

1、ArrayList 和 Array(数组)的区别? ⭐⭐⭐
ArrayList 内部基于动态数组实现,比 Array(静态数组) 使用起来更加灵活:

  • 长度可变性:ArrayList会根据实际存储的元素动态地扩容或缩容,而 Array 被创建之后就不能改变它的长度了。
  • 类型安全:ArrayList 允许你使用泛型来确保类型安全,Array 则不可以。
  • 存储内容:ArrayList 中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。Array可以直接存储基本类型数据,也可以存储对象。
  • 功能支持:ArrayList 支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如 add()、remove()等。Array只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。
  • 创建方式:ArrayList创建时不需要指定大小,而Array创建时必须指定大小。

2、如何实现数组和List之间的转换 ⭐⭐⭐

  • 数组 → List
    Arrays.asList(array) 快速转换,但返回的 固定长度 List(底层共享原数组)。
    相互影响:修改元素值会同步到原数组,但增删元素会抛异常。
    new ArrayList<>(Arrays.asList(array))
    创建独立 List,与原数组无关联,支持增删操作。
  • List → 数组
    list.toArray(new T[0])
    返回独立数组,与原 List 无关联,修改互不影响。

3、ArrayList 可以添加 null 值吗? ⭐⭐⭐
ArrayList 中可以存储任何类型的对象,包括 null 值。不过,不建议向ArrayList 中添加 null 值, null 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。

4、ArrayList 和LinkedList插入和删除元素的时间复杂度? ⭐⭐⭐⭐
它们的性能差异主要源于底层数据结构:

  • ArrayList 基于数组,尾部插入/删除在无需扩容时是 O(1)(扩容时为 O(n)),而头部或中间操作需移动元素,时间复杂度O(n)。

  • LinkedList 基于双向链表,头尾插入/删除直接操作指针(O(1)),但中间操作需遍历链表,时间复杂度 O(n)。因此,LinkedList 更适合频繁头尾操作,而 ArrayList 适合随机访问和尾部操作。”

5、ArrayList和LinkedList有什么区别 ⭐⭐⭐⭐ ⭐
ArrayList和LinkedList是)java集合框架中List接口的两个常见实现类,它们在底层实现和性能特点上有以下几点区别:
1).底层数据结构:ArrayList使用动态数组来存储元素,而LinkedList使用双向链表来存储元素。
2).随机访向性能:Arraylist支持高效的随机访问,因为它可以通过下标计算元素在数组中的位置。而Linkedlst在随机访问方面性能较差,获取元素需要从头或尾部开始遍历链表找到对应位置。
3)、插入和删除性能:ArayList在尾部添加或删除元素的性能较好,因为它不涉及数组的移动。而在中间插入或删除元素时,ArrayList涉及到元素的移动,性能相对较低。LinkedList在任意位置进行插入和删除操作的性能较好,因为只需要调整链表中的指针即可。
4).内存占用:ArrayList底层是数组,内存连续,节省内存,而LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
5)、扩展性:ArrayList 在元素超过当前容量时,需要创建一个更大的数组并复制原有元素,可能会造成性能开销。LinkedList 可以方便地在任意位置插入元素,不需要移动其他元素。
综上所述,当频繁访问元素时,使用 ArrayList 更高效,当频繁插入和删除操作时,使用 LinkedList 更合适。

6、ArrayList底层的实现原理是什么/扩容机制 ⭐⭐⭐⭐⭐
1. 底层数据结构
ArrayList 底层基于 Object[] 数组 实现,是一个动态数组,支持自动扩容。
2. 初始容量
无参构造:初始容量为 0(实际是一个空数组),第一次添加元素时扩容为默认容量 10。
指定容量构造:例如 new ArrayList(100),直接初始化为指定容量(100)。
3. 扩容机制
扩容条件:当添加元素后,数组已使用长度(size+1)超过当前数组容量时触发扩容。
扩容规则:新容量为原容量的 1.5 倍(源码:newCapacity = oldCapacity + (oldCapacity >> 1))。
扩容代价:需通过 Arrays.copyOf 将旧数组数据拷贝到新数组,频繁扩容会影响性能。
4. 添加元素流程
容量检查:确保当前容量足够存入新元素(size+1 > capacity)。
触发扩容:若不足,按 1.5 倍扩容并拷贝数据。
插入元素:将新元素放入数组 size 位置,size 自增 1。
返回结果:返回 true 表示添加成功。

2.3、Set集合

1、比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同 ⭐⭐⭐⭐⭐

特性HashSetLinkedHashSetTreeSet
底层数据结构哈希表哈希表 + 双向链表红黑树
顺序性无序插入顺序自然/自定义排序
性能O(1) 查找/插入略低于 HashSetO(log n) 操作
是否允许 null❌(需排序无法比较)
线程安全
适用场景快速去重、无需顺序需保留插入顺序的去重需排序、范围查询

2.4、Map集合

1、HashMap底层的实现原理 ⭐⭐⭐⭐⭐

  • 数据存储:HashMap底层采用哈希表结构,通过key的哈希值计算数组下标,若哈希冲突,则同一位置的元素以链表或红黑树存储。
  • 扰动算法:JDK1.8通过高位参与运算(key.hashCode()高16位异或低16位)减少哈希碰撞。
  • 版本差异:JDK1.8前:数组+链表,冲突时链表插入头部(头插法)。JDK1.8后:数组+链表+红黑树,当链表长度超过8且数组长度≥64时,链表转为红黑树(提高查询效率);红黑树节点数≤6时退化为链表。冲突时插入链表尾部(尾插法)

2、HashMap的put流程 ⭐⭐⭐⭐⭐

  • 初始化检查:若数组(table)为空,先调用resize()初始化(默认容量16,阈值12)。
  • 计算下标:通过key计算hash值确定键值对存放的桶位置。
  • 处理空桶:若当前桶无数据(table[i]==null),直接新建节点放入。
  • 处理冲突:若桶已有数据:Key相同:若头节点key与待插入key相同(equals为真),直接覆盖value。
    红黑树插入:若桶为红黑树结构,调用putTreeVal()插入或更新。
    链表遍历:遍历链表,若无相同key则在尾部插入新节点。插入后若链表长度≥8,触发树化检查(需数组长度≥64才转为红黑树)。若有相同key则直接覆盖value
  • 扩容判断:插入成功后,若总键值对数超过阈值(size > threshold),调用resize()扩容。

3、HashMap的扩容机制 ⭐⭐⭐⭐⭐

  • 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)
  • 每次扩容的时候,都是扩容之前容量的2倍;
  • 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
    没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
    如果是红黑树,走红黑树的添加
    如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,为0的话该元素的位置停留在原始位置,不为0的话移动到原始位置+增加的数组大小这个位置上

4、HashMap 和 Hashtable 的区别 ⭐⭐⭐⭐⭐

  • 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable内部的方法基本都经过synchronized 修饰。(如果要保证线程安全的话就使用 ConcurrentHashMap ); 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它
  • 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出NullPointerException。
  • 初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2的幂次方大小(HashMap 中的tableSizeFor()方法保证)。也就是说 HashMap 总是使用 2的幂作为哈希表的大小,
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。Hashtable没有这样的机制。
  • 哈希函数的实现:HashMap 对哈希值进行了高位和低位的混合扰动处理以减少冲突,而 Hashtable 直接使用键的hashCode()值。

5、HashMap 和 TreeMap 区别 ⭐⭐⭐⭐⭐

  • 1、底层数据结构: HashMap:采用哈希表作为底层数据结构,通过哈希函数将键映射到桶中,存放键值对。 TreeMap:基于红黑树实现,能够保持键的排序。

  • 2、顺序: HashMap:不保证元素的顺序,插入的顺序可能会被打乱。 TreeMap:根据键的自然顺序(或根据比较器提供的顺序)进行排序,因此迭代时会按照键的顺序返回元素。

  • 3、性能: HashMap:在大多数情况下,插入、删除和查找的时间复杂度为 O(1),但最坏情况下为 O(n)(当发生哈希冲突严重时)。 TreeMap:插入、删除和查找的时间复杂度为 O(log n),因为需要维护树的平衡性。

  • 4、键的要求: HashMap:键可以为 null,最多只能有一个 null 键。 TreeMap:不允许使用 null 作为键,因为其需要比较键的大小。

  • 5、用途: HashMap:适合对元素的顺序没有要求,主要用于快速访问。 TreeMap:适合需要排序或者范围查询的场景。
    总结来说,如果对元素的顺序没有要求并且希望获得更好的性能,使用 HashMap。如果需要对元素进行排序并能接受更高的操作复杂度,使用 TreeMap。

6、HashMap 的长度为什么是 2 的幂次方 ⭐⭐⭐⭐⭐

  • 高效位运算替代取模:
    哈希计算下标时,公式为 (n-1) & hash(n 为容量)。
    若 n 是 2 的幂,n-1 的二进制全为 1(如 n=16 → 15=0b1111),此时 & 操作等价于 hash % n,但位运算效率远高于取模。
  • 哈希分布更均匀:
    2 的幂次方的容量设计,配合扰动函数(hash = key.hashCode() ^ (hash >>> 16)),能让哈希值的高位参与下标计算,减少哈希碰撞。
  • 扩容便捷性:
    每次扩容容量翻倍(仍保持 2 的幂),旧数据迁移时只需判断 hash & oldCap 是否为 0,即可快速分配到新位置(原位置或原位置+旧容量),无需重新计算所有哈希。
  • 避免空间浪费:
    若容量非 2 的幂,用户自定义初始容量时可能导致实际分配的容量与预期不符(需通过 tableSizeFor() 方法修正为最近的 2 的幂),可能浪费内存。

7、HashMap 多线程操作导致死循环问题 ⭐⭐⭐

  • JDK1.7 及之前版本的 HashMap在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。
  • 为了解决这个问题,JDK1.8 版本的 HashMap采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。
  • 但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用ConcurrentHashMap 。

8、HashMap 为什么线程不安全? ⭐⭐⭐⭐⭐

核心原因:HashMap 的设计未考虑多线程并发操作,导致以下问题:

  • 多线程扩容导致死循环(JDK1.7 及之前)
    原因:多线程扩容时,头插法可能导致链表反转,形成环形链表(如线程 A 和 B同时扩容并修改节点引用)。
    结果:后续操作遍历链表时陷入死循环,CPU 飙升。
    JDK1.8 改进:改用尾插法,但线程不安全问题依然存在(只是降低了死循环概率)。
  • 多线程扩容导致数据丢失
    场景:多线程同时插入数据,触发哈希冲突。
    结果:若两个线程计算相同下标,后插入的键值对可能覆盖前一个线程的数据。
  • 多线程扩容导致数据丢失
    场景:多线程同时触发扩容(resize)。
    结果:链表或红黑树结构被破坏,部分数据丢失或引用混乱。
  • 迭代导致抛出异常
    场景:一个线程迭代遍历时,另一个线程修改结构(如增删)。
    结果:迭代器的 modCount 与预期值不一致,抛出异常(即使单线程也可能触发,但多线程更不可控)。

9、ConcurrentHashMap 线程安全的具体实现方式/底层具体实现 ⭐⭐⭐⭐⭐

  • JDK1.7(分段锁)
    数据结构:由多个 Segment 组成,每个 Segment 是一个独立的哈希表。
    并发控制:每个 Segment用 ReentrantLock 加锁,不同 Segment 可并发操作。
    缺点:并发度固定,扩容仅限于单个 Segment。
  • JDK1.8(CAS + synchronized)
    数据结构:数组 + 链表/红黑树,Node 的 val 和 next 用 volatile 保证可见性。
    并发控制:无锁读:直接访问 volatile 变量。CAS 写:空桶用 CAS 插入;非空桶锁头节点(synchronized)。多线程扩容:线程插入时若发现扩容,协助迁移数据。
    优点:锁粒度更细,并发度动态提升。

总结
ConcurrentHashMap 通过分段锁(JDK 1.7 及之前版本)和基于 CAS 操作的无锁算法(JDK 1.8 及之后版本)来实现线程安全,从而在高并发环境下提供了更好的性能。读操作通常无锁,写操作使用 CAS 操作或加锁来确保线程安全。ConcurrentHashMap 的设计使得它能够在多线程环境下安全高效地操作。

10、ConcurrentHashMap 为什么 key 和 value 不能为 null? ⭐⭐⭐⭐⭐

  • 二义性问题: get(key) 返回 null 时,无法区分是“键不存在”还是“值本身为 null”。 并发场景下,containsKey和 get 之间的修改可能导致误判。
  • 简化并发设计: 避免处理 null 的特殊逻辑,减少代码复杂度。
  • 与 Hashtable 一致性: 延续 Hashtable 的设计原则,强制显式处理缺失键值。

11、ConcurrentHashMap 能保证复合操作的原子性吗? ⭐⭐⭐

  • ConcurrentHashMap本身是线程安全的,但它不能自动保证复合操作的原子性。例如‘检查后更新’操作,在并发环境下可能导致竞态条件。

  • 为了保证原子性,ConcurrentHashMap 提供了以下内置方法: putIfAbsent():不存在则插入。 compute()/ computeIfAbsent():根据 key 计算新值。 merge():合并新旧值。 这些方法通过内部锁或 CAS 操作保证了原子性,避免了手动实现复合操作的风险。

12、ConcurrentHashMap 和 Hashtable 的区别 ⭐⭐⭐⭐⭐

特性ConcurrentHashMapHashtable
并发机制JDK1.7分段锁;JDK1.8 CAS+桶锁全局锁(所有方法加 synchronized)
性能高并发下高效高并发下性能差
迭代器强一致性(不抛异常)弱一致性(可能抛异常)
初始容量16(2的幂)11
扩容规则翻倍(2的幂)2n+1
Null支持键值均不能为null键值均不能为null
版本JDK1.5+JDK1.0+

核心区别:
锁粒度:ConcurrentHashMap 细粒度锁(段或桶),Hashtable 全局锁。
设计目标:ConcurrentHashMap 为高并发优化,Hashtable 已淘汰。

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案【点击此处即可/免费获取】https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho

三、Redis

3.1、概述

1、什么是Redis? ⭐⭐⭐⭐
基于内存的键值(Key-Value)数据库系统。常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)

**2、Redis为什么快?**⭐⭐⭐⭐⭐

  • 单线程模型:避免上下文切换和锁竞争,通过IO多路复用监听多个Socket事件
  • 内存操作:数据全存内存,读写速度远高于磁盘
  • 高效数据结构:如跳表、压缩列表等,优化内存和查询效率

3、解释一下I/O多路复用模型? ⭐⭐⭐⭐

  • I/O多路复用是一种高效的网络通信模型,它允许单个线程同时监听多个Socket连接,并在其中任何一个Socket可读或可写时得到通知,从而避免无效等待,提高CPU利用率。
  • 目前的I/O多路复用都是采用的epoll模式实现,epoll会在通知Redis服务Socket就绪的同时,把已就绪的Socket信息直接传递给Redis服务,避免了遍历所有Socket的开销,大幅提升了性能。"
  • 在Redis6.0之后,为了提升更好的性能,多线程处理网络I/O(解析请求/发送回复),单线程执行命令:保证原子性,避免锁竞争 这种设计既提升吞吐量,又维持了Redis的线程安全特性。

3.2、缓存

1、什么是Redis的缓存穿透,怎么解决 ⭐⭐⭐⭐⭐
缓存穿透是指查询一个既不在缓存也不在数据库中的数据,导致每次请求都直接访问数据库,从而造成数据库压力过大甚至宕机的问题。这种情况通常由恶意攻击或无效请求(如不存在的ID)引发。
缓存穿透的解决方案

  • 空值缓存(缓存空对象)
    原理:当数据库查询结果为空时,将空值(如key-null)写入缓存,并设置较短的过期时间(如30秒)。后续相同请求会命中空值缓存,避免重复访问数据库。
    优点:实现简单,能有效拦截短时间内的高频无效请求。
    缺点:短时间内存占用增加。若后续数据库新增了该数据,可能导致缓存与数据库的短期不一致
  • 布隆过滤器
    原理:它的底层原理是,先初始化一个比较大的数组,里面存放的是二进制0或1。一开始都是0,当一个key来了之后,经过3次hash计算,模数组长度找到数据的下标,然后把数组中原来的0改为1。这样,三个数组的位置就能标明一个key的存在。
    请求到达时,先通过布隆过滤器判断数据是否存在:
    若返回“不存在”,直接拦截请求。
    若返回“可能存在”,继续查询缓存和数据库。
    优点:内存占用极小,适合大规模数据场景。
    缺点:存在误判率(可能将不存在的误判为存在,但不会漏判)。需在数据写入时同步更新布隆过滤器

2、什么是Redis的缓存击穿,怎么解决 ⭐⭐⭐⭐⭐
Redis的缓存击穿是指某个热点数据在缓存中过期(或失效)的瞬间,大量并发请求直接穿透缓存层访问数据库,导致数据库压力骤增甚至崩溃的现象。这种情况通常发生在高并发场景下,例如秒杀商品、热门新闻等热点数据突然失效时。
解决方案

  • 互斥锁(分布式锁)
    原理:当缓存失效时,仅允许一个线程查询数据库并重建缓存,其他线程等待或重试。例如通过Redis的SETNX命令实现锁机制。
    优点:强一致性,确保数据最新。
    缺点:性能下降(线程需等待锁),可能引发死锁或超时问题
  • 逻辑过期(逻辑删除)
    原理:缓存中不设置物理过期时间(TTL),而是将过期时间写入数据值中。当发现逻辑过期后,异步更新缓存。
    优点:高可用性,避免线程等待,性能高。
    缺点:短期数据不一致,需容忍旧数据

3、什么是Redis的缓存雪崩,怎么解决 ⭐⭐⭐⭐⭐
Redis的缓存雪崩是指在同一时间段内,大量缓存数据集中过期失效或缓存服务器宕机,导致所有请求直接涌向后端数据库,造成数据库压力激增甚至崩溃的现象。
解决方案

  • 随机化过期时间:为每个Key的TTL添加随机值(例如基础时间±随机分钟),分散过期时间点,避免同一时段大量数据失效
  • 热点数据永不过期:对高频访问的数据(如首页推荐商品),设置逻辑过期而非物理过期,通过异步线程定期更新数据,保证缓存始终可用
  • Redis高可用部署:采用主从复制(Master-Slave)、哨兵模式(Sentinel)或集群模式(Cluster)实现故障自动转移,确保单点故障不影响整体服务
  • 多级缓存架构:结合本地缓存与分布式缓存(Redis),形成多级缓存屏障。例如,本地缓存应对高频请求,Redis作为二级缓存分担压力
  • 熔断与限流策略:在网关或服务层设置限流(如令牌桶算法),限制数据库访问QPS;当请求量超过阈值时触发熔断,直接返回降级内容(如默认页面)(这个缓存三兄弟都可以说)

4、mysql的数据如何与Redis同步呢(双写一致性) ⭐⭐⭐⭐⭐

  • 延时双删(不保证强一致):1在更新数据库前,先删除 Redis 中的旧缓存,防止后续查询直接命中旧数据。2执行 MySQL 的更新操作,确保数据持久化。3设置延时时间(通常为 1-5 秒),等待主从同步完成。延时结束后再次删除缓存,清理可能在此期间被其他线程写入的旧数据。延时机制的作用:等待主从数据库同步完成(主库到从库的同步延迟)。允许其他可能的并发操作完成缓存写入,确保第二次删除能清理残留的脏数据
  • 分布式锁(强一致):机制:写锁(排他锁):更新数据时加锁,阻塞其他读写操作。读锁(共享锁):允许并发读,但禁止写操作。性能较低
  • MQ或者Canal中间件异步通知(最终一致性):
    MQ
    步骤1:数据库更新后发送消息。业务代码在更新 MySQL 后,向 MQ(如 Kafka、RabbitMQ)发送一条消息,携带数据标识及操作类型(如删除缓存或更新缓存)。
    步骤2:消费者异步处理缓存。独立服务监听 MQ 消息,解析后执行缓存操作(如删除或更新 Redis 数据)
    Canal
    步骤1:监听 MySQL Binlog。Canal 伪装为 MySQL 从库,实时解析主库的 Binlog 日志,捕获数据变更事件(如 INSERT、UPDATE)。
    步骤2:发送变更消息到 MQ。将解析后的变更数据(如表名、主键 ID、新值)发送到 MQ(如 Kafka)。
    步骤3:消费者更新缓存。消费者从 MQ 读取消息,根据操作类型删除或更新 Redis 数据。

5、redis做为缓存,数据的持久化是怎么做的 ⭐⭐⭐⭐⭐

  • RDB:定期将 Redis 内存中的数据生成 二进制快照文件(dump.rdb)。
    可通过 SAVE(阻塞式)或 BGSAVE(后台异步)手动触发。
    优点:
    恢复速度快(二进制格式,文件小)。
    适合大规模数据备份(如每天全量备份)。
    对性能影响小(子进程处理,主进程不阻塞)。
    缺点:
    可能丢失数据(最后一次快照后的修改会丢失)。
    大数据量时 fork 可能短暂阻塞 Redis(取决于内存大小)。
  • AOF:记录所有写操作命令(文本格式),类似 MySQL 的 binlog。
    重启时 重放 AOF 日志 恢复数据。
    优点
    数据安全性高(最多丢失1秒数据)。
    支持日志重写(BGREWRITEAOF 压缩冗余命令)。
    可读性强(可用于数据审计)。
    缺点:
    文件体积通常比 RDB 大(需定期重写优化)。
    恢复速度较慢(需逐条执行命令)。

推荐组合使用 RDB + AOF:RDB 用于快速恢复和备份、AOF 确保数据安全性

6、Redis的数据过期策略 ⭐⭐⭐⭐
在redis中提供了两种数据过期删除策略。

  • 第一种是惰性删除。在设置该key过期时间后,我们不去管它。当需要该key时,我们检查其是否过期。如果过期,我们就删掉它,返回 nil;反之,返回该key。对 CPU 友好,只处理被访问的 key,但可能导致大量已过期但未被访问的 key 堆积
  • 第二种是定期删除。就是说,每隔一段时间,我们就对一些key进行检查,并删除里面过期的key。默认每秒 10 次。可以减少内存浪费,但CPU 开销比惰性删除大
    Redis的过期删除策略是:惰性删除 + 定期删除两种策略配合使用

7、Redis的数据淘汰策略 ⭐⭐⭐⭐
1)不淘汰(默认)
策略名:noeviction
行为:当内存不足时,新写入操作会报错(如 OOM),拒绝所有可能增加内存的请求。
适用场景:数据不允许丢失(例如关键业务数据),需严格避免内存超限。

2)淘汰设置了过期时间的键
策略名:
volatile-lru:淘汰最近最少使用(LRU)的键(仅针对有过期时间的键)。
volatile-lfu:淘汰最不经常使用(LFU)的键(Redis 4.0+,针对有过期时间的键)。
volatile-ttl:淘汰存活时间最短(TTL 最小)的键。
volatile-random:随机淘汰任意有过期时间的键。
适用场景:内存中同时存在永久数据和临时缓存数据,需优先淘汰缓存。

3)淘汰所有键(不区分是否设置过期时间)
策略名:
allkeys-lru:淘汰最近最少使用(LRU)的键(所有键)。
allkeys-lfu:淘汰最不经常使用(LFU)的键(Redis 4.0+,所有键)。
allkeys-random:随机淘汰任意键。
适用场景:内存中所有数据都可被淘汰(例如纯缓存场景)。

3.3、分布式锁

1、Redis分布式锁如何实现? ⭐⭐⭐⭐⭐
在redis中提供了一个命令SETNX(SET if not exists)。由于redis是单线程的,用了这个命令之后,只能有一个客户端对某一个key设置值。在没有过期或删除key的时候,其他客户端是不能设置这个key的。

2、如何控制Redis实现分布式锁的有效时长呢 ⭐⭐⭐⭐
Redis 的 SETNX 指令确实难以直接控制锁的有效时长,但可以通过 Redisson 框架优化实现,其核心逻辑如下:

  • 看门狗自动续期机制:如果业务未执行完但锁即将过期,Redisson的看门狗会每隔一段时间检查锁是否仍被持有。若持有,则自动延长锁的失效时间,避免锁提前释放。
  • 释放锁与流程闭环:业务执行完成后,手动释放锁即可,确保锁资源及时回收。
  • 自旋锁提升并发性能:在高并发场景下,若客户1持有锁,客户2不会立即被拒绝,而是以自旋(循环尝试)的方式等待锁释放。一旦客户1释放锁,客户2能立即获取,减少阻塞时间,提升系统吞吐量。

3、Redisson实现的分布式锁是可重入的吗? ⭐⭐⭐⭐
是可重入的。这样做是为了避免死锁的产生。
第一次加锁时,记录你的线程ID和持有锁的次数(计数器=1)。
第二次加锁时,发现是同一个线程,计数器+1(变成2),直接放行。
每次解锁时,计数器-1,直到计数器归零才真正释放锁。

4、Redisson实现的分布式锁能解决主从一致性的问题吗? ⭐⭐⭐
这个是不能的。

Redisson 分布式锁的默认实现(主从场景风险)
依赖 Redis 主节点:Redisson 的 RLock 默认通过 Redis 主节点加锁(SET + Lua 脚本)
主从异步复制问题:
当主节点加锁成功后,若数据未同步到从节点时主节点崩溃
从节点晋升为新主节点后,锁状态丢失,导致多个客户端同时获取锁

Redisson 的解决方案:RedLock 算法:
向多个独立 Redis 节点(通常 ≥3,且主从部署在不同机器)发起加锁请求
当大多数节点(N/2 +1)加锁成功时,视为全局加锁成功
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变得非常低,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。

3.4、集群

1、介绍一下Redis主从同步 ⭐⭐⭐⭐
单节点Redis的并发能力是有上限的,要进一步提高Redis的高并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。
主从同步分为 全量同步 和 增量同步 两个阶段,核心目标是确保主从节点数据一致性。

  1. 全量同步(首次建立连接时触发)
    触发条件:从节点首次连接主节点,或主从 replication id 不匹配。
    流程:
    1)从节点发起请求:携带自身 replication id 和 offset(偏移量)。
    2)主节点校验:若 replication id 不同,判定为首次同步。主节点将自己的 replication id 和 offset 发送给从节点,达成元数据一致。
    3)生成并传输 RDB 快照:主节点执行 BGSAVE,生成 RDB 文件(内存数据快照)。
    发送 RDB 文件到从节点,从节点清空旧数据后加载 RDB。
    4)同步缓冲命令:主节点在生成 RDB 期间,将新写入命令记录到 缓冲区(repl_backlog)。RDB 传输完成后,主节点将缓冲区命令发送给从节点执行,确保数据完全一致。
  2. 增量同步(从节点重启或断线重连时触发)
    触发条件:主从 replication id 一致,但 offset 落后。
    流程:
    1)从节点上报 offset:发送当前 replication id 和 offset。
    2)主节点推送差异数据:从缓冲区(repl_backlog)中找到从节点 offset 之后的命令。发送这些命令到从节点执行,追平数据差异。

2、怎么保证Redis的高并发高可用? ⭐⭐⭐⭐⭐

  • 主从集群(读写分离) 作用:
    高并发:主节点处理写操作,从节点分担读请求,提升整体吞吐量。
    数据冗余:主节点数据同步到从节点,实现数据备份。
  • 哨兵模式(故障自动恢复) 核心功能:
    监控:持续检查主从节点的健康状态。
    自动故障转移:主节点(Master)宕机时,哨兵(Sentinel)选举一个从节点(Slave)晋升为新主节点。原主节点恢复后,自动降级为从节点并同步新主节点数据。
    服务发现:客户端通过哨兵获取最新主节点地址,故障转移后自动切换连接。

3、Redis集群脑裂,该怎么解决呢? ⭐⭐⭐⭐⭐

  • 脑裂场景: 网络分区导致 Sentinel 误判原主节点(old master)宕机,触发故障转移,选举从节点(slave)为新主节点。此时客户端仍可能向原主节点写入数据,但新主节点无法同步这些数据。
    后果:网络恢复后,原主节点降级为从节点并清空数据,导致写入原主节点的数据永久丢失。
  • 在Redis的配置中可以设置:第一可以设置最少的slave节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。

4、Redis的分片集群有什么作用? ⭐⭐⭐⭐⭐
分片集群主要解决的是海量数据存储的问题

  • 集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。
  • 同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点。
  • Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围,key通过CRC16校验后对16384取模来决定放置哪个槽,通过槽找到对应的节点进行存储。取值的逻辑是一样的。

四、MySQL

4.1、慢查询与优化

1、MySQL 中如何定位慢查询? ⭐⭐⭐

  • 方法一:运维监控系统(如 Skywalking) 监控接口响应时间:通过报表查看哪些接口响应时间超过 2 秒。
    分析接口耗时:定位接口中耗时较多的部分,包括具体的 SQL 执行时间。
  • 方法二:MySQL 慢查询日志 开启慢查询日志:在 MySQL 配置文件中设置: Ini slow_query_log = 1 long_query_time = 2 # 记录执行超过 2 秒的 SQL
    查看日志:从慢查询日志中找到执行慢的 SQL。

2、 如何分析慢 SQL? ⭐⭐⭐

使用 EXPLAIN 命令分析 SQL 执行情况:

  • 检查索引:key 和 key_len:查看是否命中索引及索引长度。
  • 优化扫描方式:type:判断是否存在全表扫描(ALL)或全索引扫描(index),优化查询条件或索引。性能排序:system > const > ref > range > index > ALL
  • 避免回表:Extra:查看是否出现回表(如 Using where),尝试添加索引或减少返回字段。

3、MySQL超大分页怎么处理? ⭐⭐⭐⭐
超大分页通常发生在数据量大的情况下,使用LIMIT分页查询且需要排序时效率较低。可以通过覆盖索引和子查询来解决。首先查询数据的ID字段进行分页,然后根据ID列表用子查询来过滤只查询这些ID的数据,因为查询ID时使用的是覆盖索引,所以效率可以提升。

4、SQL的优化经验有哪些? ⭐⭐⭐⭐⭐

  • 表结构优化
    原则:根据数据范围选择最小适用类型,避免空间浪费。
    字段类型:数值用TINYINT/INT/BIGINT,字符串用CHAR/VARCHAR(定长选CHAR),大文本用TEXT。

  • 索引优化
    高频字段建索引:WHERE、ORDER BY、GROUP BY的字段。
    复合索引覆盖查询:如(a,b)覆盖SELECT a,b。
    避免失效操作:索引字段不做运算、不隐式类型转换。
    前缀索引:长字段(如地址)取前N个字符。

  • SQL语句优化
    拒绝SELECT *:只取必要字段,减少传输和回表。
    JOIN策略:优先INNER JOIN,必须用LEFT/RIGHT JOIN时小表驱动大表。
    聚合优化:UNION ALL替代UNION(无需去重时)。
    分页技巧:避免大偏移量,改用WHERE id > N。

  • 架构扩展
    读写分离:主库写,从库读,分摊压力。
    分库分表:数据量大时,按业务垂直拆分,或按规则(如用户ID哈希)水平拆分。

4.2、索引

1、什么是索引 ⭐⭐⭐⭐⭐
它是一种帮助MySQL高效获取数据的数据结构,主要用来提高数据检索效率,降低数据库的I/O成本。同时,索引列可以对数据进行排序,降低数据排序的成本,也能减少CPU的消耗。

2、索引的底层数据结构了解过吗?B树和B+树的区别是什么呢? ⭐⭐⭐⭐⭐
MySQL的默认存储引擎InnoDB使用的是B+树作为索引的存储结构。选择B+树的原因包括:

  • 树高更低:B+树每个节点可容纳更多子节点,显著降低树的高度,缩短查询路径。
  • 磁盘IO更少:非叶子节点仅存储键值和指针,节省空间以容纳更多索引层级;叶子节点集中存储数据,减少磁盘IO次数。
  • 范围查询高效:叶子节点通过双向链表连接,天然支持顺序遍历和范围查询,避免回退到上层节点。

3、什么是聚簇索引、非聚簇索引、回表查询、覆盖索引 ⭐⭐⭐⭐⭐

  • 聚簇索引是指数据与索引放在一起,B+树的叶子节点保存了整行数据,通常只有一个聚簇索引,一般是由主键构成。
  • 非聚簇索引则是数据与索引分开存储,B+树的叶子节点保存的是主键值,可以有多个非聚簇索引,通常我们自定义的索引都是非聚簇索引。
  • 回表查询是指通过二级索引(非聚簇索引)找到对应的主键值,然后再通过主键值查询聚簇索引中对应的整行数据的过程。
  • 覆盖索引是指在SELECT查询中,返回的列全部能在索引中找到,避免了回表查询,提高了性能。使用覆盖索引可以减少对主键索引的查询次数,提高查询效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值