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

43 阅读3分钟

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

在互联网大厂的一间明亮的面试室内,严肃的面试官正襟危坐,对面坐着略显紧张的求职者王铁牛。一场关于 Java 核心知识、JUC、JVM 等多个领域的面试即将展开。

第一轮面试 面试官:“首先问你几个基础问题。Java 中的多态有哪几种实现方式?” 王铁牛:“多态有两种实现方式,一种是方法重载,也就是在同一个类中,有多个方法名相同但参数列表不同的方法;另一种是方法重写,子类重写父类的方法。” 面试官:“回答得不错。那 ArrayList 和 LinkedList 在使用场景上有什么区别?” 王铁牛:“ArrayList 基于数组实现,它适合随机访问元素,因为可以通过下标直接访问,但是在插入和删除元素时效率较低,尤其是在数组中间插入或删除元素。而 LinkedList 基于双向链表实现,插入和删除元素效率较高,但是随机访问元素的效率较低。” 面试官:“很好。再问你,HashMap 的底层数据结构是什么?” 王铁牛:“HashMap 的底层数据结构是数组 + 链表 + 红黑树。当链表长度达到 8 且数组长度达到 64 时,链表会转换为红黑树,以提高查找效率。” 面试官:“非常棒,你的基础很扎实。”

第二轮面试 面试官:“接下来我们聊聊多线程和 JUC 相关的问题。什么是线程池?为什么要使用线程池?” 王铁牛:“线程池就是一个管理线程的容器,它可以预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。使用线程池可以减少线程创建和销毁的开销,提高系统的性能,还可以对线程进行统一的管理和监控。” 面试官:“回答得很清晰。那在 Java 中,有哪些常见的线程池?” 王铁牛:“常见的线程池有 FixedThreadPool、CachedThreadPool、ScheduledThreadPool 和 SingleThreadExecutor。FixedThreadPool 是固定大小的线程池,CachedThreadPool 是可缓存的线程池,ScheduledThreadPool 可以定时执行任务,SingleThreadExecutor 是只有一个线程的线程池。” 面试官:“不错。那说说 CountDownLatch 和 CyclicBarrier 的区别。” 王铁牛:“嗯……这个……它们好像都是用来控制线程同步的,具体区别我有点不太清楚了。” 面试官:“这两个还是有比较明显区别的,你后续可以再深入学习一下。”

第三轮面试 面试官:“现在来谈谈框架相关的问题。Spring 框架的核心特性有哪些?” 王铁牛:“Spring 框架的核心特性有 IoC(控制反转)和 AOP(面向切面编程)。IoC 是指将对象的创建和依赖关系的管理交给 Spring 容器,AOP 是指在不修改原有代码的基础上,对程序进行增强。” 面试官:“很好。那 Spring Boot 有什么优势?” 王铁牛:“Spring Boot 可以快速搭建 Spring 应用,它提供了自动配置的功能,减少了大量的配置文件,还可以通过 Starter 依赖来简化依赖管理。” 面试官:“不错。那 MyBatis 中 #{} 和 ${} 的区别是什么?” 王铁牛:“这个……好像一个是预编译,一个不是预编译,具体哪个是哪个我有点搞混了。” 面试官:“这个是 MyBatis 的一个基础知识点,你需要回去再巩固一下。”

面试接近尾声,面试官整理了一下手中的资料,说道:“今天的面试就到这里,你整体有一定的基础,对于一些基础问题回答得很不错,但在一些复杂的知识点上还存在不足,需要进一步加强学习。你先回家等通知吧,我们后续会综合评估后给你反馈。”

问题答案详细解析

  1. Java 中的多态有哪几种实现方式?
    • 方法重载(Overloading):在同一个类中,允许存在多个同名方法,但这些方法的参数列表(参数的类型、个数、顺序)必须不同。调用方法时,会根据传入的参数来决定调用哪个具体的方法。例如:
public class OverloadingExample {
    public int add(int a, int b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }
}
- 方法重写(Overriding):子类继承父类后,对父类中已有的方法进行重新定义。重写的方法需要和父类方法具有相同的方法名、参数列表和返回值类型(或者是其子类型)。例如:
class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}
  1. ArrayList 和 LinkedList 在使用场景上有什么区别?
    • ArrayList:基于数组实现,在内存中是连续存储的。因此,它支持随机访问,通过下标可以快速定位到元素,时间复杂度为 O(1)。但在插入和删除元素时,尤其是在数组中间插入或删除元素,需要移动大量的元素,时间复杂度为 O(n)。所以,当需要频繁随机访问元素,而插入和删除操作较少时,适合使用 ArrayList。
    • LinkedList:基于双向链表实现,每个节点包含数据和指向前一个节点和后一个节点的引用。插入和删除元素只需要修改节点的引用,时间复杂度为 O(1)(如果是在指定位置插入或删除,需要先遍历到该位置,时间复杂度为 O(n))。但随机访问元素时,需要从头节点或尾节点开始遍历,时间复杂度为 O(n)。所以,当需要频繁进行插入和删除操作,而随机访问操作较少时,适合使用 LinkedList。
  2. HashMap 的底层数据结构是什么?
    • HashMap 的底层数据结构是数组 + 链表 + 红黑树。数组被称为哈希桶,每个桶存储一个链表或红黑树的头节点。当有新的键值对插入时,首先通过哈希函数计算键的哈希值,然后根据哈希值找到对应的桶。如果该桶为空,则直接将键值对插入该桶;如果该桶已经有元素,则遍历链表或红黑树,查找是否已经存在相同的键,如果存在则更新其值,如果不存在则插入新的键值对。
    • 当链表长度达到 8 且数组长度达到 64 时,链表会转换为红黑树,以提高查找效率。红黑树是一种自平衡的二叉搜索树,查找、插入和删除操作的时间复杂度为 O(log n),而链表的查找时间复杂度为 O(n)。当树的节点数小于 6 时,红黑树会转换回链表。
  3. 什么是线程池?为什么要使用线程池?
    • 线程池:是一个管理线程的容器,它可以预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。任务执行完毕后,线程不会销毁,而是返回到线程池中等待下一个任务。
    • 使用线程池的原因
      • 减少线程创建和销毁的开销:线程的创建和销毁需要消耗系统资源,频繁地创建和销毁线程会影响系统的性能。使用线程池可以复用线程,减少这些开销。
      • 提高系统的响应速度:当有任务提交时,可以直接从线程池中获取线程来执行任务,而不需要等待线程的创建,从而提高系统的响应速度。
      • 统一管理和监控线程:线程池可以对线程进行统一的管理和监控,例如可以控制线程的数量、设置线程的优先级等。
  4. 在 Java 中,有哪些常见的线程池?
    • FixedThreadPool:固定大小的线程池,创建时需要指定线程的数量。当有任务提交时,如果线程池中有空闲线程,则分配一个线程来执行任务;如果没有空闲线程,则任务会被放入任务队列中等待。例如:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
