Java基础&大厂高频&后台开发-面试常考八股题

2025博客之星年度评选已开启 10w+人浏览 807人参与

一、基础概念

1)JVM是什么:
● 概念:运行 Java 字节码的虚拟机。针对不同系统有不同的实现,保证Java"一次编译,到处运行"
● 作用:将Java字节码转换为特定平台的机器码,实现跨平台性
● 关键:JVM规范允许多种实现(如HotSpot VM是最常用的实现)

2)JDK和JRE是什么:
JDK:
● 概念:Java开发工具包,用于开发和编译 Java 程序
● 包含:JRE以及javac编译器、javadoc等开发工具
JRE:
● 概念:运行已编译Java字节码的环境
● 包含:JVM和Java基础类库(应用代码底层需要调用Java基础类)

JDK 9之后的改变【加分-补充点】
● 模块化:JDK被重构为模块化结构
● jlink工具:可以创建只包含应用所需模块的自定义运行时镜像,减小运行时环境大小
● 不再提供单独 JRE:JDK 11 之后,Oracle 不再提供单独的 JRE 下载

3)字节码是什么?
字节码的概念:Java 字节码是 .class 文件中的代码,是 JVM 可以理解的指令集(它不面向任何特定的硬件平台,只面向 JVM)

字节码的作用:

  1. 跨平台性(核心优势):
    ● 采用字节码最主要的好处是实现了跨平台性。Java 源代码编译成字节码后,可以在任何安装了 JVM 的操作系统上运行
  2. 字节码配合JVM,实现性能优化:
    ● 字节码给JVM提供了一个中间层,通过JIT编译器,JVM 可以将热点代码编译成本地机器码,从而提高程序的执行效率

4)为什么Java是"编译与解释并存"的语言?
● 编译型语言(如Go、C、C++):源代码会被一次性编译成目标机器可以直接执行的二进制机器码(比如 .exe 或二进制文件),程序运行时不再依赖编译器,直接由操作系统调用机器码执行
● 解释型语言(如Python、JavaScript、PHP):源代码不会提前编译成机器码,而是由解释器逐行读取并执行,解释器负责把源码翻译成机器能理解的操作

Java 语言同时具有编译型和解释型语言的特点:
● Java源代码首先被编译成字节码(javac编译器),然后JVM通过解释器逐行解释执行字节码
● 程序运行时,对于频繁执行的热点代码,JVM会使用JIT编译器将其编译成本地机器码并进行缓存,以提高执行效率

二、面向对象

1)⭐封装是什么?
举例:用Person类封装Po,内部封装它的属性age、gender、name,内部定义get()、set()方法以供外部调用,内部一些方法操作属性
概念:利用抽象数据类型,将数据和基于数据的操作封装在一起,构成一个独立的实体(称为对象)。数据被保护在实体中,隐藏内部的细节,只对外暴露接口以供调用。

优点:

  1. 可以减少耦合,方便独立开发迭代
  2. 模块化提高了系统的可复用性

2)⭐继承是什么?
概念:java中通过extends关键字,让子类可以继承父类。通过继承,子类可以复用父类的代码,并增加或修改父类的行为

优点:实现代码的复用,避免了重复编写相同的代码,提高开发速度

注意:

  1. 一个类只能继承一个父类
  2. 子类不继承父类的构造方法,但是可以通过super调用
  3. 子类继承父类的public、protected属性,不能继承父类的private属性

3)⭐多态是什么?
概念:多态指同一个引用在不同时刻表现出多种不同的形态

  1. 编译时多态:指方法重载(方法名相同,参数列表不同),编译器通过参数列表来确定调用的具体方法
  2. 运行时多态:指方法重写(子类重写父类的方法),初始化时子类对象使用父类引用,运行时根据对象具体类型来调用对应的方法(编译看左边,运行看右边)
    如,子类初始化时,Animal a1 = new Dog();,a1使用了父类Animal的引用

优点:提高灵活性,代码复用提高开发速度

