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

52 阅读12分钟

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

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

第一轮面试开始

面试官:首先,我想问一下,Java 中多态的实现方式有哪些? 王铁牛:多态的实现方式主要有两种,一种是方法重载,在同一个类中,方法名相同但参数列表不同;另一种是方法重写,在子类中重写父类的方法。 面试官:回答得不错。那你说说 JVM 的内存结构是怎样的? 王铁牛:JVM 内存结构主要包括堆、栈、方法区、程序计数器和本地方法栈。堆是存放对象实例的地方,栈主要存储局部变量等,方法区存储类的信息、常量等。 面试官:很好。那在多线程编程中,synchronized 关键字的作用是什么? 王铁牛:synchronized 关键字可以保证在同一时刻,只有一个线程可以访问被它修饰的代码块或方法,起到同步的作用,防止多个线程同时操作共享资源导致数据不一致。 面试官:非常棒,你的基础很扎实。

第二轮面试开始

面试官:我们接着聊聊线程池。线程池有哪些常见的创建方式? 王铁牛:线程池可以通过 Executors 工具类创建,比如 newFixedThreadPool 可以创建固定大小的线程池,newCachedThreadPool 可以创建可缓存的线程池。 面试官:那你说说 HashMap 的底层数据结构是什么? 王铁牛:HashMap 的底层数据结构是数组 + 链表 + 红黑树。当链表长度超过 8 且数组长度大于 64 时,链表会转换为红黑树。 面试官:不错。那 ArrayList 和 LinkedList 的区别是什么?在什么场景下使用它们? 王铁牛:ArrayList 底层是数组,它的优点是随机访问速度快,适合需要频繁随机访问元素的场景;LinkedList 底层是链表,插入和删除元素的效率高,适合需要频繁插入和删除元素的场景。 面试官:回答得很清晰,看来你对这些数据结构有深入的理解。

第三轮面试开始

面试官:现在我们来谈谈框架。Spring 框架的核心特性有哪些? 王铁牛:Spring 框架的核心特性有依赖注入和面向切面编程。依赖注入可以降低代码的耦合度,面向切面编程可以实现一些通用的功能,比如日志记录、事务管理等。 面试官:那 Spring Boot 相对于 Spring 有什么优势? 王铁牛:Spring Boot 简化了 Spring 的配置,它有自动配置的功能,能让开发者快速搭建项目,提高开发效率。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,能防止 SQL 注入,它会把参数当成一个字符串处理;{} 是直接替换,可能存在 SQL 注入风险。 面试官:Dubbo 框架的核心功能有哪些? 王铁牛:Dubbo 框架的核心功能有服务注册与发现、远程调用、集群容错等。 面试官:看来你对这些框架有一定的了解。那 RabbitMQ 的工作模式有哪些? 王铁牛:RabbitMQ 的工作模式有简单模式、工作队列模式、发布订阅模式、路由模式、主题模式等。 面试官:xxl - job 的核心原理是什么? 王铁牛:呃……这个我有点不太清楚,好像是和任务调度有关吧。 面试官:Redis 有哪些数据类型?在实际项目中如何应用? 王铁牛:Redis 有字符串、哈希、列表、集合、有序集合这些数据类型。在实际项目中,字符串可以用于缓存数据,哈希可以存储对象,列表可以实现消息队列,集合可以用于去重,有序集合可以用于排行榜。不过对于一些复杂的应用场景我还不是特别熟悉。

面试接近尾声,面试官整理了一下手中的资料。 面试官:王铁牛,今天的面试就到这里。你对一些基础的 Java 知识和常见框架掌握得还可以,但对于一些稍微复杂的知识点,比如 xxl - job 的核心原理,回答得不够清晰。我们会综合评估你的表现,你先回家等通知吧。

