腾讯云(一面)
1. spring 和 springboot的区别是什么?
配置方式的区别:Spring 应用的配置较为繁琐,通常需要编写大量的 XML 配置文件或者使用 Java 注解进行配置。例如,配置数据源、事务管理器等都需要手动编写详细的配置代码。Spring Boot 采用了 “约定优于配置” 的原则,它会根据项目中引入的依赖自动进行合理的默认配置。
开发效率的区别:Spring 由于需要手动配置大量的组件和依赖,开发一个 Spring 应用的前期准备工作较多,开发效率相对较低,特别是在搭建一个新的项目时,需要花费较多的时间来配置环境和集成各种功能。Spring Boot提供了很多开箱即用的功能和快速启动的依赖(Starter),开发者可以通过添加相应的 Starter 依赖来快速集成各种功能,如 Web 开发、数据库访问、安全认证等,这样可以大大减少开发时间,提高开发效率。
部署难道的区别:Spring 应用通常需要部署到外部的应用服务器(如 Tomcat、Jetty 等)中,部署过程相对复杂,需要将应用打包成 WAR 文件,然后部署到应用服务器中,还需要对应用服务器进行相应的配置。Spring Boot 应用可以以独立的 Java 程序形式运行,它内置了嵌入式的 Web 服务器(如 Tomcat、Jetty 等),可以将应用打包成可执行的 JAR 文件,直接通过
java -jar
命令运行,这样大大简化了部署过程,降低了部署难度。
2. @Autowire 注入的属性可不可以是一个接口?
可以的,@Autowired
注解注入的属性可以是一个接口。
Spring 的依赖注入机制能够依据类型来查找合适的 Bean 进行注入,当使用@Autowired
注解对一个接口类型的属性进行注入时,Spring 会在容器中寻找实现了该接口的 Bean,如果存在多个实现类,Spring 会依据特定的规则来确定具体注入哪个 Bean。
以下是一个简单示例,展示了如何使用@Autowired
注入接口类型的属性:
// 定义一个接口
interface MessageService {
String getMessage();
}
// 接口的一个实现类
@Component
class EmailMessageService implements MessageService {
@Override
public String getMessage() {
return"This is an email message.";
}
}
// 另一个实现类
@Component
class SmsMessageService implements MessageService {
@Override
public String getMessage() {
return"This is an SMS message.";
}
}
// 使用接口的类
@Component
class MessagePrinter {
@Autowired
private MessageService messageService;
public void printMessage() {
System.out.println(messageService.getMessage());
}
}
在上述代码中,MessageService
是一个接口,EmailMessageService
和SmsMessageService
是它的两个实现类。MessagePrinter
类中有一个MessageService
类型的属性,使用@Autowired
进行注入。Spring 会从容器中找到一个实现了MessageService
接口的 Bean 注入到messageService
属性中。
3. 如果接口有多个实现类,怎么指定注入哪个?
如果存在多个实现类,Spring 默认会根据类型进行匹配。如果无法唯一确定一个 Bean,会抛出NoUniqueBeanDefinitionException
异常。
为了避免这种情况,可以使用@Qualifier
注解可以指定要注入的 Bean 的名称。
@Component
class MessagePrinter {
@Autowired
@Qualifier("emailMessageService")
private MessageService messageService;
public void printMessage() {
System.out.println(messageService.getMessage());
}
}
也可以,在某个实现类上使用@Primary
注解,将其标记为首选的 Bean。
@Component
@Primary
class EmailMessageService implements MessageService {
@Override
public String getMessage() {
return "This is an email message.";
}
}
4. AOP底层原理是什么?
Spring AOP(面向切面编程)的底层原理主要基于 动态代理 技术,通过在运行时动态生成代理对象,将切面逻辑(如日志、事务等)织入目标方法中,底层主要基于两种代理机制来实现,分别是 JDK 动态代理和 CGLIB 代理
JDK 动态代理:JDK 动态代理是基于接口的代理方式。它利用 Java 的反射机制,在运行时创建一个实现了目标对象所实现的所有接口的代理对象。当调用代理对象的方法时,会触发一个
InvocationHandler
的invoke
方法,在这个方法中可以添加额外的逻辑,从而实现对目标方法的增强。JDK 是 Java 内置的代理机制,无需额外引入依赖,使用方便。并且由于是基于接口的代理,符合面向接口编程的原则,代码的可维护性和可扩展性较好。CGLIB 代理:使用CGLIB库来创建代理对象,代理对象继承了目标对象,并且覆盖了目标对象的方法,通过方法拦截来实现代理逻辑。因为CGLIB动态代理可以代理任意的目标对象,所以它的应用场景更加广泛,通常用来实现基于继承的代理。
在 Spring AOP 中,会根据目标对象是否实现了接口来选择使用 JDK 动态代理还是 CGLIB 代理,具体规则如下:
如果目标对象实现了接口,Spring 默认会使用 JDK 动态代理。
如果目标对象没有实现接口,Spring 会使用 CGLIB 代理。 也可以通过配置强制 Spring 使用 CGLIB 代理,只需要在配置文件中添加相应的配置即可。
5. 你知道动态代理有哪些实现方式?jdk和cglib有什么区别?
Java的动态代理是一种在运行时动态创建代理对象的机制,主要用于在不修改原始类的情况下对方法调用进行拦截和增强。
Java动态代理主要分为两种类型:
基于接口的代理(JDK动态代理): 这种类型的代理要求目标对象必须实现至少一个接口。Java动态代理会创建一个实现了相同接口的代理类,然后在运行时动态生成该类的实例。这种代理的实现核心是
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口。每一个动态代理类都必须实现InvocationHandler
接口,并且每个代理类的实例都关联到一个handler
。当通过代理对象调用一个方法时,这个方法的调用会被转发为由InvocationHandler
接口的invoke()
方法来进行调用。基于类的代理(CGLIB动态代理): CGLIB是一个强大的高性能的代码生成库,它可以在运行时动态生成一个目标类的子类。CGLIB代理不需要目标类实现接口,而是通过继承的方式创建代理类。因此,如果目标对象没有实现任何接口,可以使用CGLIB来创建动态代理。
6. 哪些场景适合用反射?
反射是允许程序在运行时动态地获取类的信息、创建对象、调用方法和访问字段等,适合反射的场景如下。
框架开发:像 Spring 框架就大量运用反射来达成依赖注入。在 Spring 里,容器会在运行时借助反射创建对象实例,并且将依赖的对象注入到目标对象中。例如,当你在配置文件或者使用注解声明一个 Bean 时,Spring 容器会通过反射机制来实例化这个 Bean,同时把它所依赖的其他 Bean 注入进去。
插件化开发:在插件化系统中,主程序通常需要在运行时动态加载和使用插件。反射可以帮助主程序在不知道插件具体实现类的情况下,创建插件对象并调用其方法。例如,一个图形处理软件可能允许用户安装不同的图像滤镜插件,主程序可以通过反射来加载并使用这些插件。
JSON 序列化和反序列化:在将 Java 对象转换为 JSON 字符串或者将 JSON 字符串转换为 Java 对象时,反射可以用来动态获取对象的属性和值。例如,Gson 库在进行 JSON 序列化和反序列化时,就会使用反射来访问对象的字段。
代码分析和调试:在开发过程中,有时候需要在运行时获取类的信息,例如类的所有方法、字段等。反射可以帮助开发者实现这样的功能,方便进行代码分析和调试。例如,开发者可以编写一个工具类,使用反射来打印出一个类的所有方法和字段。
动态代理:在实现动态代理时,反射是必不可少的。JDK 动态代理就是基于反射机制实现的,它允许在运行时创建一个实现了指定接口的代理对象,并在代理对象的方法调用中添加额外的逻辑。
7. 怎么判断对象是否应该被回收?
垃圾回收器(GC)负责回收不再使用的对象所占用的内存。判断对象是否应该被回收,主要有以下几种算法:
引用计数算法:为每个对象维护一个引用计数器,每当有一个地方引用该对象时,计数器加 1;当引用失效时,计数器减 1。当计数器的值为 0 时,就认为该对象不再被使用,可以被回收。优点是实现简单,判断效率高,能及时回收不再使用的对象,缺点是难以解决对象之间的循环引用问题。例如,对象 A 引用对象 B,对象 B 又引用对象 A,即使它们不再被其他对象引用,计数器的值也不会为 0,导致无法被回收。
可达性分析算法:从一系列被称为 “GC Roots” 的对象作为起始点,开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 到该对象不可达),则证明此对象是不可用的,可被回收。优点是可以有效解决循环引用问题,是目前主流的垃圾回收算法所采用的判断方式,缺点是实现较为复杂,需要遍历整个引用链,会带来一定的性能开销。
8. 哪些对象可以被视为GC Root?
可作为 GC Roots 的对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象:例如,在方法中创建的对象,被方法的局部变量引用。
方法区中类静态属性引用的对象:类的静态变量引用的对象。
方法区中常量引用的对象:例如,字符串常量池中的字符串对象。
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象:本地方法中引用的对象。
9. 你说的这些对象,你可以各举一些例子吗?
虚拟机栈中引用的对象,在
main
方法中,obj
是局部变量,存于虚拟机栈中,它所引用的MyClass
实例就是通过虚拟机栈中引用可达的对象。
public class StackReferenceExample {
public static void main(String[] args) {
// 方法中的局部变量,在虚拟机栈中
MyClass obj = new MyClass();
// 此时 obj 引用的对象是可达的,不会被回收
}
}
class MyClass {
// 类的定义
}
**方法区中类静态属性引用的对象,
staticObj
是StaticReferenceExample
类的静态属性,位于方法区,它所引用的MyClass
实例通过方法区中的静态属性引用可达。
public class StaticReferenceExample {
public static MyClass staticObj;
public static void main(String[] args) {
staticObj = new MyClass();
// 此时 staticObj 引用的对象由于是类的静态属性引用,是可达的,不会被回收
}
}
class MyClass {
// 类的定义
}
**方法区中常量引用的对象,
CONSTANT_STR
是一个常量,它引用的字符串对象 "Hello, World" 存于方法区的常量池中,通过常量引用可达。
public class ConstantReferenceExample {
public static final String CONSTANT_STR = "Hello, World";
public static void main(String[] args) {
// CONSTANT_STR 是常量,在方法区中,它引用的字符串对象 "Hello, World" 不会被回收
}
}
本地方法栈中 JNI 引用的对象,在下述代码中,
nativeMethod
是本地方法,在本地方法栈中执行,若在本地方法中通过 JNI 引用了某个对象,那么这个对象就属于本地方法栈中 JNI 引用的对象,在其被引用期间不会被回收。
public class JNIReferenceExample {
// 本地方法声明
public native void nativeMethod();
public static void main(String[] args) {
JNIReferenceExample example = new JNIReferenceExample();
// 假设 nativeMethod 中会通过 JNI 引用一个对象
example.nativeMethod();
}
static {
System.loadLibrary("nativeLibrary");
}
}
10. BIO、NIO、AIO的区别是什么?
BIO(blocking IO):就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。优点是代码比较简单、直观;缺点是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
NIO(non-blocking IO) :Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
AIO(Asynchronous IO) :是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
11. 同步和阻塞这两个概念介绍一下?
同步指的是任务执行的顺序性。在同步模式下,任务必须按预定的顺序依次执行,前一个任务完成后,才能执行下一个任务。
比如,同步函数调用过程,函数A调用函数B,必须等待函数B执行完毕并返回结果后,函数A才能继续执行。又或者同步I/O场景下,程序发起一个读取文件的操作,必须等待文件内容完全读入内存后才能继续执行后续代码。
同步不一定是阻塞的(例如非阻塞轮询),但通常同步操作会表现为阻塞行为。
阻阻塞指的是调用方的状态。当调用一个操作时,如果当前线程/进程必须等待该操作完成才能继续执行,则称为阻塞。
比如,阻塞I/O场景下:线程调用read()
读取网络数据时,如果数据未就绪,线程会被挂起(进入睡眠状态),直到数据到达。又或者,线程尝试获取一个已被占用的锁时,会被阻塞直到锁释放。
阻塞一定是同步的(因为必须等待操作完成),但同步不一定阻塞(见非阻塞轮询)。
12. 手撕:三个线程循环打印1-100
实现思路:
线程协作:通过
wait()
和notifyAll()
实现线程间的同步。共享状态:使用一个共享变量
current
表示当前应该打印的数字,并通过取模运算(% 3
)决定哪个线程应该执行打印。线程安全:所有操作在
synchronized
块中完成,避免竞态条件。
public class CyclicPrinting {
privatestaticfinalint MAX_NUMBER = 100;
privatestaticint current = 1; // 共享变量,表示当前应打印的数字
privatestaticfinal Object lock = new Object(); // 锁对象
public static void main(String[] args) {
// 创建三个线程
Thread threadA = new Thread(new Printer(0), "ThreadA");
Thread threadB = new Thread(new Printer(1), "ThreadB");
Thread threadC = new Thread(new Printer(2), "ThreadC");
// 启动线程
threadA.start();
threadB.start();
threadC.start();
}
staticclass Printer implements Runnable {
privatefinalint remainder; // 当前线程的标识(0、1、2)
public Printer(int remainder) {
this.remainder = remainder;
}
@Override
public void run() {
while (true) {
synchronized (lock) {
// 检查是否超过最大值
if (current > MAX_NUMBER) {
break;
}
// 如果当前线程不该打印,则等待
while (current % 3 != remainder) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
// 打印并更新 current
System.out.println(Thread.currentThread().getName() + ": " + current);
current++;
// 唤醒其他线程
lock.notifyAll();
}
}
}
}
}
13. 反问
请问部门用的技术栈是什么?有什么挑战?
面试官你觉得我有哪些可以加强的地方?