ChatGPT带你刷Java经典面试题

131 阅读27分钟

Java基础

1. 为什么重写 equals 还要重写 hashcode?

重写 equals 方法是为了比较两个对象的内容是否相等,而重写 hashCode 方法是为了在使用哈希表等数据结构时能够正确地存储和查找对象。因为哈希表等数据结构是根据对象的哈希码来存储和查找的,如果两个对象的哈希码不同,那么即使它们的内容相等,也会被认为是不同的对象。因此,为了保证对象在使用哈希表等数据结构时能够正确地存储和查找,需要同时重写 equals 和 hashCode 方法。

2. == 和 equals 比较的区别

== 是比较两个对象的引用是否相同,也就是比较它们是否指向同一个内存地址。而 equals 方法是比较两个对象的内容是否相等,也就是比较它们的属性值是否相同。因此,当我们想要比较两个对象的内容时,应该使用 equals 方法,而不是 == 运算符。

3. 为啥有时会出现 4.0 - 3.6 = 0.40000001 这种现象?

这是因为在计算机中,浮点数的存储和运算都是有误差的。浮点数采用二进制表示,而有些十进制数无法精确地转化为二进制数,因此在进行运算时会产生舍入误差。这种误差是由于浮点数的精度有限,无法表示所有的实数,所以会出现一些近似值。如果需要进行高精度计算,可以考虑使用 BigDecimal 类型。

4. final 关键字的作用

final 关键字可以用于修饰类、方法和变量,其作用如下:

  • 当用 final 修饰类时,该类不能被继承。
  • 当用 final 修饰方法时,该方法不能被子类重写。
  • 当用 final 修饰变量时,该变量只能被赋值一次,即成为常量。

使用 final 修饰的类、方法或变量具有不可改变性,可以提高代码的安全性和可读性。同时,使用 final 修饰的常量也可以被视为代码中的魔数,使代码更易于维护和修改。

5. 介绍 Java 的集合类

Java 中的集合类是一组用于存储和操作一组对象的类。它们提供了各种数据结构,如列表、集、映射等,可以轻松地操作和管理数据。Java 集合框架包括以下接口和类:

  • Collection 接口:表示一组对象,提供了添加、删除、查找等操作。
  • List 接口:表示一个元素的有序集合,可以通过索引访问元素。
  • Set 接口:表示一组不重复的元素,不保证元素的顺序。
  • Map 接口:表示一组键值对,可以通过键来访问值。
  • ArrayList 类:实现了可调整大小的数组,可以动态添加和删除元素。
  • LinkedList 类:实现了链表,可以在任意位置添加和删除元素。
  • HashSet 类:基于哈希表实现的 Set 接口,可以快速地添加和查找元素。
  • TreeSet 类:基于红黑树实现的 Set 接口,可以对元素进行排序。
  • HashMap 类:基于哈希表实现的 Map 接口,可以快速地添加和查找键值对。
  • TreeMap 类:基于红黑树实现的 Map 接口,可以对键进行排序。

Java 集合类提供了丰富的操作方法和算法,可以满足各种数据处理需求。使用集合类可以提高代码的可读性和可维护性,减少代码量,并且能够更加高效地处理数据。

6. ArrayList 和 LinkedList 的区别

ArrayList 和 LinkedList 都是 Java 集合框架中的 List 接口的实现类,它们的主要区别在于底层数据结构的不同。

ArrayList 是基于动态数组实现的,它可以随机访问元素,因此在随机访问元素时效率较高。但是在插入和删除元素时,需要移动其他元素,因此效率较低。

LinkedList 是基于双向链表实现的,它可以在任意位置添加和删除元素,并且不需要移动其他元素,因此在插入和删除元素时效率较高。但是在随机访问元素时,需要从头或尾开始遍历链表,因此效率较低。

因此,如果需要随机访问元素的场景,应该使用 ArrayList;如果需要频繁地添加和删除元素的场景,应该使用 LinkedList。