问题答案详细解析

  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 makeSound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}
  1. JVM 的内存结构是怎样的?

    • 堆(Heap):是 JVM 中最大的一块内存区域,所有对象实例和数组都在这里分配内存。堆是垃圾收集器管理的主要区域,根据对象的存活时间不同,又可以分为新生代和老年代。
    • 栈(Stack):主要分为虚拟机栈和本地方法栈。虚拟机栈为 Java 方法服务,每个方法在执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。本地方法栈为本地方法服务。
    • 方法区(Method Area):用于存储类的信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK 1.8 之前,方法区也被称为永久代,JDK 1.8 及以后,使用元空间(Metaspace)来替代永久代。
    • 程序计数器(Program Counter Register):可以看作是当前线程所执行的字节码的行号指示器,每个线程都有一个独立的程序计数器,它是线程私有的。
    • 本地方法栈(Native Method Stack):与虚拟机栈类似,只不过它是为本地方法服务的。
  2. 在多线程编程中,synchronized 关键字的作用是什么?

    • synchronized 关键字可以保证在同一时刻,只有一个线程可以访问被它修饰的代码块或方法。它可以用来解决多线程环境下的线程安全问题,防止多个线程同时操作共享资源导致数据不一致。synchronized 关键字可以修饰实例方法、静态方法和代码块。例如:
public class SynchronizedExample {
    private int count = 0;

    // 修饰实例方法
    public synchronized void increment() {
        count++;
    }

    // 修饰代码块
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }

    // 修饰静态方法
    public static synchronized void staticMethod() {
        // 静态方法同步代码
    }
}
  1. 线程池有哪些常见的创建方式?
    • newFixedThreadPool:通过 Executors 工具类的 newFixedThreadPool 方法创建一个固定大小的线程池。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}
- **newCachedThreadPool**:创建一个可缓存的线程池,如果线程池中的线程数量超过了处理任务所需的数量,多余的线程会在一定时间内自动回收。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}
- **newSingleThreadExecutor**:创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}
- **newScheduledThreadPool**:创建一个定长线程池,支持定时及周期性任务执行。例如:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolExample {
    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
        executor.schedule(() -> {
            System.out.println("Task is executed after 2 seconds");
        }, 2, TimeUnit.SECONDS);
        executor.shutdown();
    }
}
  1. HashMap 的底层数据结构是什么?

    • HashMap 的底层数据结构是数组 + 链表 + 红黑树。数组被称为哈希桶(bucket),每个桶中可以存储一个链表或红黑树。当一个键值对要插入到 HashMap 中时,首先会根据键的哈希值计算出它在数组中的位置,如果该位置为空,则直接插入;如果该位置已经有元素,则会通过链表或红黑树来处理哈希冲突。当链表长度超过 8 且数组长度大于 64 时,链表会转换为红黑树,以提高查找效率;当红黑树的节点数小于 6 时,红黑树会转换为链表。
  2. ArrayList 和 LinkedList 的区别是什么?在什么场景下使用它们?

    • 数据结构:ArrayList 底层是基于数组实现的,而 LinkedList 底层是基于双向链表实现的。
    • 随机访问:ArrayList 支持随机访问,通过索引可以快速访问数组中的元素,时间复杂度为 O(1);LinkedList 不支持随机访问,要访问指定位置的元素,需要从头或尾开始遍历链表,时间复杂度为 O(n)。
    • 插入和删除:ArrayList 在插入和删除元素时,可能需要移动大量的元素,尤其是在数组中间插入或删除元素时,时间复杂度为 O(n);LinkedList 在插入和删除元素时,只需要修改相邻节点的指针,时间复杂度为 O(1)。
    • 应用场景:如果需要频繁随机访问元素,建议使用 ArrayList;如果需要频繁插入和删除元素,建议使用 LinkedList。
  3. Spring 框架的核心特性有哪些?

    • 依赖注入(Dependency Injection,DI):是一种设计模式,它允许对象之间的依赖关系由外部容器来管理,而不是在对象内部硬编码。通过依赖注入,可以降低代码的耦合度,提高代码的可维护性和可测试性。例如:
public class UserService {
    private UserDao userDao;

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

