一、Java 基础
1. 请简述 Java 中多态的实现方式
答案解析: Java 中多态主要通过继承、接口和方法重写来实现。
- 继承:子类继承父类,并重写父类的方法。当使用父类引用指向子类对象时,调用相同的方法会根据实际的子类对象执行不同的逻辑。例如:
-
class Animal {
-
public void makeSound() {
-
System.out.println("Animal makes a sound");
-
}
-
}
-
class Dog extends Animal {
-
@Override
-
public void makeSound() {
-
System.out.println("Dog barks");
-
}
-
}
-
public class Main {
-
public static void main(String[] args) {
-
Animal animal = new Dog();
-
animal.makeSound();
-
}
-
}
AI写代码
- 接口:一个类可以实现多个接口,通过接口引用指向实现类的对象,调用接口中定义的方法,实现不同的行为。例如:
-
interface Shape {
-
double area();
-
}
-
class Circle implements Shape {
-
private double radius;
-
public Circle(double radius) {
-
this.radius = radius;
-
}
-
@Override
-
public double area() {
-
return Math.PI * radius * radius;
-
}
-
}
-
class Rectangle implements Shape {
-
private double length;
-
private double width;
-
public Rectangle(double length, double width) {
-
this.length = length;
-
this.width = width;
-
}
-
@Override
-
public double area() {
-
return length * width;
-
}
-
}
-
public class Main {
-
public static void main(String[] args) {
-
Shape circle = new Circle(5);
-
Shape rectangle = new Rectangle(4, 6);
-
System.out.println(circle.area());
-
System.out.println(rectangle.area());
-
}
-
}
AI写代码
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
2. 解释 Java 中的静态绑定和动态绑定
答案解析:
- 静态绑定:也称为前期绑定,在编译时就确定了方法的调用。主要用于方法重载和静态方法调用。例如,对于方法重载,编译器根据方法调用时传递的参数类型和数量来确定具体调用哪个方法。
-
class Calculator {
-
public static int add(int a, int b) {
-
return a + b;
-
}
-
public static double add(double a, double b) {
-
return a + b;
-
}
-
public static void main(String[] args) {
-
int result1 = add(1, 2);
-
double result2 = add(1.5, 2.5);
-
}
-
}
AI写代码
- 动态绑定:也称为后期绑定,在运行时根据对象的实际类型来确定要调用的方法。主要用于方法重写。如前面多态的例子中,
Animal animal = new Dog(); animal.makeSound();
这里在运行时根据animal
实际指向的Dog
对象来调用Dog
类重写后的makeSound
方法。
3. Java 中如何实现浅拷贝和深拷贝
答案解析:
- 浅拷贝:创建一个新对象,新对象的属性和原对象相同,但对于引用类型的属性,新对象和原对象共享同一个引用。在 Java 中,实现浅拷贝可以通过实现
Cloneable
接口并重写clone
方法。
-
class Address {
-
String street;
-
public Address(String street) {
-
this.street = street;
-
}
-
}
-
class Person implements Cloneable {
-
String name;
-
Address address;
-
public Person(String name, Address address) {
-
this.name = name;
-
this.address = address;
-
}
-
@Override
-
protected Object clone() throws CloneNotSupportedException {
-
return super.clone();
-
}
-
}
-
public class Main {
-
public static void main(String[] args) throws CloneNotSupportedException {
-
Address address = new Address("123 Main St");
-
Person person1 = new Person("Alice", address);
-
Person person2 = (Person) person1.clone();
-
person2.address.street = "456 Elm St";
-
System.out.println(person1.address.street);
-
}
-
}
AI写代码
- 深拷贝:创建一个新对象,新对象的属性和原对象相同,对于引用类型的属性,会递归地创建新的对象。可以通过序列化和反序列化来实现深拷贝。
-
import java.io.*;
-
class Address implements Serializable {
-
String street;
-
public Address(String street) {
-
this.street = street;
-
}
-
}
-
class Person implements Serializable {
-
String name;
-
Address address;
-
public Person(String name, Address address) {
-
this.name = name;
-
this.address = address;
-
}
-
public Person deepClone() throws IOException, ClassNotFoundException {
-
ByteArrayOutputStream bos = new ByteArrayOutputStream();
-
ObjectOutputStream oos = new ObjectOutputStream(bos);
-
oos.writeObject(this);
-
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
-
ObjectInputStream ois = new ObjectInputStream(bis);
-
return (Person) ois.readObject();
-
}
-
}
-
public class Main {
-
public static void main(String[] args) throws IOException, ClassNotFoundException {
-
Address address = new Address("123 Main St");
-
Person person1 = new Person("Alice", address);
-
Person person2 = person1.deepClone();
-
person2.address.street = "456 Elm St";
-
System.out.println(person1.address.street);
-
}
-
}
AI写代码
二、集合框架
4. 比较 ArrayList
和 LinkedList
的优缺点
答案解析:
ArrayList
- 优点:
- 支持随机访问,通过索引可以快速访问元素,时间复杂度为 O(1)O(1)。
- 内存空间连续,相对紧凑,节省空间。
- 缺点:
- 插入和删除元素效率较低,尤其是在中间位置,需要移动大量元素,时间复杂度为 O(n)O(n)。
- 扩容时需要重新分配内存并复制元素,有一定的性能开销。
- 优点:
LinkedList
- 优点:
- 插入和删除元素效率高,只需要修改指针,时间复杂度为 O(1)O(1)(如果已知插入或删除位置)。
- 不需要连续的内存空间,适合频繁插入和删除的场景。
- 缺点:
- 不支持随机访问,访问元素需要从头或尾开始遍历,时间复杂度为 O(n)O(n)。
- 每个节点需要额外的指针来指向前一个和后一个节点,占用更多的内存空间。
5.
HashMap
在 JDK 1.8 中有哪些优化答案解析:
- 数据结构优化:JDK 1.8 之前
HashMap
采用数组 + 链表的结构,当链表过长时,查找效率会降低。JDK 1.8 引入了红黑树,当链表长度大于 8 且数组长度大于 64 时,链表会转换为红黑树,将查找、插入和删除操作的时间复杂度从 O(n)O(n) 降低到 O(logn)O(logn)。 - 哈希函数优化:对哈希函数进行了优化,通过
(h = key.hashCode()) ^ (h >>> 16)
使得哈希值的分布更加均匀,减少了哈希冲突的发生。 - 扩容机制优化:在扩容时,JDK 1.8 采用了更高效的方式,不需要重新计算每个元素的哈希值,而是根据原哈希值的某一位是 0 还是 1 来决定元素在新数组中的位置,减少了元素移动的次数。
-
6. 如何保证
HashMap
在多线程环境下的线程安全答案解析:
- 使用
Hashtable
:Hashtable
是线程安全的,它的所有方法都使用synchronized
关键字进行同步,保证了在同一时刻只有一个线程可以访问Hashtable
。但由于它的同步粒度较大,会影响性能。 -
-
import java.util.Hashtable;
-
public class Main {
-
public static void main(String[] args) {
-
Hashtable<String, Integer> hashtable = new Hashtable<>();
-
hashtable.put("one", 1);
-
int value = hashtable.get("one");
-
}
-
}
AI写代码
-
- 使用
Collections.synchronizedMap
:可以将一个非线程安全的HashMap
转换为线程安全的Map
。 -
-
import java.util.Collections;
-
import java.util.HashMap;
-
import java.util.Map;
-
public class Main {
-
public static void main(String[] args) {
-
Map<String, Integer> hashMap = new HashMap<>();
-
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(hashMap);
-
synchronizedMap.put("one", 1);
-
int value = synchronizedMap.get("one");
-
}
-
}
AI写代码
-
- 使用
ConcurrentHashMap
:ConcurrentHashMap
是 Java 并发包中的线程安全的Map
实现,它采用分段锁(JDK 1.7)或 CAS + synchronized(JDK 1.8)机制,在保证线程安全的同时,提高了并发性能。 -
-
import java.util.concurrent.ConcurrentHashMap;
-
public class Main {
-
public static void main(String[] args) {
-
ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
-
concurrentHashMap.put("one", 1);
-
int value = concurrentHashMap.get("one");
-
}
-
}
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
-
-
三、多线程与并发
7. 简述
Thread
和Runnable
的区别及使用场景答案解析:
- 区别:
Thread
是一个类,Runnable
是一个接口。- 继承
Thread
类的方式,由于 Java 是单继承的,会限制类的扩展性;而实现Runnable
接口的方式,一个类可以同时实现多个接口,扩展性更好。
- 使用场景:
- 当一个类需要继承其他类,同时又要实现多线程功能时,使用
Runnable
接口。 - 当一个类不需要继承其他类,且实现多线程逻辑相对简单时,可以继承
Thread
类。
- 当一个类需要继承其他类,同时又要实现多线程功能时,使用
-
-
// 继承 Thread 类
-
class MyThread extends Thread {
-
@Override
-
public void run() {
-
System.out.println("Running in MyThread");
-
}
-
}
-
// 实现 Runnable 接口
-
class MyRunnable implements Runnable {
-
@Override
-
public void run() {
-
System.out.println("Running in MyRunnable");
-
}
-
}
-
public class Main {
-
public static void main(String[] args) {
-
MyThread myThread = new MyThread();
-
myThread.start();
-
MyRunnable myRunnable = new MyRunnable();
-
Thread thread = new Thread(myRunnable);
-
thread.start();
-
}
-
}
AI写代码
8. 解释
volatile
关键字的作用和原理答案解析:
-
- 作用:
- 保证可见性:当一个变量被声明为
volatile
时,它会保证对该变量的写操作会立即刷新到主内存中,读操作会从主内存中读取最新的值。这样可以保证在多线程环境下,一个线程对volatile
变量的修改能够及时被其他线程看到。 - 禁止指令重排序:
volatile
关键字会在指令序列中插入内存屏障,防止编译器和处理器对指令进行重排序,保证代码的执行顺序符合程序员的预期。
- 保证可见性:当一个变量被声明为
- 原理:
volatile
关键字底层是通过内存屏障来实现的。在写操作时,会插入一个写屏障,将缓存中的数据刷新到主内存;在读操作时,会插入一个读屏障,从主内存中读取最新的数据。例如: -
-
class VolatileExample {
-
private volatile boolean flag = false;
-
public void writer() {
-
flag = true;
-
}
-
public void reader() {
-
if (flag) {
-
// 这里可以保证读取到的 flag 是最新值
-
}
-
}
-
}
AI写代码
9. 如何使用
CountDownLatch
实现线程同步答案解析:
CountDownLatch
是 Java 并发包中的一个同步工具类,它可以让一个或多个线程等待其他线程完成操作后再继续执行。-
import java.util.concurrent.CountDownLatch;
-
public class Main {
-
public static void main(String[] args) throws InterruptedException {
-
int threadCount = 3;
-
CountDownLatch latch = new CountDownLatch(threadCount);
-
for (int i = 0; i < threadCount; i++) {
-
new Thread(() -> {
-
try {
-
System.out.println(Thread.currentThread().getName() + " is working");
-
Thread.sleep(1000);
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
} finally {
-
latch.countDown();
-
}
-
}).start();
-
}
-
latch.await();
-
System.out.println("All threads have finished working");
-
}
-
}
AI写代码
在上述代码中,
CountDownLatch
的初始计数为 3,表示需要等待 3 个线程完成操作。每个线程完成工作后调用countDown
方法将计数减 1,主线程调用await
方法会阻塞,直到计数变为 0 才会继续执行。10. 简述
ReentrantLock
的可重入性和公平锁、非公平锁的区别答案解析:
-
- 可重入性:
ReentrantLock
是可重入锁,即同一个线程可以多次获取同一把锁,而不会造成死锁。每次获取锁时,锁的计数器会加 1,每次释放锁时,计数器会减 1,当计数器为 0 时,锁才会真正被释放。 -
-
import java.util.concurrent.locks.ReentrantLock;
-
public class Main {
-
private static ReentrantLock lock = new ReentrantLock();
-
public static void main(String[] args) {
-
lock.lock();
-
try {
-
System.out.println("First acquire the lock");
-
lock.lock();
-
try {
-
System.out.println("Acquire the lock again");
-
} finally {
-
lock.unlock();
-
}
-
} finally {
-
lock.unlock();
-
}
-
}
-
}
AI写代码
-
- 公平锁和非公平锁的区别:
- 公平锁:线程会按照请求锁的顺序依次获取锁,即先来先得。创建
ReentrantLock
时,通过new ReentrantLock(true)
可以创建公平锁。公平锁可以避免线程饥饿问题,但会降低系统的吞吐量。 - 非公平锁:线程在请求锁时,会直接尝试获取锁,而不考虑请求的顺序。如果锁刚好处于可用状态,即使有其他线程在等待,该线程也可以立即获取锁。通过
new ReentrantLock(false)
或默认构造函数new ReentrantLock()
创建的是非公平锁。非公平锁的吞吐量较高,但可能会导致某些线程长时间得不到锁。四、JVM
11. 简述 JVM 的内存结构
答案解析:
JVM 内存结构主要分为以下几个部分: - 程序计数器:是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,它是线程私有的。
- Java 虚拟机栈:也是线程私有的,它描述的是 Java 方法执行的内存模型。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法的调用和返回对应着栈帧的入栈和出栈操作。
- 本地方法栈:与 Java 虚拟机栈类似,不过它是为本地方法服务的。
- Java 堆:是 JVM 中最大的一块内存区域,所有对象实例和数组都在堆上分配内存。堆是线程共享的,是垃圾回收的主要区域。根据对象的存活周期,堆又可以分为新生代和老年代。
- 方法区:也是线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK 1.8 及以后,方法区被元空间(Metaspace)所取代,元空间使用的是本地内存。
-
12. 常见的垃圾回收算法有哪些,各有什么优缺点
答案解析:
- 标记 - 清除算法:
- 原理:先标记出所有需要回收的对象,然后在标记完成后统一回收所有被标记的对象。
- 优点:实现简单,不需要移动对象。
- 缺点:会产生大量的内存碎片,可能导致后续大对象无法分配到连续的内存空间。
- 复制算法:
- 原理:将内存分为大小相等的两块,每次只使用其中一块。当这一块内存用完时,将存活的对象复制到另一块,然后清除当前这块内存。
- 优点:不会产生内存碎片,回收效率高。
- 缺点:内存利用率低,只能使用一半的内存空间。
- 标记 - 整理算法:
- 原理:先标记出所有需要回收的对象,然后将存活的对象向一端移动,最后清理掉边界以外的内存。
- 优点:不会产生内存碎片,适合用于老年代。
- 缺点:需要移动对象,效率相对较低。
- 分代收集算法:
- 原理:根据对象的存活周期将内存分为新生代和老年代。新生代中对象存活率低,使用复制算法;老年代中对象存活率高,使用标记 - 清除或标记 - 整理算法。
- 优点:结合了不同算法的优点,提高了垃圾回收的效率。
- 缺点:需要对内存进行分区管理,增加了管理的复杂度。
-
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的即可免费获取
-
13. 如何进行 JVM 性能调优
-
答案解析:
- 调整堆内存大小:通过
-Xms
和-Xmx
参数分别设置堆内存的初始大小和最大大小,避免频繁的垃圾回收。例如,-Xms512m -Xmx512m
表示将堆内存的初始大小和最大大小都设置为 512MB。 - 调整新生代和老年代的比例:使用
-Xmn
参数设置新生代的大小,或者使用-XX:SurvivorRatio
参数调整新生代中 Eden 区和 Survivor 区的比例。例如,-Xmn256m
表示将新生代大小设置为 256MB,-XX:SurvivorRatio=8
表示 Eden 区和一个 Survivor 区的比例为 8:1。 - 选择合适的垃圾收集器:根据应用的特点和性能需求选择合适的垃圾收集器。例如,对于吞吐量要求较高的应用,可以选择 Parallel Scavenge 收集器;对于响应时间要求较高的应用,可以选择 CMS 或 G1 收集器。
- 分析 GC 日志:通过
-XX:+PrintGCDetails
、-XX:+PrintGCDateStamps
等参数开启 GC 日志记录,分析 GC 日志可以了解垃圾回收的频率、停顿时间等信息,从而找出性能瓶颈并进行优化。 -
14. 简述类加载的过程
答案解析:
类加载的过程主要分为以下几个阶段: - 加载:通过类的全限定名获取定义该类的二进制字节流,将字节流所代表的静态存储结构转化为方法区的运行时数据结构,并在内存中生成一个代表该类的
java.lang.Class
对象。 - 验证:确保被加载的类的字节码符合 JVM 的规范,不会危害 JVM 的安全。验证过程包括文件格式验证、元数据验证、字节码验证和符号引用验证等。
- 准备:为类的静态变量分配内存,并将其初始化为默认值。例如,
int
类型的静态变量会被初始化为 0,boolean
类型的静态变量会被初始化为false
等。 - 解析:将常量池中的符号引用替换为直接引用的过程。符号引用是一种以一组符号来描述所引用的目标,而直接引用是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。
- 初始化:执行类的初始化代码,包括静态变量的赋值和静态代码块的执行。初始化过程是类加载的最后一个阶段,只有在需要使用该类时才会进行初始化。
-
15. 什么是双亲委派模型,有什么作用
答案解析:
- 双亲委派模型:当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中。只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
- 作用:
- 避免类的重复加载:通过双亲委派模型,同一个类只会被加载一次,保证了类的唯一性。
- 保证 Java 核心类库的安全性:Java 的核心类库由启动类加载器加载,用户自定义的类加载器无法加载这些核心类,防止了恶意代码对核心类库的篡改。
-
五、数据库(以 MySQL 为例)
16. 简述索引的作用和原理
答案解析:
- 作用:索引可以提高数据库查询的效率,减少数据库的查询时间。通过在表的列上创建索引,数据库可以快速定位到满足查询条件的数据行,避免全表扫描。
- 原理:常见的索引数据结构是 B+ 树。B+ 树是一种平衡的多路搜索树,它将数据按照一定的顺序组织成树状结构。叶子节点包含了数据的指针,通过对树的遍历可以快速找到目标数据。在 B+ 树中,所有的数据都存储在叶子节点上,并且叶子节点之间通过指针相连,形成一个有序的链表,方便进行范围查询。例如,在一个用户表中,对
username
列创建索引,当执行SELECT * FROM users WHERE username = 'John';
时,数据库可以通过 B+ 树索引快速定位到username
为John
的数据行。 -
17. 解释数据库事务的 ACID 特性
答案解析:
- 原子性(Atomicity):事务是一个不可分割的操作单元,事务中的所有操作要么全部成功,要么全部失败。如果事务中的某个操作失败,整个事务会回滚到初始状态。例如,在银行转账操作中,从一个账户扣款和向另一个账户存款这两个操作必须作为一个事务来执行,要么都成功,要么都失败。
- 一致性(Consistency):事务执行前后,数据库的状态必须保持一致。也就是说,事务的执行不会破坏数据库的完整性约束。例如,在一个账户表中,所有账户的余额总和应该是一个固定的值,任何转账操作都不能改变这个总和。
- 隔离性(Isolation):事务之间应该相互隔离,一个事务的执行不应该受到其他事务的干扰。不同的隔离级别规定了事务之间的隔离程度,常见的隔离级别有读未提交、读已提交、可重复读和串行化。
- 持久性(Durability):一旦事务提交,它对数据库的修改应该是永久性的,即使数据库发生故障也不会丢失。通常通过日志文件来保证事务的持久性,当数据库发生故障时,可以通过日志文件进行恢复。
-
18. 如何优化 SQL 查询语句
答案解析:
- 合理使用索引:在经常用于查询条件、排序和连接的列上创建索引,但要注意避免创建过多索引,因为索引会占用额外的存储空间,并且在插入、更新和删除操作时会影响性能。
- 避免在索引列上使用函数或表达式:例如,
SELECT * FROM users WHERE YEAR(created_at) = 2023;
这样的查询会导致索引失效,应该尽量避免。可以改为SELECT * FROM users WHERE created_at >= '2023-01-01' AND created_at < '2024-01-01';
- 优化子查询:尽量使用连接查询代替子查询,因为子查询的性能通常较低。例如,
SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE country = 'USA');
可以改为SELECT o.* FROM orders o JOIN customers c ON o.customer_id = c.id WHERE c.country = 'USA';
- 分页查询优化:对于大偏移量的分页查询,可以采用延迟关联的方式,先通过子查询获取需要查询的行的主键,然后再根据主键关联查询出具体的数据。例如,
SELECT * FROM users LIMIT 10000, 10;
可以改为SELECT * FROM users WHERE id IN (SELECT id FROM users LIMIT 10000, 10);
-
19. 简述 MySQL 的主从复制原理
答案解析:
MySQL 的主从复制主要分为以下几个步骤: - 主库记录二进制日志:主库上的所有写操作都会记录到二进制日志(Binary Log)中,二进制日志包含了对数据库的所有修改信息。
- 从库连接主库并请求日志:从库通过 I/O 线程连接到主库,并请求主库发送二进制日志。
- 主库发送日志:主库接收到从库的请求后,通过 Binlog Dump 线程将二进制日志发送给从库。
- 从库接收并保存日志:从库的 I/O 线程将接收到的二进制日志保存到本地的中继日志(Relay Log)中。
- 从库执行日志中的操作:从库的 SQL 线程读取中继日志中的内容,并在从库上执行相应的 SQL 语句,从而实现主从数据的同步。
-
20. 如何处理 MySQL 中的死锁问题
答案解析:
- 合理设计事务:尽量缩短事务的执行时间,减少事务持有锁的时间。例如,将大事务拆分成多个小事务,避免长时间占用锁资源。
- 优化查询语句:通过合理使用索引,减少全表扫描,降低锁的竞争。例如,在查询时使用合适的索引,避免使用范围查询时产生间隙锁。
- 调整事务隔离级别:不同的事务隔离级别会影响锁的使用和并发性能。可以根据业务需求选择合适的隔离级别,例如,将隔离级别从可重复读调整为读已提交,减少锁的持有时间。
- 检测和处理死锁:MySQL 会自动检测死锁,并回滚其中一个事务来解除死锁。可以通过
SHOW ENGINE INNODB STATUS;
命令查看最近一次死锁的详细信息,分析死锁的原因并进行优化。 -
六、Spring 框架
21. 简述 Spring 的 IoC(控制反转)和 DI(依赖注入)
答案解析:
- IoC(控制反转):是一种设计原则,它将对象的创建和依赖关系的管理从应用程序代码中转移到 Spring 容器中。在传统的编程方式中,对象的创建和依赖关系的管理是由应用程序代码自己负责的;而在 IoC 模式下,对象的创建和依赖关系的管理由 Spring 容器负责,应用程序只需要从容器中获取所需的对象即可。
- DI(依赖注入):是 IoC 的一种具体实现方式,它通过在对象创建时将其依赖的对象注入到该对象中,实现对象之间的解耦。依赖注入可以通过构造函数注入、Setter 方法注入和接口注入等方式实现。例如:
-
-
import org.springframework.beans.factory.annotation.Autowired;
-
import org.springframework.stereotype.Component;
-
@Component
-
class UserService {
-
private UserRepository userRepository;
-
@Autowired
-
public UserService(UserRepository userRepository) {
-
this.userRepository = userRepository;
-
}
-
public void saveUser() {
-
userRepository.save();
-
}
-
}
-
@Component
-
class UserRepository {
-
public void save() {
-
System.out.println("Saving user...");
-
}
-
}
AI写代码
在上述代码中,
UserService
依赖于UserRepository
,通过构造函数注入的方式将UserRepository
注入到UserService
中。22. 解释 Spring 的 AOP(面向切面编程)及其应用场景
答案解析:
-
- AOP(面向切面编程):是一种编程范式,它允许在不修改目标对象代码的情况下,对方法的执行进行增强。AOP 通过将横切关注点(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,提高了代码的可维护性和可扩展性。
- 应用场景:
- 日志记录:在方法执行前后记录日志,方便调试和监控。
- 事务管理:在方法执行前后进行事务的开启、提交和回滚操作。
- 权限控制:在方法执行前检查用户的权限,只有具有相应权限的用户才能执行该方法。
- 性能监控:记录方法的执行时间,分析方法的性能瓶颈。
-
23. Spring Bean 的生命周期是怎样的
答案解析:
Spring Bean 的生命周期主要包括以下几个阶段: - 实例化:Spring 容器通过反射机制创建 Bean 的实例。
- 属性注入:将配置文件或注解中定义的属性值注入到 Bean 中。
- 初始化:如果 Bean 实现了
InitializingBean
接口,会调用其afterPropertiesSet
方法;如果配置了init - method
,会调用该方法进行初始化操作。 - 使用:Bean 可以被应用程序使用。
- 销毁:当 Bean 不再需要时,Spring 容器会销毁该 Bean。如果 Bean 实现了
DisposableBean
接口,会调用其destroy
方法;如果配置了destroy - method
,会调用该方法进行销毁操作。 -
24. 如何在 Spring 中实现事务管理
答案解析:
在 Spring 中可以通过编程式事务管理和声明式事务管理两种方式来实现事务管理。 - 编程式事务管理:通过编写代码来管理事务的开启、提交和回滚。例如:
-
-
import org.springframework.beans.factory.annotation.Autowired;
-
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
-
import org.springframework.stereotype.Service;
-
import org.springframework.transaction.TransactionDefinition;
-
import org.springframework.transaction.TransactionStatus;
-
import org.springframework.transaction.support.DefaultTransactionDefinition;
-
@Service
-
class UserService {
-
@Autowired
-
private DataSourceTransactionManager transactionManager;
-
public void saveUser() {
-
TransactionDefinition def = new DefaultTransactionDefinition();
-
TransactionStatus status = transactionManager.getTransaction(def);
-
try {
-
// 业务逻辑
-
transactionManager.commit(status);
-
} catch (Exception e) {
-
transactionManager.rollback(status);
-
}
-
}
-
}
AI写代码
-
- 声明式事务管理:通过注解或 XML 配置来管理事务。使用注解方式时,在需要进行事务管理的方法上添加
@Transactional
注解。例如: -
-
import org.springframework.stereotype.Service;
-
import org.springframework.transaction.annotation.Transactional;
-
@Service
-
class UserService {
-
@Transactional
-
public void saveUser() {
-
// 业务逻辑
-
}
-
}
AI写代码
25. 简述 Spring MVC 的工作流程
答案解析:
Spring MVC 的工作流程如下: -
- 客户端发送请求:客户端向服务器发送 HTTP 请求。
- 请求到达 DispatcherServlet:
DispatcherServlet
是 Spring MVC 的核心控制器,它接收所有的请求。 - 查找 HandlerMapping:
DispatcherServlet
根据请求的 URL 查找对应的HandlerMapping
,HandlerMapping
会返回处理该请求的Handler
(通常是一个Controller
类的方法)。 - 调用 HandlerAdapter:
DispatcherServlet
调用HandlerAdapter
来执行Handler
。HandlerAdapter
负责将请求参数绑定到Handler
的方法上,并调用该方法。 - 执行 Handler:
Handler
执行相应的业务逻辑,并返回一个ModelAndView
对象,包含了视图名称和模型数据。 - 查找 ViewResolver:
DispatcherServlet
根据视图名称查找对应的ViewResolver
,ViewResolver
会返回具体的View
对象。 - 渲染视图:
DispatcherServlet
将模型数据填充到View
中,并将渲染后的结果返回给客户端。 -
七、MyBatis
26. 简述 MyBatis 的工作原理
答案解析:
MyBatis 的工作原理主要包括以下几个步骤: - 读取配置文件:MyBatis 首先读取配置文件(如
mybatis - config.xml
)和映射文件(如UserMapper.xml
),配置文件中包含了数据库连接信息、插件配置等,映射文件中定义了 SQL 语句和 Java 对象之间的映射关系。 - 创建 SqlSessionFactory:根据配置文件创建
SqlSessionFactory
对象,SqlSessionFactory
是 MyBatis 的核心工厂类,用于创建SqlSession
。 - 创建 SqlSession:通过
SqlSessionFactory
创建SqlSession
对象,SqlSession
是与数据库交互的核心对象,它提供了执行 SQL 语句的方法。 - 执行 SQL 语句:通过
SqlSession
调用映射文件中定义的 SQL 语句,将参数传递给 SQL 语句进行预编译,然后执行 SQL 语句并将结果映射到 Java 对象中返回。 - 关闭 SqlSession:操作完成后,关闭
SqlSession
释放资源。 -
27. MyBatis 中的一级缓存和二级缓存有什么区别
答案解析:
- 一级缓存:是
SqlSession
级别的缓存,它存在于SqlSession
内部。在同一个SqlSession
中,对于相同的 SQL 语句和参数,只会执行一次查询,结果会缓存在一级缓存中,下次查询直接从缓存中获取。当SqlSession
关闭时,一级缓存会被清空。 - 二级缓存:是
Mapper
级别的缓存,它存在于Mapper
实例中,多个SqlSession
可以共享二级缓存。二级缓存需要手动开启,在映射文件中添加<cache />
标签即可。当一个SqlSession
执行了插入、更新或删除操作后,会清空二级缓
- 公平锁:线程会按照请求锁的顺序依次获取锁,即先来先得。创建
- 优点: