《互联网大厂面试:揭秘 Java 核心、框架及中间件考察风暴》

42 阅读4分钟

互联网大厂面试:揭秘 Java 核心、框架及中间件考察风暴

在互联网大厂的一间明亮的面试室内,气氛紧张而严肃。面试官正襟危坐,面前摆放着求职者的简历,而求职者王铁牛则略显紧张地坐在对面。面试开始了。

第一轮面试 面试官:我们先从 Java 核心知识开始。你能说说 Java 中多态的实现方式有哪些吗? 王铁牛:嗯,Java 多态的实现方式主要有两种,一种是方法重载,就是在一个类中可以有多个同名方法,但参数列表不同;另一种是方法重写,子类可以重写父类的方法。 面试官:回答得不错。那再问你,Java 的基本数据类型有哪些,它们的包装类又是什么? 王铁牛:Java 的基本数据类型有 byte、short、int、long、float、double、char、boolean。对应的包装类分别是 Byte、Short、Integer、Long、Float、Double、Character、Boolean。 面试官:很好。那你讲讲 HashMap 的工作原理。 王铁牛:HashMap 底层是基于数组和链表(JDK8 之后还有红黑树)实现的。它通过 key 的 hash 值来确定数组的索引位置,如果该位置没有元素就直接插入,如果有元素就以链表的形式挂在后面。当链表长度达到一定阈值(默认是 8),就会转化为红黑树。

第二轮面试 面试官:接下来我们聊聊 JUC 和多线程。你知道线程的生命周期有哪些状态吗? 王铁牛:线程的生命周期有新建、就绪、运行、阻塞、死亡这几种状态。新建就是刚创建线程对象,就绪是线程获取了除 CPU 之外的所有资源,运行就是正在执行,阻塞是因为某些原因暂停执行,死亡就是线程执行完毕。 面试官:不错。那你说说线程池的好处以及常用的线程池有哪些。 王铁牛:线程池的好处就是可以降低资源消耗,提高响应速度,便于管理线程。常用的线程池有 FixedThreadPool,它是固定大小的线程池;CachedThreadPool,它是可缓存的线程池;ScheduledThreadPool,它可以执行定时任务;SingleThreadExecutor,它是单线程的线程池。 面试官:很好。那在多线程环境下,如何保证线程安全,说说你的理解。 王铁牛:这个嘛,我觉得可以用 synchronized 关键字来保证线程安全,它可以修饰方法或者代码块,同一时间只有一个线程能访问。还有就是用 Lock 接口的实现类,像 ReentrantLock 也能保证线程安全。

第三轮面试 面试官:现在我们来谈谈框架相关的。你说说 Spring 的核心特性有哪些。 王铁牛:Spring 的核心特性有依赖注入和面向切面编程。依赖注入就是对象之间的依赖关系由容器来管理,而不是在对象内部创建。面向切面编程可以在不修改原有代码的情况下,对程序进行增强。 面试官:那 Spring Boot 相比 Spring 有什么优势呢? 王铁牛:Spring Boot 可以快速搭建项目,它有自动配置的功能,减少了很多繁琐的配置。而且它内置了服务器,像 Tomcat,方便开发和测试。 面试官:最后问你,MyBatis 中 #{} 和 {} 的区别是什么。 **王铁牛**:这个……嗯……好像 #{} 是预编译的,能防止 SQL 注入,而 {} 是直接替换,可能会有 SQL 注入的风险。不过具体的我有点不太清楚。

面试官总结:王铁牛,今天的面试就到这里。从整体表现来看,你对一些基础的 Java 核心知识、多线程和框架的基本概念有一定的了解,在前面的回答中表现得还不错,说明你有一定的知识储备。但是在一些细节和复杂问题上,比如 MyBatis 中 #{} 和 ${} 的区别,你回答得不够清晰准确,这反映出你对某些知识点的掌握还不够深入。我们后续会综合评估所有面试者的情况,你先回家等通知吧。

问题答案详细解析

  1. Java 中多态的实现方式有哪些
    • 方法重载(Overloading):在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。参数列表不同包括参数的类型、个数、顺序不同。方法重载与返回值类型无关。例如:
public class OverloadExample {
    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 sound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("Dog barks");
    }
}
  1. Java 的基本数据类型有哪些,它们的包装类又是什么
    • 基本数据类型
      • 整数类型:byte(1 字节)、short(2 字节)、int(4 字节)、long(8 字节)。
      • 浮点类型:float(4 字节)、double(8 字节)。
      • 字符类型:char(2 字节)。
      • 布尔类型:boolean(理论上 1 位,但在 Java 中通常占 1 字节)。
    • 包装类:分别是 Byte、Short、Integer、Long、Float、Double、Character、Boolean。包装类的作用主要是将基本数据类型封装成对象,方便在一些需要对象的场景中使用,比如集合框架只能存储对象,不能存储基本数据类型。
  2. HashMap 的工作原理
    • 底层数据结构:JDK8 之前,HashMap 底层是数组 + 链表的结构;JDK8 及之后,是数组 + 链表 + 红黑树的结构。
    • 存储过程:当调用 put(key, value) 方法时,首先会对 key 进行 hash 计算,得到一个 hash 值,然后通过这个 hash 值和数组的长度进行取模运算,得到数组的索引位置。如果该位置没有元素,就直接将该键值对插入到这个位置。如果该位置已经有元素,就会遍历链表(或红黑树),如果找到相同的 key,就更新其对应的 value;如果没有找到,就将新的键值对插入到链表的尾部(JDK8 之前是插入到链表头部)。当链表的长度达到 8 且数组长度达到 64 时,链表会转化为红黑树,以提高查找效率;当红黑树的节点数小于 6 时,红黑树会退化为链表。
  3. 线程的生命周期有哪些状态
    • 新建(New):当创建一个 Thread 对象时,线程处于新建状态,此时线程还没有开始执行。例如:Thread thread = new Thread();
    • 就绪(Runnable):调用线程的 start() 方法后,线程进入就绪状态,此时线程已经获取了除 CPU 之外的所有资源,等待操作系统的调度来获取 CPU 时间片。例如:thread.start();
    • 运行(Running):当线程获得 CPU 时间片后,就进入运行状态,开始执行线程的 run() 方法中的代码。
    • 阻塞(Blocked):线程在执行过程中,可能会因为某些原因进入阻塞状态,比如调用了 sleep()、wait()、join() 等方法,或者在等待获取锁。阻塞状态的线程会暂停执行,直到满足特定的条件才会重新进入就绪状态。
    • 死亡(Terminated):线程的 run() 方法执行完毕,或者因为异常退出,线程就进入死亡状态,此时线程的生命周期结束。
  4. 线程池的好处以及常用的线程池有哪些
    • 好处
      • 降低资源消耗:通过重复利用已创建的线程,减少了线程创建和销毁所带来的开销。
      • 提高响应速度:当有任务提交时,线程池中有空闲线程可以立即执行任务,而不需要等待线程的创建。
      • 便于管理:可以对线程进行统一的管理,比如设置线程的数量、线程的优先级等。
    • 常用线程池
      • FixedThreadPool:固定大小的线程池,线程数量固定,当有新任务提交时,如果线程池中有空闲线程,就会立即执行任务;如果没有空闲线程,任务会被放入队列中等待。例如:ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
      • CachedThreadPool:可缓存的线程池,线程数量不固定,当有新任务提交时,如果线程池中有空闲线程,就会立即执行任务;如果没有空闲线程,就会创建新的线程来执行任务。当线程空闲时间超过 60 秒,就会被回收。例如:ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
      • ScheduledThreadPool:可以执行定时任务的线程池,例如可以定时执行任务或者延迟执行任务。例如:ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
      • SingleThreadExecutor:单线程的线程池,线程池中只有一个线程,任务会按照提交的顺序依次执行。例如:ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
  5. 在多线程环境下,如何保证线程安全
    • synchronized 关键字
      • 修饰方法:当一个方法被 synchronized 修饰时,同一时间只有一个线程能访问该方法。例如:
public class SynchronizedExample {
    public synchronized void method() {
        // 线程安全的代码
    }
}
    - **修饰代码块**:可以指定要同步的对象,同一时间只有一个线程能访问该代码块。例如:
public class SynchronizedExample {
    private Object lock = new Object();

    public void method() {
        synchronized (lock) {
            // 线程安全的代码
        }
    }
}
- **Lock 接口**:常用的实现类是 ReentrantLock。Lock 接口提供了比 synchronized 更灵活的锁机制,可以手动加锁和解锁。例如:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private Lock lock = new ReentrantLock();

    public void method() {
        lock.lock();
        try {
            // 线程安全的代码
        } finally {
            lock.unlock();
        }
    }
}
  1. Spring 的核心特性有哪些
    • 依赖注入(Dependency Injection,DI):对象之间的依赖关系由容器来管理,而不是在对象内部创建。例如,一个类需要另一个类的实例,可以通过构造函数、setter 方法等方式将该实例注入到类中。这样可以降低类之间的耦合度,提高代码的可维护性和可测试性。例如:
public class UserService {
    private UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public void addUser() {
        userDao.addUser();
    }
}
- **面向切面编程(Aspect-Oriented Programming,AOP)**:可以在不修改原有代码的情况下,对程序进行增强。例如,可以在方法执行前后添加日志记录、事务管理等功能。AOP 的实现主要基于代理模式,Spring 提供了两种代理方式:JDK 动态代理和 CGLIB 代理。例如:
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}

    @Before("serviceMethods()")
    public void beforeServiceMethod() {
        System.out.println("Before service method execution");
    }

    @After("serviceMethods()")
    public void afterServiceMethod() {
        System.out.println("After service method execution");
    }
}
  1. Spring Boot 相比 Spring 有什么优势
    • 快速搭建项目:Spring Boot 提供了很多 Starter 依赖,只需要在项目中添加相应的依赖,就可以快速集成各种功能,比如 Spring Boot Starter Web 可以快速搭建一个 Web 项目。
    • 自动配置:Spring Boot 会根据项目中添加的依赖自动进行配置,减少了很多繁琐的 XML 配置或者 Java 配置。例如,添加了 Spring Boot Starter Data JPA 依赖后,Spring Boot 会自动配置数据源、JPA 等。
    • 内置服务器:Spring Boot 内置了服务器,如 Tomcat、Jetty 等,不需要单独部署服务器,方便开发和测试。只需要运行项目的主类,就可以启动一个 Web 服务器。
  2. MyBatis 中 #{} 和 ${} 的区别是什么
    • #{}:是预编译的,它会将参数部分用占位符? 代替,然后在执行 SQL 语句时,将参数的值安全地传递给占位符。这样可以防止 SQL 注入攻击。例如:
<select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>
- **${}**:是直接替换,它会将参数的值直接插入到 SQL 语句中。如果参数的值是用户输入的,可能会导致 SQL 注入攻击。例如:
<select id="getUserByColumnName" parameterType="String" resultType="User">
    SELECT * FROM users WHERE ${columnName} = 'value'
</select>

在实际开发中,尽量使用 #{} 来避免 SQL 注入问题,只有在一些特殊情况下,比如需要动态指定表名、列名时,才使用 ${}。