7. java 8 有哪些新特性?

Java 8 的新特性包括:

  1. Lambda 表达式和函数式接口:可以使用 Lambda 表达式来简化代码,同时引入了函数式接口,使得函数式编程更加容易。

  2. Stream API:提供了一种新的处理集合和数组的方式,可以通过流式操作来处理数据,使得代码更加简洁和易于维护。

  3. 新的日期和时间 API:提供了一组全新的日期和时间类,使得处理日期和时间更加简单和可读。

  4. 接口的默认方法和静态方法:可以在接口中定义默认方法和静态方法,使得接口的功能更加丰富。

  5. 方法引用:可以使用方法引用来简化代码,使得代码更加简洁和易于维护。

  6. Optional 类:可以用来处理空值,避免了空指针异常的出现。

  7. Nashorn JavaScript 引擎:可以在 Java 程序中运行 JavaScript 代码。

这些新特性使得 Java 8 更加现代化和强大,使得开发人员能够更加方便地编写高效、简洁、可读性强的代码。

8. HashMap 在 jdk 1.7 和 1.8 的区别?

HashMap 在 JDK 1.7 和 1.8 中的主要区别如下:

  1. 存储结构不同

JDK 1.7 中的 HashMap 底层实现是基于数组和链表的组合,即链表散列。而在 JDK 1.8 中,当链表长度超过阈值时,链表会转化为红黑树,即树形散列,以提高查找效率。

  1. 扩容机制不同

JDK 1.7 中的 HashMap 在扩容时采用的是“头插法”,即新节点插入到链表的头部。而在 JDK 1.8 中,采用的是“尾插法”,即新节点插入到链表的尾部或红黑树的节点上。

  1. 并发处理方式不同

JDK 1.7 中的 HashMap 在并发环境下可能会出现死循环或数据丢失等问题,因此需要使用线程安全的 ConcurrentHashMap。而在 JDK 1.8 中,HashMap 在并发环境下使用了一种新的方式来处理冲突,即使用红黑树来代替链表,以提高并发处理能力。

  1. 计算 hash 值的方式不同

JDK 1.7 中的 HashMap 在计算 hash 值时采用的是“扰动函数”,即通过位运算来混淆 hash 值。而在 JDK 1.8 中,采用了一种新的方式来计算 hash 值,即使用了更加高效的算法,以提高性能。

总体来说,JDK 1.8 中的 HashMap 在性能和并发处理能力方面都有所提升,同时也增加了一些新特性,如红黑树等。

并发

1. volatile 关键字的作用

volatile 关键字的作用是使得多个线程之间对该变量的读写操作可见,即保证了可见性。当一个变量被 volatile 修饰时,它会被立即刷入主内存中,并清空本地缓存和其他线程的工作内存中的值。这样,在其他线程中读取该变量时,就能够得到最新的值,而不是过期的值。

volatile 关键字还可以保证一定的有序性,即禁止指令重排序。在编写多线程程序时,由于指令重排序可能会导致程序出现意想不到的结果,因此需要使用 volatile 关键字来保证有序性。

需要注意的是,volatile 关键字只能保证对单个变量的读写操作具有原子性,而不能保证对多个变量的复合操作具有原子性。如果需要保证复合操作的原子性,需要使用其他的同步机制,如 synchronized 关键字或 Lock 接口等。

2. 使用线程池有哪些好处?

使用线程池的好处包括:

  1. 提高系统性能

线程池可以避免频繁地创建和销毁线程,从而减少了系统开销,提高了系统的性能。

  1. 更好地管理线程

线程池可以更好地管理线程,包括线程的数量、状态、优先级等,从而更加灵活地控制线程的执行。

  1. 提供更好的响应性

线程池可以提供更好的响应性,因为线程池中的线程已经预先创建好了,可以立即执行任务,而不需要等待线程的创建和启动。

  1. 提供更好的可靠性

