49道必背Java基础八股文面试题总结+答案解析(2024最新版 上篇)
2024秋招又开始了,相信忙着找工作的同学都没忘记要背面试题。
那么,java 基础, Java 集合、Java 并发编程,JVM,Spring ,SpringBoot,微服务, Kafka ,分布式,Redis ,分布式事务,设计模式,算法,数据结构,MySQL ……这么多相关内容,你都背熟了吗?
我们整理了49道必背的Java面试题,并附上了详尽题解。需要扩展知识点和阅读其他知识题库的同学,欢迎使用我们的面试鸭「链接」,现在已经有4000多道面试题了!网页和小程序双端都可以刷题!
1. 你认为 Java 的优势是什么?
这道题主要考察你是否有形成体系的理解。
跨平台
首先 Java 是跨平台的,不同平台执行的机器码是不一样的,而 Java 因为加了一层中间层 JVM ,所以可以做到一次编写多平台运行,即 「Write once,Run anywhere」。
编译执行过程是先把 Java 源代码编译成字节码,字节码再由 JVM 解释或 JIT 编译执行,而因为 JIT 编译时需要预热的,所以还提供了 AOT(Ahead-of-Time Compilation),可以直接把字节码转成机器码,来让程序重启之后能迅速拉满战斗力。
(解释执行比编译执行效率差,你想想每次给你英语让你翻译阅读,还是直接给你看中文,哪个快?)
垃圾回收
Java 还提供垃圾自动回收功能,虽说手动管理内存意味着自由、精细化地掌控,但是很容易出错。
在内存较充裕的当下,将内存的管理交给 GC 来做,减轻了程序员编程的负担,提升了开发效率,更加划算!
生态
现在 Java 生态圈太全了,丰富的第三方类库、网上全面的资料、企业级框架、各种中间件等等,总之你要的都有。
2. 什么是 Java 的多态特性?
多态其实是一种抽象行为,它的主要作用是让程序员可以面对抽象编程而不是具体的实现类,这样写出来的代码扩展性会更强。
大家可能不是很理解什么是抽象什么是具体,我举个可能不是很恰当,但是很好理解的例子:比如某个人很喜欢吃苹果,我们在写文章描述他的时候可以写他很喜欢吃苹果,也可以写他很喜欢吃水果。
水果就是抽象,苹果就是具体的实现类。
假设这个人某天开始换口味了,他喜欢吃桃子了,如果我们之前的文章写的是水果,那么完全不需要改,如果写的是苹果,是不是需要把苹果替换成桃子了?
这就是多态的意义。
再举个代码的例子:
比如 Person person = new Student()
Person 是父类,含有一个工作的方法,student 重写工作方法,比如上学。
class Person {
void work() {
System.out.println("工作");
}
}
class Student extends Person {
@Override
void work() {
System.out.println("上学");
}
}
public class Test {
public static void main(String[] args) {
Person person = new Student();
person.work(); // 输出 "上学"
}
}
这样在使用的时候,对象都是 person,但是 new 不同的实现类,表现的形式不同,这也就从字面上解释的什么叫多态。
3. Java 中的参数传递是按值还是按引用?
Java 只有按值传递,不论是基本类型还是引用类型。
基本类型是值传递很好理解,引用类型有些同学可能有点理解不了,特别是初学者。
JVM 内存有划分为栈和堆,局部变量和方法参数是在栈上分配的,引用类型占 4 个字节,基本类型看具体类型,例如 long 和 double 占 8 个字节。
而对象所占的空间是在堆中开辟的,引用类型变量存储对象在堆中地址来访问对象,所以传递的时候可以理解为把变量存储的地址给传递过去,因此引用类型也是值传递。
4. 接口和抽象类有什么区别?
接口:只能包含抽象方法(但在 Java8 之后可以设置 default 方法或者静态方法),成员变量只能是 public static final 类型,当 like-a 的情况下用接口。
接口是对行为的抽象,类似于条约。在 Java 中接口可以多实现,从 like-a 角度来说接口先行,也就是先约定接口,再实现。
抽象类: 可以包含成员变量和一般方法和抽象方法,当 is-a 并且主要用于代码复用的场景下使用抽象类继承的方式,子类必须实现抽象类中的抽象方法。
在 Java 中只支持单继承。从 is a 角度来看一般都是先写,然后发现代码能复用,然后抽象一个抽象类。
5.为什么 Java 不支持多重继承?
主要是因为多继承会产生菱形继承(也叫钻石继承)问题,Java 之父就是吸取 C++ 他们的教训,因此在不支持多继承。
所谓的菱形继承很好理解,我们来看下这个图:
是不是看起来很像一个菱形,BC 继承了 A,然后 D 继承了 BC, 假设此时要调用 D 内定义在 A 的方法,因为 B 和 C 都有不同的实现,此时就会出现歧义,不知道应该调用哪个了。
6. Java 中的序列化和反序列化是什么?
序列化其实就是将对象转化成可传输的字节序列格式,以便于存储和传输。
因为对象在 JVM 中可以认为是“立体”的,会有各种引用,比如在内存地址Ox1234 引用了某某对象,那此时这个对象要传输到网络的另一端时候就需要把这些引用“压扁”。
因为网络的另一端的内存地址 Ox1234 可以没有某某对象,所以传输的对象需要包含这些信息,然后接收端将这些扁平的信息再反序列化得到对象。
所以,反序列化就是将字节序列格式转换为对象的过程。
7.什么是 Java 中的不可变类?
不可变类指的是无法修改对象的值,比如 String 就是典型的不可变类,当你创建一个 String 对象之后,这个对象就无法被修改。
因为无法被修改,所以像执行s += "a"; 这样的方法,其实返回的是一个新建的 String 对象,老的 s 指向的对象不会发生变化,只是 s 的引用指向了新的对象而已。
所以不要在字符串拼接频繁的场景使用 + 来拼接,因为这样会频繁的创建对象。
不可变类的好处就是安全,因为知晓这个对象不可能会被修改,因此可以放心大胆的用,在多线程环境下也是线程安全的。
8. Java 中的 Exception 和 Error 有什么区别?
Exception 是程序正常运行过程中可以预料到的意外情况,应该被开发者捕获并且进行相应的处理。
Error 是指在正常情况下不太可能出现的情况,绝大部分的 Error 都会导致程序处于不正常、不可恢复的状态。
所以不便也不需被开发者捕获,因为这个情况下你捕获了也无济于事。
Exception和Error都是继承了Throwable类,在Java代码中只有继承了Throwable类的实例才可以被throw或者被catch。
9. Java 面向对象编程与面向过程编程的区别是什么?
面向对象编程(Object Oriented Programming,OOP)是一种编程范式或者说编程风格。把类或对象作为基本单元来组织代码,并且运用提炼出的:封装、继承和多态来作为代码设计指导。
面向过程编程是以过程作为基本单元来组织代码的,过程其实就是动作,对应到代码中来就是函数,面向过程中函数和数据是分离的,数据其实就是成员变量。
面向过程其实就是一条道的思路,因为起初就是按计算机的思路来编写程序。
而面向对象是先抽象,把事物分类得到不同的类,划分每个类的职责,暴露出每个类所能执行的动作,然后按逻辑执行时调用每个类的方法即可,不关心内部的逻辑。
面向过程编程和面向对象编程从思想上的变化是:从计算机思维转变成了人类的思维来编写编码。
而面向对象编程的类中数据和动作是在一起的,这也是两者的一个显著的区别。
10. Java方法重载和方法重写之间的区别是什么?
重载:在同一个类中定义多个方法,它们具有相同的名字但参数列表不同。主要用于提供相同功能的不同实现。
重写:在子类中定义一个与父类方法具有相同签名的方法,以便提供子类的特定实现。主要用于实现运行时多态性。
重载通常用于提供同一操作的不同实现,例如构造函数的重载、不同类型输入的处理等。
重写通常用于在子类中提供父类方法的具体实现,以实现多态性。例如,子类对父类方法进行扩展或修改以适应特定需求。
11.什么是 Java 内部类?它有什么作用?
内部类顾名思义就是定义在一个类的内部的类。它主要作用是为了封装和逻辑分组,提供更清晰的代码组织结构。
通过内部类,可以把逻辑上相关的类组织在一起,提升封装性和代码的可读性。后期维护时都在一个类里面,不需要在各地方找来找去。
按位置分:在成员变量的位置定义,则是成员内部类,在方法内定义,则是局部内部类。
如果用 static 修饰则为静态内部类,最后还有匿名内部类。
局部内部类用的比较少,常用成员内部类、静态内部类和匿名内部类。
实际上内部类是一个编译层面的概念,像一个语法糖一样,经过编译器之后其实内部类会提升为外部顶级类,和外部类没有任何区别,所以在 JVM 中是没有内部类的概念的。
12. JDK8 有哪些新特性?
JDK8 较为重要和平日里经常被问的特性如下:
1)用元空间替代了永久代 2)引入了 Lambda 表达式 3)引入了日期类、接口默认方法、静态方法 4)新增 Stream 流式接口 5)引入 Optional 类 6)新增了 CompletableFuture 、StampedLock 等并发实现类。
元空间替代了永久代
因为 JDK8 要把 JRockit 虚拟机和 Hotspot 虚拟机融合,而 JRockit 没有永久代,所以把 Hotspot 永久代给去了(本质也是永久代回收效率太低)。
详细可看:面试鸭《为什么 Java8 移除了永久代,加了元空间?》 这题。
Lambda 表达式
Lambda 是 Java 8 引入的一种匿名函数,可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。
其本质是作为函数式接口的实例。例如:
// 传统方式
Runnable runnable1 = new Runnable() {
@Override
public void run() {
System.out.println("mianshiya.com");
}
};
// Lambda 表达式
Runnable runnable2 = () -> System.out.println("mianshiya.com");
日期类
Java 8 引入了新的日期和时间 API(位于 java.time 包中),它们更加简洁和易于使用,解决了旧版日期时间 API 的许多问题。
例如 Date、Calendar 都是可变类且线程不安全。而新的日期类都是不可变的,一旦创建就不能修改,这样可以避免意外的修改,提升代码的安全性和可维护性。
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
Date 本身不包含时区信息,必须使用 Calendar 类来处理时区,但使用起来非常复杂且容易出错。
新 API 提供了专门的时区类(如 ZonedDateTime, OffsetDateTime, ZoneId 等),简化了时区处理,并且这些类的方法更加直观和易用。
接口默认方法、静态
默认方法允许在接口中定义方法的默认实现,这样接口的实现类不需要再实现这些方法。之所以提供静态方法,是为了将相关的方法内聚在接口中,而不必创建新的对象。
interface MyInterface {
default void defaultMethod() {
System.out.println("Default Method");
}
static void hello() {
System.out.println("Hello, New Static Method Here");
}
}
Stream 流式接口
Stream API 提供了一种高效且易于使用的方式来处理数据集合。它支持链式操作、惰性求值和并行处理。
List<String> list = Arrays.asList("a", "b", "c", "d");
List<String> result = list.stream()
.filter(s -> s.startsWith("a"))
.collect(Collectors.toList());
Optional
Optional 类用来解决可能出现的 NullPointerException 问题,提供了一种优雅的方式来处理可能为空的值。
Optional<String> optional = Optional.of("mianshiya.com");
optional.ifPresent(System.out::println);
Optional 详细可查看面试鸭《什么是 Optional 类?》 这题
CompletableFuture
CompletableFuture 提供了一个新的异步编程模型,简化了异步任务的编写和管理。
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World")
.thenAccept(System.out::println);
StampedLock 可查看面试鸭 《StampedLock 用过吗?》 这题。
13. Java 中 String、StringBuffer 和 StringBuilder 的区别是什么?
String 是 Java 中基础且重要的类,并且 String 也是 Immutable 类的典型实现,被声明为 final class,除了 hash 这个属性其它属性都声明为 final。
因为它的不可变性,所以例如拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。
StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类,提供 append 和 add 方法,可以将字符串添加到已有序列的末尾或指定位置。
它的本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上了 synchronized。但是保证了线程安全是需要性能的代价的。
在很多情况下我们的字符串拼接操作不需要线程安全,这时候 StringBuilder 登场了,StringBuilder是JDK1.5发布的,它和 StringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。
StringBuffer 和 StringBuilder 二者都继承了 AbstractStringBuilder ,底层都是利用可修改的 char 数组(JDK 9 以后是 byte 数组)。
所以如果我们有大量的字符串拼接,如果能预知大小的话最好在 new StringBuffer 或者 StringBuilder 的时候设置好 capacity,避免多次扩容的开销(扩容要抛弃原有数组,还要进行数组拷贝创建新的数组)。
选择建议
- String:适用于少量字符串操作或需要字符串常量池优化的场景。
- StringBuffer:适用于多线程环境下频繁的字符串操作。
- StringBuilder:适用于单线程环境下频繁的字符串操作。
14. Java 的 StringBuilder 是怎么实现的?
平时 StringBuilder 都用了哪些方法:
- append
- insert
- delete
- replace
- charAt
- ....
脑子浮现这几个方法之后,直接说出来即可:StringBuilder 主要用于动态拼接字符串,大致需要实现 append、insert……等功能。
然后底层使用 char 数组来存储字符,用 count 来记录存放的字符数。
由于 StringBuilder 底层是用 char 数组存放字符,而数组是连续内存结构,为了防止频繁地复制和申请内存,需要提供 capacity 参数来设置初始化数组的大小,这样在预先已经知晓大字符串的情况下,可以减少数组的扩容次数,有效的提升效率。
数组是连续内存的结构,并且要体现出你有节省内存和提高效率的意识,熟悉 HashMap 的同学对这类操作应该很有经验。
StringBuilder 的内部实现就是数组的操作,而数组的特性就是内存连续,下标访问快。
针对内存连续这点,又要保持 StringBuilder 的动态性,那不可避免的就需要扩容操作,扩容操作简单来说就是申请一个更大 char 数组,把老 char 数组的数据拷贝过去。
从源码来看,StringBuilder 没有实现缩容操作。
所以回答这个设计题的时候,可以先说下需要实现哪些关键方法:append、delete 等等,然后点明底层是 char 数组实现,在执行 append、insert 等操作的时候需要先判断数组容量是否足够容纳字符来判断是否需要扩容,然后修改之类的操作就是调用 System.arraycopy 来完成字符串的变更。
15. Java 中包装类型和基本类型的区别是什么?
Java 中有 8 种基本数据类型,这些基本类型又有对应的包装类。
因为 Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将int 、double等类型放进去的。因为集合的容器要求元素是 Object类型。
为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
基本类型与包装类型的区别
- 默认值不同:基本类型的默认值是 0,false 等,包装类默认为 null
- 初始化的方式不同:一个需要采用 new 的方式创建,一个则不需要
- 存储方式有所差异:基本类型主要保存在栈上面,包装类对象保存在堆上(成员变量的话,在不考虑JIT优化的栈上分配时,都是随着对象一起保存在堆上的)
16. JDK 和 JRE 有什么区别?
JRE( Java Runtime Environment )指的是 Java 运行环境,包含了 JVM、核心类库和其他支持运行 Java 程序的文件。
- JVM(Java Virtual Machine):执行 Java 字节码,提供了 Java 程序的运行环境。
- 核心类库:一组标准的类库(如 java.lang、java.util 等),供 Java 程序使用。
- 其他文件:如配置文件、库文件等,支持 JVM 的运行。
JDK( Java Development Kit )可以视为 JRE 的超集,是用于开发 Java 程序的完整开发环境,它包含了 JRE,以及用于开发、调试和监控 Java 应用程序的工具。
- JRE:JDK 包含了完整的 JRE,因此它也能运行 Java 程序。
- 开发工具:如编译器( javac )、调试器( jdb )、打包工具( jar )等,用于开发和管理 Java 程序。
- 附加库和文件:支持开发、文档生成和其他开发相关的任务。
17. 你使用过哪些 JDK 提供的工具?
这个题目主要考察你平日里面是否有过利用 JDK 的工具进行问题的分析、排查。
比如排查内存问题的时候,利用 jmap 生成堆转储文件,下载后利用 Eclipse 的 MAT 工具进行分析。
列几个常见工具:
- jps:虚拟机进程状况工具
- jstat:虚拟机统计信息监视工具
- jmap:Java内存映像工具
- jhat:虚拟机堆转储快照分析工具
- jstack:Java堆栈跟踪工具
- jinfo:Java配置信息工具
- VisualVM:图形化工具,可以得到虚拟机运行时的一些信息:内存分析、CPU 分析等等,在 jdk9 开始不再默认打包进 jdk 中。
18. Java 中 hashCode 和 equals 方法是什么?它们与 == 操作符有什么区别?
hashCode、equals 和 == 都是 Java 中用于比较对象的三种方式,但是它们的用途和实现还是有挺大区别的。
hashCode
方法返回对象的哈希码(整数),主要用于支持基于哈希表的集合,用来确定对象的存储位置,如 HashMap、HashSet 等。
Object 类中的默认实现会根据对象的内存地址生成哈希码(native 方法)。
在 Java 中,hashCode 方法和 equals 方法有一个通用合同:
- 如果两个对象根据 equals 方法被认为是相等的,那么它们必须具有相同的哈希码。
- 如果两个对象具有相同的哈希码,它们并不一定相等,但会被放在同一个哈希桶中。
equals
用于比较两个对象的内容是否相等。Object 类中的默认实现会使用 == 操作符来比较对象的内存地址。
通常我们需要在自定义类中重写 equals 方法,以基于对象的属性进行内容比较。比如你可以自定义两个对象的名字一样就是相等的、年龄一样就是相等,可以灵活按照需求定制。
如果两个对象的 equals 方法返回 true,则它们的 hashCode 方法必须返回相同的值,反之则不需要。
对于 equals 定义的比较,实际上还有以下五个要求:
- 自反性:对于任何非空引用值 x,x.equals(x) 必须返回 true。
- 对称性:对于任何非空引用值 x 和 y,如果 x.equals(y) 返回 true,则 y.equals(x) 也必须返回 true。
- 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true 且 y.equals(z) 返回 true,则 x.equals(z) 也必须返回 true。
- 一致性:对于任何非空引用值 x 和 y,只要对象在比较中没有被修改,多次调用 x.equals(y) 应返回相同的结果。
- 对于任何非空引用值 x,x.equals(null) 必须返回 false。
==
== 操作符用于比较两个引用是否指向同一个对象(即比较内存地址),如果是基本数据类型,== 直接比较它们的值。
区别总结
hashCode 用于散列存储结构中确定对象的存储位置。可用于快速比较两个对象是否不同,因为如果它们的哈希码不同,那么它们肯定不相等。
equals 用于比较两个对象的内容是否相等,通常需要重写自定义比较逻辑。
== 用于比较两个引用是否指向同一个对象(即内存地址)。对于基本数据类型,比较它们的值。
19. Java 中的hashCode 和 equals 方法之间有什么关系?
一般情况下两者是没啥关系。但,如果是将一个对象用在散列表的相关类的时候,是有关系的。
比如 HashSet,我们常用来得到一个不重复的集合。
现在有个 Yes 类的 HashSet 集合,我只重写了 Yes 类的 equals 方法,表明如果 name 相同就返回 true。
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Yes) {
Yes other = (Yes) obj;
return name.equals(other.name);
}
return false;
}
就重写一个 equals 的话,HashSet 中会出现相同 name 的 Yes 对象。
原因就是 hashCode 没有重写,那为什么会这样呢?因为 HashSet 是复用 HashMap 的能力存储对象,而塞入 key 的时候要计算 hash 值,可以看到这里实际会调用对象的 hashCode 方法来计算 hash 值。
然后在具体执行 putVal 方法的时候,相关的判断条件会先判断 hash 值是否相等,如果 hash 值都不同,那就认为这两个对象不相等,这与我们之前设定的 name 一样的对象就是相等的条件就冲突了,我们简单看下源码就清楚了:
可以看到,相关的判断条件都是先判断 hash 值,如果 hash 值相等,才会接着判断 equals。如果 hash 值不等,这个判断条件直接就 false 了。
因此规定,重写 equals 方法的时候,也要重写 hashCode 方法,这样才能保持条件判断的同步。
20.什么是 Java 中的动态代理?
动态代理是 Java 提供的一种强大机制,用于在运行时创建代理类或代理对象,以实现接口的行为,而不需要提前在代码中定义具体的类。动态是相对于静态来说的,之所以动态就是因为动作发生在运行时。
代理可以看作是调用目标的一个包装,通常用来在调用真实的目标之前进行一些逻辑处理,消除一些重复的代码。
静态代理指的是我们预先编码好一个代理类,而动态代理指的是运行时生成代理类。
动态更加方便,可以指定一系列目标来动态生成代理类(AOP),而不像静态代理需要为每个目标类写对应的代理类。
代理也是一种解耦,目标类和调用者之间的解耦,因为多了代理类这一层。
动态代理的主要用途包括:
- 简化代码:通过代理模式,可以减少重复代码,尤其是在横切关注点(如日志记录、事务管理、权限控制等)方面。
- 增强灵活性:动态代理使得代码更具灵活性和可扩展性,因为代理对象是在运行时生成的,可以动态地改变行为。
- 实现 AOP:动态代理是实现面向切面编程(AOP, Aspect-Oriented Programming)的基础,可以在方法调用前后插入额外的逻辑。
21. JDK 动态代理和 CGLIB 动态代理有什么区别?
回答:
JDK 动态代理是基于接口的,所以要求代理类一定是有定义接口的。使用 java.lang.reflect.Proxy 类和
java.lang.reflect.InvocationHandler 接口实现。
CGLIB 基于 ASM 字节码生成工具,它是通过继承的方式来实现代理类,所以要注意 final 方法。
它们之间的性能随着 JDK 版本的不同而不同:
jdk6 下,在运行次数较少的情况下,jdk动态代理与 cglib 差距不明显,甚至更快一些;而当调用次数增加之后, cglib 表现稍微更快一些
jdk7 下,情况发生了逆转!在运行次数较少(1,000,000)的情况下,jdk动态代理比 cglib 快了差不多30%;而当调用次数增加之后(50,000,000), 动态代理比 cglib 快了接近1倍
jdk8 表现和 jdk7 基本一致
22. Java中的注解原理是什么?
注解其实就是一个标记,可以标记在类上、方法上、属性上等,标记自身也可以设置一些值。
有了标记之后,我们就可以在解析的时候得到这个标记,然后做一些特别的处理,这就是注解的用处。
比如我们可以定义一些切面,在执行一些方法的时候看下方法上是否有某个注解标记,如果是的话可以执行一些特殊逻辑(RUNTIME类型的注解)。
注解生命周期有三大类,分别是:
- RetentionPolicy.SOURCE:给编译器用的,不会写入 class 文件
- RetentionPolicy.CLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了
- RetentionPolicy.RUNTIME:会写入 class 文件,永久保存,可以通过反射获取注解信息
所以我上文写的是解析的时候,没写具体是解析啥,因为不同的生命周期的解析动作是不同的。
像常见的:
就是给编译器用的,编译器编译的时候检查没问题就over了,class文件里面不会有 Override 这个标记。
再比如 Spring 常见的 Autowired ,就是 RUNTIME 的,所以在运行的时候可以通过反射得到注解的信息,还能拿到标记的值 required 。
所以注解就是一个标记,可以给编译器用、也能运行时候用。
23.你使用过 Java 的反射机制吗?如何应用反射?
反射其实就是 Java 提供的能在运行期得到对象信息的能力,包括属性、方法、注解等,也可以调用其方法。
一般在业务编码中不会用到反射,在框架上用的较多,因为很多场景需要很灵活,不确定目标对象的类型,届时只能通过反射动态获取对象信息。
例如 Spring 使用反射机制来读取和解析配置文件,从而实现依赖注入和面向切面编程等功能。
比如动态代理场景可以使用反射机制在运行时动态地创建代理对象。
所以反射机制的优点是:
- 可以动态地获取类的信息,不需要在编译时就知道类的信息。
- 可以动态地创建对象,不需要在编译时就知道对象的类型。
- 可以动态地调用对象的属性和方法,在运行时动态地改变对象的行为。
虽然反射很灵活,但是它有个明显的缺点,性能问题。
如果正常调用没影响,但是在高并发场景下就一点性能问题就会放大。
之所以反射有性能问题是因为反射是在运行时进行的,所以程序每次反射解析检查方法的类型等都需要从 class 的类信息加载进行运行时的动态检查。
所以 Apache BeanUtils 的 copy 在高并发下就有性能问题。
如何优化呢?
缓存,例如把第一次得到的 Method 缓存起来,后续就不需要再调用 Class.getDeclaredMethod 也就不需要再次动态加载了,这样就可以避免反射性能问题。
24.什么是 Java 的 SPI ( Service Provider Interface ) 机制?
SPI(Service Provider Interface)服务提供接口是 Java 的机制,主要用于实现模块化开发和插件化扩展。SPI 机制允许服务提供者通过特定的配置文件将自己的实现注册到系统中,然后系统通过反射机制动态加载这些实现,而不需要修改原始框架的代码,从而实现了系统的解耦、提高了可扩展性。
一个典型的 SPI 应用场景是 JDBC(Java 数据库连接库),不同的数据库驱动程序开发者可以使用 JDBC 库,然后定制自己的数据库驱动程序。
此外,我们使用的主流 Java 开发框架中,几乎都使用到了 SPI 机制,比如 Servlet 容器、日志框架、ORM 框架、Spring 框架。所以这是 Java 开发者必须掌握的一个重要特性!
SPI 的实现分为系统实现和自定义实现。细节可以来面试鸭:「链接」查看。
25. Java 泛型的作用是什么?什么是泛型擦除?
泛型可以把类型当作参数一样传递,使得像一些集合类可以明确存储的对象类型,不用显示地强制转化(在没泛型之前只能是Object,然后强转)。
并且在编译期能识别类型,类型错误则会提醒,增加程序的健壮性和可读性。
泛型擦除指的是参数类型在编译之后就被抹去了,也就是生成的 class 文件是没有泛型信息的,所以称之为擦除。
不过这个擦除有个细节,我们来看下代码就很清晰了,代码如下:
然后我们再来看看编译后的 class 文件。
以看到 yess 是有类型信息的,所以在代码里写死的泛型类型是不会被擦除的!
这也解释了为什么根据反射是可以拿到泛型信息的,因为这种写死的就没有被擦除!
至于泛型擦除是为了向后兼容,因为在 JDK 5 之前是没有泛型的,所以要保证 JDK 5 之前编译的代码可以在之后的版本上跑,而类型擦除就是能达到这一目标的一个实现手段。
其实 Java 也可以搞别的手段来实现泛型兼容,只是擦除比较容易实现。
最后再推荐下鸭鸭目前努力在做面试神器面试鸭「链接」,已经有 4000 多道面试题目啦,欢迎大家来阅读!如果大家有不会的面试题,也可以在小程序内反馈!鸭鸭会第一时间为大家解答!