这篇文章中所列出的面试题主要提供 Java 相关职位面试使用。其中主要包括 Java 的基础知识、进阶知识和 Spring 系列框架和工具的基础知识、进价知识。还包括一部分使用 Java 操作数据库的相关知识。
另外针对数据库的具体 SQL 编写以及数据库的性能调优等,请见收集数据库面试题的文章。
专题系列文章:
Java 基本知识
什么是自动装箱与拆箱?
装箱:将基本类型用它们对应的引用类型包装起来。
拆箱:将包装类型转换为基本数据类型。
在一个静态方法内调用一个非静态成员为什么是非法的?
静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
接口和抽象类的区别是什么?
- 接口的方法默认是
public
,而抽象类可以有非抽象的方法。 - 接口中除了
static
、final
变量,不能有其他变量,而抽象类中则不一定。 - 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过
extends
关键字扩展多个接口。 - 接口方法默认修饰符是
public
,抽象方法可以有public
、protected
和default
这些修饰符(抽象方法目标是为了被重写所以不能使用private
关键字修饰!)。 - 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为规范。
==
与 equals
的功能是相同的吗?
==
: 作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型比较的是内存地址)。
equals()
: 有两种使用情况,如果类没有覆盖 equals()
方法,则通过 equals()
比较该类的两个对象时,等价于通过 ==
比较这两个对象。如果类覆盖了 equals()
方法,则通过自定义的比较策略来比较对象,此时不一定与 ==
的结果相同。
你重写过 hashCode
和 equals
么,为什么重写 equals
时必须重写 hashCode
方法?
hashCode()
的作用是获取哈希码,也称为散列码。它实际上是返回一个 int
整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()
在散列表中才有用,在其它情况下没用。在散列表中 hashCode()
的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
在 HashSet
中如果发现有相同 hashcode
值的对象,这时会调用 equals()
方法来检查 hashcode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让元素加入操作成功。所以如果俩个对象的 hashcode
相等时,equals()
将决定它们是否能够在 HashSet
中共存。
什么是深拷贝?什么是浅拷贝?
浅拷⻉:对基本数据类型进行值传递,对引用数据类型进行引用传递的拷⻉。
深拷⻉:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容。
ArrayList
与 LinkedList
区别?
- 是否保证线程安全:
ArrayList
和LinkedList
都是不同步的,也就是不保证线程安全。 - 底层数据结构:
Arraylist
底层使用的是Object
数组;LinkedList
底层使用的是双向链表数据结构。 - 插入和删除是否受元素位置的影响:
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)$ 因为需要先移动到指定位置再插入。 - 是否支持快速随机访问:
LinkedList
不支持高效的随机元素访问,而ArrayList
支持。 快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。 - 内存空间占用:
ArrayList
的空间浪费主要体现在在list
列表的结尾会预留一定的容量空间,而LinkedList
的空间花费则体现在它的每一个元素都需要消耗比ArrayList
更多的空间(因为要存放直接后继和直接前驱以及数据)。
ConcurrentHashMap
和 Hashtable
的区别?
底层数据结构: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
,竞争会越来越激烈效率越低。
什么是线程死锁?如何避免死锁?
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
产生死锁必须具备以下四个条件:
- 互斥:该资源任意一个时刻只由一个线程占用。
- 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不可剥夺:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。
要避免死锁,只需要破坏上述四个条件中的任意一个即可。
在使用 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 件事情:
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口。
final
在 Java 中有什么作用?
- 声明一个常量:使用final关键字可以将变量声明为常量,即其值不能被修改。常量在声明时必须进行初始化,并且一旦初始化后就不能再被修改。
- 禁止继承:使用final关键字可以将类声明为最终类,即不能被其他类继承。
- 禁止方法重写:使用final关键字可以将方法声明为最终方法,即不能被子类重写。
- 线程安全:在多线程环境下,使用final关键字可以确保对象的状态不会被修改。当一个对象被声明为final时,它的状态在初始化后就不能被修改,从而避免了多线程并发修改的问题。
String
属于基础的数据类型吗?
不属于,String
是一个类,属于引用数据类型。
Java 中操作字符串都有哪些类?它们之间有什么区别?
String
类:String
类是 Java 中用于表示字符串的类,它提供了一系列用于操作字符串的方法,如获取字符串的长度、比较字符串、拼接字符串等。StringBuilder
类:StringBuilder
类是可变的字符串类,它提供了一系列用于操作字符串的方法,如添加字符串、插入字符串、删除字符串等。StringBuilder
类的方法是非线程安全的。StringBuffer
类:StringBuffer
类也是可变的字符串类,它与StringBuilder
类类似,提供了一系列用于操作字符串的方法。不同的是,StringBuffer
类的方法是线程安全的,适用于多线程环境下的字符串操作。StringTokenizer
类:StringTokenizer
类用于将字符串拆分成多个子串,可以按照指定的分隔符将字符串进行分割。它提供了一系列用于获取和遍历子串的方法。
【熟练级别】String str = "hello"
与 String str = new String("hello")
是一样的吗?
不完全一样,虽然两者都创建了一个字符串对象,但是其在内存中的存储方式略有不同。
String str = "hello"
是使用一个字符串字面量创建一个字符串对象,这个字符串字面量会被放入字符串常量池中,字符串变量 str
实际上是对字符串常量池中的字符串的引用。
String str = new String("hello")
则会在堆内存中分配一个新的空间来存储字符串的指定内容。
在大部分情况下,两种用法的效果是相同的,但是在例如字符串比较等特殊情况下,会存在一定的差异。
如何将字符串反转?
反转一个字符串可以使用以下两种方法:
- 使用
StringBuilder
或StringBuffer
类:这两个类都提供了reverse()
方法,可以用于反转字符串。将字符串转换为StringBuilder
或StringBuffer
对象后调用reverse()
方法即可。 - 使用递归:可以编写一个递归函数来反转字符串。递归函数的基本情况是当字符串的长度为
0
或1
时,直接返回该字符串。否则,将字符串的第一个字符与剩余部分的反转字符串拼接起来。
抽象类必须要有抽象方法吗?
抽象类不一定需要包含抽象方法,但是如果一个类中包含有抽象方法,那么这个类必须声明为抽象类。
具体来说,当一个类声明为抽象类时,它表示这是一个不完整的类,需要子类继承并实现相应的方法才能变得完整。抽象类可以定义抽象方法,这些方法没有实现体,不同的子类可以根据自己的需求来实现抽象方法,但是抽象类也可以具有实例方法和变量,这些方法和变量是正常实现的。因此,抽象类只是具有一些抽象方法的普通类,这些方法需要子类去实现。
BIO
、NIO
、AIO
之间有什么区别?
BIO(Blocking I/O)、NIO(Non-Blocking I/O)和AIO(Asynchronous I/O)是三种不同的 I/O 模型。
- BIO(Blocking I/O):BIO 是最传统的 I/O 模型,它是同步阻塞的方式。在 BIO 中,每个I/O操作都会阻塞当前线程,直到数据准备好或操作完成。这意味着在进行 I/O 操作时,线程会一直等待,无法进行其他任务。BIO 适用于连接数较少且请求处理时间较短的场景。
- NIO(Non-Blocking I/O):NIO 是 Java 1.4 引入的新的 I/O 模型,它是同步非阻塞的方式。在 NIO 中,通过使用选择器(
Selector
)和通道(Channel
),可以实现一个线程处理多个 I/O 操作。当一个通道上的数据准备好时,线程可以进行其他任务,而不需要一直等待。NIO 适用于连接数较多且请求处理时间较长的场景。 - AIO(Asynchronous I/O):AIO 是 Java 1.7 引入的新的 I/O 模型,它是异步非阻塞的方式。在 AIO 中,I/O 操作的结果不需要通过轮询或回调来获取,而是通过
Future
和CompletionHandler
来处理。当一个 I/O 操作完成时,操作系统会通知应用程序,应用程序可以继续进行其他任务。AIO 适用于连接数较多且请求处理时间较长的场景,且相较于 NIO,AIO 具有更好的性能。
抽象类能使用 final
修饰吗?
Java的抽象类中可以使用final修饰符。当一个抽象类被声明为final时,它不能被继承。这意味着没有任何类可以继承这个抽象类,也就不能重写它的方法。
Java 中的容器都有哪些?
在Java中,常见的容器类包括以下几种:
- List:List 是一个有序的集合,可以存储重复的元素。常见的实现类有
ArrayList
和LinkedList
。 - Set:Set 是一个不允许重复元素的集合,它没有固定的顺序。常见的实现类有
HashSet
和TreeSet
。 - Map:Map 是一个键值对的集合,每个键都是唯一的。常见的实现类有
HashMap
和TreeMap
。 - Queue:Queue 是一个先进先出(FIFO)的集合,通常用于实现队列。常见的实现类有
LinkedList
和ArrayDeque
。 - Stack:Stack 是一个后进先出(LIFO)的集合,通常用于实现栈。常见的实现类有
LinkedList
。
如何决定使用 HashMap
还是 TreeMap
?
HashMap
:HashMap
是一种基于哈希表实现的Map,它提供了快速的插入、查找和删除操作。HashMap
不保证元素的顺序,即元素的顺序可能会发生变化。使用HashMap
时,可以通过键值对的方式存储和获取数据。HashMap
适用于需要高效地进行查找、插入和删除操作,并且不关心元素的顺序的场景。TreeMap
:TreeMap
是一种基于红黑树实现的有序Map,它按照键的自然顺序(或者自定义的比较器)对元素进行排序。TreeMap
保持了元素的顺序,因此可以按照键的顺序遍历元素。使用TreeMap
时,可以通过键值对的方式存储和获取数据。TreeMap
适用于需要按照键的顺序进行遍历、查找和范围查询的场景。
总之,如果对元素的顺序没有特殊要求,并且需要高效地进行查找、插入和删除操作,可以使用 HashMap
。如果需要按照键的顺序进行遍历、查找和范围查询,可以使用 TreeMap
。
如何实现数组和 List
之间的转换?
- 将数组转换为
List
可以使用Array
类中的静态方法asList()
将数组转换为List
。 - 将
List
转换为数组可以帅帅List
接口中规定的toArray()
方法将List
中的内容转为数组。
ArrayList
和 Vector
之间的区别是什么?
ArrayList
和 Vector
都是可调整大小的数组实现的类,它们之间的区别主要有以下几点:
- 同步性:
Vector
是线程安全的,而ArrayList
是非线程安全的。这意味着在多线程环境下,Vector
可以安全地被多个线程同时访问和修改,而ArrayList
在多线程环境下需要使用同步机制来确保线程安全。 - 性能:由于
Vector
是线程安全的,它的许多方法都使用了同步机制,这可能会导致一些性能上的开销。相比之下,ArrayList
不需要进行同步操作,因此在单线程环境下通常具有更好的性能。 - 增长方式:当元素数量超过初始容量时,
Vector
和ArrayList
都会自动增长容量。然而,它们的增长方式略有不同。Vector
的增长方式是每次增加当前容量的一半,而ArrayList
的增长方式是每次增加当前容量的一倍。这意味着在添加大量元素时,ArrayList
可能需要重新分配更多的内存空间。 - 初始容量:
Vector
和ArrayList
的初始容量默认为10
,但可以通过构造函数指定不同的初始容量。
如果在单线程环境下进行操作,并且不需要考虑线程安全性,通常推荐使用 ArrayList
,因为它具有更好的性能。如果在多线程环境下进行操作,或者需要确保线程安全性,可以使用 Vector
。
Iterator
怎么使用?有什么特点?
Iterator
是 Java 集合框架中的一个接口,用于遍历集合中的元素。通过 Iterator
,可以依次访问集合中的每个元素,并可以在遍历过程中进行删除操作。使用 Iterator
的步骤如下:
- 调用集合的
iterator()
方法获取Iterator
对象。 - 使用
hasNext()
方法检查Iterator
是否还有下一个元素。如果有下一个元素,则可以使用next()
方法获取下一个元素。 - 针对每个元素进行相应的操作。
- 可选操作:可以使用
remove()
方法删除当前元素。
Iterator
的特点如下:
- 遍历方式:
Iterator
提供了一种单向遍历集合的方式,只能向前遍历,无法回退或跳过元素。 - 删除操作:
Iterator
可以在遍历过程中删除集合中的元素,通过调用remove()
方法实现。注意,如果在调用remove()
方法之前没有调用next()
方法,或者在同一个元素上连续调用remove()
方法两次,都会抛出IllegalStateException
异常。 - 集合的修改:如果在使用
Iterator
遍历集合的过程中,对集合进行了修改(例如添加或删除元素),那么会导致ConcurrentModificationException
异常。 - 效率:相比于使用普通的
for
循环遍历集合,使用Iterator
遍历集合的效率更高,尤其是对于大型集合或需要删除元素的情况。
Runnable
和 Callable
有什么区别?
在 Java 中, Runnable
和 Callable
都是用于实现多线程的接口,它们之间有以下几点区别:
- 返回值:
Runnable
的run()
方法没有返回值,而Callable
的call()
方法可以返回一个结果。 - 异常处理:
Runnable
的run()
方法不能抛出任何异常,而Callable
的call()
方法可以抛出异常。 - 使用方式:
Runnable
通常使用在通过Thread
类创建和启动线程的方式中,而Callable
通常使用在通过ExecutorService
提交任务并获取任务执行结果的方式中。 - 并发性:使用
Runnable
时,可以通过多个线程同时执行多个Runnable
实例,并发性较好。而使用Callable
时,需要配合Future
接口,可以通过获取Future
对象来获取任务的执行结果。
Runnable
适用于不需要返回结果或抛出异常的场景,而 Callable
适用于需要返回结果或抛出异常的场景。同时,使用 Callable
可以更好地管理和控制任务的执行,并发性更好。
notify()
和 notifyAll()
有什么区别?
在 Java 中, notify()
和 notifyAll()
都是用于线程间通信的方法,它们之间的区别如下:
- 唤醒方式:
notify()
方法用于唤醒在对象上等待的单个线程,而notifyAll()
方法用于唤醒在对象上等待的所有线程。 - 竞争关系:当多个线程等待同一个对象的锁时,调用
notify()
方法只会唤醒其中一个线程,而调用notifyAll()
方法会唤醒所有等待的线程。被唤醒的线程会进入就绪状态,并竞争对象的锁。 - 选择方式:在使用
notify()
和notifyAll()
时,应根据具体的需求来选择合适的方式。如果只需要唤醒一个线程,并且能够确定唤醒哪个线程,可以使用notify()
方法。如果需要唤醒所有等待的线程,或者无法确定唤醒哪个线程,可以使用notifyAll()
方法。
调用 notify()
和 notifyAll()
方法只会唤醒等待的线程,并不会释放对象的锁。唤醒的线程会继续竞争对象的锁,并继续执行。此外,使用 wait()
方法使线程等待时,应在同步代码块中调用,以确保正确的竞争和唤醒关系。
线程的 run()
和 start()
有什么区别?
run()
方法是线程的执行体,定义线程要执行的代码逻辑。当线程对象通过调用run()
方法时,它会在当前线程中执行run()
方法中的代码。不会创建新的线程,而是在当前线程中执行。start()
方法用于启动一个新线程。当线程对象通过调用start()
方法时,它会创建一个新的线程,并在新的线程中执行run()
方法中的代码。start()
方法会将线程放入就绪队列中,等待系统调度执行。start()
方法只能调用一次,如果多次调用start()
方法,会抛出IllegalThreadStateException
异常。而run()
方法可以直接多次调用,但只会在当前线程中执行。
ThreadLocal
是什么?有哪些使用场景?
ThreadLocal
是 Java 中的一个类,它提供了线程局部变量的机制。线程局部变量是指每个线程都拥有自己独立的变量副本,互不干扰。 ThreadLocal
的使用场景如下:
- 线程上下文信息传递:
ThreadLocal
可以用于在多个方法之间传递线程上下文信息,避免显式传参的繁琐性。例如,在 Web 应用程序中,可以将用户信息存储在ThreadLocal
中,在不同的方法中方便地获取和使用。 - 线程安全性保证:
ThreadLocal
可以用于在多线程环境下保证对象的线程安全性。通过将共享的可变对象存储在ThreadLocal
中,每个线程都拥有自己独立的对象副本,避免了多线程访问共享对象时的竞争和同步开销。 - 数据库连接管理:在使用数据库连接池时,可以使用
ThreadLocal
来管理数据库连接。每个线程都可以从ThreadLocal
中获取自己的数据库连接,避免了线程之间的竞争和同步问题。 - 线程级别的缓存:
ThreadLocal
可以用于实现线程级别的缓存。例如,在计算密集型的任务中,可以将计算结果存储在ThreadLocal
中,下次需要时直接从ThreadLocal
中获取,避免重复计算。
由于 ThreadLocal
的特性,使用不当可能会导致内存泄漏或数据不一致的问题。因此,在使用 ThreadLocal
时,需要注意及时清理 ThreadLocal
中的数据,避免出现意外情况。
synchronized
和 Lock
之间有什么区别?
synchronized
和 Lock
都是 Java 中用于实现线程同步的机制,它们之间的区别如下:
- 锁的获取方式:
synchronized
是通过关键字来实现的,它是隐式锁,即在代码中使用synchronized
修饰的方法或代码块会自动获取和释放锁。而Lock
是通过Lock
接口的实现类来实现的,需要显式地调用lock()
方法来获取锁,并在合适的位置调用unlock()
方法来释放锁。 - 锁的可重入性:
synchronized
是可重入锁,即同一个线程在持有锁的情况下可以再次获取同一个锁。而Lock
也是可重入锁,但需要注意,获取锁和释放锁的次数要匹配,否则可能会导致死锁。 - 锁的粒度:
synchronized
是对整个方法或代码块进行加锁,锁的粒度较粗。而Lock
可以根据需求进行更细粒度的加锁,例如可以只对某个对象或资源进行加锁,提高并发性能。 - 锁的条件:
synchronized
在获取锁时,会自动阻塞等待锁的释放。而Lock
可以通过Condition
接口提供的await()
、signal()
和signalAll()
方法实现更灵活的条件等待和唤醒。 - 锁的可中断性:
synchronized
在获取锁时,无法被中断,即使调用了线程的interrupt()
方法。而Lock
可以通过lockInterruptibly()
方法实现可中断的获取锁操作。
总的来说, synchronized
是 Java 语言层面的内置机制,使用简单但灵活性较差。而 Lock
是 Java 提供的更灵活、可控制性更强的锁机制,适用于复杂的并发场景。
什么是反射?
反射是 Java 中的一种机制,它允许程序在运行时获取和操作类的信息。通过反射,可以在运行时动态地创建对象、调用方法、访问属性等。使用反射可以实现一些灵活的操作,例如在不知道类名的情况下创建对象,或者在运行时根据配置文件加载不同的类。
反射的核心类是 Class
类,它代表了一个类的运行时信息。通过 Class
类的实例,可以获取类的构造方法、方法、字段等信息,并进行相应的操作。通过反射,可以实现对类的动态调用和操作,但也增加了运行时的开销和复杂性,因此在使用反射时需要谨慎考虑性能和安全性的问题。
反射是一种强大但复杂的机制,不适合在普通的业务逻辑中频繁使用。
什么是动态代理?有哪些应用场景?如何实现一个动态代理?
动态代理是指在运行时动态生成代理类的机制,通过代理类来间接访问目标对象,从而实现对目标对象的控制和扩展。动态代理可以在不修改目标对象的情况下,对其方法进行增强或拦截,实现一些横切关注点(如日志记录、性能监控、事务管理等)的统一处理。
动态代理在 Java 中主要通过两种方式实现: JDK 动态代理和 CGLIB 动态代理。 JDK 动态代理是基于接口的代理,通过实现 InvocationHandler
接口和 Proxy
类来实现。 CGLIB 动态代理通过继承目标类,生成子类作为代理类,并通过重写父类方法来实现。
应用场景包括:
- AOP 编程:动态代理可以实现对业务逻辑的横切关注点的统一处理,例如事务管理、权限控制、日志记录等。
- 远程调用:通过动态代理可以实现远程方法调用,将方法调用转发到远程服务器上执行,实现分布式系统的功能。
- 延迟加载:通过动态代理可以实现延迟加载,即在需要时才真正创建对象,提高系统性能和资源利用率。
- 缓存代理:通过动态代理可以实现对方法的结果进行缓存,减少重复计算,提高系统响应速度。
动态代理的实现依赖于反射机制,会在一定程度上影响系统性能。
如何实现对象克隆?为什么要使用对象克隆?
对象克隆是指创建一个与原始对象具有相同状态的新对象。在 Java 中,可以通过以下两种方式实现对象克隆:
- 实现
Cloneable
接口:该接口是一个标记接口,没有任何方法。如果一个类实现了Cloneable
接口,并重写了Object
类的clone()
方法,就可以使用clone()
方法进行对象克隆。在clone()
方法内部,通过调用super.clone()
方法创建一个新对象,并将原始对象的字段值复制到新对象中。 - 使用序列化和反序列化:通过将对象序列化为字节流,然后再将字节流反序列化为新对象,实现对象的克隆。这种方式需要对象及其成员变量都实现
Serializable
接口,并使用ObjectOutputStream
和ObjectInputStream
进行序列化和反序列化操作。
使用对象克隆的主要原因包括:
- 避免对象引用传递:当需要创建一个新对象,并且该对象的字段值与原始对象相同,但又不希望修改原始对象时,可以使用对象克隆来避免对象引用传递。
- 提高性能:有时候,创建一个新对象的成本比复制一个已有对象的成本更低。在这种情况下,可以使用对象克隆来提高性能。
- 保护对象的不可变性:如果一个对象是不可变的,并且在创建后不会发生任何更改,那么可以使用对象克隆来保护对象的不可变性。
对象克隆是一种浅拷贝方式,即只复制对象的字段值,而不复制对象引用的成员对象。如果需要实现深拷贝,即复制对象及其所有引用的成员对象,需要在 clone()
方法中进行相应的处理。
在 try ... catch
语句中,如果在 catch
语句中 执行了 return
,那么 finally
还会执行吗?
会的。 finally
语句被设计为用来释放资源或者执行一些必要的清理操作,以确保资源的正确关闭和代码的健壮性,所以 finally
块中的代码无论在何种情况下都会被执行。
Spring 基本知识
为什么要使用 Spring?
使用 Spring 框架有以下几个主要的原因:
- 松耦合: Spring 框架采用了控制反转( IoC )和依赖注入( DI )的设计思想,通过容器来管理对象的生命周期和依赖关系,降低了组件之间的耦合度。这使得代码更易于维护、测试和扩展。
- 面向切面编程( AOP ): Spring 框架通过 AOP 模块提供了一种切面编程的方式,可以将与核心业务逻辑无关的横切关注点(如日志记录、事务管理、安全性控制等)从业务逻辑中抽离出来,使代码更加干净和可维护。
- 事务管理: Spring 框架提供了强大的事务管理支持,可以通过声明式事务管理来简化事务操作,减少了手动管理事务的复杂性。
- 简化开发: Spring 框架提供了丰富的功能和模块,如数据访问、 Web 开发、消息队列等,可以大大简化开发过程,提高开发效率。
- 测试支持: Spring 框架提供了测试支持,可以方便地进行单元测试和集成测试,保证代码的质量和稳定性。
- 开放性和可扩展性: 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 支持的一些常见作用域:
- Singleton (单例):默认的作用域,容器中只有一个 Bean 实例,所有请求都返回同一个实例。
- Prototype (原型):每次请求都会创建一个新的 Bean 实例,每个实例都是独立的。
- Request (请求):每个 HTTP 请求都会创建一个新的 Bean 实例,适用于 Web 应用程序。
- Session (会话):每个 HTTP 会话( Session )都会创建一个新的 Bean 实例,适用于 Web 应用程序。
- Global Session (全局会话):在基于 portlet 的 Web 应用程序中,每个 portlet 实例都有一个全局会话,每个全局会话都会创建一个新的 Bean 实例。
- Application (应用程序):每个 Servlet 上下文都会创建一个新的 Bean 实例。
- WebSocket ( Web 套接字):每个 WebSocket 会话都会创建一个新的 Bean 实例。
Spring 自动装配 Bean 有哪些方式?
在 Spring 中,自动装配 Bean 的方式有以下几种:
byName
:根据 Bean 的名称进行自动装配。容器会自动将与目标属性名称相同的 Bean 注入到目标 Bean 中。byType
:根据 Bean 的类型进行自动装配。容器会自动将与目标属性类型相匹配的 Bean 注入到目标 Bean 中。如果存在多个匹配的 Bean ,会抛出异常。constructor
:按照构造函数的参数类型进行自动装配。容器会自动将与目标构造函数参数类型相匹配的 Bean 注入到目标 Bean 中。autodetect
:结合byName
和byType
两种方式进行自动装配。首先根据byName
方式进行自动装配,如果找不到匹配的 Bean ,则使用byType
方式进行自动装配。no
:禁止自动装配。需要手动通过配置文件或注解指定 Bean 的依赖关系。
可以通过在配置文件中使用 <bean>
标签的 autowire
属性或在类上使用 @Autowired
注解来指定自动装配的方式。默认情况下, Spring 使用 byType
方式进行自动装配。
Spring 中事务的实现有几种方式?
在 Spring 中,事务的实现方式有以下几种:
- 编程式事务管理:通过编写代码来管理事务,手动控制事务的开始、提交和回滚。这种方式需要显式地在代码中添加事务管理代码,较为繁琐。
- 声明式事务管理:通过配置文件或注解来声明事务的属性和行为,由 Spring 框架自动管理事务。这种方式将事务管理与业务逻辑分离,简化了代码的编写。
- 注解驱动事务管理:使用注解来声明事务的属性和行为,由 Spring 框架自动管理事务。使用注解可以更加简洁地定义事务,并将事务管理与业务逻辑紧密结合。
- 基于 AspectJ 的事务管理:使用 AspectJ 框架来实现事务管理。 AspectJ 提供了更加灵活和强大的事务管理功能,可以在编译期或运行时织入事务管理代码。
简单描述一下 Spring MVC 的启动流程和运行流程
在 Spring MVC 中,启动流程主要包括以下几个步骤:
- 容器初始化:当应用程序启动时, Servlet 容器(如 Tomcat )会创建一个
ServletContext
对象,并加载应用程序的web.xml
配置文件。在web.xml
中配置的DispatcherServlet
将会被初始化。 DispatcherServlet
初始化:DispatcherServlet
是 Spring MVC 的核心组件,它继承自HttpServlet
,并负责处理所有的 HTTP 请求。在初始化过程中,DispatcherServlet
会创建一个 Spring 应用上下文(ApplicationContext
)对象,并加载应用程序的配置文件。- 处理器映射器初始化:处理器映射器(
HandlerMapping
)是用于将请求映射到对应的处理器(Controller
)的组件。在初始化过程中,DispatcherServlet
会根据配置文件中的信息,创建并初始化一个或多个处理器映射器。 - 处理器适配器初始化:处理器适配器(
HandlerAdapter
)是用于将请求交给对应的处理器进行处理的组件。在初始化过程中,DispatcherServlet
会根据配置文件中的信息,创建并初始化一个或多个处理器适配器。 - 视图解析器初始化:视图解析器(
ViewResolver
)是用于将处理器返回的逻辑视图名解析为具体的视图对象的组件。在初始化过程中,DispatcherServlet
会根据配置文件中的信息,创建并初始化一个或多个视图解析器。 - 初始化完成:当所有的组件初始化完成后,
DispatcherServlet
将准备好接收并处理 HTTP 请求。此时, Spring MVC 的启动流程就完成了。
当用户产生了一个请求的时候, Spring MVC 就会使用一下的流程来处理用户的请求和产生响应:
- 客户端发送请求:当客户端发送 HTTP 请求时,请求会被发送到
DispatcherServlet
。 DispatcherServlet
处理请求:DispatcherServlet
是 Spring MVC 的核心组件,它接收到请求后会根据配置的处理器映射器(HandlerMapping
)查找到对应的处理器(Controller
)。- 处理器处理请求:处理器会根据请求的 URL 、请求参数等信息执行相应的业务逻辑,并返回一个
ModelAndView
对象。 - 视图解析器解析视图:
DispatcherServlet
会将ModelAndView
对象传递给配置的视图解析器(ViewResolver
),通过解析器将逻辑视图名解析为具体的视图对象。 - 视图渲染:视图对象将根据模型中的数据生成最终的 HTML 或其他类型的响应内容。
- 响应返回给客户端:生成的响应内容会被发送回客户端,客户端会显示相应的页面或执行相应的操作。
Spring MVC 中支持自动装配的注解有哪些?
在 Spring MVC 中,支持自动装配的注解主要有以下几种:
@Autowired
:这是 Spring 框架中最常用的注解之一。它可以对类成员变量、方法及构造函数进行标注,让 Spring 完成自动装配工作。@Resource
:这个注解来自于 J2EE , Spring 支持它的使用。它可以标注在字段和setter
方法上,表示将某个资源注入到标注的字段或setter
方法中。@ModelAttribute
:这个注解主要用于方法参数和方法的返回值。 Spring MVC 在处理请求时,会自动将模型数据填充到标注了@ModelAttribute
的方法参数中。@RequestParam
:这个注解用于方法参数,表示将请求参数绑定到标注了@RequestParam
的方法参数中。@RequestBody
:这个注解用于方法参数,表示将请求体绑定到标注了@RequestBody
的方法参数中。@PathVariable
:这个注解用于方法参数,表示将 URL 的一个片段绑定到标注了@PathVariable
的方法参数中。@RequestHeader
:这个注解用于方法参数,表示将请求头的一个属性绑定到标注了@RequestHeader
的方法参数中。@CookieValue
:这个注解用于方法参数,表示将 cookie 的一个属性绑定到标注了@CookieValue
的方法参数中。
Spring Boot 基本知识
为什么要使用 Spring Boot?什么是 Spring Boot?
Spring Boot 是一种用于快速构建独立的、生产级的 Spring 应用程序的开源框架。它基于 Spring 框架,通过提供默认的配置和约定大于配置的原则,简化了 Spring 应用程序的开发和部署过程。 Spring Boot 提供了自动配置、起步依赖、命令行界面等特性,可以大大提高开发效率。它还集成了常用的开发工具和第三方库,如 Spring Data 、 Spring Security 、 Thymeleaf 等,使得开发者可以更轻松地构建现代化的 Web 应用程序。
使用 Spring Boot 可以带来以下几个优势:
- 简化配置: Spring Boot 通过自动配置的方式,减少了开发者在配置文件中编写大量的配置代码。它提供了默认的配置,可以快速启动和运行应用程序,减少了开发和部署的时间和工作量。
- 快速启动: Spring Boot 提供了起步依赖(
Starter
)的概念,通过引入特定的Starter
,可以一次性引入一组相关的依赖库,简化了依赖管理的过程。这样,开发者可以更快地启动和运行应用程序。 - 内嵌服务器: Spring Boot 内置了多个常用的 Web 服务器,如 Tomcat 、 Jetty 等,可以直接打包并运行应用程序,无需额外安装和配置服务器。这减少了部署的复杂性,并提高了应用程序的运行效率。
- 健康检查: Spring Boot 提供了健康检查的功能,可以通过访问特定的 URL 来监控应用程序的运行状态。这对于运维人员来说非常有用,可以及时发现并解决应用程序的问题。
- 丰富的社区支持: Spring Boot 拥有庞大的社区支持,官方提供了详细的文档和示例,开发者可以轻松找到解决问题的方法。同时,社区中也有很多优秀的开源项目和插件,可以帮助开发者更好地使用和扩展 Spring Boot 。
Spring Boot 的核心配置文件是什么?
在Spring Boot中,核心配置文件是 application.properties
或 application.yml
。这些文件用于配置应用程序的属性和行为。在这些配置文件中,可以设置数据库连接、日志级别、端口号等应用程序相关的配置。Spring Boot会自动加载这些配置文件,并根据配置信息进行相应的初始化和配置。
Spring Boot 有哪些方式可以实现热部署?
在 Spring Boot 中,有以下几种方式可以实现热部署:
- 使用 Spring DevTools : Spring DevTools 是一个开发工具,可以实现应用程序的热部署。它可以监控项目的
classpath
,并在检测到文件变化时自动重启应用程序。通过在pom.xml
文件中添加 Spring Boot DevTools 依赖,并在 IDE 中启用自动构建,即可实现热部署。 - 使用 Spring Loaded : Spring Loaded 是一个热部署工具,可以在不重启应用程序的情况下重新加载修改后的类。通过在
pom.xml
文件中添加 Spring Loaded 依赖,并在 IDE 中启用自动构建,即可实现热部署。 - 使用 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 中,核心组件包括以下几个:
Eureka
: Eureka 是一个服务注册与发现的组件,用于管理和跟踪服务的状态和位置。服务提供者通过向 Eureka 注册自己的信息,而服务消费者可以从 Eureka 获取可用的服务列表。Ribbon
: Ribbon 是一个客户端负载均衡的组件,用于在服务消费者中实现负载均衡。它可以根据一定的规则从多个服务提供者中选择一个进行调用。Feign
: Feign 是一个声明式的 HTTP 客户端,用于简化服务消费者的开发。通过使用 Feign ,开发者可以像调用本地方法一样调用远程服务。Hystrix
: Hystrix 是一个容错和断路器的组件,用于处理分布式系统中的故障和延迟。它可以防止故障扩散,并提供了故障处理和容错机制。Zuul
: Zuul 是一个网关组件,用于实现动态路由、负载均衡和安全过滤等功能。通过使用 Zuul ,开发者可以将请求转发到不同的微服务中。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.yml
或 bootstrap.yml
)中配置 Eureka 服务端的地址,然后在需要调用其他服务的地方使用 Spring Cloud 的 RestTemplate
或 WebClient
向 Eureka 服务端发送请求,获取其他服务的信息。
ORM 相关
你都使用过哪些 ORM 框架?有什么显著的特点?
在 Spring Boot 框架中,常用的 ORM 框架主要有两种:JPA 和 MyBatis 。
- JPA ( Java Persistence API ):JPA 是 Spring Boot 中常用的 ORM 框架之一。它是 Java EE 标准的一部分,可以与 Spring 框架很好地集成。 JPA 通过使用注解来映射 Java 类与数据库表之间的映射关系,使得开发者无需编写大量的 SQL 语句即可进行数据库操作。同时, JPA 还支持自定义查询语句、事务管理等功能,是一个非常强大和灵活的 ORM 框架。
- 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 对象与数据库表进行映射的功能,但两者之间还是存在一些主要的区别。
- 规范与实现: JPA 是一个规范,而 Hibernate 是 JPA 的一个实现。 JPA 通过 Java 社区进程( JCP )协同开发,作为一个 Java 规范请求( JSR )发布,而 Hibernate 则是 Red Hat 对 JPA 规范的具体实现。
- 操作符与 SQL 语句: JPA 可以自动生成 SQL 语句并自动执行,这使得 Java 程序员可以更多地使用对象编程思维来操作数据库。而 Hibernate 则更进一步,它提供了许多操作符和 SQL 语句的映射,使得开发者可以更加方便地用 HQL ( Hibernate Query Language )进行查询。
- 扩展性和性能: Hibernate 的性能可能会比 JPA 更好,因为它针对 JDBC 进行了轻量级的封装,并且提供了许多优化选项。同时, Hibernate 具有更好的扩展性,因为它支持各种方言,可以方便地与其他框架进行集成。
- 状态管理: Hibernate 和 JPA 在实体状态管理方面存在差异。 Hibernate 提供了强大的状态管理能力,可以清晰地区分出瞬态、持久化、游离等状态,更符合实际业务需求。而 JPA 则将状态管理交由开发者自行处理。
- 映射方式: JPA 通过注解和 XML 进行实体与数据库表的映射,而 Hibernate 只使用注解进行映射,不使用 XML 。
MyBatis 中有几种分页方式?
- 数组分页。在进行数据库查询操作时,获取到数据库中所有满足条件的记录,保存在应用的临时数组中,再通过
List
的subList
方法,获取到满足条件的所有记录。 - sql 语句分页。通过 sql 语句实现分页也非常简单,改变查询的语句,即在 sql 语句后面添加
limit
分页语句就可以实现分页。 - RowBounds 分页。通过
RowBounds
实现分页和通过数组方式分页原理差不多,都是一次获取所有符合条件的数据,然后在内存中对大数据进行操作,实现分页效果。RowBounds
建议在数据量相对较小的情况下使用。 - 拦截器分页。
MyBatis 的逻辑分页和物理分页的区别是什么?
MyBatis 中的逻辑分页和物理分页主要在以下三个方面存在区别:
- 分页方式:逻辑分页是通过在代码中使用游标分页来实现的,而物理分页则是通过在数据库中直接使用分页 SQL 语句(如 MySQL 的
LIMIT
和 ORACLE 的ROWNUM
)来实现的。 - 性能开销:物理分页每次查询都需要访问数据库,对数据库造成较大负担。而逻辑分页只需要在第一次查询时获取全部数据,然后在内存中对数据进行处理,因此如果数据量较大,逻辑分页的性能开销会相对较大。
- 实时性:物理分页每次查询都需要访问数据库,能够获取数据库的最新状态,实时性强。而逻辑分页则是在内存中对数据进行处理,实时性相对较差,如果数据发生改变,可能需要重新查询才能获取最新的数据状态。
如果对性能开销要求较高,且希望能够实时获取最新数据状态的场景下,建议选择物理分页。而在数据量较小,且对实时性要求不高的场景下,可以选择逻辑分页。
MyBatis 的延迟加载原理是什么?
MyBatis 的延迟加载原理主要基于代理对象的生成和拦截器的使用。
当 MyBatis 设置 lazyLoadingEnabled
为 true
时,启用延迟加载。在调用目标对象的属性时,如果该属性为 null
, MyBatis 会单独触发一个事件,保存与该属性相关的 SQL 语句,并执行该 SQL 语句查询数据。查询结果会被保存起来,而目标对象则会被设置一个代理对象。
当再次访问该属性时, MyBatis 会拦截这个属性访问,进入拦截器方法。在拦截器方法中, MyBatis 会先检查该属性是否需要延迟加载。如果需要, MyBatis 就会用保存的 SQL 语句查询数据,然后将查询结果保存到目标对象的相应属性中,完成延迟加载。