线程池可以提供更好的可靠性,因为它可以自动处理线程异常和错误,从而避免了由于线程异常导致的系统崩溃等问题。

  1. 节省资源

线程池可以限制系统中的线程数量,从而避免了过多的线程占用系统资源,导致系统资源耗尽的问题。

3. 线程池参数如何设置?

线程池参数的设置需要根据具体的业务场景和系统资源情况来确定,一般包括以下几个参数:

  1. 核心线程数

核心线程数是线程池中最小的线程数,当提交任务时,如果当前线程数小于核心线程数,则会创建新的线程来执行任务。一般情况下,核心线程数的设置应该根据系统的 CPU 核心数和内存大小来确定,以充分利用系统资源。

  1. 最大线程数

最大线程数是线程池中最大的线程数,当提交任务时,如果当前线程数已经达到最大线程数,则会将任务加入到等待队列中。最大线程数的设置应该根据系统的负载情况和可用资源来确定,以避免过多的线程占用系统资源。

  1. 等待队列

等待队列是用来存储等待执行的任务的队列,当提交任务时,如果当前线程数已经达到最大线程数,则会将任务加入到等待队列中。等待队列的大小应该根据系统负载情况和可用内存来确定,以避免等待队列过大而导致内存耗尽。

  1. 空闲线程存活时间

空闲线程存活时间是指当线程处于空闲状态时,超过一定时间后就会被销毁。空闲线程存活时间的设置应该根据系统的负载情况和可用资源来确定,以避免过多的空闲线程占用系统资源。

  1. 拒绝策略

拒绝策略是指当等待队列已满并且当前线程数已经达到最大线程数时,如何处理新提交的任务。常见的拒绝策略包括直接抛出异常、丢弃任务、丢弃等待时间最长的任务和将任务交给调用者处理等。拒绝策略的选择应该根据具体的业务场景和系统需求来确定。

示例如下:

// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        10,  // 核心线程数
        50,  // 最大线程数
        60, TimeUnit.SECONDS,  // 空闲线程存活时间
        new ArrayBlockingQueue<>(1000),  // 等待队列
        new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);

// 提交任务
executor.execute(() -> {
    // 执行任务代码
});

// 关闭线程池
executor.shutdown();

以上示例中,创建了一个核心线程数为 10,最大线程数为 50,等待队列大小为 1000 的线程池,空闲线程存活时间为 60 秒,拒绝策略为将任务交给调用者处理。提交任务时,使用 execute() 方法提交一个 Runnable 对象,执行任务代码在 Runnable 对象的 run() 方法中实现。关闭线程池时,使用 shutdown() 方法关闭线程池。

4. 什么是线程安全问题,如何解决?

线程安全问题是指当多个线程同时访问共享资源时,可能会出现数据不一致、数据丢失、死锁等问题。常见的线程安全问题包括竞态条件、死锁、饥饿等。

解决线程安全问题的方法有很多,常用的方法包括:

  1. 加锁

加锁是最常用的解决线程安全问题的方法之一。通过加锁可以保证同一时间只有一个线程访问共享资源,从而避免了数据不一致、数据丢失等问题。Java 中常用的锁包括 synchronized 关键字和 Lock 接口。

  1. 使用线程安全的数据结构

Java 中提供了很多线程安全的数据结构,例如 ConcurrentHashMap、CopyOnWriteArrayList 等,这些数据结构内部已经实现了加锁等线程安全机制,可以直接使用。

  1. 使用原子类

Java 中提供了很多原子类,例如 AtomicInteger、AtomicLong 等,这些类提供了原子操作,可以保证同一时间只有一个线程修改共享变量。

  1. 使用可重入锁

可重入锁是一种特殊的锁,允许同一个线程多次获取同一个锁,从而避免了死锁等问题。Java 中的 ReentrantLock 就是一种可重入锁。

  1. 避免共享资源

如果可能的话,可以尽量避免共享资源,例如使用局部变量代替全局变量等。这样可以避免因共享资源导致的线程安全问题。