4)接口和抽象类的区别?

  1. 一个子类只能继承一个抽象类;可以实现多个接口
  2. 抽象类可以有构造方法、普通成员变量;接口不可以
  3. 抽象类和接口都可以有静态成员变量(static):抽象类中静态成员变量的访问类型任意;接口只能public static final(默认)
  4. 抽象类可以有普通方法;接口jdk8后可以有default方法,jdk9后允许有private私有方法
  5. 抽象类可以有静态方法;接口jdk8后可以有静态方法,但只能被该类直接调用

5)为什么有了基本类型还需要封装类?

  1. 对象的需求:Java是一种面向对象的语言,很多时候必须使用对象,如集合ArrayList等只能存储对象(Java的泛型只能使用对象)
  2. 额外的功能:封装类提供了许多有用的方法来处理数据,更方便处理数据(如Integer.parseInt(“123”);)
  3. null值的表示:基本数据类型不能为null,而封装类可以为null

6)⭐讲讲内部类和匿名内部类?
● 内部类:在类中再定义一个类,这个类叫做内部类

作用:

  1. 内部类只在外部类中被使用,利于代码的可读性
  2. (封装)内部类可以隐藏实现细节,只在外部类内部使用

访问特点:内部类可以直接访问外部类的成员变量,外部类需要先创建内部类对象,才可以访问内部类成员

使用场景:外部类是 LinkedList,内部类是 Node,节点类只为链表服务,没有必要暴露出来

● 匿名内部类:没有名字的内部类,通常用来创建只使用一次的类实例,他必须继承一个类或实现一个接口
interface Greeting {
void sayHello();
}

public class Main {
public static void main(String[] args) {
// 匿名内部类实现Greeting接口
Greeting anonymousGreeting = new Greeting() {
@Override
public void sayHello() {
System.out.println(“Hello from anonymous class!”);
}
};

    anonymousGreeting.sayHello();
}

}

作用:

  1. 当只需要一个实现类的实例,避免显式地定义一个新实现类,简化代码
  2. 当方法的参数是类或接口时,可以使用匿名内部类来传递参数的实现类对象

使用场景:定义Event接口用于事件监听时,在addListener时用匿名内部类实现Event接口传入事件触发的逻辑

7)如何实现菱形继承:
是什么:接口B、C继承接口A,要求实现类D同时实现接口B、C

做法:D实现接口A,同时将接口B、C的实例定义为属性即可
由于 Java 接口允许多重继承,因此不会出现传统菱形继承中的二义性问题。类 D 只需要实现接口 A、B 和 C 中定义的所有方法即可。
interface A {
void methodA();
}

// 继承自 A 的接口 B
interface B extends A {
void methodB();
}

// 继承自 A 的接口 C
interface C extends A {
void methodC();
}

// 使用组合的类 D
class DWithComposition implements A {
private B b;
private C c;

public DWithComposition(B b, C c) {
    this.b = b;
    this.c = c;
}

@Override
public void methodA() {
    // 可以选择调用 B 或 C 的 methodA
    b.methodA();
    // c.methodA();
}

public void methodB() {
    b.methodB();
}

public void methodC() {
    c.methodC();
}

}

8)⭐创建对象的方式?

  1. new关键字:
    class MyClass {
    public MyClass() {
    System.out.println(“Object created using new keyword”);
    }
    }

public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass(); // 使用new关键字创建对象
}
}

  1. Class.newInstance()方法:
    获取类的Class对象,newInstance()使用类的无参构造方法创建一个新的对象实例。
    这种方式需要在编译时知道类的类型,并且类必须有一个无参数的构造方法
    从Java 9开始,newInstance()方法已被标记为过时,推荐使用java.lang.reflect.Constructor类的newInstance()方法
    class MyClass {
    public MyClass() {
    System.out.println(“Object created using Class.newInstance()”);
    }
    }

public class Main {
public static void main(String[] args) throws Exception {
Class<?> clazz = MyClass.class; // 获取 MyClass 对应的 Class 对象
MyClass obj = (MyClass) clazz.newInstance(); // 使用Class.newInstance()创建对象
}
}

  1. Constructor.newInstance()方法:反射机制
    java.lang.reflect.Constructor类的newInstance()方法提供了更大的灵活性,可以调用类的任意构造方法(包括带参数的构造方法)来创建对象。

