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

60 阅读5分钟

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

王铁牛怀揣着对互联网大厂的向往,走进了这场紧张的 Java 面试。严肃的面试官正襟危坐,一场技术的较量即将展开。

第一轮提问

  • 面试官:首先问几个 Java 核心知识的问题。Java 中基本数据类型有哪些?
  • 王铁牛:这个我知道,有 byte、short、int、long、float、double、char、boolean。
  • 面试官:不错。那 Java 里的访问修饰符有哪些,分别有什么作用?
  • 王铁牛:访问修饰符有 public、protected、default(默认,不写修饰符时)、private。public 修饰的成员可以被任何类访问;protected 修饰的成员可以被同一个包内的类以及不同包的子类访问;默认的成员只能被同一个包内的类访问;private 修饰的成员只能在本类中访问。
  • 面试官:很好。那说说 Java 中的多态是怎么实现的?
  • 王铁牛:多态主要通过继承和接口实现。有两种表现形式,方法重载和方法重写。方法重载是在同一个类中,方法名相同但参数列表不同;方法重写是子类重写父类的方法,当通过父类引用指向子类对象时,调用重写方法会表现出不同的行为。
  • 面试官:回答得很清晰。看来你对 Java 核心知识掌握得不错。

第二轮提问

  • 面试官:接下来聊聊 JUC、JVM 和多线程的内容。JUC 包下有哪些常用的类或工具?
  • 王铁牛:我知道有 CountDownLatch、CyclicBarrier、Semaphore 这些。CountDownLatch 可以让一个或多个线程等待其他线程完成操作;CyclicBarrier 是让一组线程达到一个屏障时被阻塞,直到最后一个线程到达屏障时,所有线程才会继续执行;Semaphore 用来控制同时访问特定资源的线程数量。
  • 面试官:回答得挺好。那 JVM 的内存区域是如何划分的?
  • 王铁牛:嗯……JVM 内存区域主要有堆、栈、方法区等。堆是存储对象实例的地方;栈是每个线程私有的,存储局部变量等;方法区存储类的信息、常量、静态变量等。
  • 面试官:那多线程中,线程的生命周期有哪些状态?
  • 王铁牛:有新建、就绪、运行、阻塞、死亡这几种状态。新建就是刚创建线程对象;就绪是线程具备了运行条件,等待 CPU 调度;运行就是线程正在执行;阻塞是线程因为某些原因暂停执行;死亡就是线程执行完毕或者出现异常终止。
  • 面试官:表现不错,对这块知识也有一定了解。

第三轮提问

  • 面试官:现在问一些关于集合、框架和中间件的问题。HashMap 的底层数据结构是什么?
  • 王铁牛:HashMap 底层是数组 + 链表 + 红黑树。当链表长度达到 8 且数组长度大于 64 时,链表会转换为红黑树,以提高查找效率。
  • 面试官:那 Spring 框架的核心特性有哪些?
  • 王铁牛:Spring 的核心特性有依赖注入和面向切面编程。依赖注入就是将对象的依赖关系交给 Spring 容器来管理,降低了代码的耦合度;面向切面编程可以在不修改原有代码的基础上,对程序进行增强,比如实现日志记录、事务管理等。
  • 面试官:Spring Boot 是如何实现自动配置的?
  • 王铁牛:呃……这个……好像是通过一些注解和配置文件吧,具体的我有点不太清楚了。
  • 面试官:那 MyBatis 中 #{} 和 ${} 的区别是什么?
  • 王铁牛:这个……我记得 #{} 是预编译处理,能防止 SQL 注入;${} 是字符串替换,可能会有 SQL 注入风险,但具体原理我不太能说清楚。
  • 面试官:最后问一下,Dubbo、RabbitMq、xxl - job、Redis 这些技术,你能简单说一下它们的作用吗?
  • 王铁牛:Dubbo 是一个分布式服务框架,用于实现服务的远程调用;RabbitMq 是消息队列,能实现异步通信和系统解耦;xxl - job 是分布式任务调度平台;Redis 是缓存数据库,能提高数据访问速度。不过它们的具体使用和实现细节我不是很熟悉。

