《互联网大厂 Java 面试:核心知识、并发、框架与中间件大考验》

84 阅读3分钟

在互联网大厂的一间安静面试室内,严肃的面试官正对面坐着略显紧张的求职者王铁牛,一场关于 Java 技术的面试即将展开。

第一轮面试 面试官:首先问你几个基础的 Java 核心知识问题。Java 中面向对象的四大特性是什么,能简单阐述一下吗? 王铁牛:这个我知道,面向对象的四大特性是封装、继承、多态和抽象。封装就是把数据和操作数据的方法绑定起来,隐藏内部实现细节;继承是子类继承父类的属性和方法;多态是同一个行为具有多个不同表现形式;抽象就是定义抽象类和抽象方法,让子类去实现。 面试官:回答得很不错。那 Java 中的基本数据类型有哪些,它们的包装类分别是什么? 王铁牛:Java 的基本数据类型有 byte、short、int、long、float、double、char、boolean。对应的包装类分别是 Byte、Short、Integer、Long、Float、Double、Character、Boolean。 面试官:很好。那说说 String、StringBuilder 和 StringBuffer 的区别。 王铁牛:String 是不可变的,一旦创建,其值不能被改变。而 StringBuilder 和 StringBuffer 是可变的。StringBuffer 是线程安全的,它的方法大多是同步的;StringBuilder 是非线程安全的,但是它的性能比 StringBuffer 要高一些。

第二轮面试 面试官:接下来问一些 JUC、JVM 和多线程相关的问题。JUC 包中常用的并发工具类有哪些,能举例说明它们的使用场景吗? 王铁牛:嗯……我知道有 CountDownLatch,好像是可以让一个或多个线程等待其他线程完成操作。具体场景嘛,就是比如很多任务并发执行,主线程要等这些任务都完成了再继续执行。其他的我有点记不太清了。 面试官:那说说 JVM 的内存结构,各个部分的作用是什么? 王铁牛:JVM 内存结构有堆、栈、方法区等。堆主要是存放对象实例的,栈是每个线程私有的,用来存储局部变量等。方法区嘛,好像是存一些类信息什么的,具体我也说不太明白。 面试官:多线程中,线程的生命周期有哪些状态,它们是如何转换的? 王铁牛:线程生命周期有新建、就绪、运行、阻塞和死亡。转换嘛,新建之后就到就绪,然后可能就运行,运行过程中可能会阻塞,之后又回到就绪,最后死亡。但具体的触发条件我不太清楚。

第三轮面试 面试官:现在问你一些框架和中间件的问题。Spring 框架中,依赖注入的方式有哪些,各有什么优缺点? 王铁牛:依赖注入方式好像有属性注入、构造器注入。优缺点嘛,我觉得都差不多,没太仔细研究过。 面试官:Spring Boot 是如何实现自动配置的? 王铁牛:这个……好像是有一些配置文件,自动加载什么的,具体我也不太懂。 面试官:MyBatis 中,#{} 和 ${} 的区别是什么? 王铁牛:这两个……我记得一个是预编译,一个不是,但具体哪个是哪个我搞混了。

面试官推了推眼镜,说道:“今天的面试就到这里,你先回家等通知吧。后续我们会综合评估你的表现,有结果会及时联系你。”