import java.lang.reflect.Constructor;

class MyClass {
private String message;

public MyClass(String message) {
    this.message = message;
    System.out.println("Object created using Constructor.newInstance() with message: " + message);
}

}

public class Main {
public static void main(String[] args) throws Exception {
Class<?> clazz = MyClass.class; Constructor<?> constructor = clazz.getDeclaredConstructor(String.class); // 先获取构造器对象
MyClass obj = (MyClass) constructor.newInstance(“Hello, Reflection!”); // 使用Constructor.newInstance()创建对象
}
}

  1. clone()方法:实现Cloneable接口
    Cloneable接口:
    是一个标记接口,用于告诉JVM,实现该接口的类支持clone()操作

clone()方法:用于创建现有对象的一个副本

  1. 浅拷贝(Shallow Copy):浅拷贝复制基本数据类型的值和引用类型的引用
    ● 基本数据类型: 如果原始对象的基本数据类型字段发生改变,不会影响克隆对象,因为它们是独立的值
    ● 引用类型: 如果原始对象的引用类型字段指向的对象的状态发生改变,会影响克隆对象,因为它们共享同一个对象
  2. 深拷贝(Deep Copy):深拷贝复制基本数据类型的值和复制引用类型指向的对象(要实现深拷贝,需要手动复制所有引用类型的字段)
    ● 基本数据类型: 如果原始对象的基本数据类型字段发生改变,不会影响克隆对象
    ● 引用类型: 如果原始对象的引用类型字段指向的对象的状态发生改变,不会影响克隆对象,因为它们指向的是不同的对象

好处:
● 减少时间: 克隆可以减少创建对象的时间,特别是当对象包含大量字段或者创建过程比较复杂时。
● 保持状态一致性: 克隆可以确保新对象与原始对象具有相同的状态,这在某些场景下非常重要。

介绍:
clone()方法用于创建现有对象的一个副本。要使用clone()方法,类必须实现Cloneable接口,并重写clone()方法。这种方式创建的对象与原始对象具有相同的状态,但它们是不同的对象

例子:
class MyClass implements Cloneable {
private String message;

public MyClass(String message) {
    this.message = message;
}

@Override
protected Object clone() throws CloneNotSupportedException {
    return super.clone();
}

public String getMessage() {
    return message;
}

}

public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
MyClass obj1 = new MyClass(“Original Object”);
MyClass obj2 = (MyClass) obj1.clone(); // 使用clone()方法创建对象

    System.out.println("obj1 message: " + obj1.getMessage());
    System.out.println("obj2 message: " + obj2.getMessage());
}

}

  1. 反序列化:实现Serializable接口
    序列化:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
    反序列化:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程

介绍:通过反序列化,可以从输入流中读取并创建对象实例。要使用反序列化,类必须实现Serializable接口
底层实现:JVM 通过反射 + 内存分配创建对象
import java.io.*;

class MyClass implements Serializable {
private String message;

public MyClass(String message) {
    this.message = message;
}

public String getMessage() {
    return message;
}

}

public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 创建对象并序列化
MyClass obj1 = new MyClass(“Original Object”);
FileOutputStream fileOut = new FileOutputStream(“object.ser”);
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj1);
out.close();
fileOut.close();

    // 反序列化对象
    FileInputStream fileIn = new FileInputStream("object.ser");
    ObjectInputStream in = new ObjectInputStream(fileIn);
    MyClass obj2 = (MyClass) in.readObject(); // 使用反序列化创建对象
    in.close();
    fileIn.close();

    System.out.println("obj2 message: " + obj2.getMessage());
}

}

  1. 工厂模式:设计模式
    这是一种设计模式,本质上也是使用new关键字进行对象的创建
    interface Animal {
    void makeSound();
    }

class Dog implements Animal {
@Override
public void makeSound() {
System.out.println(“Woof!”);
}
}