- **CachedThreadPool**:可缓存的线程池,线程池的大小会根据任务的数量动态调整。当有任务提交时,如果线程池中有空闲线程,则分配一个线程来执行任务;如果没有空闲线程,则创建一个新的线程来执行任务。如果线程在 60 秒内没有被使用,则会被销毁。例如:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- **ScheduledThreadPool**:可以定时执行任务的线程池。它可以在指定的延迟时间后执行任务,也可以定期执行任务。例如:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("Task executed after delay");
    }
}, 5, TimeUnit.SECONDS);
- **SingleThreadExecutor**:只有一个线程的线程池,它可以保证任务按照提交的顺序依次执行。例如:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
  1. CountDownLatch 和 CyclicBarrier 的区别
    • CountDownLatch:是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。它通过一个计数器来实现,计数器的初始值为需要等待的线程数量。每个线程完成操作后,计数器的值减 1。当计数器的值为 0 时,等待的线程可以继续执行。CountDownLatch 只能使用一次,计数器的值不能重置。例如:
import java.util.concurrent.CountDownLatch;

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

        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " is working");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " has finished");
                latch.countDown();
            }).start();
        }

        latch.await();
        System.out.println("All threads have finished, main thread can continue");
    }
}
- **CyclicBarrier**:也是一个同步工具类,它允许一组线程相互等待,直到所有线程都到达某个屏障点,然后所有线程可以继续执行。CyclicBarrier 有一个初始的计数器,当一个线程到达屏障点时,计数器的值减 1。当计数器的值为 0 时,所有线程会同时被释放,并且计数器可以重置,以便下次使用。例如:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("All threads have reached the barrier");
        });

        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " is working");
                try {
                    Thread.sleep(1000);
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " has passed the barrier");
            }).start();
        }
    }
}
  1. Spring 框架的核心特性有哪些?
    • IoC(控制反转):也称为依赖注入(DI),是指将对象的创建和依赖关系的管理交给 Spring 容器。在传统的编程中,对象的创建和依赖关系的管理是由对象本身负责的,而在 Spring 中,这些工作由 Spring 容器来完成。通过 IoC,可以降低对象之间的耦合度,提高代码的可维护性和可测试性。例如:
public class UserService {
    private UserDao userDao;

    // 通过构造函数注入依赖
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public void addUser() {
        userDao.addUser();
    }
}
- **AOP(面向切面编程)**:是指在不修改原有代码的基础上,对程序进行增强。AOP 可以将一些通用的功能(如日志记录、事务管理等)从业务逻辑中分离出来,形成独立的切面,然后在需要的地方进行切入。这样可以提高代码的复用性和可维护性。例如,使用 Spring AOP 实现日志记录:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    @After("execution(* com.example.service.*.*(..))")
    public void logAfterMethod(JoinPoint joinPoint) {
        System.out.println("Method " + joinPoint.getSignature().getName() + " has been executed");
    }
}
  1. Spring Boot 有什么优势?
    • 快速搭建 Spring 应用:Spring Boot 提供了大量的 Starter 依赖,通过引入这些依赖,可以快速搭建一个 Spring 应用,减少了大量的配置文件和样板代码。例如,引入 spring-boot-starter-web 依赖就可以快速搭建一个 Web 应用。
    • 自动配置:Spring Boot 会根据项目中引入的依赖自动进行配置,大部分情况下不需要手动进行配置。例如,如果引入了 Spring Data JPA 和 MySQL 驱动,Spring Boot 会自动配置数据源和 JPA 相关的配置。
    • 嵌入式服务器:Spring Boot 内置了嵌入式服务器(如 Tomcat、Jetty 等),可以直接将应用打包成可执行的 JAR 文件,通过 java -jar 命令运行,无需部署到外部服务器。
    • 生产级特性:Spring Boot 提供了一些生产级特性,如健康检查、指标监控、外部配置等,方便对应用进行管理和监控。
  2. MyBatis 中 #{} 和 ${} 的区别是什么?
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为占位符 ?,然后使用 PreparedStatement 进行预编译。这样可以防止 SQL 注入攻击,因为 PreparedStatement 会自动对传入的参数进行转义。例如:
<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>

所以,在使用 MyBatis 时,尽量使用 #{} 来避免 SQL 注入问题,只有在需要动态传入表名、列名等无法使用占位符的情况下才使用 ${}。