前面发了60个,这篇把剩下的61个面试题也补上,如果对你有帮忙,收藏不迷路!
61. 装箱和拆箱的原理和作用⭐⭐⭐⭐
装箱和拆箱是指基本类型与其对应的包装类之间的相互转换。装箱和拆箱的引入简化了基本类型与对象类型之间的转换操作。
装箱
装箱是将基本类型转换为其对应的包装类对象的过程。例如,将 int 转换为 integer,将 double 转换为 Double 等。
自动装箱
Java 5 引入了自动装箱功能,使得在需要对象的地方可以自动将基本类型转换为其对应的包装类对象。编译器会自动插入必要的转换代码。
手动装箱
在没有自动装箱的情况下,可以手动进行装箱操作:
拆箱
拆箱是将包装类对象转换为其对应的基本类型的过程。例如,将 Integer 转换为 int,将 Double 转换为 double 等。
自动拆箱
自动拆箱与自动装箱类似,编译器会在需要基本类型的地方自动将包装类对象转换为基本类型。
手动拆箱
在没有自动拆箱的情况下,可以手动进行拆箱操作:
装箱和拆箱的作用
1、 自动装箱和拆箱可以减少显式转换的代码,使代码更简洁易读。
2、Java 的集合框架只能存储对象,不能直接存储基本类型。装箱和拆箱使得在集合中使用基本类型变得更加方便。
3、 装箱和拆箱允许基本类型和对象类型之间的无缝转换,使得在方法参数、返回值等场景中可以更灵活地处理数据。
性能影响
尽管装箱和拆箱简化了代码,但它们也带来了一些性能上的开销:装箱会创建新的对象,这会带来内存分配和垃圾回收的开销。在拆箱过程中,如果包装类对象为 null,会抛出空指针异常。
62. Enumeration和Iterator接口的区别?⭐⭐
Enumeration和Iterator是 Java 中用于遍历集合的两个接口。虽然它们有相似的功能,但它们有不同的设计和使用方式。
Enumeration接口
Enumeration是一个较老的接口,存在于 Java 1.0 中。它主要用于遍历旧的集合类,如Vector和Hashtable。
常用方法
boolean hasMoreElements(): 如果枚举中仍有更多元素,则返回true。
Object nextElement(): 返回枚举中的下一个元素。
代码 Demo
Iterator接口
Iterator是在 Java 2 (JDK 1.2) 中引入的。它是集合框架的一部分,适用于所有集合类(如ArrayList、HashSet、HashMap等)。Iterator提供了更灵活的遍历方法,并允许在遍历过程中安全地移除元素。
方法
boolean hasNext(): 如果迭代器中仍有更多元素,则返回true。
E next(): 返回迭代器中的下一个元素。
void remove(): 从集合中移除迭代器返回的最后一个元素(可选操作)。
代码 Demo
主要区别
接口引入时间:
Enumeration:引入于 Java 1.0。
Iterator:引入于 Java 2 (JDK 1.2)。
方法名称和功能:
Enumeration:使用hasMoreElements()和nextElement()方法。
Iterator:使用hasNext()和next()方法,并增加了remove()方法。
元素移除:
Enumeration:不支持在遍历过程中移除元素。
Iterator:支持在遍历过程中安全地移除元素(通过remove()方法)。
适用范围:
Enumeration:主要用于旧的集合类,如Vector和Hashtable。
Iterator:适用于所有集合类,是集合框架的一部分。
63. java函数参数是值拷贝还是引用拷贝?⭐
在 Java 中,函数参数传递的机制是基于值传递的。
基本类型参数传递
对于基本类型,传递的是值的拷贝。在函数内部对参数的任何修改都不会影响到原始变量。
在这个例子中,modifyValue函数内部修改了 x 的值,但这不会影响到 main 函数中的 a。
引用类型参数传递
对于引用类型,传递的也是值的拷贝,但这个值是对象的引用。这意味着在函数内部可以通过引用修改对象的内容,但不能改变引用本身指向的对象。
在这个例子中,modifyObject函数可以通过引用 o 修改 obj 的内容,因此 main 函数中的 obj.value 被修改为 20。
但是,如果试图在函数内部改变引用本身指向另一个对象,这种修改不会影响到原始引用。
在这个例子中,changeReference函数试图改变引用 o 指向一个新的对象,但这不会影响到 main 函数中的 obj。
传递的是引用的拷贝,函数内部可以通过引用修改对象的内容,但不能改变引用本身指向的对象。
这种传递机制有时会被误解为“引用传递”,但实际上 Java 中所有参数传递都是值传递。只不过对于引用类型,传递的是引用的值。
64. 基本数据类型和包装类的区别⭐⭐⭐⭐⭐
基本数据类型
Java 中的基本数据类型(也称为原始数据类型)有 8 种:
- byte: 8 位,有符号整数,范围 -128 到 127。
- short: 16 位,有符号整数,范围 -32,768 到 32,767。
- int: 32 位,有符号整数,范围 -2^31 到 2^31-1。
- long: 64 位,有符号整数,范围 -2^63 到 2^63-1。
- float: 32 位,单精度浮点数。
- double: 64 位,双精度浮点数。
- char: 16 位,Unicode 字符,范围 0 到 65,535。
- boolean: 表示 true 或 false。
包装类
Java 为每种基本数据类型提供了对应的包装类,这些包装类位于java.lang包中:
- Byte对应byte
- Short对应short
- Integer对应int
- Long对应long
- Float对应float
- Double对应double
- Character对应char
- Boolean对应boolean
65. 什么是fail-fast机制?⭐
在Java集合框架中,fail-fast是一种机制,用于检测在遍历集合时的结构性修改,并立即抛出异常以防止不一致状态。fail-fast迭代器在检测到集合在迭代过程中被修改后,会抛出ConcurrentModificationException异常。
工作原理
fail-fast迭代器通过在遍历集合时维护一个修改计数器(modification count)来工作。每当集合结构发生变化(如添加或删除元素)时,这个计数器就会增加。当创建迭代器时,它会保存当前的修改计数器值。在每次调用next()方法时,迭代器会检查当前的修改计数器值是否与保存的值一致。如果不一致,说明集合在迭代过程中被修改了,迭代器会立即抛出ConcurrentModificationException。
代码 Demo
在上面的代码中,当迭代器遍历到元素 “B” 时,集合被修改(添加了新元素 “D”),因此迭代器将抛出ConcurrentModificationException。
注意事项
快速失败并不保证:fail-fast机制并不能保证在所有情况下都能检测到并发修改。它是尽力而为的检测机制,不能依赖于它来实现并发安全。如果需要并发安全的集合,可以使用java.util.concurrent包中的并发集合类。
避免并发修改:在遍历集合时,避免在外部修改集合。可以使用迭代器的remove方法来安全地移除元素。
使用remove方法
为了避免ConcurrentModificationException,可以使用迭代器的remove方法来移除元素:
在这个示例中,使用iterator.remove()方法安全地移除了元素 “B”。
66. 什么是fail-safe机制?⭐
fail-safe机制是与fail-fast机制相对的一种并发处理机制。在 Java 集合框架中,fail-safe迭代器在检测到集合在遍历过程中被修改时,不会抛出异常,而是允许这种修改继续进行。fail-safe迭代器通常是通过在遍历时使用集合的副本来实现的,这样即使原集合被修改,迭代器也不会受到影响。
工作原理
fail-safe迭代器在遍历集合时,实际上是遍历集合的一个副本。因此,任何对原集合的修改都不会影响到迭代器正在遍历的副本。这种机制保证了遍历操作的安全性,但也意味着迭代器不能反映集合的实时变化。
代码 Demo
在上面的代码中,使用CopyOnWriteArrayList作为集合。CopyOnWriteArrayList是一个典型的fail-safe集合类,它在每次修改时都会创建集合的一个副本,因此迭代器不会检测到并发修改,不会抛出ConcurrentModificationException。
主要特点
常见的fail-safe集合类
CopyOnWriteArrayList,ConcurrentHashMap,ConcurrentLinkedQueue,ConcurrentSkipListMap,ConcurrentSkipListSet
注意事项
67. BlockingQueue是什么?⭐
BlockingQueue是 Java 中定义在java.util.concurrent包下的一个接口,它扩展了Queue接口,并添加了阻塞操作。BlockingQueue提供了一种线程安全的机制,用于在多线程环境中处理生产者-消费者问题。
特点和功能
阻塞操作:BlockingQueue提供了阻塞的put和take方法:
put(E e):如果队列已满,则阻塞直到有空间可插入元素。
take():如果队列为空,则阻塞直到有元素可取。
线程安全:所有方法都使用内部锁或其他同步机制来确保线程安全。
多种实现:BlockingQueue有多种实现方式,适用于不同的场景:
ArrayBlockingQueue:基于数组的有界阻塞队列。
LinkedBlockingQueue:基于链表的可选有界阻塞队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:支持延迟元素的无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待一个对应的移除操作。
LinkedTransferQueue:基于链表的无界阻塞队列,支持传输操作。
代码 Demo
如何使用BlockingQueue实现生产者-消费者模式:
LinkedBlockingQueue被用作BlockingQueue的实现。生产者线程不断地向队列中添加元素,而消费者线程不断地从队列中取出元素。如果队列已满,生产者线程会阻塞,直到有空间可插入元素;如果队列为空,消费者线程会阻塞,直到有元素可取。
主要方法
BlockingQueue提供了一些常用的方法,这些方法分为四类:
- 抛出异常:
add(E e):如果队列已满,抛出IllegalStateException。
remove():如果队列为空,抛出NoSuchElementException。
element():如果队列为空,抛出NoSuchElementException。
- 返回特殊值:
offer(E e):如果队列已满,返回false。
poll():如果队列为空,返回null。
peek():如果队列为空,返回null。
- 阻塞操作:
put(E e):如果队列已满,阻塞直到有空间可插入元素。
take():如果队列为空,阻塞直到有元素可取。
- 超时操作:
offer(E e, long timeout, TimeUnit unit):在指定的时间内插入元素,如果队列已满,等待直到超时或插入成功。
poll(long timeout, TimeUnit unit):在指定的时间内取出元素,如果队列为空,等待直到超时或取出成功。
68. Java提供了哪些队列?⭐
LinkedList
基于链表实现的双向链表,实现了List、Deque和Queue接口,支持在头部和尾部进行快速插入和删除操作。
使用场景:
需要频繁插入和删除元素的场景。
需要双端队列(Deque)功能的场景,如在头部和尾部进行操作。
PriorityQueue
基于优先级堆(Priority Heap)实现的无界队列。元素按照自然顺序或指定的比较器顺序排列。不允许插入null元素。
使用场景:
需要按优先级处理元素的场景,如任务调度、事件处理等。
需要动态调整元素顺序的场景。
ArrayDeque
基于数组实现的双端队列(Deque),没有容量限制,可以动态扩展,比LinkedList更高效,尤其是在栈和队列操作方面。
使用场景:
需要高效的栈或队列操作的场景。
需要双端队列功能,但不需要线程安全的场景。
ConcurrentLinkedQueue
基于链表实现的无界非阻塞队列。使用无锁算法,提供高效的并发性能。线程安全,适用于高并发环境。
使用场景:
高并发环境下的无界队列。
需要高效的非阻塞并发操作的场景。
LinkedBlockingQueue
基于链表实现的可选有界阻塞队列,支持阻塞的put和take操作,线程安全,适用于生产者-消费者模式。
使用场景:
生产者-消费者模式,特别是在需要限制队列大小的场景。需要线程安全的阻塞队列。
ArrayBlockingQueue
基于数组实现的有界阻塞队列,必须指定容量,支持阻塞的put和take操作。线程安全,适用于生产者-消费者模式。
使用场景:
生产者-消费者模式,特别是在需要固定大小的队列时。需要线程安全的有界阻塞队列。
DelayQueue
支持延迟元素的无界阻塞队列,元素只有在其延迟时间到期后才能被取出。线程安全,适用于并发环境。
使用场景:
需要延迟处理元素的场景,如任务调度、缓存过期处理等。
定时任务执行场景。
LinkedBlockingDeque
基于链表实现的可选有界阻塞双端队列,支持阻塞的put和take操作。线程安全,适用于生产者-消费者模式。
使用场景:生产者-消费者模式,特别是在需要限制队列大小的双端队列场景。需要线程安全的阻塞双端队列。
69. 阻塞队列原理?⭐
阻塞队列是一种线程安全的队列,它在插入和删除操作上可以阻塞线程,以实现生产者-消费者模式等并发编程需求。阻塞队列的核心原理包括锁机制和条件变量。
基本原理
锁机制
阻塞队列使用锁(如ReentrantLock)来确保线程安全。锁保证了同一时间只有一个线程可以执行插入或删除操作,从而避免并发问题。
条件变量
阻塞队列使用条件变量(Condition)来管理线程的等待和通知。条件变量是与锁关联的,可以在特定条件下阻塞线程并在条件满足时唤醒线程。例如,notEmpty和notFull是常见的条件变量,分别用于表示队列是否为空和是否已满。
等待和通知机制:
当线程试图执行插入操作而队列已满时,它会在notFull条件变量上等待,直到队列中有空闲空间。
当线程试图执行删除操作而队列为空时,它会在notEmpty条件变量上等待,直到队列中有可用的元素。
当插入或删除操作成功后,相应的条件变量会被通知(唤醒),以便其他等待的线程可以继续执行。
具体实现 Demo
LinkedBlockingQueue是一个基于链表实现的可选有界阻塞队列。它的基本原理如下:
内部结构:使用一个链表来存储元素。使用两个锁:takeLock和putLock,分别用于控制删除和插入操作。使用两个条件变量:notEmpty和notFull,分别用于表示队列是否为空和是否已满。
插入操作(put):
删除操作(take):
等待和通知:
在插入操作中,如果队列已满,线程会在notFull条件变量上等待。
在删除操作中,如果队列为空,线程会在notEmpty条件变量上等待。
插入或删除操作成功后,会相应地通知等待的线程。
70. 重载和重写的区别⭐⭐⭐⭐⭐
重载(Overloading)和重写(Overriding)是面向对象编程中两个重要的概念,它们在方法定义和调用时有不同的用途和规则。
重载
在同一个类中,方法名称相同,但参数列表(参数的类型、数量或顺序)不同的多个方法。
方法名称:相同。
参数列表:必须不同(参数的类型、数量、或顺序)。
返回类型:可以相同也可以不同。
访问修饰符:可以相同也可以不同。
静态/实例方法:都可以重载。
编译时决定:方法的选择在编译时由编译器根据参数列表决定。
重写(Overriding)
在子类中定义一个方法,该方法与父类中的某个方法具有相同的方法名称、参数列表和返回类型,以便在子类中提供该方法的具体实现。
方法名称:相同。
参数列表:必须相同。
返回类型:必须相同(Java 5 及以后可以是协变返回类型,即返回类型可以是父类方法返回类型的子类型)。
访问修饰符:访问级别不能比父类方法更严格(可以更宽松)。
静态/实例方法:只能重写实例方法,不能重写静态方法。
运行时决定:方法的选择在运行时由 JVM 根据对象的实际类型决定(动态绑定)。
总结
重载:发生在同一个类中。方法名称相同,参数列表不同。编译时决定调用哪个方法(静态绑定)。
重写:发生在子类和父类之间。方法名称、参数列表和返回类型必须相同(或协变返回类型)。运行时决定调用哪个方法(动态绑定)。
71. 为什么要使用扰动函数?⭐
扰动函数的目的是为了提高哈希码的质量,使其在哈希表中更均匀地分布。具体来说:
减少哈希冲突:通过将高位和低位混合,扰动函数减少了哈希码的模式性,降低了哈希冲突的概率。
均匀分布:扰动后的哈希码更加均匀地分布在哈希表的桶中,从而提高了哈希表的性能。
示例 Demo
假设我们有一个键对象,其hashCode()返回值为123456。我们可以通过哈希函数计算其哈希值:
- 调用hashCode()方法:
Plain Text int h = 123456;
- 扰动函数计算:
Plain Text int hash = h ^ (h >>> 16);
- 具体计算步骤:
○ h >>> 16 = 123456 >>> 16 = 1(右移 16 位)
○ hash = 123456 ^ 1 = 123457(异或运算)
最终,哈希值为123457。
72. 进程、线程、管程、协程区别?⭐⭐⭐
进程 (Process)
进程是操作系统分配资源的基本单位。每个进程都有自己的内存空间、文件描述符、堆栈等资源。
进程的特点
独立性:进程之间是独立的,互不干扰。一个进程的崩溃不会影响其他进程。
资源丰富:每个进程拥有独立的资源,包括内存、文件句柄等。
开销大:创建和销毁进程的开销较大,进程间通信(IPC)也相对复杂。
上下文切换:进程的上下文切换开销较大,因为需要切换内存空间和资源。
使用场景
适用于需要强隔离和独立资源的场景,如独立的服务、应用程序等。
线程 (Thread)
线程是进程内的执行单元,一个进程可以包含多个线程。线程共享进程的资源(如内存空间、文件描述符)。
线程的特点
共享资源:同一进程内的线程共享内存和资源,通信方便。
轻量级:线程的创建和销毁开销较小,上下文切换较快。
并发执行:多线程可以并发执行,提高程序的响应速度和资源利用率。
同步问题:由于共享资源,线程间需要同步机制(如锁)来避免资源竞争和数据不一致。
使用场景
适用于需要并发执行的任务,如多任务处理、并行计算等。
管程 (Monitor)
管程是一种高级的同步机制,用于管理共享资源的并发访问。它将共享资源和访问资源的代码封装在一起,通过条件变量和互斥锁来实现同步。
管程特点
封装性:将共享资源和同步代码封装在一起,提供更高层次的抽象。
互斥访问:通过互斥锁确保同一时刻只有一个线程可以访问共享资源。
条件同步:使用条件变量来协调线程间的执行顺序。
使用场景
适用于需要对共享资源进行复杂同步操作的场景,如操作系统内核、并发数据结构等。
协程 (Coroutine)
协程是一种比线程更轻量级的并发执行单元。协程由程序自身调度,而不是由操作系统内核调度。协程可以在执行过程中主动让出控制权,以便其他协程运行。
协程特点
• 轻量级:协程的创建和切换开销极小,通常在用户态完成。
• 主动让出:协程通过显式的调用(如yield)让出控制权,实现合作式多任务。
• 非抢占式:协程之间的切换是合作式的,不存在抢占问题。
• 栈独立:每个协程有自己的栈,避免了线程间共享栈带来的同步问题。
使用场景
适用于需要大量并发任务且切换频繁的场景,如高并发网络服务器、异步编程等。
虚拟线程 (Virtual Thread)
虚拟线程是一个新概念,特别是在 Java 的 Project Loom 中引入。虚拟线程是一种轻量级线程,由 JVM 管理,旨在简化并发编程并提高并发性能。
特点
轻量级:虚拟线程的创建和销毁开销极小,可以高效地管理数百万个线程。
自动管理:由 JVM 自动调度和管理,不需要开发者显式地管理线程池。
兼容性:与传统的 Java 线程 API 兼容,开发者可以用熟悉的线程模型编写高并发程序。
阻塞操作:虚拟线程可以在阻塞操作(如 I/O 操作)时高效地让出 CPU,而不会浪费资源。
使用场景
适用于高并发应用程序,如高性能服务器、Web 应用等。
73. 用户线程与守护线程区别⭐⭐⭐
用户线程
用户线程是应用程序创建的普通线程,也称为非守护线程。当所有用户线程都结束时,Java 虚拟机 (JVM) 也会退出。
特点
生命周期:用户线程的生命周期由应用程序控制。只要有一个用户线程在运行,JVM 就会继续运行。
重要性:用户线程通常用于执行应用程序的主要任务,例如处理业务逻辑、执行计算等。
关闭 JVM:JVM 只有在所有用户线程都结束后才会退出,即使还有守护线程在运行。
使用场景
适用于需要执行重要任务且不能中途被终止的线程。例如:处理用户请求的线程,执行关键业务逻辑的线程
守护线程 (Daemon Thread)
守护线程是为其他线程提供服务和支持的线程。当所有非守护线程(用户线程)都结束时,JVM 会自动退出,即使守护线程还在运行。
特点
生命周期:守护线程的生命周期依赖于用户线程。当所有用户线程结束时,守护线程也会自动终止。
后台任务:守护线程通常用于执行后台任务,如垃圾回收、日志记录等。
低优先级:守护线程通常优先级较低,因为它们主要为用户线程提供支持。
使用场景
适用于执行后台任务或辅助任务的线程,这些任务不需要在 JVM 退出时完成。例如:JVM 的垃圾回收线程,日志记录线程,监控和统计线程
代码 Demo
在这个例子中:
userThread是一个用户线程,它会运行 5 秒钟。daemonThread是一个守护线程,它会每秒钟打印一次消息。
当userThread结束后,JVM 会退出,即使daemonThread还在运行。
74. Java线程的创建方式?⭐⭐⭐⭐⭐
继承Thread类
通过继承java.lang.Thread类并重写其run方法来创建线程。
实现Runnable接口
通过实现java.lang.Runnable接口并将其传递给Thread对象来创建线程。
实现Callable接口和使用FutureTask
通过实现java.util.concurrent.Callable接口来创建线程,并使用FutureTask来管理返回结果。

最低0.47元/天 解锁文章
2880

被折叠的 条评论
为什么被折叠?