class Cat implements Animal {
@Override
public void makeSound() {
System.out.println(“Meow!”);
}
}

class AnimalFactory {
public static Animal createAnimal(String type) {
if (“dog”.equalsIgnoreCase(type)) {
return new Dog();
} else if (“cat”.equalsIgnoreCase(type)) {
return new Cat();
} else {
throw new IllegalArgumentException("Invalid animal type: " + type);
}
}
}

public class Main {
public static void main(String[] args) {
Animal dog = AnimalFactory.createAnimal(“dog”); // 使用工厂模式创建对象
dog.makeSound();

    Animal cat = AnimalFactory.createAnimal("cat"); // 使用工厂模式创建对象
    cat.makeSound();
}

}

9)Object类里有什么方法?
Object是所有类的根类(超类),即每个类都默认继承它,它定义了一组最基本的方法:
● equals:对象比较
● hashCode:计算哈希码
● toString:转换成字符串
● getClass:获取字节码
● clone:对象克隆
● 线程同步(wait / notify系列)

三、语法基础

1)关于"+="的细节
在 Java 里,整型算术运算(+ - * /)至少是 int,所以如下+=时隐式地进行了类型的强制转换,"a+b"的结果会变为int类型,导致"b = a+b"赋值类型冲突
b += a其实是一个特殊写法,它等价于将b类型转换为int,所以成功
byte a = 127;
byte b = 127;
b = a + b; // error : cannot convert from int to byte

b += a; // ok,等价于b = (byte)(b + a);

2)⭐浮点数精度问题与BigDecimal解决
2.1)丢失精度问题与其原因
问题:3*0.1 == 0.3会返回 true 还是 false ?
答案:false

原因:
计算机底层用二进制来表示数字,某些十进制数在二进制是无法精确表示的,就比如1/3在十进制是无限循环小数一样
// 0.1 的二进制表示是一个无限循环的小数:
0.1 (十进制) = 0.000110011001100110011001100110011…(二进制)

所以,在计算"3 * 0.1"时,底层得到的是一个二进制的近似值

2.2)BigDecimal解决
底层:大整数(BigInteger)+ 小数位数(scale)的组合,可以精确表示
BigDecimal a = new BigDecimal(“0.1”);
BigDecimal b = new BigDecimal(“0.2”);

System.out.println(a.add(b)); // 0.3
System.out.println(a.multiply(b)); // 0.02
System.out.println(a.divide(new BigDecimal(“3”), 2, RoundingMode.HALF_UP)); // 0.03

3)能在switch中用String吗
从java7后可以,switch语句通过String.equals()方法来比较字符串

需要注意:

  1. 大小写敏感
  2. null值会出现NPE(NullPointerException )

4)⭐对equals()和hashCode()的理解
● equals():
作用:用于比较两个对象是否相等
方法重写:通常需要重写equals()方法,根据对象的内容来判断是否相等

使用场景:
// 如果不重写,相同的对象equals()不相等
Person person1 = new Person(“Xiaomin”, 20);
Person person2 = new Person(“Xiaomin”, 20);

System.out.println(person1.equals(person2)); // false

● hashCode():
作用:用于生成对象的哈希码
方法重写:调用父类Object的hash()方法

使用场景:在对象使用散列数据结构(需要计算对象哈希值)时,要求相等的对象有相等的哈希码
// 如果不重写,则相等的两个对象存储在Set的不同位置
Person person1 = new Person(“Xiaomin”, 20);
Person person2 = new Person(“Xiaomin”, 20);

Set set = new HashSet<>();
set.add(person1);

System.out.println(set.contains(person2); // false

5)⭐String、StringBuffer与StringBuilder的区别

  1. 可变性
    String对象是不可变的,而StringBuffer和StringBuilder是可变字符序列,每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象
    所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将会对系统性能产生影响

  2. 线程安全
    String由于有final修饰,是不可变的
    StringBuilder不保证同步但效率更高,StringBuffer线程安全(大部分方法加了synchronized 修饰)

6)讲讲java中的static?
static是什么:static 是 Java 的静态修饰符,用于修饰类的成员(变量,方法或代码块),表示它属于类而不是对象

static变量(类变量):
● 所有对象共享同一份内存,节省空间
● 在类加载时分配内存,程序结束释放
● 可通过类名直接访问

static方法:
● 可通过类名直接调用
● 无法使用 this 或 super
● 只能访问静态变量和静态方法,不能直接访问实例成员

static静态代码块:
● 类加载时执行,只执行一次,常用于初始化静态变量或一次性配置

7)⭐泛型是什么?
概念:给类、接口或方法预留类型参数,在使用时再指定具体类型
特点:

  1. 泛型检查在编译时完成(如,Box只能放字符串,否则编译报错);编译后生成的字节码中,不再保留具体类型信息,即“擦除类型”
  2. 不能直接创建泛型数组:T[] arr = new T[10];,会导致编译报错
    优点:减少了重复代码,并提供了编译时类型安全

8)Java支不支持运算符重载?
运算符重载:允许开发者为自定义类型重新定义已有运算符的行为,如让"+"在自定义类型上也能使用
不支持,c++支持

9)⭐讲讲方法重写和方法重载?
重写(Overriding)和重载(Overloading)是Java中实现多态性的两种重要方式

  1. 定义
    ● 重写:发生在子类和父类之间,子类重新定义了父类中已有的方法,方法名、参数列表和返回类型必须相同
    ● 重载:发生在同一个类中,可以有多个方法具有相同的方法名,但是参数列表必须不同(参数类型、参数个数或参数顺序不同)
  2. 作用范围
    ● 重写:只能发生在继承关系的类之间
    ● 重载:可以发生在同一个类中
  3. 其他规则
    a. 重写:
    ⅰ. 访问修饰符的限制:子类方法的访问修饰符的可见性必须大于或等于父类方法的访问修饰符。例如,如果父类方法是protected,子类方法可以是protected或public,但不能是private
    ⅱ. 异常的限制:子类方法不能抛出比父类方法更多或更宽泛的检查型异常。可以抛出更具体的检查型异常,或者任何非检查型异常
    ⅲ. 不能重写final方法
    ⅳ. @Override注解:建议在重写方法上使用@Override注解。这会告诉编译器你正在尝试重写一个方法,如果方法没有正确重写(例如,方法签名不匹配),编译器会报错
    b. 重载:
    ⅰ. 返回类型可以相同也可以不同:重载方法的返回类型可以相同,也可以不同,如果两个方法的方法名和参数列表完全相同,只有返回类型不同,Java编译器会认为这是同一个方法的重复定义,从而导致编译错误。

四、注解

1)⭐注解是什么?
概念:注解是JAVA 5版本开始引入的一个特性,用于对代码进行说明

常见分类:
● Java自带的标准注解:用这些注解标明后编译器就会进行检查。
○ @Override:标明重写某个方法
○ @Deprecated:用于标明类或方法过时
○ @SuppressWarnings:标明要忽略的警告
● 元注解:元注解是用于定义注解的注解
○ @Retention:用于标明注解被保留的阶段
○ @Target:用于标明注解使用的范围
○ @Inherited:用于标明注解可继承
○ @Documented:用于标明是否生成javadoc文档
● 自定义注解:可以根据自己的需求定义注解,并可用元注解对自定义注解进行定义

自定义注解的优点:
● 减少编写配置文件:如Mybatis Plus,在@Select注解中编写sql语句,避免了编写XML配置文件
● 简化开发:如Lombok的@Data等注解,自动生成getter/setter、toString等方法
● 可以结合反射,实现功能:如Spring的@Service注解,在程序启动时会被扫描并进行bean注入

2)⭐注解底层如何实现的?

  1. 存储:编译器把注解信息写入.class文件的字节码元数据里(@Retention决定存活阶段,如running)
  2. 加载:类加载时,注解元数据进入JVM
  3. 读取:运行时通过反射API获取注解信息,底层会返回一个动态代理对象
    为什么是动态代理对象:注解本质上是一个接口,但不是普通对象,所以查询时不能直接返回,而是要根据注解值实例化一个动态代理对象,实现注解这个接口并返回
  4. 使用:框架通过扫描注解 → 解析元数据 → 执行逻辑(如Spring扫描@Service注册 Bean)