5. 介绍 synchronized 的锁升级机制

synchronized 是 Java 中最常用的锁机制之一,它提供了一种简单的方式来实现线程安全。在 Java 6 及以前的版本中,synchronized 的实现采用的是重量级锁,也就是每次加锁都需要获取对象的监视器锁,并且需要进行用户态和内核态之间的切换,这种方式效率较低。

从 Java 6 开始,synchronized 的实现引入了锁升级机制,通过将锁从无锁状态升级为偏向锁、轻量级锁、重量级锁三种状态,来提高锁的性能。锁的升级过程如下:

  1. 无锁状态

当一个对象被创建时,它的对象头中的 Mark Word 被初始化为 0,表示该对象处于无锁状态。

  1. 偏向锁状态

当一个线程第一次访问一个对象时,JVM 会在对象头中设置线程 ID,表示该对象被该线程偏向。此时该对象处于偏向锁状态。

  1. 轻量级锁状态

当一个线程尝试获取一个偏向于另一个线程的偏向锁时,偏向锁就会升级为轻量级锁。轻量级锁是通过 CAS 操作来实现的,它可以避免多次切换用户态和内核态。

  1. 重量级锁状态

当多个线程竞争同一个锁时,轻量级锁就会升级为重量级锁。重量级锁是通过操作系统内核来实现的,需要进行用户态和内核态之间的切换。

通过锁升级机制,synchronized 可以在大部分情况下避免使用重量级锁,从而提高了性能。

6. CopyOnWriteArrayList 适用于哪种场景?

CopyOnWriteArrayList 适用于读多写少的场景。它是 ArrayList 的线程安全版本,内部使用了一种写时复制的技术,即在写操作时,先将原有数据复制一份,然后在新的数据上进行修改,最后使用新的数据替换旧的数据。这样可以保证读操作不需要加锁,从而提高了读取性能。

由于每次写操作都需要复制一份数据,因此 CopyOnWriteArrayList 不适用于写操作频繁的场景。另外,由于每次写操作都会创建一个新的副本,因此 CopyOnWriteArrayList 会占用较多的内存空间,不适用于数据量较大的场景。

常见的使用场景包括:读多写少的缓存、事件监听器列表等。

JVM

1. 介绍 JVM 的内存模型?

JVM 的内存模型定义了 Java 程序中各个变量、对象等数据在内存中的存储方式。JVM 内存模型主要分为线程私有部分和线程共享部分两部分。

线程私有部分主要包括程序计数器、虚拟机栈和本地方法栈。程序计数器用于记录当前线程执行的字节码指令地址,虚拟机栈用于存储方法的局部变量、操作数栈、动态链接、方法出口等信息,本地方法栈用于支持 Native 方法的执行。

线程共享部分主要包括堆和方法区。堆用于存储 Java 对象实例,是 JVM 中最大的一块内存区域。堆可以被所有线程共享,但是访问堆中的数据需要进行同步操作,因此会影响性能。方法区用于存储类信息、常量、静态变量等数据,也是所有线程共享的。

JVM 内存模型中还有一个重要的概念是 happens-before 关系,它定义了在多线程环境下,一个操作对另一个操作的可见性。如果操作 A happens-before 操作 B,那么 A 对 B 的结果可见。JVM 内存模型通过 happens-before 关系来保证多线程环境下的正确性。

JVM 内存模型是 Java 并发编程的基础,了解 JVM 内存模型对于编写高效、正确的并发程序非常重要。

2. JVM 内存为什么要分代?

JVM 内存分代是为了更好地管理内存,提高程序的性能。根据经验,不同的对象具有不同的生命周期,有些对象很快就会被垃圾回收,而有些对象则可能存活很长时间。因此,JVM 将内存分为不同的代,每一代都具有不同的生命周期和垃圾回收策略。

