【中大厂面试题】腾讯云 java 后端 最新面试题

发布于:2025-04-20 ⋅ 阅读:(23) ⋅ 点赞:(0)

腾讯云(一面) 

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是一个接口,EmailMessageServiceSmsMessageService是它的两个实现类。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

实现思路:

  1. 线程协作:通过 wait() 和 notifyAll() 实现线程间的同步。

  2. 共享状态:使用一个共享变量 current 表示当前应该打印的数字,并通过取模运算(% 3)决定哪个线程应该执行打印。

  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. 反问

  • 请问部门用的技术栈是什么?有什么挑战?

  • 面试官你觉得我有哪些可以加强的地方?


网站公告

今日签到

点亮在社区的每一天
去签到