答案详解

  1. Java 中面向对象的四大特性
    • 封装:将数据和操作数据的方法绑定在一起,隐藏对象的内部实现细节,只对外提供必要的接口。例如,一个类中的私有属性,通过公有的 getter 和 setter 方法来访问和修改,这样可以保证数据的安全性和完整性。
    • 继承:子类可以继承父类的属性和方法,从而实现代码的复用和扩展。子类可以在父类的基础上添加新的属性和方法,或者重写父类的方法。例如,定义一个父类 Animal,子类 Dog 继承自 Animal,Dog 可以拥有 Animal 的属性和方法,同时还可以有自己特有的属性和方法。
    • 多态:同一个行为具有多个不同表现形式。多态通过继承和方法重写或者接口实现来实现。例如,定义一个父类 Shape,有一个 draw() 方法,子类 Circle 和 Rectangle 都重写了 draw() 方法,当使用父类引用指向子类对象时,调用 draw() 方法会根据实际的子类对象调用相应的方法。
    • 抽象:抽象是指将一类对象的共同特征总结出来,形成抽象类或抽象方法。抽象类不能实例化,它的主要作用是为子类提供一个通用的模板,子类必须实现抽象类中的抽象方法。例如,定义一个抽象类 Shape,其中有一个抽象方法 area(),子类 Circle 和 Rectangle 必须实现 area() 方法来计算各自的面积。
  2. Java 中的基本数据类型及其包装类
    • 基本数据类型
      • byte:8 位有符号整数,范围是 -128 到 127。
      • short:16 位有符号整数,范围是 -32768 到 32767。
      • int:32 位有符号整数,范围是 -2147483648 到 2147483647。
      • long:64 位有符号整数,范围更大,在数值后面需要加 L 或 l 表示。
      • float:32 位单精度浮点数,在数值后面需要加 F 或 f 表示。
      • double:64 位双精度浮点数,是 Java 中默认的浮点类型。
      • char:16 位 Unicode 字符,用单引号表示。
      • boolean:表示布尔值,只有 true 和 false 两个值。
    • 包装类:基本数据类型的包装类提供了一些方法和属性,方便对基本数据类型进行操作。例如,Integer 类提供了 parseInt() 方法将字符串转换为整数,Double 类提供了 doubleValue() 方法将包装类对象转换为基本数据类型。
  3. String、StringBuilder 和 StringBuffer 的区别
    • String:String 类是不可变的,一旦创建,其值不能被改变。每次对 String 进行修改操作,都会创建一个新的 String 对象,这会导致大量的内存开销。例如:
String s = "hello";
s = s + " world"; 

这里实际上创建了一个新的 String 对象 "hello world",原来的 "hello" 对象仍然存在于内存中。 - StringBuilder:StringBuilder 是可变的,它在内部使用一个可变的字符数组来存储字符串。它是非线程安全的,但是性能较高,适合在单线程环境下进行大量的字符串拼接操作。例如:

StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append(" world");
- **StringBuffer**:StringBuffer 也是可变的,同样使用可变的字符数组来存储字符串。它是线程安全的,因为它的方法大多是同步的,适合在多线程环境下进行字符串拼接操作。但是由于同步操作会带来一定的性能开销,所以在单线程环境下性能不如 StringBuilder。

4. JUC 包中常用的并发工具类及使用场景 - CountDownLatch:允许一个或多个线程等待其他线程完成操作。例如,有一个主线程需要等待多个子线程完成任务后再继续执行,可以使用 CountDownLatch。示例代码如下:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 模拟线程执行任务
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 完成任务");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            }).start();
        }

        // 主线程等待所有子线程完成任务
        latch.await();
        System.out.println("所有子线程任务完成,主线程继续执行");
    }
}
- **CyclicBarrier**:让一组线程达到一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。例如,多个线程需要同时开始执行某个任务,可以使用 CyclicBarrier。
- **Semaphore**:用于控制同时访问某个资源的线程数量。例如,一个数据库连接池,只允许一定数量的线程同时获取连接,可以使用 Semaphore 来控制。

5. JVM 的内存结构及各部分作用 - 堆(Heap):是 JVM 中最大的一块内存区域,用于存放对象实例。所有通过 new 关键字创建的对象都存放在堆中。堆是线程共享的,垃圾回收主要就是针对堆进行的。 - 栈(Stack):每个线程都有自己的栈,栈中存储局部变量、方法调用信息等。栈是线程私有的,每个方法在执行时会创建一个栈帧,栈帧中包含局部变量表、操作数栈、动态链接、方法出口等信息。方法执行结束后,栈帧会被弹出。 - 方法区(Method Area):用于存储类的信息、常量、静态变量等。方法区也是线程共享的,在 JDK 1.8 之前,方法区也被称为永久代,JDK 1.8 之后,使用元空间(Metaspace)来替代永久代。 - 程序计数器(Program Counter Register):是一块较小的内存区域,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,它是线程私有的。 - 本地方法栈(Native Method Stack):与栈类似,但是它是为本地方法服务的。本地方法是使用本地语言(如 C、C++)实现的方法。 6. 多线程中线程的生命周期及状态转换 - 新建(New):当创建一个 Thread 对象时,线程处于新建状态。例如:

Thread thread = new Thread();
- **就绪(Runnable)**:调用线程的 start() 方法后,线程进入就绪状态,等待获取 CPU 时间片。例如:
thread.start();
- **运行(Running)**:当线程获得 CPU 时间片后,进入运行状态,开始执行线程的 run() 方法。
- **阻塞(Blocked)**:线程在运行过程中,可能会因为某些原因进入阻塞状态,例如调用了 sleep()、wait()、join() 方法,或者获取锁失败等。阻塞状态的线程暂时放弃 CPU 时间片,等待阻塞原因解除后,再进入就绪状态。
- **死亡(Terminated)**:线程的 run() 方法执行完毕或者抛出异常,线程进入死亡状态,生命周期结束。

7. Spring 框架中依赖注入的方式及优缺点 - 属性注入:通过 setter 方法或直接在属性上使用注解(如 @Autowired)进行注入。优点是代码简洁,方便使用;缺点是可能会导致对象在未完全初始化时被使用,因为属性注入可以在对象创建后随时进行。示例代码如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class UserService {
    private UserDao userDao;

    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}
- **构造器注入**:通过构造方法进行依赖注入。优点是可以保证对象在创建时就完成依赖注入,避免对象在未完全初始化时被使用;缺点是如果依赖的对象过多,构造方法会变得很长,代码可读性变差。示例代码如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class UserService {
    private UserDao userDao;

    @Autowired
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}
  1. Spring Boot 实现自动配置的原理 Spring Boot 的自动配置是基于 Spring 的条件注解和类路径扫描实现的。在启动 Spring Boot 应用时,会通过 @SpringBootApplication 注解引入 @EnableAutoConfiguration 注解,该注解会触发自动配置机制。Spring Boot 会根据类路径下的依赖和配置文件中的配置,自动配置一些常用的组件和功能。例如,如果类路径下存在 MyBatis 的依赖,Spring Boot 会自动配置 MyBatis 的相关组件。具体步骤如下:
    • Spring Boot 启动时,会扫描 META - INF/spring.factories 文件,该文件中定义了所有的自动配置类。
    • 根据条件注解(如 @ConditionalOnClass、@ConditionalOnMissingBean 等)来判断是否需要进行自动配置。例如,@ConditionalOnClass 表示只有当指定的类存在于类路径中时,才会进行自动配置。
    • 如果满足条件,Spring Boot 会将相应的组件注册到 Spring 容器中。
  2. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ? 占位符,然后使用 PreparedStatement 进行参数设置,这样可以防止 SQL 注入攻击。例如:
<select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>
- **${}**:是字符串替换,MyBatis 在处理 ${} 时,会直接将 ${} 替换为传入的参数值。这种方式可能会导致 SQL 注入攻击,因为如果传入的参数包含恶意 SQL 代码,会直接拼接到 SQL 语句中。例如:
<select id="getUserByTableName" parameterType="String" resultType="User">
    SELECT * FROM ${tableName}
</select>

所以,在实际开发中,尽量使用 #{} 来防止 SQL 注入。只有在需要动态拼接表名、列名等情况时,才使用 ${},并且要对传入的参数进行严格的验证和过滤。