JVM 内存分为新生代和老年代。新生代用于存储新创建的对象,它又分为 Eden 区和两个 Survivor 区。当 Eden 区满时,会触发一次 Minor GC,将存活的对象复制到 Survivor 区中。在 Survivor 区中经过多次复制后,仍然存活的对象会被晋升到老年代中。

老年代用于存储长时间存活的对象。当老年代空间不足时,会触发一次 Full GC,对整个堆进行垃圾回收。Full GC 的成本比 Minor GC 高得多,因此尽量减少 Full GC 的次数是非常重要的。

通过将内存分为不同的代,JVM 可以根据不同的生命周期和垃圾回收策略来管理内存,提高程序的性能和可靠性。

3. 介绍一次完整的 GC 流程

一次完整的 GC 流程包括以下步骤:

  1. 初始标记:首先,GC 算法会暂停所有应用程序的线程,然后标记所有的根对象(如静态变量、虚拟机栈中引用的对象等),并标记它们直接引用的对象。这个过程需要遍历整个对象图,但是由于只标记了根对象,因此速度比较快。

  2. 并发标记:在初始标记之后,GC 算法会启动一个并发线程,继续遍历对象图,标记所有可达的对象。由于此时应用程序线程已经恢复运行,因此在并发标记过程中可能会有新的对象被创建或者被标记为垃圾,需要特别处理。

  3. 重新标记:在并发标记完成之后,GC 算法会再次暂停应用程序线程,重新标记所有在并发标记过程中产生的新对象,并确定这些对象是否是可达的。由于这个过程只需要标记那些在并发标记过程中产生的新对象,因此速度比较快。

  4. 清除:最后,GC 算法会清除所有未被标记的对象,并回收它们占用的内存空间。这个过程可能会导致内存碎片问题,需要特别处理。

在整个 GC 过程中,应用程序线程会被暂停多次,因此 GC 的性能和效率是非常重要的。同时,在并发标记过程中需要特别处理新对象的产生和可达性变化,以确保 GC 的正确性。

4. 介绍双亲委派模型,为什么需要它?

双亲委派模型是 Java 类加载器的一种工作机制,它规定了类加载器的层次结构和加载类的顺序。具体来说,当一个类需要被加载时,首先会委托它的父类加载器去加载,如果父类加载器无法完成加载,才会由当前类加载器自行加载。这个过程会一直递归到顶层的启动类加载器,如果仍然无法完成加载,则会抛出 ClassNotFoundException 异常。

双亲委派模型的优点在于可以保证类的唯一性和安全性。由于每个类加载器都只负责自己的命名空间,因此不同的类加载器可以加载同名但不同版本的类,从而避免了类冲突的问题。同时,由于系统类库都是由启动类加载器加载的,因此可以保证系统类库的安全性和稳定性。

双亲委派模型的实现使用了组合模式,即每个类加载器都有一个父类加载器,所有的类加载器最终都会委托给顶层的启动类加载器。这种层次结构可以有效地避免了重复加载和类冲突的问题。

总之,双亲委派模型是 Java 类加载器的一种重要机制,它保证了类的唯一性和安全性,避免了类冲突和安全漏洞问题。

设计模式

1. 单例模式有哪些实现方式?有哪些优缺点?请手写其中一种

单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点。常见的单例模式实现方式包括:

  1. 饿汉式单例:在类加载时就创建好实例,线程安全,但可能会浪费资源。

  2. 懒汉式单例:在第一次使用时创建实例,线程不安全,需要加锁或使用双重校验锁来保证线程安全。

  3. 枚举单例:利用枚举类型的特性,保证只有一个实例,并且是线程安全的。

  4. 静态内部类单例:利用静态内部类的特性,保证只有一个实例,并且是线程安全的。

单例模式的优点在于可以减少系统中的对象数量,节省内存空间和系统资源。但过度使用单例模式也会导致系统耦合度过高,降低代码的可测试性和可维护性。

以下是懒汉式单例的示例代码:

public class Singleton {
    private static volatile Singleton instance = null;

