在一间明亮却略显严肃的面试房间里,一位求职者正襟危坐,对面的面试官表情沉稳,一场决定命运的互联网大厂Java面试即将拉开帷幕。
面试官:“第一轮提问开始。先说说Java中ArrayList和HashMap的底层数据结构分别是什么?”
王铁牛:“ArrayList底层是数组,HashMap底层是数组加链表,JDK 1.8 后引入了红黑树。”
面试官:“回答得不错。那ArrayList在扩容时具体是怎么操作的?”
王铁牛:“当元素个数达到容量阈值时,会创建一个新的更大的数组,然后把旧数组的元素复制到新数组。”
面试官:“很好。HashMap在什么情况下会发生哈希碰撞,又是如何解决的?”
王铁牛:“当不同的键计算出相同的哈希值时就会发生哈希碰撞。JDK 1.8 之前通过链表解决,之后在链表长度大于8且数组长度大于64时,链表会转化为红黑树来优化查找性能。”
面试官:“这一轮回答得很不错。接下来第二轮。讲讲Spring框架中IOC和AOP的概念分别是什么?”
王铁牛:“IOC是控制反转,把对象创建和管理的控制权交给Spring容器。AOP是面向切面编程,在不修改原有代码的基础上,对业务逻辑进行增强。”
面试官:“那Spring是如何实现IOC的?”
王铁牛:“通过配置文件或者注解,Spring容器读取这些信息,创建和管理对象。”
面试官:“Spring AOP中常用的通知类型有哪些?”
王铁牛:“有前置通知、后置通知、环绕通知、异常通知和最终通知。”
面试官:“第二轮表现也可以。最后一轮。说说Spring Boot的自动配置原理是什么?”
王铁牛:“嗯……就是Spring Boot根据类路径下的依赖,自动配置一些Bean。”
面试官:“那MyBatis中#{}和${}的区别是什么?”
王铁牛:“#{}是预编译处理,${}是字符串替换,#{}更安全,能防止SQL注入。”
面试官:“Dubbo的服务调用流程是怎样的?”
王铁牛:“嗯……就是服务提供者注册服务,消费者从注册中心获取服务,然后进行调用。”
面试官:“RabbitMQ有哪些常见的应用场景?”
王铁牛:“可以用于异步处理、解耦系统、流量削峰。”
面试官:“xxl - job的核心功能有哪些?”
王铁牛:“能进行分布式任务调度,有任务管理、执行、监控这些功能。”
面试官:“Redis有哪些数据类型,分别适用于什么场景?”
王铁牛:“有字符串、哈希、列表、集合、有序集合。字符串存简单数据,哈希存对象,列表做队列,集合去重,有序集合可以根据分数排序。”
面试官:“好的,今天的面试就到这里。你对这些知识有一定的了解,但部分回答还可以更深入。回去等通知吧,我们会综合评估后给你答复。”
答案:
- ArrayList和HashMap的底层数据结构:
- ArrayList:底层是数组结构,它允许以数组下标的方式快速访问元素。例如,
ArrayList<Integer> list = new ArrayList<>(); list.add(1); int num = list.get(0);这里通过get(0)就能快速获取第一个元素,因为它基于数组实现。 - HashMap:JDK 1.8之前底层是数组加链表,数组的每个位置是一个链表头节点。当发生哈希碰撞时,新的键值对会以链表的形式存储在该位置。JDK 1.8引入红黑树,当链表长度大于8且数组长度大于64时,链表会转化为红黑树,以提高查找效率。比如,
HashMap<String, Integer> map = new HashMap<>(); map.put("key1", 1);通过哈希算法计算“key1”的哈希值,确定在数组中的位置,如果该位置已有元素(哈希碰撞),则以链表或红黑树的形式存储。
- ArrayList:底层是数组结构,它允许以数组下标的方式快速访问元素。例如,
- ArrayList扩容操作:
- ArrayList有一个默认初始容量,通常为10。当向ArrayList中添加元素,元素个数达到容量阈值(一般是当前容量的0.75倍)时,会触发扩容。
- 扩容时,会创建一个新的数组,新数组的容量是原数组容量的1.5倍。例如,原数组容量为10,扩容后变为15。
- 然后通过
System.arraycopy()方法将旧数组中的元素复制到新数组中。这样就完成了扩容操作,使得ArrayList可以继续添加新元素。
- HashMap哈希碰撞及解决:
- 哈希碰撞发生:由于哈希算法的局限性,不同的键可能计算出相同的哈希值,这就导致哈希碰撞。比如,两个不同的字符串“abc”和“cba”,经过哈希算法可能得到相同的哈希值。
- 解决方式:
- JDK 1.8之前:采用链地址法,即数组的每个位置是一个链表头节点。当发生哈希碰撞时,新的键值对以链表的形式存储在该位置。查找时,先根据哈希值找到数组位置,再遍历链表找到对应的键值对。
- JDK 1.8之后:在链表长度大于8且数组长度大于64时,链表会转化为红黑树。红黑树是一种自平衡的二叉查找树,相比链表,在查找、插入和删除操作上具有更好的时间复杂度(平均和最坏情况下都是O(log n)),从而优化了哈希碰撞时的查找性能。
- Spring中IOC和AOP概念:
- IOC(控制反转):传统编程中,对象的创建和管理由应用程序自身负责。而在Spring框架中,IOC将这种控制权反转给Spring容器。例如,有一个
UserService类,传统方式是在其他类中通过new UserService()创建实例。在Spring中,可以通过配置文件(如XML)或注解(如@Component)将UserService交给Spring容器管理,其他类需要使用时,由Spring容器注入。这样使得代码的耦合度降低,更易于维护和扩展。 - AOP(面向切面编程):它是一种编程范式,旨在将横切关注点(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来。以日志记录为例,在多个业务方法中都可能需要记录日志,如果在每个业务方法中都编写日志记录代码,会导致代码重复。使用AOP,可以定义一个切面,在切面中编写日志记录逻辑,然后通过配置将这个切面应用到需要记录日志的业务方法上,在不修改原有业务代码的基础上实现日志记录功能。
- IOC(控制反转):传统编程中,对象的创建和管理由应用程序自身负责。而在Spring框架中,IOC将这种控制权反转给Spring容器。例如,有一个
- Spring实现IOC:
- 基于XML配置:在XML配置文件中,通过
<bean>标签定义需要Spring容器管理的对象。例如:
这里定义了<bean id="userService" class="com.example.service.UserService"> <property name="userDao" ref="userDao"/> </bean> <bean id="userDao" class="com.example.dao.UserDao"/>UserService和UserDao两个Bean,UserService中通过<property>标签注入userDao依赖。Spring容器启动时,会读取这个XML文件,创建并管理这些Bean。- 基于注解:使用
@Component、@Service、@Repository、@Controller等注解标记需要Spring容器管理的类。例如:
@Service public class UserService { @Autowired private UserDao userDao; //业务方法 } @Repository public class UserDao { //数据访问方法 }@Service标记UserService类,@Repository标记UserDao类,Spring容器扫描到这些注解后,会创建相应的Bean,并通过@Autowired注解实现依赖注入。 - 基于XML配置:在XML配置文件中,通过
- Spring AOP常用通知类型:
- 前置通知(Before Advice):在目标方法调用前执行。例如,可以在方法执行前进行权限检查。通过
@Before注解定义,如:
@Aspect public class LogAspect { @Before("execution(* com.example.service.UserService.*(..))") public void beforeMethod(JoinPoint joinPoint) { System.out.println("前置通知:方法 " + joinPoint.getSignature().getName() + " 即将执行"); } }- 后置通知(After Advice):在目标方法正常执行结束后执行,无论方法是否有返回值。例如,可以在方法执行后记录日志。通过
@After注解定义。 - 环绕通知(Around Advice):围绕目标方法执行,既可以在方法调用前执行逻辑,也可以在方法调用后执行逻辑,还可以控制方法是否执行以及修改方法的返回值。通过
@Around注解定义,如:
@Aspect public class TransactionAspect { @Around("execution(* com.example.service.UserService.*(..))") public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { try { // 开启事务 System.out.println("环绕通知:开启事务"); Object result = proceedingJoinPoint.proceed(); // 提交事务 System.out.println("环绕通知:提交事务"); return result; } catch (Exception e) { // 回滚事务 System.out.println("环绕通知:回滚事务"); throw e; } } }- 异常通知(After - throwing Advice):在目标方法抛出异常时执行。例如,可以在方法抛出异常时记录异常信息。通过
@AfterThrowing注解定义。 - 最终通知(After - returning Advice):在目标方法正常执行结束后执行,只有方法正常返回才会执行,与后置通知的区别在于它可以获取方法的返回值。通过
@AfterReturning注解定义。
- 前置通知(Before Advice):在目标方法调用前执行。例如,可以在方法执行前进行权限检查。通过
- Spring Boot自动配置原理:
- Spring Boot依赖大量的starter依赖,这些starter依赖中包含了自动配置类。
- Spring Boot启动时,会扫描
META - INF/spring.factories文件,该文件中定义了一系列自动配置类。例如,spring - boot - starter - web依赖中的WebMvcAutoConfiguration类就是用于自动配置Spring MVC相关功能的。 - 自动配置类通过条件注解(如
@ConditionalOnClass、@ConditionalOnProperty等)来判断是否需要进行配置。比如,@ConditionalOnClass(WebMvcConfigurer.class)表示当类路径下存在WebMvcConfigurer类时,才进行相关配置。如果项目中引入了spring - boot - starter - web依赖,类路径下就会有WebMvcConfigurer类,从而触发Spring MVC的自动配置。
- MyBatis中#{}和${}的区别:
- #{}:是预编译处理,MyBatis在处理
#{}时,会将SQL中的#{}替换为?,然后使用PreparedStatement的setXXX()方法设置参数值。这样可以有效防止SQL注入攻击。例如:
实际执行的SQL是<select id="getUserById" parameterType="int" resultType="User"> SELECT * FROM user WHERE id = #{id} </select>SELECT * FROM user WHERE id =?,然后通过PreparedStatement设置id的值。- **{}
时,会直接将${}`中的内容替换到SQL中。例如:
如果传入的<select id="getUserByUsername" parameterType="string" resultType="User"> SELECT * FROM user WHERE username = '${username}' </select>username值为“'; DROP TABLE user; --”,就会导致SQL注入,执行恶意的SQL语句。所以${}一般用于传入数据库对象(如表名、列名)等,使用时要特别小心。 - #{}:是预编译处理,MyBatis在处理
- Dubbo服务调用流程:
- 服务注册:服务提供者将自己提供的服务注册到注册中心(如Zookeeper)。服务提供者在启动时,会读取配置文件中关于注册中心的地址等信息,然后将服务的接口、实现类等信息注册到注册中心。例如,在Dubbo的XML配置中:
<dubbo:registry address="zookeeper://127.0.0.1:2181"/> <dubbo:service interface="com.example.service.UserService" ref="userServiceImpl"/>- 服务订阅:服务消费者从注册中心订阅自己需要的服务。消费者启动时,同样读取配置文件中注册中心的信息,向注册中心订阅所需服务。注册中心会将服务提供者的地址等信息返回给消费者。
- 服务调用:消费者根据从注册中心获取的服务提供者地址,通过网络调用服务提供者的方法。Dubbo支持多种通信协议(如Dubbo协议、HTTP协议等),默认使用Dubbo协议。在调用过程中,Dubbo会进行负载均衡(如随机、轮询等策略),选择一个服务提供者实例进行调用。
- RabbitMQ常见应用场景:
- 异步处理:例如,在一个电商系统中,用户下单后,除了创建订单记录,还可能需要发送短信通知、更新库存等操作。如果这些操作都同步执行,会导致用户等待时间过长。可以将发送短信通知、更新库存等操作封装成消息发送到RabbitMQ队列中,订单创建完成后,系统立即返回给用户下单成功,而短信通知和库存更新等操作由后台消费者从队列中获取消息异步处理。
- 解耦系统:假设一个大型系统由多个子系统组成,如订单系统、支付系统、物流系统等。订单系统在订单创建成功后,需要通知支付系统和物流系统。如果直接调用支付系统和物流系统的接口,会导致订单系统与其他系统紧密耦合。通过RabbitMQ,订单系统只需将订单相关消息发送到队列,支付系统和物流系统从各自订阅的队列中获取消息进行处理,这样各个系统之间的耦合度大大降低。
- 流量削峰:在一些高并发场景下,如电商的秒杀活动,瞬间会有大量请求涌入。如果直接处理这些请求,可能会导致系统崩溃。可以将这些请求封装成消息发送到RabbitMQ队列中,系统按照自己的处理能力从队列中逐步获取消息进行处理,从而实现流量削峰,保护系统不被高并发请求压垮。
- xxl - job核心功能:
- 任务管理:提供可视化的界面,用于创建、编辑、删除任务。可以在界面上配置任务的执行参数,如任务的执行周期(定时任务)、任务的执行脚本或方法等。例如,可以设置一个任务每天凌晨1点执行一次数据备份操作。
- 任务执行:xxl - job有执行器,负责实际执行任务。执行器可以部署在多个服务器上,实现分布式执行。当任务到达执行时间或被触发时,执行器会根据任务配置执行相应的脚本或调用方法。例如,执行一个Java方法来清理过期的缓存数据。
- 任务监控:可以在管理界面查看任务的执行状态(如执行成功、执行失败、正在执行等)、执行日志等信息。如果任务执行失败,能及时发现并根据日志排查问题。还可以设置任务失败重试策略,如失败后自动重试3次等。
- Redis数据类型及适用场景:
- 字符串(String):最基本的数据类型,可以存储任何类型的数据,如整数、字符串、二进制数据等。适用于存储简单的键值对,例如存储用户的登录信息(
key为用户ID,value为用户登录状态),或者用于计数器,通过INCR命令对存储的整数进行自增操作。 - 哈希(Hash):用于存储对象,以字段和值的形式存储。例如,可以将用户信息(姓名、年龄、地址等)存储在一个哈希中,
key为用户ID,字段为“name”“age”“address”等,对应的值为具体的用户信息。这样在获取或修改用户部分信息时非常方便,不需要操作整个对象。 - 列表(List):是一个有序的字符串链表,可以从链表的两端进行插入和删除操作。适用于实现队列,如消息队列,生产者将消息从列表的一端插入,消费者从另一端取出消息。也可以用于实现栈,从同一端进行插入和删除操作。
- 集合(Set):无序的字符串集合,集合中的元素是唯一的,不存在重复元素。适用于去重场景,例如统计网站的独立访客,将每个访客的标识作为元素存入集合,由于集合的唯一性,重复的访客标识不会被重复存储。还可以进行集合的交、并、差运算,如计算两个用户群体的共同关注列表。
- 有序集合(Sorted Set):与集合类似,但每个元素都关联一个分数(score),根据分数进行排序。适用于排行榜场景,例如游戏中的玩家积分排行榜,玩家ID作为元素,积分作为分数,通过有序集合可以方便地获取积分排名靠前的玩家。