Java语言常用面试题

发布时间:2023-09-20 15:04
最后更新:2023-09-20 15:04
所属分类:
面试题集锦

这篇文章中所列出的面试题主要提供 Java 相关职位面试使用。其中主要包括 Java 的基础知识、进阶知识和 Spring 系列框架和工具的基础知识、进价知识。还包括一部分使用 Java 操作数据库的相关知识。

另外针对数据库的具体 SQL 编写以及数据库的性能调优等,请见收集数据库面试题的文章。

专题系列文章:

  1. 关于面试题集锦的使用
  2. IT系列职位通用常用面试题
  3. 基本IT知识常用面试题
  4. Go语言常用面试题
  5. Java语言常用面试题
  6. Python语言常用面试题
  7. 数据库知识常用面试题
  8. 前端技术栈常用面试题

Java 基本知识

什么是自动装箱与拆箱?

装箱:将基本类型用它们对应的引用类型包装起来。

拆箱:将包装类型转换为基本数据类型。

在一个静态方法内调用一个非静态成员为什么是非法的?

静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。

接口和抽象类的区别是什么?

  1. 接口的方法默认是 public,而抽象类可以有非抽象的方法。
  2. 接口中除了 staticfinal 变量,不能有其他变量,而抽象类中则不一定。
  3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。
  4. 接口方法默认修饰符是 public,抽象方法可以有 publicprotecteddefault 这些修饰符(抽象方法目标是为了被重写所以不能使用 private 关键字修饰!)。
  5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为规范。

==equals 的功能是相同的吗?

==: 作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型比较的是内存地址)。

equals(): 有两种使用情况,如果类没有覆盖 equals() 方法,则通过 equals() 比较该类的两个对象时,等价于通过 == 比较这两个对象。如果类覆盖了 equals() 方法,则通过自定义的比较策略来比较对象,此时不一定与 == 的结果相同。

你重写过 hashCodeequals 么,为什么重写 equals 时必须重写 hashCode 方法?

hashCode() 的作用是获取哈希码,也称为散列码。它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 在散列表中才有用,在其它情况下没用。在散列表中 hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。

HashSet 中如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让元素加入操作成功。所以如果俩个对象的 hashcode 相等时,equals() 将决定它们是否能够在 HashSet 中共存。

扩展问题
HashSet如何检查重复?

什么是深拷贝?什么是浅拷贝?

浅拷⻉:对基本数据类型进行值传递,对引用数据类型进行引用传递的拷⻉。

深拷⻉:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容。

扩展问题
什么操作会使用浅拷贝?

ArrayListLinkedList 区别?

  1. 是否保证线程安全:ArrayListLinkedList 都是不同步的,也就是不保证线程安全。
  2. 底层数据结构:Arraylist 底层使用的是 Object 数组; LinkedList 底层使用的是双向链表数据结构。
  3. 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行 add(E e) 方法的时候, ArrayList 会默认在将指 定的元素追加到此列表的末尾,这种情况时间复杂度就是 $O(1)$。但是如果要在指定位置 i 插入 和删除元素的话 add(int index, E element) 时间复杂度就为 $O(n-i)$。因为在进行上述操作 的时候集合中第 i 和第 i 个元素之后的 (n-i) 个元素都要执行向后/向前移一位的操作。LinkedList 采用链表存储,所以对于 add(​E e) 方法的插入,删除元素时间复杂度不受元素位 置的影响,近似 $O(1)$,如果是要在指定位置 i 插入和删除元素的话 (add(int index, E element) 时间复杂度近似为 $o(n)$ 因为需要先移动到指定位置再插入。
  4. 是否支持快速随机访问:LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。 快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) 方法)。
  5. 内存空间占用:ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
扩展问题
从数据库取回的数据集,适合使用哪种数据类型存放?为什么?

ConcurrentHashMapHashtable 的区别?

底层数据结构:JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap 的结构一样,数组+链表/红黑二叉树Hashtable 和JDK1.8之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。

