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

75 阅读9分钟

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

在互联网大厂的一间明亮的面试室内,严肃的面试官坐在桌前,对面坐着略显紧张的求职者王铁牛。一场对 Java 核心知识、JUC、JVM 等多方面技术的面试拉开了帷幕。

第一轮面试 面试官:首先,我们来聊聊 Java 核心知识。Java 中基本数据类型有哪些? 王铁牛:Java 基本数据类型有 byte、short、int、long、float、double、char、boolean。 面试官:回答得不错。那你说说 String 类为什么是不可变的? 王铁牛:因为 String 类是用 final 修饰的,它的底层是一个用 final 修饰的字符数组,一旦创建就不能改变。 面试官:很好。那在 Java 里,什么是多态?请举例说明。 王铁牛:多态就是同一个行为具有多个不同表现形式或形态的能力。比如父类有一个方法,子类重写了这个方法,当通过父类引用指向子类对象时,调用这个方法会执行子类重写后的方法。就像动物类有叫的方法,猫类和狗类继承动物类并重写叫的方法,用动物类引用指向猫对象或者狗对象,调用叫的方法会有不同的表现。 面试官:非常棒,你的基础很扎实。

第二轮面试 面试官:接下来我们谈谈 JUC、多线程和线程池。JUC 里的 CountDownLatch 是做什么用的? 王铁牛:CountDownLatch 可以让一个或多个线程等待其他线程完成操作。比如在一个任务中,主线程需要等待其他几个子线程都完成工作后再继续执行,就可以用 CountDownLatch。 面试官:回答正确。那多线程编程中,如何避免死锁? 王铁牛:可以通过按照相同顺序获取锁、设置锁的超时时间等方法来避免死锁。 面试官:不错。那线程池有哪些核心参数? 王铁牛:线程池的核心参数有 corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(线程空闲存活时间)、unit(时间单位)、workQueue(工作队列)、threadFactory(线程工厂)和 handler(拒绝策略)。 面试官:很好,看来你对多线程和线程池这块掌握得很清晰。

第三轮面试 面试官:现在我们说说一些框架和中间件。Spring 中的 IoC 和 AOP 是什么? 王铁牛:IoC 是控制反转,就是把对象的创建和依赖关系的管理交给 Spring 容器。AOP 是面向切面编程,它可以在不修改原有代码的情况下,对程序进行增强,比如实现日志记录、事务管理等。 面试官:解释得挺准确。那 Spring Boot 相比 Spring 有什么优势? 王铁牛:Spring Boot 可以快速搭建项目,它有自动配置的功能,减少了很多配置文件,开发起来更方便。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,它会把参数部分用占位符 ? 代替,能防止 SQL 注入。{} 是字符串替换,会直接把参数值拼接到 SQL 语句中,有 SQL 注入的风险。 面试官:整体表现不错。不过在面试过程中,对于一些问题虽然能说出大概,但在细节阐述上还可以更深入。我们会综合评估你的表现,你先回家等通知吧。

问题答案详解

1. Java 基本数据类型

Java 中有 8 种基本数据类型,可分为 4 类:

  • 整数类型
    • byte:占 1 个字节,取值范围 -128 到 127。
    • short:占 2 个字节,取值范围 -32768 到 32767。
    • int:占 4 个字节,取值范围 -2147483648 到 2147483647。
    • long:占 8 个字节,取值范围 -2^63 到 2^63 - 1,定义 long 类型常量时需要在数字后面加 L 或 l。
  • 浮点类型
    • float:占 4 个字节,单精度浮点数,定义 float 类型常量时需要在数字后面加 F 或 f。
    • double:占 8 个字节,双精度浮点数。
  • 字符类型
    • char:占 2 个字节,用于表示单个字符,用单引号括起来,如 'A'。
  • 布尔类型
    • boolean:只有两个值,true 和 false,用于逻辑判断。

2. String 类为什么是不可变的

String 类被 final 修饰,意味着它不能被继承。其底层是一个用 final 修饰的字符数组 private final char value[]。当一个 String 对象被创建后,它所包含的字符序列就被固定下来,不能再改变。如果对 String 对象进行拼接等操作,实际上是创建了一个新的 String 对象,原对象并不会改变。这样设计的好处有:

  • 安全性:在多线程环境下,不可变对象是线程安全的,因为它的状态不能被改变。
  • 缓存哈希码:String 类重写了 hashCode() 方法,由于 String 不可变,所以它的哈希码可以被缓存,提高了使用 String 作为键在哈希表中的性能。
  • 字符串常量池:可以让多个引用指向同一个字符串常量,节省内存。

3. 多态

多态是面向对象编程的一个重要特性,它允许不同的对象对同一消息做出不同的响应。实现多态有三个必要条件:

  • 继承:子类继承父类,形成类的层次结构。
  • 重写:子类重写父类的方法,提供自己的实现。
  • 父类引用指向子类对象:通过父类的引用调用重写后的方法,根据实际指向的子类对象来决定执行哪个子类的方法。

示例代码如下:

