# 后端经典面试题 by 爽爽学编程
# JDK 和 JRE 和 JVM 分别是什么,有什么区别?
JDK、JRE和JVM是Java编程语言中的核心组件,它们在Java开发和运行环境中扮演着不同的角色:
JDK(Java Development Kit):
- JDK是Java开发工具包,它包含了编写Java程序所需的所有工具,包括Java编译器
javac
、Java运行时环境JRE以及一些其他的开发工具,如调试器、Java文档生成器javadoc
等 - JDK是面向Java开发者的,它提供了开发Java应用程序所需的完整环境
JRE(Java Runtime Environment):
- JRE是Java运行时环境,它包含了Java虚拟机JVM和Java核心类库,但不包含开发工具
- JRE是面向最终用户的,它允许用户运行Java程序,但不支持开发
JVM(Java Virtual Machine):
- JVM是Java虚拟机,它是一个可以执行Java字节码的虚拟计算机。JVM为Java程序提供了一个抽象的计算机环境,使得Java程序可以在不同的操作系统和硬件平台上运行
- JVM负责加载Java类、执行字节码,并提供自动内存管理等功能
区别:
- 功能范围:JDK包含了JRE的所有功能,同时还提供了开发工具。JRE只包含运行Java程序所需的环境,而JVM是JRE的核心部分,负责执行Java字节码
- 目标用户:JDK主要面向开发者,JRE面向最终用户,JVM则是运行Java程序的底层平台
- 包含内容:JDK > JRE > JVM,JDK包含了JRE和JVM,JRE包含了JVM
# 什么是字节码?采用字节码的最大好处是什么?
字节码是一种中间状态的代码,它是编译器将高级语言编写的源代码编译成机器可以执行的指令之前的一个步骤。对于Java语言来说,字节码是编译器将Java源代码(.java文件)编译成的一种中间形式(.class文件),这种代码不是直接在硬件上执行的机器码,而是需要通过Java虚拟机(JVM)来解释执行
采用字节码的最大好处包括:
- 平台无关性(Write Once, Run Anywhere, WORA):
- Java语言的一个核心特性就是跨平台性。字节码可以在任何安装了JVM的平台上运行,而不需要针对不同的操作系统重新编译。这使得Java程序具有很好的移植性
- 安全性:
- JVM在执行字节码之前会进行一系列的安全检查,比如类型检查、数组越界检查等,这有助于防止恶意代码的执行和系统崩溃
- 优化执行:
- JVM可以在运行时对字节码进行即时编译(JIT, Just-In-Time compilation),将热点代码(频繁执行的代码)编译成机器码并缓存,从而提高程序的执行效率
- 内存管理:
- JVM提供了自动垃圾回收机制,可以自动管理内存,释放不再使用的内存空间,减轻了程序员的负担,同时减少了内存泄漏的风险
- 多线程支持:
- JVM支持多线程的实现,使得Java程序能够更好地利用多核处理器的优势,提高程序的并发性能
- 异常处理:
- Java的异常处理机制允许程序更加健壮,能够优雅地处理运行时错误,而不会直接导致程序崩溃
- 动态性:
- JVM支持动态加载和链接类,可以在运行时动态地添加类和资源,这为动态语言特性和插件架构提供了支持
# Java 和 C++、Go 语言的区别,各自的优缺点?
Java
优点:
- 跨平台性:Java的"一次编写,到处运行"(WORA)特性,使得Java程序可以在任何支持JVM的平台上运行
- 内存管理:Java有自动垃圾回收机制,减轻了内存管理的负担
- 面向对象:Java是一种纯面向对象的语言,支持封装、继承和多态
- 丰富的API:Java拥有大量的类库和框架,适用于各种应用开发
- 社区支持:Java有着庞大的开发者社区,提供了大量的资源和支持
缺点:
- 性能:Java程序通常比C++慢,因为JVM的存在和自动内存管理
- 内存消耗:Java程序可能比其他语言消耗更多的内存。
- 学习曲线:对于初学者来说,Java的面向对象特性可能需要一些时间来适应
C++
优点:
- 性能:C++提供了接近硬件级别的控制,因此性能通常优于Java
- 内存管理:C++允许手动内存管理,提供了更高的灵活性
- 多范式编程:C++支持过程式、面向对象和泛型编程
- 广泛的应用:C++被广泛应用于系统软件、游戏开发、嵌入式系统等领域
缺点:
- 复杂性:C++语言的复杂性较高,学习曲线陡峭
- 内存安全:手动内存管理可能导致内存泄漏和野指针等问题
- 跨平台性:C++程序需要针对不同的平台进行编译,跨平台性不如Java
Go
优点:
- 简洁性:Go语言设计简洁,语法清晰,易于学习
- 并发支持:Go内置了并发编程的支持,使用goroutine和channel简化了并发编程
- 编译速度:Go的编译速度快,有助于快速开发
- 内存管理:Go有自动垃圾回收机制,同时提供了一些手动内存管理的机制
- 现代工具链:Go拥有现代化的工具链,包括格式化工具、文档生成器等
缺点:
- 泛型:Go直到最近的版本才引入泛型,这可能限制了某些类型的编程
- 类型系统:Go的类型系统相对简单,可能不如Java或C++灵活
- 运行时性能:虽然Go的性能很好,但通常不如C++
总结
- Java:适合需要跨平台、高性能、内存管理自动化的大型企业级应用
- C++:适合需要极致性能和精细控制内存的应用,如游戏开发、嵌入式系统
- Go:适合需要快速开发、高效并发处理的网络服务和分布式系统
# JDK 动态代理和 CGLIB 动态代理的区别是什么?
JDK动态代理和CGLIB动态代理是Java中两种常用的动态代理实现方式,它们各自有不同的特点和使用场景
JDK动态代理是基于接口实现的代理模式。它使用Java的反射机制,在运行时通过Proxy
类和InvocationHandler
接口来创建代理类,代理类实现了指定的接口,并且所有的方法调用都会被转发到InvocationHandler
的invoke
方法中处理3。这种方式的优点是实现简单,最小化依赖关系,缺点是只能为实现了接口的类创建代理
CGLIB动态代理(Code Generation Library)则是一种基于继承的代理方式,它可以为任何类创建代理,无论这个类是否实现了接口,甚至可以代理final
类的方法(除了private
和static
方法)16。CGLIB通过使用ASM字节码生成框架,在运行时动态生成目标类的子类,并重写方法来实现代理16。CGLIB的优点是性能较高,因为它使用了FastClass机制直接调用方法,而不是通过反射1。但它的缺点是使用上相对复杂,且不能代理final
修饰的方法
# Java 中 final 关键字有什么用?
- final变量(常量):
- 当一个变量被声明为
final
,它的值在初始化之后就不能被再次修改。这意味着final
变量成为了一个常量。通常,常量的名字会使用全大写字母来表示,单词间用下划线分隔 - 局部变量(方法内定义的变量)如果不需要修改,也可以被声明为
final
,以明确表示它们是不可变的
- 当一个变量被声明为
- final方法:
- 声明为
final
的方法不能被子类重写。这可以用来限制该方法的进一步扩展,确保在类的整个继承链中,该方法的行为不会被改变 final
方法可以提高性能,因为JVM可能对final
方法进行优化,如内联
- 声明为
- final类:
- 被声明为
final
的类不能被继承。这通常用于工具类或者那些不希望被扩展的类 final
类中的所有成员方法默认都是final
的,因为没有办法重写它们
- 被声明为
- final参数:
- 将方法参数声明为
final
可以防止方法内部修改传入的参数值。这是一种保护参数不被修改的做法,有助于方法的安全性
- 将方法参数声明为
- final在匿名内部类中的使用:
- 在匿名内部类中,如果需要使用外部类的局部变量,该变量必须被声明为
final
,即使实际上没有打算修改它
- 在匿名内部类中,如果需要使用外部类的局部变量,该变量必须被声明为
- final与多态:
- 当使用多态调用一个方法时,如果该方法是
final
的,那么将调用实际对象类中的实现,而不是父类中的实现
- 当使用多态调用一个方法时,如果该方法是
- final与反射:
- 即使方法被声明为
final
,反射(Reflection)仍然可以调用它。但是,不能通过反射改变访问控制,即不能通过反射使final
方法变为可重写
- 即使方法被声明为
# Java 中 hashCode 和 equals 方法是什么?它们与 == 操作符有什么区别?
在Java中,hashCode
和equals
是两个在Object
类中定义的方法,它们在对象的比较和哈希表(如HashMap
、HashSet
等)的使用中有重要的作用
hashCode方法
hashCode
方法返回对象的哈希码,这是一个整数值,用于在哈希表中确定对象的存储位置- 不同对象的哈希码可以相同,但相同哈希码的对象不一定是相等的
- 一个好的哈希函数应该尽量减少哈希冲突,即尽量使不同对象的哈希码不同
equals方法
equals
方法用于比较两个对象是否相等。默认情况下,它比较对象的内存地址,但通常需要重写以比较对象的逻辑状态或内容- 如果两个对象通过
equals
方法比较结果为true
,则它们的哈希码也应该相同
hashCode与equals的一致性
- 根据Java的官方文档,
hashCode
方法必须和equals
方法保持一致性,即如果两个对象通过equals
方法比较结果为true
,那么调用这两个对象的hashCode
方法也应该返回相同的哈希码 - 违反这个一致性原则可能会导致哈希表的行为异常
==操作符
==
操作符用于比较对象的内存地址,即比较两个对象是否是同一个实例- 对于原始数据类型(如int、double等),
==
操作符比较的是值
hashCode与equals的区别
hashCode
是一个整数,用于快速比较和索引哈希表,而equals
是一个方法,用于比较对象的逻辑等价性hashCode
是equals
的补充,用于提高性能,特别是在哈希表的实现中hashCode
可能发生冲突,即不同的对象可能有相同的hashCode
,但equals
则不会,如果两个对象equals
返回true
,它们一定是同一个对象
实践中的注意事项
- 当你重写
equals
方法时,通常也需要重写hashCode
方法,以保持一致性 - 如果对象的逻辑等价性不包含所有参与
hashCode
计算的字段,那么不应该重写hashCode
方法,以避免违反一致性原则
# 什么是反射机制?说说反射机制的优缺点、应用场景?
反射机制是Java语言提供的一种能力,允许程序在运行时查询、访问和修改它自身的属性和行为。它是Java核心库的一部分,由java.lang.reflect
包提供支持
反射机制的主要特点:
- 动态获取类信息:可以获取到所有对象的运行时类(
getClass()
方法) - 动态创建对象:可以创建任意类的实例,即使该类是私有的构造函数(
Constructor
类的newInstance()
方法) - 动态访问对象的属性:可以获取和设置对象的属性值,不论访问修饰符是什么(
Field
类) - 动态调用方法:可以调用对象的方法,无论访问修饰符是什么(
Method
类) - 动态获取接口实现:可以获取一个类实现了哪些接口(
Class
类的getInterfaces()
方法)
反射机制的优点:
- 提高灵活性:可以在运行时动态地创建对象和调用方法,使程序更加灵活
- 解耦代码:可以在不修改源代码的情况下,通过配置文件来改变程序的行为
- 框架开发:许多流行的Java框架(如Spring、Hibernate)都大量使用反射机制来实现依赖注入、ORM映射等
- 简化代码编写:可以编写通用的代码来处理不同类型的对象,减少样板代码
反射机制的缺点:
- 性能开销:反射操作通常比直接的方法调用慢,因为它需要额外的类型检查和动态解析
- 安全问题:反射可以访问和修改私有的属性和方法,这可能会破坏封装性,导致安全问题
- 代码可读性降低:过度使用反射可能会使代码难以理解和维护
- 可能导致错误:由于反射允许执行一些非正常的操作,如访问不可见的属性和方法,这可能导致运行时错误
应用场景:
- 框架开发:如上所述,许多Java框架依赖反射来实现核心功能
- 动态代理:JDK动态代理就是基于反射机制实现的
- 单元测试:测试框架(如JUnit)使用反射来访问私有方法和属性,以便进行测试
- 配置文件解析:使用反射可以根据配置文件动态创建对象和调用方法
- 类浏览器和IDE:集成开发环境(IDE)通常使用反射来显示类的信息和成员
# Java 访问修饰符 public、private、protected,以及无修饰符(默认)的区别
Java中的访问修饰符决定了类、接口、方法、构造函数和变量的访问级别。主要有四种访问修饰符:public
、private
、protected
,以及不使用修饰符的情况(默认访问级别)。下面是它们各自的特性和区别:
- public(公共的):
public
是访问级别最高的修饰符。声明为public
的类、接口、方法或变量可以被任何其他类访问,不受限制- 例如,
public class MyClass
可以在任何地方被实例化
- private(私有的):
private
是访问级别最低的修饰符。声明为private
的成员只能在其所在的类内部访问,不能被外部类访问private
方法或变量不能被子类继承,也不能被同一个包中的其他类访问- 例如,
private void myMethod()
只能在声明它的类内部调用
- protected(受保护的):
protected
成员可以在其所在的类、同一个包中的其他类以及子类中访问,不论子类是否在同一个包中protected
的访问级别低于public
,但高于private
- 例如,
protected int myValue
可以在声明它的类、同一个包中的其他类和任何子类中访问
- 无修饰符(默认):
- 如果没有指定访问修饰符,那么类成员的访问级别是包级私有的,也就是说,只有同一个包中的其他类可以访问它们
- 这种访问级别通常被称为“默认”或“包”访问级别
- 例如,一个没有修饰符的类成员只能在同一个包中的其他类中访问
访问级别的比较:
public
>protected
>default
>private
public
类或成员:无限制访问protected
类或成员:可以被子类以及同一个包中的其他类访问default
(无修饰符)类或成员:只能被同一个包中的其他类访问private
类或成员:只能在声明它的类内部访问
使用场景:
public
:当你希望某个类或成员可以被任何其他类访问时使用private
:当你希望隐藏类的实现细节,确保类成员只能在类内部访问时使用protected
:当你希望允许子类访问父类的某些成员,但不希望这些成员对同一个包中的非子类可见时使用default
:当你希望限制类成员的访问范围仅在包内时使用
# String 和 StringBuffer、StringBuilder 的区别是什么?
String
、StringBuffer
和StringBuilder
是Java中常用的三种操作字符串的工具,它们各自有不同的特性和用途:
- String:
String
是不可变的字符串,一旦创建,其内容就不能被修改- 每次对
String
类型进行修改操作时,实际上都会生成一个新的String
对象 String
适合用于不需要频繁修改的字符串场景
- StringBuffer:
StringBuffer
是可变的字符串缓冲区,可以对它进行添加、删除和替换等操作StringBuffer
是线程安全的,因为它的所有公开方法都是同步的,可以在多线程环境中使用- 由于线程安全的特性,
StringBuffer
在单线程环境下的性能比StringBuilder
稍差
- StringBuilder:
StringBuilder
也是可变的字符串缓冲区,与StringBuffer
类似,可以进行修改操作StringBuilder
不是线程安全的,因为它的方法不是同步的,因此在单线程环境下性能更优- 由于不是线程安全的,
StringBuilder
在多线程环境下使用时需要谨慎
主要区别:
- 不变性:
String
是不可变的,而StringBuffer
和StringBuilder
是可变的 - 线程安全:
String
是不可变的,因此是线程安全的;StringBuffer
是线程安全的;StringBuilder
不是线程安全的 - 性能:在单线程环境下,
StringBuilder
由于没有同步的开销,通常比StringBuffer
性能更好;在多线程环境下,StringBuffer
由于其线程安全性,可能更适合使用
使用场景:
String
:当你需要一个不可变的字符串,或者字符串创建后不需要修改时使用StringBuffer
:在多线程环境中,需要对字符串进行修改时使用,例如,多个线程需要共享同一个字符串缓冲区StringBuilder
:在单线程环境中,需要对字符串进行频繁修改时使用,以获得更好的性能
代码示例:
String str = "Hello";
str = str + " World!"; // 实际上创建了一个新的String对象
StringBuffer sb = new StringBuffer("Hello");
sb.append(" World!"); // 在原有对象上修改
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World!"); // 在原有对象上修改
2
3
4
5
6
7
8
在Java 9之后,StringBuffer
和StringBuilder
都位于java.lang
包中,不再需要单独导入。此外,StringBuilder
通常是处理字符串的首选,除非你确定需要线程安全的字符串缓冲区,在这种情况下,才使用StringBuffer
# 什么是 Java 内部类? 内部类的分类有哪些 ?内部类有哪些优点和应用场景?
Java内部类是指定义在另一个类内部的类,它是Java语言中一个重要的特性,提供了一种新的类定义方式和作用域规则。内部类可以访问外部类的所有成员,包括私有成员,但反之则不行
内部类的分类:
- 成员内部类:
- 成员内部类是定义在外部类的一个成员位置的类,它与外部类其他成员(属性和方法)处于同一级别
- 它不能包含静态成员(除了静态内部类外)
- 局部内部类:
- 局部内部类是定义在一个方法内部的类,其作用域限定在该方法内
- 它通常用于匿名内部类的定义
- 匿名内部类:
- 匿名内部类是没有名字的内部类,常用于实现回调或者事件处理器
- 它主要用于实现接口或继承抽象类,并且只用于一个方法调用
- 静态内部类:
- 静态内部类是定义在外部类内部,但使用
static
修饰的类 - 它可以有静态成员,并且不依赖于外部类的实例,即它不持有对外部类实例的引用
- 静态内部类是定义在外部类内部,但使用
- 内部枚举和内部接口:
- Java也允许在类内部定义枚举和接口
内部类的优点:
- 增强封装性:内部类可以访问外部类的所有成员,但外部类不能直接访问内部类的成员
- 代码组织:内部类允许将逻辑相关的类组织在一起,使代码更加清晰
- 名称空间复用:内部类可以有与外部类相同的成员名称,避免了名称冲突
- 多态性:内部类可以覆盖外部类的同名方法,实现多态
- 易于使用:局部内部类和匿名内部类可以非常方便地在需要的地方快速定义和使用
应用场景:
- 实现回调:使用匿名内部类实现监听器或回调接口
- 构建代理:使用内部类创建代理类,实现装饰者模式或代理模式
- 组织逻辑相关的类:将功能相关的类组织在一起,提高代码的可读性
- 实现多重继承:由于Java不支持多重继承,可以使用内部类实现类似功能
- 创建线程:使用内部类创建线程,因为它可以访问外部类的私有成员
代码示例:
public class OuterClass {
private int outerVar = 0;
// 成员内部类
class InnerClass {
void accessOuterVar() {
outerVar = 10;
}
}
// 静态内部类
static class StaticInnerClass {
void accessOuterVar() {
OuterClass obj = new OuterClass();
obj.outerVar = 10; // 需要通过外部类实例访问
}
}
// 局部内部类
void method() {
class LocalInnerClass {
void display() {
System.out.println("I am a local inner class");
}
}
LocalInnerClass loc = new LocalInnerClass();
loc.display();
}
// 匿名内部类
interface MyInterface {
void sayHello();
}
void useInterface() {
MyInterface obj = new MyInterface() {
public void sayHello() {
System.out.println("Hello from anonymous inner class");
}
};
obj.sayHello();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
内部类提供了一种灵活的代码组织方式,使得代码更加模块化,但同时也增加了复杂性,因此在使用时需要权衡利弊
# 什么是数据库事务?讲一下事务的 ACID 特性?
数据库事务是一系列操作,这些操作作为一个整体被执行,以确保数据库的状态从一个一致的状态转移到另一个一致的状态。如果事务中的所有操作都成功完成,那么事务将被提交,所有的更改将被永久保存到数据库中。如果事务中的任何一个操作失败,整个事务将被回滚,所有的更改都会被撤销,数据库将返回到事务开始之前的状态
事务的ACID特性:
- 原子性(Atomicity):
- 事务中的所有操作要么全部完成,要么全部不完成,不会结束在中间某个点。这意味着事务中的操作具有原子性,它们被捆绑在一起,作为一个不可分割的单元
- 一致性(Consistency):
- 事务必须保证数据库从一个一致的状态转移到另一个一致的状态。一致性确保了数据库的完整性约束在事务执行前后保持一致
- 隔离性(Isolation):
- 并发执行的事务之间不会互相影响。每个事务都与其他事务隔离开来,好像它是在独立的数据库副本上操作一样。不同的隔离级别可以防止脏读、不可重复读和幻读等问题
- 持久性(Durability):
- 一旦事务提交,它对数据库的改变就是永久性的,即使系统发生故障也不会丢失。持久性保证事务提交后的结果能够被保存,不会因为系统崩溃、电源故障或其他问题而丢失
应用场景:
- 银行交易:在银行系统中,事务被用来确保转账操作的原子性和一致性。如果转账过程中任何一步失败,整个交易将回滚,以保证账户余额的正确性
- 库存管理:在库存系统中,事务用来确保库存数量的一致性。例如,当一个商品被购买时,库存数量需要相应减少,这个过程需要在一个事务中完成
- 订单处理:在电子商务中,订单处理涉及到多个步骤,如用户信息验证、库存检查、支付处理等,这些步骤需要在一个事务中完成,以保证订单的一致性
事务管理:
- 数据库管理系统(DBMS)通常提供了事务管理的机制,允许用户定义事务的边界(开始和结束),并确保ACID特性得到满足
- 在编程中,可以通过特定的API或框架来管理事务,如JDBC的
setAutoCommit(false)
和commit()
方法,或者Spring框架的声明式事务管理
事务是数据库操作的基石,确保了数据的完整性和一致性,是现代数据库系统不可或缺的一部分