实现线程安全的方式(重要):在JDK1.7的时候,ConcurrentHashMap (分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数 据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。Hashtable (同一把锁)使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

什么是线程死锁?如何避免死锁?

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

产生死锁必须具备以下四个条件:

  1. 互斥:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不可剥夺:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。

要避免死锁,只需要破坏上述四个条件中的任意一个即可。

在使用 Stream 语法的过程中,如何处理其中可能出现的 NullPointerException

Optional 是 Stream 语法的一部分,专门用来包裹可能出现空值的对象,所以可以在 Stream 处理过程中,将可能出现控制的对象使用 Optional 包裹。

【精通级别】简要介绍一下堆内存中对象分配的基本策略。

堆内存中分为 eden 区、s0 区、s1 区(属于新生代),tentired 区(属于老年代)。

大部分情况,对象都会首先在 eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的 年龄还会加 1 (eden 区移到 Survivor 区后对象的初始年龄变为 1 ),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。大对象和⻓期存活的对象会直接进入老年代。

【精通级别】Minor GC和Full GC 有什么不同?

新生代 GC (Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。当用于分配新对象的eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。

老年代 GC (Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC (并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

如何判断对象是否死亡?

引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。

可达性分析法:通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

简单阐述一下强引用,软引用,弱引用和虚引用。

强引用:使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那 就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

软引用:如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用主要通过使用 SoftReference 类来应用。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生

弱引用:如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。弱引用主要通过 WeakReference 类来应用。

虚引用:与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用主要通过 PhantomReference 类来应用。

【精通级别】简单描述一下类加载的过程,加载的时候都做了什么。

类加载过程:加载 → 连接 → 初始化。连接过程又可分为三步:验证 → 准备 → 解析。

类加载过程的第一步,主要完成下面 3 件事情:

  1. 通过全类名获取定义此类的二进制字节流。
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。

final 在 Java 中有什么作用?

  1. 声明一个常量:使用final关键字可以将变量声明为常量,即其值不能被修改。常量在声明时必须进行初始化,并且一旦初始化后就不能再被修改。
  2. 禁止继承:使用final关键字可以将类声明为最终类,即不能被其他类继承。
  3. 禁止方法重写:使用final关键字可以将方法声明为最终方法,即不能被子类重写。
  4. 线程安全:在多线程环境下,使用final关键字可以确保对象的状态不会被修改。当一个对象被声明为final时,它的状态在初始化后就不能被修改,从而避免了多线程并发修改的问题。

String 属于基础的数据类型吗?

不属于,String 是一个类,属于引用数据类型。

Java 中操作字符串都有哪些类?它们之间有什么区别?

  1. StringString 类是 Java 中用于表示字符串的类,它提供了一系列用于操作字符串的方法,如获取字符串的长度、比较字符串、拼接字符串等。
  2. StringBuilderStringBuilder 类是可变的字符串类,它提供了一系列用于操作字符串的方法,如添加字符串、插入字符串、删除字符串等。StringBuilder 类的方法是非线程安全的。
  3. StringBufferStringBuffer 类也是可变的字符串类,它与 StringBuilder 类类似,提供了一系列用于操作字符串的方法。不同的是,StringBuffer 类的方法是线程安全的,适用于多线程环境下的字符串操作。
  4. StringTokenizerStringTokenizer 类用于将字符串拆分成多个子串,可以按照指定的分隔符将字符串进行分割。它提供了一系列用于获取和遍历子串的方法。

【熟练级别】String str = "hello"String str = new String("hello") 是一样的吗?

不完全一样,虽然两者都创建了一个字符串对象,但是其在内存中的存储方式略有不同。

String str = "hello" 是使用一个字符串字面量创建一个字符串对象,这个字符串字面量会被放入字符串常量池中,字符串变量 str 实际上是对字符串常量池中的字符串的引用。

String str = new String("hello") 则会在堆内存中分配一个新的空间来存储字符串的指定内容。

在大部分情况下,两种用法的效果是相同的,但是在例如字符串比较等特殊情况下,会存在一定的差异。

如何将字符串反转?

反转一个字符串可以使用以下两种方法:

  1. 使用 StringBuilderStringBuffer:这两个类都提供了 reverse() 方法,可以用于反转字符串。将字符串转换为 StringBuilderStringBuffer 对象后调用 reverse() 方法即可。
  2. 使用递归:可以编写一个递归函数来反转字符串。递归函数的基本情况是当字符串的长度为 01 时,直接返回该字符串。否则,将字符串的第一个字符与剩余部分的反转字符串拼接起来。

抽象类必须要有抽象方法吗?

抽象类不一定需要包含抽象方法,但是如果一个类中包含有抽象方法,那么这个类必须声明为抽象类。

具体来说,当一个类声明为抽象类时,它表示这是一个不完整的类,需要子类继承并实现相应的方法才能变得完整。抽象类可以定义抽象方法,这些方法没有实现体,不同的子类可以根据自己的需求来实现抽象方法,但是抽象类也可以具有实例方法和变量,这些方法和变量是正常实现的。因此,抽象类只是具有一些抽象方法的普通类,这些方法需要子类去实现。

BIONIOAIO 之间有什么区别?

BIO(Blocking I/O)、NIO(Non-Blocking I/O)和AIO(Asynchronous I/O)是三种不同的 I/O 模型。

  1. BIO(Blocking I/O):BIO 是最传统的 I/O 模型,它是同步阻塞的方式。在 BIO 中,每个I/O操作都会阻塞当前线程,直到数据准备好或操作完成。这意味着在进行 I/O 操作时,线程会一直等待,无法进行其他任务。BIO 适用于连接数较少且请求处理时间较短的场景。
  2. NIO(Non-Blocking I/O):NIO 是 Java 1.4 引入的新的 I/O 模型,它是同步非阻塞的方式。在 NIO 中,通过使用选择器(Selector)和通道(Channel),可以实现一个线程处理多个 I/O 操作。当一个通道上的数据准备好时,线程可以进行其他任务,而不需要一直等待。NIO 适用于连接数较多且请求处理时间较长的场景。
  3. AIO(Asynchronous I/O):AIO 是 Java 1.7 引入的新的 I/O 模型,它是异步非阻塞的方式。在 AIO 中,I/O 操作的结果不需要通过轮询或回调来获取,而是通过 FutureCompletionHandler 来处理。当一个 I/O 操作完成时,操作系统会通知应用程序,应用程序可以继续进行其他任务。AIO 适用于连接数较多且请求处理时间较长的场景,且相较于 NIO,AIO 具有更好的性能。

抽象类能使用 final 修饰吗?

Java的抽象类中可以使用final修饰符。当一个抽象类被声明为final时,它不能被继承。这意味着没有任何类可以继承这个抽象类,也就不能重写它的方法。

Java 中的容器都有哪些?

在Java中,常见的容器类包括以下几种:

  1. List:List 是一个有序的集合,可以存储重复的元素。常见的实现类有 ArrayListLinkedList
  2. Set:Set 是一个不允许重复元素的集合,它没有固定的顺序。常见的实现类有 HashSetTreeSet
  3. Map:Map 是一个键值对的集合,每个键都是唯一的。常见的实现类有 HashMapTreeMap
  4. Queue:Queue 是一个先进先出(FIFO)的集合,通常用于实现队列。常见的实现类有 LinkedListArrayDeque
  5. Stack:Stack 是一个后进先出(LIFO)的集合,通常用于实现栈。常见的实现类有 LinkedList

如何决定使用 HashMap 还是 TreeMap

  • HashMapHashMap 是一种基于哈希表实现的Map,它提供了快速的插入、查找和删除操作。HashMap 不保证元素的顺序,即元素的顺序可能会发生变化。使用 HashMap 时,可以通过键值对的方式存储和获取数据。HashMap 适用于需要高效地进行查找、插入和删除操作,并且不关心元素的顺序的场景。
  • TreeMapTreeMap 是一种基于红黑树实现的有序Map,它按照键的自然顺序(或者自定义的比较器)对元素进行排序。TreeMap 保持了元素的顺序,因此可以按照键的顺序遍历元素。使用 TreeMap 时,可以通过键值对的方式存储和获取数据。TreeMap 适用于需要按照键的顺序进行遍历、查找和范围查询的场景。

总之,如果对元素的顺序没有特殊要求,并且需要高效地进行查找、插入和删除操作,可以使用 HashMap。如果需要按照键的顺序进行遍历、查找和范围查询,可以使用 TreeMap

如何实现数组和 List 之间的转换?

  • 将数组转换为 List 可以使用 Array 类中的静态方法 asList() 将数组转换为 List
  • List 转换为数组可以帅帅 List 接口中规定的 toArray() 方法将 List 中的内容转为数组。

ArrayListVector 之间的区别是什么?

ArrayListVector 都是可调整大小的数组实现的类,它们之间的区别主要有以下几点:

  1. 同步性: Vector 是线程安全的,而 ArrayList 是非线程安全的。这意味着在多线程环境下, Vector 可以安全地被多个线程同时访问和修改,而 ArrayList 在多线程环境下需要使用同步机制来确保线程安全。
  2. 性能:由于 Vector 是线程安全的,它的许多方法都使用了同步机制,这可能会导致一些性能上的开销。相比之下, ArrayList 不需要进行同步操作,因此在单线程环境下通常具有更好的性能。
  3. 增长方式:当元素数量超过初始容量时, VectorArrayList 都会自动增长容量。然而,它们的增长方式略有不同。 Vector 的增长方式是每次增加当前容量的一半,而 ArrayList 的增长方式是每次增加当前容量的一倍。这意味着在添加大量元素时, ArrayList 可能需要重新分配更多的内存空间。
  4. 初始容量: VectorArrayList 的初始容量默认为 10 ,但可以通过构造函数指定不同的初始容量。

如果在单线程环境下进行操作,并且不需要考虑线程安全性,通常推荐使用 ArrayList ,因为它具有更好的性能。如果在多线程环境下进行操作,或者需要确保线程安全性,可以使用 Vector

Iterator 怎么使用?有什么特点?

Iterator 是 Java 集合框架中的一个接口,用于遍历集合中的元素。通过 Iterator ,可以依次访问集合中的每个元素,并可以在遍历过程中进行删除操作。使用 Iterator 的步骤如下:

  1. 调用集合的 iterator() 方法获取 Iterator 对象。
  2. 使用 hasNext() 方法检查 Iterator 是否还有下一个元素。如果有下一个元素,则可以使用 next() 方法获取下一个元素。
  3. 针对每个元素进行相应的操作。
  4. 可选操作:可以使用 remove() 方法删除当前元素。

Iterator 的特点如下:

  1. 遍历方式: Iterator 提供了一种单向遍历集合的方式,只能向前遍历,无法回退或跳过元素。
  2. 删除操作: Iterator 可以在遍历过程中删除集合中的元素,通过调用 remove() 方法实现。注意,如果在调用 remove() 方法之前没有调用 next() 方法,或者在同一个元素上连续调用 remove() 方法两次,都会抛出 IllegalStateException 异常。
  3. 集合的修改:如果在使用 Iterator 遍历集合的过程中,对集合进行了修改(例如添加或删除元素),那么会导致 ConcurrentModificationException 异常。
  4. 效率:相比于使用普通的 for 循环遍历集合,使用 Iterator 遍历集合的效率更高,尤其是对于大型集合或需要删除元素的情况。

RunnableCallable 有什么区别?

在 Java 中, RunnableCallable 都是用于实现多线程的接口,它们之间有以下几点区别:

  1. 返回值: Runnablerun() 方法没有返回值,而 Callablecall() 方法可以返回一个结果。
  2. 异常处理: Runnablerun() 方法不能抛出任何异常,而 Callablecall() 方法可以抛出异常。
  3. 使用方式: Runnable 通常使用在通过 Thread 类创建和启动线程的方式中,而 Callable 通常使用在通过 ExecutorService 提交任务并获取任务执行结果的方式中。
  4. 并发性:使用 Runnable 时,可以通过多个线程同时执行多个 Runnable 实例,并发性较好。而使用 Callable 时,需要配合 Future 接口,可以通过获取 Future 对象来获取任务的执行结果。

Runnable 适用于不需要返回结果或抛出异常的场景,而 Callable 适用于需要返回结果或抛出异常的场景。同时,使用 Callable 可以更好地管理和控制任务的执行,并发性更好。

notify()notifyAll() 有什么区别?

在 Java 中, notify()notifyAll() 都是用于线程间通信的方法,它们之间的区别如下:

  1. 唤醒方式: notify() 方法用于唤醒在对象上等待的单个线程,而 notifyAll() 方法用于唤醒在对象上等待的所有线程。
  2. 竞争关系:当多个线程等待同一个对象的锁时,调用 notify() 方法只会唤醒其中一个线程,而调用 notifyAll() 方法会唤醒所有等待的线程。被唤醒的线程会进入就绪状态,并竞争对象的锁。
  3. 选择方式:在使用 notify()notifyAll() 时,应根据具体的需求来选择合适的方式。如果只需要唤醒一个线程,并且能够确定唤醒哪个线程,可以使用 notify() 方法。如果需要唤醒所有等待的线程,或者无法确定唤醒哪个线程,可以使用 notifyAll() 方法。

调用 notify()notifyAll() 方法只会唤醒等待的线程,并不会释放对象的锁。唤醒的线程会继续竞争对象的锁,并继续执行。此外,使用 wait() 方法使线程等待时,应在同步代码块中调用,以确保正确的竞争和唤醒关系。

线程的 run()start() 有什么区别?

  1. run() 方法是线程的执行体,定义线程要执行的代码逻辑。当线程对象通过调用 run() 方法时,它会在当前线程中执行 run() 方法中的代码。不会创建新的线程,而是在当前线程中执行。
  2. start() 方法用于启动一个新线程。当线程对象通过调用 start() 方法时,它会创建一个新的线程,并在新的线程中执行 run() 方法中的代码。 start() 方法会将线程放入就绪队列中,等待系统调度执行。
  3. start() 方法只能调用一次,如果多次调用 start() 方法,会抛出 IllegalThreadStateException 异常。而 run() 方法可以直接多次调用,但只会在当前线程中执行。

ThreadLocal 是什么?有哪些使用场景?

ThreadLocal 是 Java 中的一个类,它提供了线程局部变量的机制。线程局部变量是指每个线程都拥有自己独立的变量副本,互不干扰。 ThreadLocal 的使用场景如下:

  1. 线程上下文信息传递ThreadLocal 可以用于在多个方法之间传递线程上下文信息,避免显式传参的繁琐性。例如,在 Web 应用程序中,可以将用户信息存储在 ThreadLocal 中,在不同的方法中方便地获取和使用。
  2. 线程安全性保证ThreadLocal 可以用于在多线程环境下保证对象的线程安全性。通过将共享的可变对象存储在 ThreadLocal 中,每个线程都拥有自己独立的对象副本,避免了多线程访问共享对象时的竞争和同步开销。
  3. 数据库连接管理:在使用数据库连接池时,可以使用 ThreadLocal 来管理数据库连接。每个线程都可以从 ThreadLocal 中获取自己的数据库连接,避免了线程之间的竞争和同步问题。
  4. 线程级别的缓存ThreadLocal 可以用于实现线程级别的缓存。例如,在计算密集型的任务中,可以将计算结果存储在 ThreadLocal 中,下次需要时直接从 ThreadLocal 中获取,避免重复计算。

由于 ThreadLocal 的特性,使用不当可能会导致内存泄漏或数据不一致的问题。因此,在使用 ThreadLocal 时,需要注意及时清理 ThreadLocal 中的数据,避免出现意外情况。

synchronizedLock 之间有什么区别?

synchronizedLock 都是 Java 中用于实现线程同步的机制,它们之间的区别如下:

  1. 锁的获取方式synchronized 是通过关键字来实现的,它是隐式锁,即在代码中使用 synchronized 修饰的方法或代码块会自动获取和释放锁。而 Lock 是通过 Lock 接口的实现类来实现的,需要显式地调用 lock() 方法来获取锁,并在合适的位置调用 unlock() 方法来释放锁。
  2. 锁的可重入性synchronized 是可重入锁,即同一个线程在持有锁的情况下可以再次获取同一个锁。而 Lock 也是可重入锁,但需要注意,获取锁和释放锁的次数要匹配,否则可能会导致死锁。
  3. 锁的粒度synchronized 是对整个方法或代码块进行加锁,锁的粒度较粗。而 Lock 可以根据需求进行更细粒度的加锁,例如可以只对某个对象或资源进行加锁,提高并发性能。
  4. 锁的条件synchronized 在获取锁时,会自动阻塞等待锁的释放。而 Lock 可以通过 Condition 接口提供的 await()signal()signalAll() 方法实现更灵活的条件等待和唤醒。
  5. 锁的可中断性synchronized 在获取锁时,无法被中断,即使调用了线程的 interrupt() 方法。而 Lock 可以通过 lockInterruptibly() 方法实现可中断的获取锁操作。

总的来说, synchronized 是 Java 语言层面的内置机制,使用简单但灵活性较差。而 Lock 是 Java 提供的更灵活、可控制性更强的锁机制,适用于复杂的并发场景。

什么是反射?

反射是 Java 中的一种机制,它允许程序在运行时获取和操作类的信息。通过反射,可以在运行时动态地创建对象、调用方法、访问属性等。使用反射可以实现一些灵活的操作,例如在不知道类名的情况下创建对象,或者在运行时根据配置文件加载不同的类。

反射的核心类是 Class 类,它代表了一个类的运行时信息。通过 Class 类的实例,可以获取类的构造方法、方法、字段等信息,并进行相应的操作。通过反射,可以实现对类的动态调用和操作,但也增加了运行时的开销和复杂性,因此在使用反射时需要谨慎考虑性能和安全性的问题。

反射是一种强大但复杂的机制,不适合在普通的业务逻辑中频繁使用。

什么是动态代理?有哪些应用场景?如何实现一个动态代理?

动态代理是指在运行时动态生成代理类的机制,通过代理类来间接访问目标对象,从而实现对目标对象的控制和扩展。动态代理可以在不修改目标对象的情况下,对其方法进行增强或拦截,实现一些横切关注点(如日志记录、性能监控、事务管理等)的统一处理。

动态代理在 Java 中主要通过两种方式实现: JDK 动态代理和 CGLIB 动态代理。 JDK 动态代理是基于接口的代理,通过实现 InvocationHandler 接口和 Proxy 类来实现。 CGLIB 动态代理通过继承目标类,生成子类作为代理类,并通过重写父类方法来实现。

应用场景包括:

  1. AOP 编程:动态代理可以实现对业务逻辑的横切关注点的统一处理,例如事务管理、权限控制、日志记录等。
  2. 远程调用:通过动态代理可以实现远程方法调用,将方法调用转发到远程服务器上执行,实现分布式系统的功能。
  3. 延迟加载:通过动态代理可以实现延迟加载,即在需要时才真正创建对象,提高系统性能和资源利用率。
  4. 缓存代理:通过动态代理可以实现对方法的结果进行缓存,减少重复计算,提高系统响应速度。

动态代理的实现依赖于反射机制,会在一定程度上影响系统性能。

如何实现对象克隆?为什么要使用对象克隆?

对象克隆是指创建一个与原始对象具有相同状态的新对象。在 Java 中,可以通过以下两种方式实现对象克隆:

  1. 实现 Cloneable 接口:该接口是一个标记接口,没有任何方法。如果一个类实现了 Cloneable 接口,并重写了 Object 类的 clone() 方法,就可以使用 clone() 方法进行对象克隆。在 clone() 方法内部,通过调用 super.clone() 方法创建一个新对象,并将原始对象的字段值复制到新对象中。
  2. 使用序列化和反序列化:通过将对象序列化为字节流,然后再将字节流反序列化为新对象,实现对象的克隆。这种方式需要对象及其成员变量都实现 Serializable 接口,并使用 ObjectOutputStreamObjectInputStream 进行序列化和反序列化操作。

使用对象克隆的主要原因包括:

  1. 避免对象引用传递:当需要创建一个新对象,并且该对象的字段值与原始对象相同,但又不希望修改原始对象时,可以使用对象克隆来避免对象引用传递。
  2. 提高性能:有时候,创建一个新对象的成本比复制一个已有对象的成本更低。在这种情况下,可以使用对象克隆来提高性能。
  3. 保护对象的不可变性:如果一个对象是不可变的,并且在创建后不会发生任何更改,那么可以使用对象克隆来保护对象的不可变性。

对象克隆是一种浅拷贝方式,即只复制对象的字段值,而不复制对象引用的成员对象。如果需要实现深拷贝,即复制对象及其所有引用的成员对象,需要在 clone() 方法中进行相应的处理。

try ... catch 语句中,如果在 catch 语句中 执行了 return,那么 finally 还会执行吗?

会的。 finally 语句被设计为用来释放资源或者执行一些必要的清理操作,以确保资源的正确关闭和代码的健壮性,所以 finally 块中的代码无论在何种情况下都会被执行。

Spring 基本知识

为什么要使用 Spring?

使用 Spring 框架有以下几个主要的原因:

  1. 松耦合: Spring 框架采用了控制反转( IoC )和依赖注入( DI )的设计思想,通过容器来管理对象的生命周期和依赖关系,降低了组件之间的耦合度。这使得代码更易于维护、测试和扩展。
  2. 面向切面编程( AOP ): Spring 框架通过 AOP 模块提供了一种切面编程的方式,可以将与核心业务逻辑无关的横切关注点(如日志记录、事务管理、安全性控制等)从业务逻辑中抽离出来,使代码更加干净和可维护。
  3. 事务管理: Spring 框架提供了强大的事务管理支持,可以通过声明式事务管理来简化事务操作,减少了手动管理事务的复杂性。
  4. 简化开发: Spring 框架提供了丰富的功能和模块,如数据访问、 Web 开发、消息队列等,可以大大简化开发过程,提高开发效率。
  5. 测试支持: Spring 框架提供了测试支持,可以方便地进行单元测试和集成测试,保证代码的质量和稳定性。
  6. 开放性和可扩展性: Spring 框架是一个开放的框架,可以与其他框架和技术进行集成,如 Hibernate 、 MyBatis 、 Spring Boot 等。同时, Spring 框架也提供了扩展点和插件机制,可以根据具体需求进行扩展和定制。

使用 Spring 框架可以提高代码的可维护性、灵活性和可测试性,降低开发和维护的成本。

什么是 IOC?如何实现 IOC?

IOC ( Inversion of Control ,控制反转)是一种设计原则,它将对象的创建和依赖关系的管理从应用程序代码中解耦,交由容器来管理。在传统的编程模式中,应用程序代码负责创建对象并维护对象之间的依赖关系,而在 IOC 模式下,应用程序代码只需要定义对象的行为,而不需要关心对象的创建和依赖关系。

实现 IOC 的方式有很多种,其中一种常见的方式是使用依赖注入( Dependency Injection ,DI )。依赖注入是指通过构造函数、属性或方法参数的方式将依赖对象注入到目标对象中。在 IOC 容器中,通过配置文件或注解的方式定义对象的依赖关系,容器在创建对象时自动解析依赖并注入。

手动实现 IOC 可以定义一个容器类,该容器类负责创建和管理对象,并通过配置文件或代码来定义对象的依赖关系。在应用程序中,通过容器类来获取所需的对象,而不需要直接创建对象或维护对象之间的依赖关系。

什么是 AOP?

AOP ( Aspect-Oriented Programming ,面向切面编程)是一种编程范式,它通过将与核心业务逻辑无关的横切关注点(如日志记录、事务管理、安全性控制等)从业务逻辑中抽离出来,以模块化的方式进行封装和管理。 AOP 的核心思想是将这些横切关注点称为切面( Aspect ),并将它们与业务逻辑进行解耦。

在 AOP 中,切面由切点( Pointcut )和通知( Advice )组成。切点定义了在哪些位置插入通知,通知定义了在切点位置执行的动作。 AOP 框架会根据切点的定义,将通知插入到目标对象的方法调用中。

AOP 的实现方式主要有两种:静态代理和动态代理。静态代理是通过手动编写代理类来实现 AOP ,而动态代理是在运行时生成代理类,实现对目标对象的增强。

Spring 框架提供了强大的 AOP 支持。 Spring AOP 是基于动态代理实现的,通过配置文件或注解的方式定义切点和通知, Spring 框架会在运行时动态生成代理类,并在切点位置执行通知。这样,可以将与业务逻辑无关的横切关注点统一管理,提高代码的可维护性和可重用性。

Spring 中的 Bean 是线程安全的吗?

Spring 中的 Bean 的线程安全性取决于具体的 Bean 实现。 Spring 框架本身并不提供对 Bean 的线程安全性的保证,而是由开发者根据具体需求来确保 Bean 的线程安全。

如果一个 Bean 是无状态的,并且没有共享的可变状态或资源,那么该 Bean 可以被认为是线程安全的。无状态的 Bean 在多个线程之间共享时不会引发竞态条件或其他线程安全问题。

另一方面,如果一个 Bean 具有可变状态或共享资源,那么需要采取相应的措施来保证其线程安全性。可以使用同步机制(如锁或原子操作)来对访问共享资源的代码进行同步,或者使用线程安全的数据结构来存储共享状态。

此外, Spring 还提供了一些线程安全的作用域,如 prototype 作用域和 request 作用域。使用这些作用域可以确保每个线程都拥有一个独立的 Bean 实例,从而实现线程安全性。

Spring 支持几种 Bean 的作用域?

以下是 Spring 支持的一些常见作用域:

  1. Singleton (单例):默认的作用域,容器中只有一个 Bean 实例,所有请求都返回同一个实例。
  2. Prototype (原型):每次请求都会创建一个新的 Bean 实例,每个实例都是独立的。
  3. Request (请求):每个 HTTP 请求都会创建一个新的 Bean 实例,适用于 Web 应用程序。
  4. Session (会话):每个 HTTP 会话( Session )都会创建一个新的 Bean 实例,适用于 Web 应用程序。
  5. Global Session (全局会话):在基于 portlet 的 Web 应用程序中,每个 portlet 实例都有一个全局会话,每个全局会话都会创建一个新的 Bean 实例。
  6. Application (应用程序):每个 Servlet 上下文都会创建一个新的 Bean 实例。
  7. WebSocket ( Web 套接字):每个 WebSocket 会话都会创建一个新的 Bean 实例。

Spring 自动装配 Bean 有哪些方式?

在 Spring 中,自动装配 Bean 的方式有以下几种:

  1. byName :根据 Bean 的名称进行自动装配。容器会自动将与目标属性名称相同的 Bean 注入到目标 Bean 中。
  2. byType :根据 Bean 的类型进行自动装配。容器会自动将与目标属性类型相匹配的 Bean 注入到目标 Bean 中。如果存在多个匹配的 Bean ,会抛出异常。
  3. constructor :按照构造函数的参数类型进行自动装配。容器会自动将与目标构造函数参数类型相匹配的 Bean 注入到目标 Bean 中。
  4. autodetect :结合 byNamebyType 两种方式进行自动装配。首先根据 byName 方式进行自动装配,如果找不到匹配的 Bean ,则使用 byType 方式进行自动装配。
  5. no :禁止自动装配。需要手动通过配置文件或注解指定 Bean 的依赖关系。

可以通过在配置文件中使用 <bean> 标签的 autowire 属性或在类上使用 @Autowired 注解来指定自动装配的方式。默认情况下, Spring 使用 byType 方式进行自动装配。

Spring 中事务的实现有几种方式?

在 Spring 中,事务的实现方式有以下几种:

  1. 编程式事务管理:通过编写代码来管理事务,手动控制事务的开始、提交和回滚。这种方式需要显式地在代码中添加事务管理代码,较为繁琐。
  2. 声明式事务管理:通过配置文件或注解来声明事务的属性和行为,由 Spring 框架自动管理事务。这种方式将事务管理与业务逻辑分离,简化了代码的编写。
  3. 注解驱动事务管理:使用注解来声明事务的属性和行为,由 Spring 框架自动管理事务。使用注解可以更加简洁地定义事务,并将事务管理与业务逻辑紧密结合。
  4. 基于 AspectJ 的事务管理:使用 AspectJ 框架来实现事务管理。 AspectJ 提供了更加灵活和强大的事务管理功能,可以在编译期或运行时织入事务管理代码。

简单描述一下 Spring MVC 的启动流程和运行流程

在 Spring MVC 中,启动流程主要包括以下几个步骤:

  1. 容器初始化:当应用程序启动时, Servlet 容器(如 Tomcat )会创建一个 ServletContext 对象,并加载应用程序的 web.xml 配置文件。在 web.xml 中配置的 DispatcherServlet 将会被初始化。
  2. DispatcherServlet 初始化DispatcherServlet 是 Spring MVC 的核心组件,它继承自 HttpServlet ,并负责处理所有的 HTTP 请求。在初始化过程中, DispatcherServlet 会创建一个 Spring 应用上下文( ApplicationContext )对象,并加载应用程序的配置文件。
  3. 处理器映射器初始化:处理器映射器( HandlerMapping )是用于将请求映射到对应的处理器( Controller )的组件。在初始化过程中, DispatcherServlet 会根据配置文件中的信息,创建并初始化一个或多个处理器映射器。
  4. 处理器适配器初始化:处理器适配器( HandlerAdapter )是用于将请求交给对应的处理器进行处理的组件。在初始化过程中, DispatcherServlet 会根据配置文件中的信息,创建并初始化一个或多个处理器适配器。
  5. 视图解析器初始化:视图解析器( ViewResolver )是用于将处理器返回的逻辑视图名解析为具体的视图对象的组件。在初始化过程中, DispatcherServlet 会根据配置文件中的信息,创建并初始化一个或多个视图解析器。
  6. 初始化完成:当所有的组件初始化完成后, DispatcherServlet 将准备好接收并处理 HTTP 请求。此时, Spring MVC 的启动流程就完成了。

当用户产生了一个请求的时候, Spring MVC 就会使用一下的流程来处理用户的请求和产生响应:

  1. 客户端发送请求:当客户端发送 HTTP 请求时,请求会被发送到 DispatcherServlet
  2. DispatcherServlet 处理请求DispatcherServlet 是 Spring MVC 的核心组件,它接收到请求后会根据配置的处理器映射器( HandlerMapping )查找到对应的处理器( Controller )。
  3. 处理器处理请求:处理器会根据请求的 URL 、请求参数等信息执行相应的业务逻辑,并返回一个 ModelAndView 对象。
  4. 视图解析器解析视图DispatcherServlet 会将 ModelAndView 对象传递给配置的视图解析器( ViewResolver ),通过解析器将逻辑视图名解析为具体的视图对象。
  5. 视图渲染:视图对象将根据模型中的数据生成最终的 HTML 或其他类型的响应内容。
  6. 响应返回给客户端:生成的响应内容会被发送回客户端,客户端会显示相应的页面或执行相应的操作。

Spring MVC 中支持自动装配的注解有哪些?

在 Spring MVC 中,支持自动装配的注解主要有以下几种:

  1. @Autowired :这是 Spring 框架中最常用的注解之一。它可以对类成员变量、方法及构造函数进行标注,让 Spring 完成自动装配工作。
  2. @Resource :这个注解来自于 J2EE , Spring 支持它的使用。它可以标注在字段和 setter 方法上,表示将某个资源注入到标注的字段或 setter 方法中。
  3. @ModelAttribute :这个注解主要用于方法参数和方法的返回值。 Spring MVC 在处理请求时,会自动将模型数据填充到标注了 @ModelAttribute 的方法参数中。
  4. @RequestParam :这个注解用于方法参数,表示将请求参数绑定到标注了 @RequestParam 的方法参数中。
  5. @RequestBody :这个注解用于方法参数,表示将请求体绑定到标注了 @RequestBody 的方法参数中。
  6. @PathVariable :这个注解用于方法参数,表示将 URL 的一个片段绑定到标注了 @PathVariable 的方法参数中。
  7. @RequestHeader :这个注解用于方法参数,表示将请求头的一个属性绑定到标注了 @RequestHeader 的方法参数中。
  8. @CookieValue :这个注解用于方法参数,表示将 cookie 的一个属性绑定到标注了 @CookieValue 的方法参数中。

Spring Boot 基本知识

为什么要使用 Spring Boot?什么是 Spring Boot?

Spring Boot 是一种用于快速构建独立的、生产级的 Spring 应用程序的开源框架。它基于 Spring 框架,通过提供默认的配置和约定大于配置的原则,简化了 Spring 应用程序的开发和部署过程。 Spring Boot 提供了自动配置、起步依赖、命令行界面等特性,可以大大提高开发效率。它还集成了常用的开发工具和第三方库,如 Spring Data 、 Spring Security 、 Thymeleaf 等,使得开发者可以更轻松地构建现代化的 Web 应用程序。

使用 Spring Boot 可以带来以下几个优势:

  1. 简化配置: Spring Boot 通过自动配置的方式,减少了开发者在配置文件中编写大量的配置代码。它提供了默认的配置,可以快速启动和运行应用程序,减少了开发和部署的时间和工作量。
  2. 快速启动: Spring Boot 提供了起步依赖( Starter )的概念,通过引入特定的 Starter ,可以一次性引入一组相关的依赖库,简化了依赖管理的过程。这样,开发者可以更快地启动和运行应用程序。
  3. 内嵌服务器: Spring Boot 内置了多个常用的 Web 服务器,如 Tomcat 、 Jetty 等,可以直接打包并运行应用程序,无需额外安装和配置服务器。这减少了部署的复杂性,并提高了应用程序的运行效率。
  4. 健康检查: Spring Boot 提供了健康检查的功能,可以通过访问特定的 URL 来监控应用程序的运行状态。这对于运维人员来说非常有用,可以及时发现并解决应用程序的问题。
  5. 丰富的社区支持: Spring Boot 拥有庞大的社区支持,官方提供了详细的文档和示例,开发者可以轻松找到解决问题的方法。同时,社区中也有很多优秀的开源项目和插件,可以帮助开发者更好地使用和扩展 Spring Boot 。

Spring Boot 的核心配置文件是什么?

在Spring Boot中,核心配置文件是 application.propertiesapplication.yml 。这些文件用于配置应用程序的属性和行为。在这些配置文件中,可以设置数据库连接、日志级别、端口号等应用程序相关的配置。Spring Boot会自动加载这些配置文件,并根据配置信息进行相应的初始化和配置。

Spring Boot 有哪些方式可以实现热部署?

在 Spring Boot 中,有以下几种方式可以实现热部署:

  1. 使用 Spring DevTools : Spring DevTools 是一个开发工具,可以实现应用程序的热部署。它可以监控项目的 classpath ,并在检测到文件变化时自动重启应用程序。通过在 pom.xml 文件中添加 Spring Boot DevTools 依赖,并在 IDE 中启用自动构建,即可实现热部署。
  2. 使用 Spring Loaded : Spring Loaded 是一个热部署工具,可以在不重启应用程序的情况下重新加载修改后的类。通过在 pom.xml 文件中添加 Spring Loaded 依赖,并在 IDE 中启用自动构建,即可实现热部署。
  3. 使用 JRebel : JRebel 是一个商业化的热部署工具,可以在不重启应用程序的情况下实时加载修改后的类。它支持多种 Java 框架,包括 Spring Boot 。通过在 pom.xml 文件中添加 JRebel 插件,并在 IDE 中启用 JRebel ,即可实现热部署。

上述热部署方式都需要在开发环境中使用,并不适用于生产环境。在生产环境中,建议使用传统的部署方式,将修改后的代码打包成 war 或 jar 文件,并重新部署应用程序。

为什么 Spring Boot 发布的 JAR 包可以不依赖容器?

Spring Boot 发布的 JAR 包可以不依赖容器,主要是因为 Spring Boot 内置了一个嵌入式的 Web 服务器,如 Tomcat 或 Jetty 。这个嵌入式服务器可以直接运行 Spring Boot 应用程序,无需额外安装和配置外部的 Web 容器。通过将应用程序打包成可执行的 JAR 文件,可以将所有的依赖和配置文件一同打包,并且可以通过命令行或脚本的方式直接运行。这种方式可以简化部署和运行的过程,提高应用程序的运行效率。

此外, Spring Boot 还提供了 Spring Boot Actuator 模块,可以通过 HTTP 请求监控和管理应用程序的运行状态,这也是 Spring Boot 可以独立运行的一个重要特性。

Spring Cloud 基本知识

什么是 Spring Cloud?

Spring Cloud 是一个用于构建分布式系统的开源框架。它基于 Spring Boot 和 Spring Cloud Netflix 等项目,提供了一系列的工具和组件,用于简化分布式系统的开发和部署。 Spring Cloud 提供了服务注册与发现、负载均衡、断路器、分布式配置等功能,使得开发者可以更方便地构建和管理微服务架构。

Spring Cloud 还集成了常用的分布式系统开发工具和组件,如 Eureka 、 Ribbon 、 Hystrix 等,可以快速搭建高可用、高性能的分布式系统。

Spring Cloud 的核心组件有哪些?

在 Spring Cloud 中,核心组件包括以下几个:

  1. Eureka : Eureka 是一个服务注册与发现的组件,用于管理和跟踪服务的状态和位置。服务提供者通过向 Eureka 注册自己的信息,而服务消费者可以从 Eureka 获取可用的服务列表。
  2. Ribbon : Ribbon 是一个客户端负载均衡的组件,用于在服务消费者中实现负载均衡。它可以根据一定的规则从多个服务提供者中选择一个进行调用。
  3. Feign : Feign 是一个声明式的 HTTP 客户端,用于简化服务消费者的开发。通过使用 Feign ,开发者可以像调用本地方法一样调用远程服务。
  4. Hystrix : Hystrix 是一个容错和断路器的组件,用于处理分布式系统中的故障和延迟。它可以防止故障扩散,并提供了故障处理和容错机制。
  5. Zuul : Zuul 是一个网关组件,用于实现动态路由、负载均衡和安全过滤等功能。通过使用 Zuul ,开发者可以将请求转发到不同的微服务中。
  6. Config : Config 是一个分布式配置管理的组件,用于集中管理应用程序的配置信息。开发者可以将配置信息存储在 Git 仓库或其他存储介质中,并通过 Config 服务器提供给其他微服务。
这里只是列出了比较常用的一些组件,面试时回答其他的组件也是可以的。

Spring Cloud 中断路器(熔断器)的作用是什么?

在 Spring Cloud 中,断路器( Hystrix )的作用是处理分布式系统中的故障和延迟。它可以防止故障扩散,并提供了故障处理和容错机制。断路器可以监控服务的调用情况,当服务调用失败或超时时,自动打开断路器,停止向该服务发起请求,避免对系统造成更大的负担。同时,断路器还提供了降级和熔断的功能,可以在服务不可用时,返回预设的默认值或执行备选逻辑,确保系统的可用性和稳定性。通过使用断路器,开发者可以更好地处理分布式系统中的故障和延迟问题,提高系统的容错能力和稳定性。

一个服务如何定位和发现其所依赖的另一个服务?

在 Spring Cloud 中,服务定位和发现主要依赖于服务注册与发现机制。常用的组件有 Eureka 、 Consul 、 Zookeeper 等。

在一个 Spring Cloud 应用启动时,它会自动将自身的信息(如服务名、服务实例 id 、服务实例的 IP 和端口等)注册到指定的服务注册中心。其他的服务可以通过在应用启动时添加服务发现机制(如使用 Spring Cloud 的 RestTemplate 或者 WebClient 进行 HTTP 请求)来获取这些信息。

以 Eureka 为例,首先需要在应用的配置文件( application.ymlbootstrap.yml )中配置 Eureka 服务端的地址,然后在需要调用其他服务的地方使用 Spring Cloud 的 RestTemplateWebClient 向 Eureka 服务端发送请求,获取其他服务的信息。

ORM 相关

你都使用过哪些 ORM 框架?有什么显著的特点?

在 Spring Boot 框架中,常用的 ORM 框架主要有两种:JPA 和 MyBatis 。

  1. JPA ( Java Persistence API ):JPA 是 Spring Boot 中常用的 ORM 框架之一。它是 Java EE 标准的一部分,可以与 Spring 框架很好地集成。 JPA 通过使用注解来映射 Java 类与数据库表之间的映射关系,使得开发者无需编写大量的 SQL 语句即可进行数据库操作。同时, JPA 还支持自定义查询语句、事务管理等功能,是一个非常强大和灵活的 ORM 框架。
  2. MyBatis :MyBatis 是另一种常见的 ORM 框架,它是一种半自动化的 ORM 框架,需要手动编写 SQL 语句并放在 XML 文件中,然后通过 MyBatis 的映射配置将其映射到 Java 对象上。虽然需要编写更多的代码,但 MyBatis 更加灵活,可以直接使用原生的 SQL 语句,对于复杂的数据库操作来说更加方便。此外, MyBatis 还支持事务管理、缓存等高级功能。

JPA 和 MyBatis 都有各自的优点和适用场景。如果需要更多的自动化和便利性,可以选择使用 JPA ;如果需要更加灵活和直接的控制,可以选择使用 MyBatis 。

什么是 ORM 框架?

ORM 框架( Object Relational Mapping ,对象关系映射)是一个介于面向对象编程语言(如 Java 、 Python )与关系型数据库之间的映射,将数据库的表映射到编程语言所对应的类中。通过 ORM 框架,开发者可以使用面向对象的方式操作数据库,避免直接编写 SQL 语句,从而实现更高效、易用的数据库访问。

ORM 框架的核心思想是将表实体和表之间的关系相互转化。它可以将数据库中的每一条数据映射到对应的编程语言对象中,使得开发者可以通过操作对象的方式来处理数据库中的数据。同时, ORM 框架也支持将编程语言中的对象映射到数据库中的表中,实现动态查询和处理数据的功能。

常见的 ORM 框架包括 Hibernate 、 MyBatis 、 Entity Framework 等,它们都提供了丰富的 API 和查询语法,支持各种数据库操作,开发者可以根据项目需求和个人经验选择适合自己的 ORM 框架。

JPA 和 Hibernate 有什么区别?

JPA 和 Hibernate 都是 Java 的 ORM 框架,它们都提供了将 Java 对象与数据库表进行映射的功能,但两者之间还是存在一些主要的区别。

  1. 规范与实现: JPA 是一个规范,而 Hibernate 是 JPA 的一个实现。 JPA 通过 Java 社区进程( JCP )协同开发,作为一个 Java 规范请求( JSR )发布,而 Hibernate 则是 Red Hat 对 JPA 规范的具体实现。
  2. 操作符与 SQL 语句: JPA 可以自动生成 SQL 语句并自动执行,这使得 Java 程序员可以更多地使用对象编程思维来操作数据库。而 Hibernate 则更进一步,它提供了许多操作符和 SQL 语句的映射,使得开发者可以更加方便地用 HQL ( Hibernate Query Language )进行查询。
  3. 扩展性和性能: Hibernate 的性能可能会比 JPA 更好,因为它针对 JDBC 进行了轻量级的封装,并且提供了许多优化选项。同时, Hibernate 具有更好的扩展性,因为它支持各种方言,可以方便地与其他框架进行集成。
  4. 状态管理: Hibernate 和 JPA 在实体状态管理方面存在差异。 Hibernate 提供了强大的状态管理能力,可以清晰地区分出瞬态、持久化、游离等状态,更符合实际业务需求。而 JPA 则将状态管理交由开发者自行处理。
  5. 映射方式: JPA 通过注解和 XML 进行实体与数据库表的映射,而 Hibernate 只使用注解进行映射,不使用 XML 。

MyBatis 中有几种分页方式?

  1. 数组分页。在进行数据库查询操作时,获取到数据库中所有满足条件的记录,保存在应用的临时数组中,再通过 ListsubList 方法,获取到满足条件的所有记录。
  2. sql 语句分页。通过 sql 语句实现分页也非常简单,改变查询的语句,即在 sql 语句后面添加 limit 分页语句就可以实现分页。
  3. RowBounds 分页。通过 RowBounds 实现分页和通过数组方式分页原理差不多,都是一次获取所有符合条件的数据,然后在内存中对大数据进行操作,实现分页效果。 RowBounds 建议在数据量相对较小的情况下使用。
  4. 拦截器分页。

MyBatis 的逻辑分页和物理分页的区别是什么?

MyBatis 中的逻辑分页和物理分页主要在以下三个方面存在区别:

  1. 分页方式:逻辑分页是通过在代码中使用游标分页来实现的,而物理分页则是通过在数据库中直接使用分页 SQL 语句(如 MySQL 的 LIMIT 和 ORACLE 的 ROWNUM )来实现的。
  2. 性能开销:物理分页每次查询都需要访问数据库,对数据库造成较大负担。而逻辑分页只需要在第一次查询时获取全部数据,然后在内存中对数据进行处理,因此如果数据量较大,逻辑分页的性能开销会相对较大。
  3. 实时性:物理分页每次查询都需要访问数据库,能够获取数据库的最新状态,实时性强。而逻辑分页则是在内存中对数据进行处理,实时性相对较差,如果数据发生改变,可能需要重新查询才能获取最新的数据状态。

如果对性能开销要求较高,且希望能够实时获取最新数据状态的场景下,建议选择物理分页。而在数据量较小,且对实时性要求不高的场景下,可以选择逻辑分页。

MyBatis 的延迟加载原理是什么?

MyBatis 的延迟加载原理主要基于代理对象的生成和拦截器的使用。

当 MyBatis 设置 lazyLoadingEnabledtrue 时,启用延迟加载。在调用目标对象的属性时,如果该属性为 null , MyBatis 会单独触发一个事件,保存与该属性相关的 SQL 语句,并执行该 SQL 语句查询数据。查询结果会被保存起来,而目标对象则会被设置一个代理对象。

当再次访问该属性时, MyBatis 会拦截这个属性访问,进入拦截器方法。在拦截器方法中, MyBatis 会先检查该属性是否需要延迟加载。如果需要, MyBatis 就会用保存的 SQL 语句查询数据,然后将查询结果保存到目标对象的相应属性中,完成延迟加载。


索引标签
面试题
java