    private Singleton() {
        // 私有构造方法
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

在上面的代码中,使用了双重校验锁来保证线程安全。其中 volatile 关键字用于保证 instance 变量的可见性。

2. 你用过哪些设计模式,为什么用它?

常用的设计模式有23种,它们分为三类:创建型模式、结构型模式和行为型模式。

其中,创建型模式主要用于对象的创建,包括工厂方法模式、抽象工厂模式、单例模式、建造者模式和原型模式;

结构型模式主要用于类和对象的组合,包括适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式和享元模式;

行为型模式主要用于对象之间的通信和协作,包括策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式和中介者模式。

每个设计模式都有其特定的用途和优缺点,根据具体的需求选择合适的设计模式可以提高代码的可重用性、可维护性和可读性。

例如,工厂方法模式可以帮助我们根据不同的需求创建不同的对象,单例模式可以保证只有一个实例,观察者模式可以监听并响应事件等等。

在实际开发中,我们可以根据具体情况选择合适的设计模式来解决问题,提高代码的质量和效率。

开发框架

1. Spring 的 IOC 和 AOP 是什么,有哪些优点?

Spring 的 IOC(控制反转)和 AOP(面向切面编程)是 Spring 框架的两个核心特性。

IOC 是一种设计模式,它将对象的创建和依赖关系的管理转移到容器中,而不是由应用程序代码直接控制。在 Spring 中,IOC 通过 BeanFactory 或 ApplicationContext 来实现,它们负责创建和管理对象,应用程序只需要声明依赖关系即可。IOC 的优点在于可以降低应用程序的耦合度,提高代码的可重用性和可维护性。

AOP 是一种编程范式,它通过将横切关注点(如日志、事务、安全性等)从业务逻辑中分离出来,实现了代码的模块化和重用。在 Spring 中,AOP 通过切面(Aspect)来实现,切面可以在方法执行前、执行后或抛出异常时插入通知(Advice),从而实现横切关注点的功能。AOP 的优点在于可以提高代码的模块化程度,降低代码的重复度,同时也可以提高代码的可读性和可维护性。

综上所述,Spring 的 IOC 和 AOP 是 Spring 框架的两个核心特性,它们可以大大提高代码的可重用性、可维护性和可读性。

2. Spring 框架用到了哪些设计模式?

Spring 框架用到了很多设计模式,其中一些常见的设计模式包括:

  1. 工厂模式:Spring 中的 BeanFactory 和 ApplicationContext 就是工厂模式的应用,它们负责创建和管理对象。

  2. 单例模式:Spring 中的 Bean 默认是单例的,这是通过在容器中维护一个对象池来实现的。

  3. 代理模式:Spring 中的 AOP 就是代理模式的应用,它通过动态代理来实现横切关注点的功能。

  4. 模板方法模式:Spring 中的 JdbcTemplate 和 HibernateTemplate 就是模板方法模式的应用,它们封装了一些通用的数据访问逻辑,由子类来实现具体的数据访问细节。

  5. 观察者模式:Spring 中的事件机制就是观察者模式的应用,它允许一个对象监听另一个对象的状态变化,并在状态变化时得到通知。

  6. 适配器模式:Spring 中的适配器模式主要应用在 Spring MVC 中,它允许一个控制器处理多个请求,并将请求参数转换成方法参数。

总之,Spring 框架是一个非常成熟的框架,它利用了很多经典的设计模式来实现各种功能。熟悉这些设计模式可以帮助我们更好地理解 Spring 框架的工作原理。

3. 介绍 Spring Bean 的生命周期

Spring Bean 的生命周期包括以下阶段:

  1. 实例化:当容器接收到创建 Bean 的请求时,会使用 Java 反射机制实例化 Bean 对象。

  2. 属性赋值:在实例化后,容器会使用反射机制为 Bean 的属性赋值,包括使用构造函数注入、Setter 方法注入、注解注入等方式。

  3. Aware 接口回调:如果 Bean 实现了 Aware 接口,容器会在属性赋值后回调相应的 Aware 接口方法,例如 BeanNameAware、ApplicationContextAware 等。

  4. BeanPostProcessor 前置处理器:容器会对实现了 BeanPostProcessor 接口的类进行实例化,并调用 postProcessBeforeInitialization() 方法对 Bean 进行前置处理。

  5. 初始化:容器会调用 Bean 的初始化方法,可以通过 @PostConstruct 注解或实现 InitializingBean 接口来指定初始化方法。

  6. BeanPostProcessor 后置处理器:容器会对实现了 BeanPostProcessor 接口的类进行实例化,并调用 postProcessAfterInitialization() 方法对 Bean 进行后置处理。

  7. 使用:Bean 初始化完成后,可以被容器使用。

  8. 销毁:当容器关闭或销毁 Bean 时,会调用 Bean 的销毁方法,可以通过 @PreDestroy 注解或实现 DisposableBean 接口来指定销毁方法。

上述生命周期中,开发者可以通过实现相应的接口或注解来自定义 Bean 的初始化和销毁方法,并在初始化和销毁阶段进行一些自定义的操作。

4. MyBatis 如何实现延迟加载?

MyBatis 实现延迟加载的方式是使用代理对象,当查询语句执行时,只查询主对象的信息,而关联对象的信息并不会被查询。当需要使用关联对象时,MyBatis 会根据代理对象的配置再次查询数据库并返回关联对象的信息。

具体来说,MyBatis 实现延迟加载的方式有两种:

  1. 延迟加载(lazy loading):在查询主对象时,只查询主对象的信息,而关联对象的信息并不会被查询。当需要使用关联对象时,再次查询数据库并返回关联对象的信息。这种方式需要在关联对象的属性上添加 @One(select = "xxx") 或 @Many(select = "xxx") 注解,并将 fetchType 属性设置为 LAZY。

  2. 嵌套查询(nested queries):在查询主对象时,同时查询关联对象的信息。这种方式需要在关联对象的属性上添加 @One(select = "xxx", fetchType = FetchType.EAGER) 或 @Many(select = "xxx", fetchType = FetchType.EAGER) 注解,并将 fetchType 属性设置为 EAGER。

延迟加载可以减少不必要的数据库查询,提高查询效率,但同时也会增加代码复杂度和维护难度。开发者可以根据具体情况选择合适的方式来实现延迟加载。

5. 介绍 MyBatis 的多级缓存机制

MyBatis 的多级缓存机制包括三级缓存:一级缓存、二级缓存和三级缓存。

  1. 一级缓存:也称为本地缓存,是 SqlSession 级别的缓存,其生命周期与 SqlSession 相同。当查询语句执行时,查询结果会被缓存在 SqlSession 中,当再次查询相同的语句时,直接从 SqlSession 中获取结果,不再查询数据库。一级缓存默认开启,可以通过配置文件或代码进行关闭。

  2. 二级缓存:也称为全局缓存,是 Mapper 级别的缓存,其生命周期与 Mapper 相同。当查询语句执行时,查询结果会被缓存在二级缓存中,当再次查询相同的语句时,直接从二级缓存中获取结果,不再查询数据库。二级缓存需要手动开启,并且需要在 Mapper.xml 文件中进行配置。

  3. 三级缓存:是基于集群的分布式缓存,其生命周期与应用程序相同。当查询语句执行时,查询结果会被缓存在分布式缓存中,当再次查询相同的语句时,直接从分布式缓存中获取结果,不再查询数据库。三级缓存需要手动开启,并且需要使用第三方分布式缓存组件(如 Ehcache、Redis 等)。

在使用 MyBatis 进行开发时,可以根据具体的需求选择合适的缓存策略来提高查询效率和性能。一级缓存和二级缓存是 MyBatis 内置的缓存机制,可以提高查询效率和性能;而三级缓存是基于集群的分布式缓存机制,可以提高系统的可扩展性和稳定性。