面试总结 面试官推了推眼镜,表情严肃地说:“王铁牛,通过这次面试,我能看出你对 Java 核心知识、JUC、JVM、多线程等基础内容有一定的掌握,在回答一些简单问题时表现不错,思路也比较清晰。但是在面对一些相对复杂的问题,比如 Spring Boot 自动配置原理、MyBatis 中 #{} 和 ${} 的深入原理以及 Dubbo、RabbitMq、xxl - job、Redis 等技术的具体使用细节时,你的回答就比较模糊,不够深入。我们公司对技术的要求比较高,不仅要掌握基础知识,还需要对常用的框架和中间件有深入的理解和实践经验。你回去等通知吧,后续如果有结果会及时联系你。”

问题答案详细解析

  1. Java 中基本数据类型有哪些?
    • Java 中有 8 种基本数据类型,分为 4 类:
      • 整数类型:byte(1 字节)、short(2 字节)、int(4 字节)、long(8 字节)。
      • 浮点类型:float(4 字节)、double(8 字节)。
      • 字符类型:char(2 字节),用于存储单个字符。
      • 布尔类型:boolean,只有 true 和 false 两个值。
  2. Java 里的访问修饰符有哪些,分别有什么作用?
    • public:访问权限最大,被 public 修饰的类、方法、变量可以被任何类访问。
    • protected:可以被同一个包内的类以及不同包的子类访问。主要用于在继承体系中,允许子类访问父类的部分成员。
    • default(默认):如果不写任何访问修饰符,就是默认访问权限。只能被同一个包内的类访问。
    • private:访问权限最小,被 private 修饰的成员只能在本类中访问,其他类无法直接访问,常用于封装数据,保证数据的安全性。
  3. 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. JUC 包下有哪些常用的类或工具?
    • CountDownLatch:一个同步辅助类,允许一个或多个线程等待其他线程完成操作。通过一个计数器来实现,初始化时设置计数器的值,当一个线程完成操作后,计数器减 1,当计数器为 0 时,等待的线程可以继续执行。例如:
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("Task completed");
                latch.countDown();
            }).start();
        }
        latch.await();
        System.out.println("All tasks are completed");
    }
}
- CyclicBarrier:让一组线程达到一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,所有线程才会继续执行。可以重复使用。例如:
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(() -> {
                try {
                    System.out.println("Thread is waiting at the barrier");
                    barrier.await();
                    System.out.println("Thread continues after the barrier");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
- Semaphore:用来控制同时访问特定资源的线程数量。通过 acquire() 方法获取许可,release() 方法释放许可。例如:
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println("Thread is accessing the resource");
                    Thread.sleep(1000);
                    semaphore.release();
                    System.out.println("Thread has released the resource");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  1. JVM 的内存区域是如何划分的?
    • 堆(Heap):是 JVM 中最大的一块内存区域,用于存储对象实例和数组。所有通过 new 关键字创建的对象都存放在堆中。堆是线程共享的,垃圾回收主要就是针对堆进行的。堆又可以分为新生代和老年代,新生代还可以细分为 Eden 区、Survivor 区。
    • 栈(Stack):每个线程都有自己的栈,用于存储局部变量、方法调用的上下文等。栈中的数据是线程私有的,当线程执行一个方法时,会创建一个栈帧,用于存储该方法的局部变量、操作数栈、动态链接等信息。方法执行完毕后,栈帧会被销毁。
    • 方法区(Method Area):也是线程共享的区域,用于存储类的信息(如类的结构、方法字节码等)、常量、静态变量等。在 JDK 1.8 之前,方法区也被称为永久代,JDK 1.8 及以后使用元空间(Metaspace)来替代永久代。
    • 程序计数器(Program Counter Register):每个线程都有一个独立的程序计数器,用于记录当前线程执行的字节码指令的地址。它是线程私有的,不会发生内存溢出异常。
    • 本地方法栈(Native Method Stack):与虚拟机栈类似,只不过它是为本地方法(使用 native 关键字修饰的方法)服务的。
  2. 多线程中,线程的生命周期有哪些状态?
    • 新建(New):当创建一个 Thread 对象时,线程处于新建状态,此时线程还没有开始执行。例如:Thread thread = new Thread();
    • 就绪(Runnable):当调用线程的 start() 方法后,线程进入就绪状态,此时线程已经具备了运行条件,等待 CPU 调度。例如:thread.start();
    • 运行(Running):当 CPU 调度到该线程时,线程进入运行状态,开始执行线程的 run() 方法。
    • 阻塞(Blocked):线程在执行过程中,由于某些原因(如等待锁、调用 sleep() 方法等)会进入阻塞状态,暂停执行。阻塞状态分为三种:等待阻塞(调用 Object.wait() 方法)、同步阻塞(获取对象的锁失败)、其他阻塞(调用 Thread.sleep()、Thread.join() 等方法)。
    • 死亡(Terminated):线程执行完毕或者出现异常终止,线程进入死亡状态。线程一旦死亡,就不能再重新启动。
  3. HashMap 的底层数据结构是什么?
    • HashMap 的底层数据结构是数组 + 链表 + 红黑树。
    • 数组:也称为哈希桶,每个元素是一个链表或红黑树的头节点。当插入一个键值对时,首先通过哈希函数计算键的哈希值,然后根据哈希值找到对应的数组位置。
    • 链表:当多个键的哈希值相同(哈希冲突)时,会将这些键值对存储在同一个数组位置的链表中。链表的插入和查找操作的时间复杂度是 O(n)。
    • 红黑树:当链表长度达到 8 且数组长度大于 64 时,链表会转换为红黑树。红黑树是一种自平衡的二叉搜索树,插入、查找和删除操作的时间复杂度是 O(log n),可以提高查找效率。当红黑树的节点数小于 6 时,会转换回链表。
  4. Spring 框架的核心特性有哪些?
    • 依赖注入(Dependency Injection,DI):是一种设计模式,将对象的依赖关系交给 Spring 容器来管理。通过依赖注入,对象之间的耦合度降低,提高了代码的可维护性和可测试性。例如,一个类需要使用另一个类的对象时,不需要在类内部创建该对象,而是通过构造函数、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 AOP 主要通过代理模式实现,有基于 JDK 动态代理和 CGLIB 代理两种方式。

9. Spring Boot 是如何实现自动配置的? - Spring Boot 的自动配置主要基于以下几个关键组件和机制: - @SpringBootApplication 注解:这是一个组合注解,包含了 @SpringBootConfiguration、@EnableAutoConfiguration 和 @ComponentScan。其中 @EnableAutoConfiguration 开启了自动配置功能。 - spring.factories 文件:在 Spring Boot 的类路径下,有一个 META - INF/spring.factories 文件,该文件中定义了一系列的自动配置类。例如:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
    - 条件注解:Spring Boot 使用了一系列的条件注解(如 @ConditionalOnClass@ConditionalOnMissingBean 等)来判断是否需要进行自动配置。例如,@ConditionalOnClass 表示只有当指定的类在类路径中存在时,才会进行相应的配置。
@Configuration
@ConditionalOnClass(DataSource.class)
public class DataSourceAutoConfiguration {
    // 配置数据源的相关代码
}
  1. MyBatis 中 #{} 和 ${} 的区别是什么?
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为占位符 ?,然后使用 PreparedStatement 进行预编译,能有效防止 SQL 注入攻击。例如:
<select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>
- ${}:是字符串替换,MyBatis 在处理 ${} 时,会直接将 ${} 替换为传入的参数值。由于是直接替换,可能会存在 SQL 注入风险。例如:
<select id="getUserByColumnName" parameterType="String" resultType="User">
    SELECT * FROM users ORDER BY ${columnName}
</select>
一般情况下,建议使用 #{},只有在需要动态传入表名、列名等场景下才使用 ${},并且要对传入的参数进行严格的验证和过滤。

11. Dubbo、RabbitMq、xxl - job、Redis 这些技术的作用是什么? - Dubbo:是一个分布式服务框架,用于实现服务的远程调用。在分布式系统中,不同的服务可能部署在不同的服务器上,Dubbo 可以让服务之间像本地调用一样方便。它提供了服务注册与发现、负载均衡、集群容错等功能,提高了系统的可扩展性和可用性。 - RabbitMq:是一个消息队列中间件,用于实现异步通信和系统解耦。在分布式系统中,不同的服务之间可以通过消息队列进行通信,生产者将消息发送到队列中,消费者从队列中获取