class Animal {
    public void call() {
        System.out.println("动物叫");
    }
}

class Cat extends Animal {
    @Override
    public void call() {
        System.out.println("喵喵喵");
    }
}

class Dog extends Animal {
    @Override
    public void call() {
        System.out.println("汪汪汪");
    }
}

public class PolymorphismExample {
    public static void main(String[] args) {
        Animal cat = new Cat();
        Animal dog = new Dog();
        cat.call(); 
        dog.call(); 
    }
}

4. CountDownLatch 的作用

CountDownLatch 是 JUC 包中的一个同步工具类,它允许一个或多个线程等待其他线程完成操作。它内部维护了一个计数器,在创建 CountDownLatch 对象时需要指定计数器的初始值。当一个线程完成任务后,调用 countDown() 方法将计数器减 1,当计数器的值变为 0 时,等待的线程就会被唤醒继续执行。

示例代码如下:

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 {
                    System.out.println(Thread.currentThread().getName() + " 开始工作");
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 工作完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            }).start();
        }

        latch.await();
        System.out.println("所有线程工作完成,主线程继续执行");
    }
}

5. 避免死锁的方法

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。避免死锁的方法有:

  • 按顺序获取锁:所有线程按照相同的顺序获取锁,避免出现循环等待的情况。
  • 设置锁的超时时间:如果一个线程在一定时间内无法获取到锁,就放弃并释放已经持有的锁,避免一直等待。
  • 使用可重入锁:可重入锁可以避免同一个线程多次获取同一把锁时出现死锁。
  • 检测死锁:通过工具或代码检测死锁的发生,并采取相应的措施进行处理。

6. 线程池的核心参数

  • corePoolSize:核心线程数,线程池在创建后会创建 corePoolSize 个核心线程,即使这些线程处于空闲状态,也不会被销毁(除非设置了 allowCoreThreadTimeOut 为 true)。
  • maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。当工作队列满了,并且线程数小于 maximumPoolSize 时,会创建新的线程来处理任务。
  • keepAliveTime:线程空闲存活时间,当线程池中的线程数量超过 corePoolSize 时,多余的线程在空闲时间超过 keepAliveTime 后会被销毁。
  • unit:时间单位,用于指定 keepAliveTime 的时间单位,如 TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。
  • workQueue:工作队列,用于存储等待执行的任务。常见的工作队列有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。
  • threadFactory:线程工厂,用于创建线程。可以通过自定义线程工厂来设置线程的名称、优先级等属性。
  • handler:拒绝策略,当工作队列满了,并且线程数达到 maximumPoolSize 时,新提交的任务会触发拒绝策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程处理任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。

7. Spring 中的 IoC 和 AOP

  • IoC(控制反转):是一种设计思想,将对象的创建和依赖关系的管理从代码中转移到 Spring 容器中。在传统的编程中,对象的创建和依赖关系的维护是由程序员手动完成的,而在 Spring 中,这些工作由 Spring 容器负责。通过 IoC,代码的耦合度降低,提高了代码的可维护性和可测试性。IoC 的实现方式主要有依赖注入(DI),包括构造函数注入、属性注入和接口注入。
  • AOP(面向切面编程):是对面向对象编程的一种补充,它允许在不修改原有代码的情况下,对程序进行增强。AOP 的主要概念有切面(Aspect)、连接点(Join Point)、切点(Pointcut)、通知(Advice)和引入(Introduction)。常见的应用场景有日志记录、事务管理、权限验证等。

8. Spring Boot 相比 Spring 的优势

  • 快速搭建项目:Spring Boot 提供了很多 Starter 依赖,只需要添加相应的依赖,就可以快速搭建一个项目,减少了很多配置文件和代码。
  • 自动配置:Spring Boot 会根据项目中添加的依赖自动进行配置,大大减少了开发人员的配置工作量。例如,添加了 Spring Data JPA 的依赖,Spring Boot 会自动配置数据源、JPA 等。
  • 内嵌服务器:Spring Boot 内嵌了 Tomcat、Jetty 等服务器,不需要额外部署服务器,直接运行项目就可以启动服务器。
  • 生产就绪特性:Spring Boot 提供了很多生产就绪的特性,如健康检查、指标监控、外部化配置等,方便在生产环境中对应用进行管理和监控。

9. MyBatis 中 #{} 和 ${} 的区别

  • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换成 ? 占位符,然后使用 PreparedStatement 进行预编译,防止 SQL 注入。例如:
<select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>

在执行时,MyBatis 会将 SQL 预编译为 SELECT * FROM users WHERE id = ?,然后将参数值设置到占位符中。

  • **:是字符串替换,MyBatis在处理{}**:是字符串替换,MyBatis 在处理 {} 时,会直接将 ${} 中的内容替换为参数值。例如:
<select id="getUserByTableName" parameterType="String" resultType="User">
    SELECT * FROM ${tableName}
</select>

如果传入的参数值不安全,可能会导致 SQL 注入攻击。因此,在使用时应尽量使用 #{},只有在需要动态指定表名、列名等情况下才使用 ${}。