    public void saveUser() {
        userDao.save();
    }
}
- **面向切面编程(Aspect - Oriented Programming,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() + " executed");
    }
}
  1. Spring Boot 相对于 Spring 有什么优势?

    • 简化配置:Spring Boot 提供了自动配置的功能,它会根据项目中引入的依赖自动配置 Spring 应用的各项组件,减少了大量的 XML 配置文件和 Java 配置类,开发者可以更专注于业务逻辑的开发。
    • 快速搭建项目:Spring Boot 提供了 Spring Initializr 工具,开发者可以通过该工具快速生成一个基本的 Spring Boot 项目骨架,包含必要的依赖和配置文件。
    • 内嵌服务器:Spring Boot 内嵌了 Tomcat、Jetty 等服务器,开发者可以直接将应用打包成可执行的 JAR 文件,通过 java -jar 命令运行,无需额外部署到服务器上。
    • 生产就绪:Spring Boot 提供了一系列的生产就绪功能,如健康检查、指标监控、外部化配置等,方便开发者在生产环境中对应用进行管理和监控。
  2. MyBatis 中 #{} 和 ${} 的区别是什么?

    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ? 占位符,然后使用 PreparedStatement 来执行 SQL 语句,这样可以防止 SQL 注入攻击。例如:
<select id="getUserById" parameterType="int" resultType="com.example.User">
    SELECT * FROM users WHERE id = #{id}
</select>
- **${}**:是直接替换,MyBatis 在处理 ${} 时,会将 ${} 中的内容直接替换到 SQL 语句中,可能存在 SQL 注入风险。例如:
<select id="getUserByColumnName" parameterType="String" resultType="com.example.User">
    SELECT * FROM users WHERE ${columnName} = 'value'
</select>
- 一般情况下,建议使用 #{} 来传递参数,只有在需要动态生成表名、列名等情况下才使用 ${}。

10. Dubbo 框架的核心功能有哪些? - 服务注册与发现:Dubbo 支持多种服务注册中心,如 ZooKeeper、Nacos 等。服务提供者将自己提供的服务注册到注册中心,服务消费者从注册中心获取服务提供者的地址信息,从而实现服务的发现。 - 远程调用:Dubbo 提供了高效的远程调用机制,支持多种协议,如 Dubbo 协议、HTTP 协议等。服务消费者可以像调用本地方法一样调用远程服务。 - 集群容错:Dubbo 提供了多种集群容错策略,如失败重试、快速失败、失败安全等。当服务调用失败时,Dubbo 可以根据不同的容错策略进行处理,提高系统的可靠性。 - 负载均衡:Dubbo 提供了多种负载均衡算法,如随机、轮询、最少活跃调用数等。当有多个服务提供者时,Dubbo 可以根据负载均衡算法选择合适的服务提供者进行调用,提高系统的性能。

  1. RabbitMQ 的工作模式有哪些?

    • 简单模式(Simple Mode):也称为 Hello World 模式,生产者直接将消息发送到队列,消费者从队列中获取消息。
    • 工作队列模式(Work Queues):一个生产者将消息发送到队列,多个消费者从队列中竞争获取消息,实现任务的分发和并行处理。
    • 发布订阅模式(Publish/Subscribe):生产者将消息发送到交换机,交换机将消息广播到所有绑定的队列,每个队列可以有一个或多个消费者。
    • 路由模式(Routing):生产者将消息发送到交换机,并指定一个路由键,交换机根据路由键将消息路由到与之绑定的队列。
    • 主题模式(Topics):与路由模式类似,但路由键可以使用通配符,更灵活地进行消息的路由。
  2. xxl - job 的核心原理是什么?

    • xxl - job 是一个分布式任务调度平台,其核心原理基于调度中心和执行器的架构。
    • 调度中心(Scheduler):负责任务的管理和调度。它可以配置任务的执行时间、执行周期、任务参数等信息,并根据配置的时间规则触发任务的执行。调度中心会将任务信息发送到执行器。
    • 执行器(Executor):负责接收调度中心的任务请求,并执行具体的任务。执行器会将任务执行的结果反馈给调度中心。
    • 任务注册:执行