在一间明亮却略显严肃的面试房间里,一位神色专注的面试官正准备对面前的求职者展开一场专业的考察。
面试官:“第一轮提问开始。先说说Java中ArrayList和HashMap的底层数据结构分别是什么?”
王铁牛:“ArrayList底层是数组,HashMap底层是数组加链表,JDK1.8之后链表长度超过8会转成红黑树。”
面试官:“回答得不错。那ArrayList在扩容时具体是怎么操作的?”
王铁牛:“当ArrayList的元素个数达到容量大小时,会进行扩容,新容量是原容量的1.5倍,然后把原数组的元素复制到新数组。”
面试官:“很好。HashMap在put操作时,是如何确定元素存储位置的?”
王铁牛:“通过key的hash值与数组长度减1做与运算,得到存储位置。”
面试官:“第二轮提问。讲讲多线程中线程池的核心参数有哪些,分别有什么作用?”
王铁牛:“有核心线程数、最大线程数、存活时间、时间单位、任务队列。核心线程数是线程池初始化的线程数量,最大线程数是线程池能容纳的最大线程数,存活时间是线程在没有任务时存活的时间,时间单位就是存活时间的单位,任务队列用来存放提交但未执行的任务。”
面试官:“不错。那JUC包下的CountDownLatch的作用是什么,怎么使用?”
王铁牛:“嗯……它好像是用来控制线程等待的,嗯……具体怎么用,我有点记不太清了。”
面试官:“那说说Spring框架中IOC和AOP的概念。”
王铁牛:“IOC是控制反转,就是把对象的创建和管理交给Spring容器。AOP是面向切面编程,能把一些通用功能抽取出来,在需要的地方切入。”
面试官:“第三轮提问。Spring Boot自动配置的原理是什么?”
王铁牛:“呃,好像是根据类路径下的依赖,Spring Boot自动配置一些Bean。”
面试官:“MyBatis中#{}和${}的区别是什么?”
王铁牛:“#{}是预编译处理,${}是字符串替换,#{}能防止SQL注入。”
面试官:“Dubbo的服务暴露和引用过程是怎样的?”
王铁牛:“嗯……就是服务提供者把服务暴露出去,然后消费者引用,具体细节我不太清楚。”
面试官:“最后,RabbitMQ的消息确认机制是怎样的?”
王铁牛:“好像有生产者确认和消费者确认,生产者确认是确认消息是否到达Broker,消费者确认是确认消费者是否成功处理消息。”
面试官:“好的,今天的面试就到这里。你对一些基础的知识点掌握得还可以,但对于一些稍微深入的内容理解得不够透彻。我们后续会综合评估所有面试者的情况,你回家等通知吧。如果有进一步的消息,我们会及时联系你。感谢你今天来参加面试。”
答案:
- ArrayList和HashMap的底层数据结构:
- ArrayList:底层是数组结构,它允许以数组下标的方式快速访问元素。例如,
ArrayList<Integer> list = new ArrayList<>(); list.add(10); int num = list.get(0);这里通过get(0)能快速获取到添加的第一个元素,因为它基于数组存储。 - HashMap:JDK1.7及之前,底层是数组加链表。数组的每个位置是一个链表的头节点,当发生哈希冲突(不同的key计算出相同的哈希值)时,会将新的键值对以链表节点的形式插入到对应数组位置的链表中。JDK1.8之后,当链表长度超过8且数组容量大于等于64时,链表会转成红黑树,以提高查找效率。例如,
HashMap<String, Integer> map = new HashMap<>(); map.put("key1", 1);通过key1的哈希值确定在数组中的位置,如果该位置已有元素(哈希冲突),则以链表或红黑树(满足条件时)的形式存储。
- ArrayList:底层是数组结构,它允许以数组下标的方式快速访问元素。例如,
- ArrayList扩容操作:
- 当ArrayList中元素个数达到其容量大小时,会触发扩容。新容量是原容量的1.5倍(原容量右移一位再加上原容量)。例如,原容量为10,新容量就是10 + 5 = 15。然后通过
System.arraycopy方法将原数组的元素复制到新数组中。这是为了在需要存储更多元素时,有足够的空间,同时尽量减少频繁扩容带来的性能开销。
- 当ArrayList中元素个数达到其容量大小时,会触发扩容。新容量是原容量的1.5倍(原容量右移一位再加上原容量)。例如,原容量为10,新容量就是10 + 5 = 15。然后通过
- HashMap put操作确定元素存储位置:
- 首先计算key的hash值,然后通过
(n - 1) & hash(n为数组长度)来确定元素在数组中的存储位置。例如,数组长度为16(二进制10000),减1后为15(二进制01111),假设key的hash值为10(二进制1010),那么15 & 10(即01111 & 1010)结果为1010(十进制10),则该元素会存储在数组下标为10的位置。这种方式能均匀地将元素分布在数组中,减少哈希冲突。
- 首先计算key的hash值,然后通过
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池初始化时创建的线程数量,这些线程会一直存活,即使没有任务执行,除非设置了
allowCoreThreadTimeOut为true。例如,创建一个线程池ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());这里的5就是核心线程数,线程池启动时会创建5个线程。 - 最大线程数(maximumPoolSize):线程池能容纳的最大线程数量。当任务队列已满且核心线程都在忙碌时,线程池会创建新的线程,直到线程数量达到最大线程数。如上述例子中的10就是最大线程数。
- 存活时间(keepAliveTime):当线程数量超过核心线程数时,多余的空闲线程在等待新任务到来的这段时间内,如果超过存活时间,就会被销毁。例如上述例子中,多余线程等待10秒没有新任务就会被销毁。
- 时间单位(unit):存活时间的单位,如
TimeUnit.SECONDS表示秒,TimeUnit.MINUTES表示分钟等。 - 任务队列(workQueue):用来存放提交但未执行的任务。常见的任务队列有
ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列,默认容量为Integer.MAX_VALUE)等。
- 核心线程数(corePoolSize):线程池初始化时创建的线程数量,这些线程会一直存活,即使没有任务执行,除非设置了
- CountDownLatch作用及使用:
- 作用:CountDownLatch是JUC包下的一个同步工具类,它允许一个或多个线程等待其他一组线程完成操作后再继续执行。例如,在一个多线程计算任务中,主线程需要等待多个子线程完成各自的计算任务后,再汇总结果。
- 使用:创建CountDownLatch对象时,需要传入一个初始计数值。例如
CountDownLatch latch = new CountDownLatch(3);表示需要等待3个线程完成操作。在需要等待的线程中调用latch.await()方法,该线程会被阻塞,直到计数值变为0。而在其他线程完成任务后,调用latch.countDown()方法,将计数值减1。当计数值变为0时,所有调用await()方法的线程会被唤醒继续执行。
- Spring框架中IOC和AOP概念:
- IOC(控制反转):传统方式下,对象的创建和管理由应用程序自身负责,而在Spring框架中,IOC将对象的创建和管理交给Spring容器。例如,有一个
UserService类,传统方式是UserService userService = new UserService();,在Spring中,通过配置或注解,由Spring容器创建和管理UserService实例,应用程序只需要从容器中获取即可。这样做的好处是降低了组件之间的耦合度,提高了代码的可维护性和可测试性。 - AOP(面向切面编程):它是将一些通用功能(如日志记录、事务管理、权限控制等)从业务逻辑中抽取出来,形成一个个切面。然后在需要这些功能的地方进行切入,而不影响业务逻辑的核心代码。例如,在一个电商系统中,订单处理的业务逻辑可能需要记录日志,通过AOP可以将日志记录功能作为一个切面,在订单处理方法执行前后切入记录日志,而订单处理的核心代码不需要关心日志记录的具体实现。
- IOC(控制反转):传统方式下,对象的创建和管理由应用程序自身负责,而在Spring框架中,IOC将对象的创建和管理交给Spring容器。例如,有一个
- Spring Boot自动配置原理:
- Spring Boot基于Spring框架,它通过
@SpringBootApplication注解开启自动配置。该注解包含@EnableAutoConfiguration,它会根据类路径下的依赖,自动配置一些Bean。Spring Boot在启动时,会扫描META - INF/spring.factories文件,该文件中定义了各种自动配置类。例如,当项目中引入了spring - boot - starter - web依赖,Spring Boot会自动配置Tomcat服务器、Spring MVC相关的Bean等,使得开发者可以快速搭建一个Web应用,而无需手动进行大量的配置。
- Spring Boot基于Spring框架,它通过
- MyBatis中#{}和${}的区别:
- #{}:是预编译处理,MyBatis在处理
#{}时,会将SQL中的#{}替换为?,然后使用PreparedStatement进行参数设置。例如,select * from user where username = #{username},在执行时会变成select * from user where username =?,然后通过PreparedStatement.setString(1, "具体用户名")设置参数。这种方式能有效防止SQL注入,因为参数是作为字符串传入,不会被解析为SQL语句的一部分。 - **{}
时,会直接将{username}',如果username的值为admin,则SQL语句会变成select * from user where username = 'admin'。这种方式存在SQL注入风险,因为如果username的值被恶意修改为admin' or '1'='1`,则SQL语句会被篡改,导致数据泄露等安全问题。
- #{}:是预编译处理,MyBatis在处理
- Dubbo服务暴露和引用过程:
- 服务暴露:
- 服务提供者在启动时,通过Dubbo的配置(如XML配置或注解配置),将服务接口和实现类注册到注册中心(如Zookeeper)。例如,使用注解
@Service(Dubbo的@Service,不是Spring的@Service)标注服务实现类,Dubbo会扫描到该类,并将其相关信息注册到注册中心。注册中心会记录服务提供者的地址、端口、服务接口等信息。 - 服务提供者同时会启动一个Netty等网络框架的服务端,监听指定端口,等待消费者的调用请求。
- 服务提供者在启动时,通过Dubbo的配置(如XML配置或注解配置),将服务接口和实现类注册到注册中心(如Zookeeper)。例如,使用注解
- 服务引用:
- 服务消费者在启动时,也会从注册中心获取服务提供者的相关信息,包括地址、端口、服务接口等。
- 消费者根据获取到的信息,创建一个代理对象(如使用动态代理技术),通过该代理对象向服务提供者发起远程调用。调用时,会通过Netty等网络框架将请求发送到服务提供者的监听端口,服务提供者接收到请求后,调用相应的服务实现方法,并将结果返回给消费者。
- 服务暴露:
- RabbitMQ消息确认机制:
- 生产者确认(Publisher Confirm):
- 生产者将消息发送到RabbitMQ Broker后,Broker会给生产者发送一个确认消息,告知生产者消息是否成功接收。生产者可以通过设置
channel.confirmSelect()开启确认模式。当消息成功到达Broker后,会触发ConfirmListener的handleAck方法;如果消息未成功到达,会触发handleNack方法。例如,在Spring Boot整合RabbitMQ中,可以通过实现RabbitTemplate.ConfirmCallback接口来处理确认和未确认的情况。这能让生产者知道消息是否成功发送,以便进行相应的处理,如重发消息等。
- 生产者将消息发送到RabbitMQ Broker后,Broker会给生产者发送一个确认消息,告知生产者消息是否成功接收。生产者可以通过设置
- 消费者确认(Consumer Ack):
- 消费者从RabbitMQ Broker获取消息并处理完成后,需要向Broker发送一个确认消息,告知Broker该消息已被成功处理。RabbitMQ支持自动确认和手动确认两种模式。自动确认模式下,消费者一旦接收到消息,RabbitMQ就认为该消息已被成功处理;手动确认模式下,消费者需要在处理完消息后,调用
channel.basicAck(deliveryTag, multiple)方法(deliveryTag是消息的唯一标识,multiple表示是否批量确认)向Broker发送确认。如果消费者在处理消息过程中出现异常,没有发送确认消息,RabbitMQ会认为该消息未被成功处理,会将消息重新放入队列,以便其他消费者可以再次获取处理。
- 消费者从RabbitMQ Broker获取消息并处理完成后,需要向Broker发送一个确认消息,告知Broker该消息已被成功处理。RabbitMQ支持自动确认和手动确认两种模式。自动确认模式下,消费者一旦接收到消息,RabbitMQ就认为该消息已被成功处理;手动确认模式下,消费者需要在处理完消息后,调用