五、异常

1)Java异常类的层级结构?
● Throwable是所有异常和错误的父类
○ Error:JVM层面的严重错误,比如虚拟机崩溃
○ Exception:程序可处理的异常,分两类:
ⅰ. 编译时异常(Checked Exception):
● 如 IOException、SQLException
● 编译器要求必须处理(try-catch或throws),否则无法编译通过
ⅱ. 运行时异常(RuntimeException):
● 典型如 NullPointerException、IndexOutOfBoundsException
● 编译器不强制处理,通常是代码逻辑错误引起,应该通过写好逻辑去避免

2)讲讲异常的可查性?
● 可查异常:
除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常
可查异常在一定程度上它的发生是可以预计的,特点是Java编译器会检查它

● 不可查异常:
包括运行时异常(RuntimeException与其子类)和错误(Error)
这些异常在运行时才出现。它们通常是由于编程错误引起的,未检查异常不需要在方法签名中声明,也不强制要求使用 try-catch 块捕获

3)throw和throws
● throws:异常的申明
○ 写在方法签名上,用来告诉调用者“这个方法可能会抛出哪些异常”
○ 一般用于编译时异常(Checked Exception),因为编译器要求必须处理
○ 可以声明多个异常,用逗号隔开
public static void method() throws IOException, FileNotFoundException{
//something statements
}

● throw:异常的抛出
○ 写在方法体内部,用来“制造”并抛出一个具体的异常对象
○ 可以抛出运行时异常或编译时异常
public static double method(int value) {
if(value == 0) {
throw new ArithmeticException(“参数不能为0”); // 抛出一个运行时异常
}
return 5.0 / value;
}

4)讲讲try-with-resource机制?
机制:当你在try子句中打开资源,资源会在try代码块执行后或异常处理后自动关闭
使用:如果你的资源实现了AutoCloseable接口,你可以使用这个语法。大多数的Java标准资源都实现了这个接口
public void automaticallyCloseResource() {
File file = new File(“./tmp.txt”);
try (FileInputStream inputStream = new FileInputStream(file)😉 {
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}

5)讲讲try-catch-finally机制?
Java 提供了try-catch-finally块来处理异常
● try 块:用于包含可能抛出异常的代码
● catch 块:用于捕获并处理特定类型的异常。可以有多个 catch 块来处理不同类型的异常
● finally 块:用于包含无论是否发生异常都需要执行的代码。通常用于释放资源,例如释放数据库连接
try {
// 可能抛出异常的代码
} catch (ArithmeticException e) {
// 捕获并处理 ArithmeticException 异常
} finally {
// 无论是否发生异常,都会执行的代码
}

6)讲讲常见的Error?

  1. OutOfMemoryError: 即OOM,内存溢出,JVM无法再分配内存(可能堆空间不足、方法区空间不足引起)
  2. StackOverflowError:栈溢出(递归过深或死递归)
  3. ClassNotFoundError :用Class.forName()加载类时未找到
  4. NoSuchMethodError:方法没有找到(可能JDK版本冲突)

六、反射

1)⭐什么是反射?类的私有属性如何访问?
一般结合2.一起答
概念:反射就是在运行时动态操作类和对象的能力。比如可以获取类的信息、创建对象(如Spring通过通过反射加载Bean)、调用方法、访问字段(如Jackson等序列化方式,就是通过反射将json映射到对象属性)等

缺点:
● 性能差:因为要动态解析,JVM优化用不上
● 破坏封装性:能访问私有方法和字段,可能带来安全问题

