在一间明亮却略显严肃的面试房间里,一位神色专注的面试官正准备对面前的求职者展开考察。
面试官:“第一轮提问开始。先说说Java中ArrayList和HashMap的底层数据结构分别是什么?”
王铁牛:“ArrayList底层是数组,HashMap底层是数组加链表,JDK1.8 后引入了红黑树。”
面试官:“回答不错。那ArrayList在扩容时具体是怎么操作的?”
王铁牛:“当ArrayList的元素个数达到容量阈值时,会进行扩容,新容量是原容量的1.5倍,然后把原数组的元素复制到新数组。”
面试官:“很好。HashMap在什么情况下会发生哈希碰撞,怎么解决哈希碰撞的?”
王铁牛:“当不同的键计算出相同的哈希值时就会发生哈希碰撞。JDK1.8之前通过链表解决,之后在链表长度大于8且数组长度大于64时,链表会转化为红黑树来优化查找性能。”
面试官:“第二轮提问。谈谈Spring框架中IOC和AOP的概念及作用。”
王铁牛:“IOC就是控制反转,把对象创建和管理的控制权交给Spring容器,这样代码耦合度更低。AOP是面向切面编程,能把一些通用功能如日志、事务等抽取出来,不影响主业务逻辑。”
面试官:“那Spring Boot相较于Spring,最大的优势是什么?”
王铁牛:“Spring Boot简化了Spring应用的搭建和开发过程,它有自动配置功能,能快速上手,减少了大量的样板代码。”
面试官:“在Spring Boot项目中,如何实现自定义配置属性?”
王铁牛:“嗯……好像是在配置文件里写,然后通过注解获取吧,具体不太清楚。”
面试官:“第三轮提问。MyBatis中#{}和${}有什么区别?”
王铁牛:“#{}是预编译处理,{}是字符串替换。#{}能防止SQL注入,{}一般用于传入数据库对象,比如表名。”
面试官:“Dubbo的服务调用流程是怎样的?”
王铁牛:“嗯……就是服务提供者注册服务,消费者从注册中心获取服务,然后调用,具体细节不太记得了。”
面试官:“RabbitMQ在高并发场景下如何保证消息不丢失?”
王铁牛:“好像是开启确认机制,还有持久化什么的,具体不太明白。”
面试官:“最后问个关于xxl - job的,它是如何实现分布式任务调度的?”
王铁牛:“这个……不太了解。”
面试官思索片刻后说道:“今天的面试就到这里,我们后续会综合评估,你回家等通知吧。感谢你今天来参加面试。”
问题答案:
- ArrayList和HashMap的底层数据结构:
- ArrayList:底层是数组结构,它可以动态扩容。数组的特点是可以通过索引快速访问元素,但插入和删除元素可能需要移动大量数据。
- HashMap:JDK1.8之前底层是数组加链表结构,数组的每个位置是一个链表头。当发生哈希碰撞时,新的元素会被添加到链表尾部。JDK1.8引入了红黑树,当链表长度大于8且数组长度大于64时,链表会转化为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,查找、插入和删除操作的时间复杂度平均为O(log n)。
- ArrayList扩容操作:
- ArrayList有一个容量(capacity)和实际元素个数(size)。当size达到capacity时,就会触发扩容。扩容时,新容量是原容量的1.5倍(原容量右移一位再加原容量)。然后会创建一个新的更大的数组,通过System.arraycopy方法将原数组的元素复制到新数组中。例如,原数组容量为10,当第11个元素添加进来时,就会扩容到15。
- HashMap哈希碰撞及解决方法:
- 哈希碰撞:由于哈希函数的局限性,不同的键可能会计算出相同的哈希值,这就导致了哈希碰撞。例如,两个不同的字符串通过哈希函数计算出相同的哈希值,它们就会被分配到HashMap数组的同一个位置。
- 解决方法:JDK1.8之前,采用链表法解决哈希碰撞,即把碰撞的元素以链表的形式存储在数组的同一个位置。JDK1.8之后,当链表长度大于8且数组长度大于64时,链表会转化为红黑树。因为链表在元素较多时,查找效率会降低到O(n),而红黑树能保持O(log n)的查找效率。
- Spring中IOC和AOP的概念及作用:
- IOC(控制反转):概念是将对象的创建和管理控制权从应用程序代码转移到Spring容器。例如,在传统的Java开发中,我们需要在代码中手动创建对象并管理它们的依赖关系。而在Spring中,通过配置(如XML或注解),Spring容器会负责创建对象、注入依赖。作用是降低代码的耦合度,提高代码的可维护性和可测试性。比如一个Service类依赖另一个Dao类,在IOC模式下,Service类不需要自己去创建Dao类的实例,而是由Spring容器注入,这样如果Dao类有变化,只需要在Spring配置中修改,而不需要修改Service类的代码。
- AOP(面向切面编程):概念是将一些通用功能(如日志记录、事务管理、权限控制等)从业务逻辑中抽取出来,形成一个个切面(Aspect)。这些切面可以在不修改原有业务逻辑代码的情况下,在特定的连接点(如方法调用前后)织入到业务逻辑中。作用是提高代码的复用性,减少重复代码。例如,在多个业务方法中都需要记录日志,如果不使用AOP,就需要在每个方法中都写一遍记录日志的代码,而使用AOP,只需要创建一个日志切面,在切面中定义记录日志的逻辑,然后配置在需要的方法上即可。
- Spring Boot相较于Spring的优势:
- 简化搭建和开发:Spring Boot提供了大量的starter依赖,通过引入这些starter,能快速搭建一个Spring应用。例如,引入spring - boot - starter - web依赖,就能快速搭建一个Web应用,而在Spring中需要手动配置大量的Servlet、SpringMVC等相关配置。
- 自动配置:Spring Boot能根据项目的依赖自动配置Spring应用。比如,当项目中引入了MySQL驱动和Spring Data JPA相关依赖时,Spring Boot会自动配置数据源、JPA等相关组件,开发者不需要手动编写大量的配置文件。这大大减少了开发过程中的样板代码,提高了开发效率。
- Spring Boot中实现自定义配置属性:
- 步骤:
- 在application.properties或application.yml配置文件中定义自定义属性。例如在application.properties中写:myapp.name = my - custom - app。
- 创建一个Java类,使用@ConfigurationProperties注解将配置文件中的属性绑定到该类的字段上。例如:
- 步骤:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "myapp")
public class MyAppProperties {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
- 在需要使用这些属性的地方,通过依赖注入获取MyAppProperties实例,然后使用其中的属性。例如在一个Service类中:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MyService {
private final MyAppProperties myAppProperties;
@Autowired
public MyService(MyAppProperties myAppProperties) {
this.myAppProperties = myAppProperties;
}
public void doSomething() {
String appName = myAppProperties.getName();
// 使用appName进行业务操作
}
}
- MyBatis中#{}和${}的区别:
- #{}:是预编译处理,MyBatis在处理#{}时,会将SQL中的#{}替换为?占位符,然后使用PreparedStatement的set方法来设置参数值。这样可以有效防止SQL注入攻击。例如,SQL语句为:SELECT * FROM user WHERE username = #{username},实际执行时会变为:SELECT * FROM user WHERE username =?,然后通过PreparedStatement的setString方法设置username的值。
- **{}时,会直接将{tableName},如果tableName变量的值为user,那么实际执行的SQL就是:SELECT * FROM user。由于它是直接替换,所以可能会导致SQL注入问题,一般用于传入数据库对象(如表名、列名等),但使用时要特别小心。
- Dubbo的服务调用流程:
- 服务注册:服务提供者启动时,会向注册中心(如Zookeeper)注册自己提供的服务。它会将服务的接口、实现类、地址等信息注册到注册中心。
- 服务订阅:服务消费者启动时,会从注册中心订阅自己需要的服务。注册中心会将服务提供者的地址列表返回给消费者。
- 服务调用:消费者根据从注册中心获取的服务提供者地址列表,通过负载均衡算法选择一个服务提供者进行调用。Dubbo支持多种负载均衡算法,如随机、轮询、最少活跃调用数等。调用时,消费者会通过Dubbo的远程通信协议(如Dubbo协议、HTTP协议等)与服务提供者进行通信,获取服务结果。
- RabbitMQ在高并发场景下保证消息不丢失:
- 开启确认机制:生产者开启confirm模式,当消息成功到达Broker后,Broker会给生产者发送一个确认消息。生产者可以通过监听这个确认消息来判断消息是否发送成功。如果没有收到确认消息,生产者可以进行消息重发。例如,在Java中使用Spring AMQP时,可以通过设置publisher - confirm - type属性为correlated来开启confirm模式,并通过实现ConfirmCallback接口来处理确认消息。
- 持久化:
- 队列持久化:通过将队列声明为持久化队列,即使RabbitMQ服务器重启,队列也不会丢失。在Java中使用Spring AMQP时,可以在声明队列时设置durable属性为true。
- 消息持久化:将消息的deliveryMode属性设置为2,这样消息会被持久化到磁盘。当RabbitMQ服务器重启时,持久化的消息不会丢失。例如在Java中使用Spring AMQP发送消息时,可以这样设置:
MessageProperties messageProperties = new MessageProperties();
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
Message message = new Message("message content".getBytes(), messageProperties);
rabbitTemplate.send("queueName", message);
- xxl - job实现分布式任务调度:
- 架构:xxl - job由调度中心、执行器和任务组成。
- 调度中心:负责管理调度任务,包括任务的新增、修改、删除、暂停、恢复等操作。它会按照设定的调度规则(如Cron表达式)触发任务调度,并将调度请求发送给对应的执行器。调度中心还提供了任务日志查看、运行状态监控等功能。
- 执行器:部署在各个业务系统中,负责接收调度中心的调度请求,并执行具体的任务逻辑。执行器可以是一个独立的Java应用,也可以是集成在现有业务系统中的一个模块。执行器启动时会向调度中心注册自己,包括执行器名称、地址等信息。
- 任务:定义在执行器中,是具体要执行的业务逻辑。任务可以是一个Java方法,通过注解等方式将其注册为xxl - job的任务。调度中心根据任务配置,将调度请求发送给对应的执行器,执行器执行任务并将执行结果返回给调度中心。例如,在一个Spring Boot项目中集成xxl - job执行器,通过@XxlJob注解定义任务方法,然后在配置文件中配置调度中心地址等信息,就可以实现分布式任务调度。