2)⭐如何使用反射?

  1. 获取Class对象
    Class<?> clazz = Class.forName(“MyClass”); // 或 MyClass.class / obj.getClass()
  2. 创建对象:先getConstructor()再newInstance()
    Object obj = clazz.getDeclaredConstructor().newInstance();
  3. 调用方法:获取Method对象,再invoke()
    Method m = clazz.getMethod(“myMethod”, String.class);
    m.invoke(obj, “Hello”);
  4. 访问私有字段:获取Field对象,setAccessible()设置可访问,set()具体值
    Field f = clazz.getDeclaredField(“myField”);
    f.setAccessible(true);
    f.set(obj, “New Value”);

七、SPI机制

1)SPI机制是什么?
SPI(Service Provider Interface)是JDK提供的一种服务发现机制
它的作用是:
● 接口和实现分离,调用方只依赖接口,不关心具体实现
● 支持扩展和替换实现,比如框架可以在运行时加载不同的实现类,而不用改调用方代码

2)SPI的服务发现流程?
服务提供者:
当服务的提供者提供了一种接口的实现之后,需要在resource目录下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类

服务调用者:
当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了

3)SPI简单示例
我们现在需要使用一个内容搜索接口,搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索。
● 先定义好接口
public interface Search {
public List searchDoc(String keyword);
}

● 文件搜索实现
public class FileSearch implements Search{
@Override
public List searchDoc(String keyword) {
System.out.println("文件搜索 "+keyword);
return null;
}
}

● 数据库搜索实现
public class DatabaseSearch implements Search{
@Override
public List searchDoc(String keyword) {
System.out.println("数据搜索 "+keyword);
return null;
}
}

● 新建META-INF/services/目录,然后新建接口全限定名的文件:com.cainiao.ys.spi.learn.Search,里面加上我们需要用到的实现类
com.cainiao.ys.spi.learn.FileSearch

● 测试方法
public class TestCase {
public static void main(String[] args) {
ServiceLoader s = ServiceLoader.load(Search.class);
Iterator iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchDoc(“hello world”); // 可以看到输出结果:文件搜索 hello world
}
}
}
可加载多个实现:如果在com.cainiao.ys.spi.learn.Search文件里写上两个实现类,那最后的输出结果就是两行了

4)Seata对SPI机制的扩展?
Seata自定义EnhancedServiceLoader

自定义路径扫描机制:实现双路径扫描,适配不同版本的兼容
// 同时扫描标准SPI路径和Seata扩展路径
private static final String SERVICES_DIRECTORY = “META-INF/services/”;
private static final String SEATA_DIRECTORY = “META-INF/seata/”;

注解驱动扩展:在实现类上添加@LoadLevel,可用于定义优先级、作用域控制
@LoadLevel(name = “nacos”, order = 1)
public class NacosRegistryProvider implements RegistryProvider {
// 实现类
}

public enum Scope {
SINGLETON, // 单例模式
PROTOTYPE // 原型模式
}

5)⭐SPI和API的区别?
API:面向开发者,是系统/库/框架对外暴露的接口,开发者直接调用即可完成某个功能
● 如List.add()

SPI:面向框架使用者/服务提供者,是系统/库/框架留给第三方实现的接口,框架只定义接口,由外部“服务提供者”来实现
● 如,JDBC里的Driver接口就是SPI,具体由MySQL、Oracle驱动厂商提供不同的实现
● Seata中,通过SPI机制,对同一类场景(如注册中心、配置中心)定义SPI接口,并提供不同的实现,加载时根据配置进行对应依赖的加载,避免需要同时加载全部支持的注册中心的依赖,并允许用户自定义更灵活的实现方案

八、JDK新特性

1)JDK8新特性?

  1. 支持函数式编程,如Lambda表达式
  2. 引入了Stream API
  3. 新的日期时间 API,如LocalDate
  4. 接口可以带默认的方法

2)JDK17新特性?

  1. Switch表达式支持返回值
  2. record语法,public record User(String name, int age) {},自动生成 toString、equals、hashCode、构造器
  3. 引入新垃圾回收器,如ZGC,它几乎不会打断程序的运行

九、其他

1)序列化时,如果不想序列化某个字段,怎么做?
可以使用transient关键字标注这个字段,如private transient String password;
序列化时,该字段不会被写入,反序列化时该字段为默认值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值