在一间明亮却略显严肃的面试房间里,一位求职者正襟危坐在桌前,对面的面试官表情沉稳,一场决定命运的互联网大厂Java面试即将拉开帷幕。
面试官:“第一轮提问开始。先说说Java中ArrayList和HashMap的底层数据结构分别是什么?” 王铁牛:“ArrayList底层是数组,HashMap底层是数组加链表,JDK1.8 后引入了红黑树。” 面试官:“回答不错。那ArrayList在扩容时具体是怎么操作的?” 王铁牛:“当元素个数达到容量阈值时,会创建一个新的更大的数组,然后把旧数组的元素复制到新数组。” 面试官:“很好。HashMap在什么情况下会发生哈希碰撞,又如何解决的?” 王铁牛:“当不同的键计算出相同的哈希值时就会碰撞。JDK1.8 前通过链表解决,1.8 后链表长度大于8且数组长度大于64时会转为红黑树。”
面试官:“第二轮。讲讲Spring的IOC和AOP是什么?” 王铁牛:“IOC是控制反转,把对象创建和管理交给Spring容器。AOP是面向切面编程,在不修改原有代码基础上增加功能。” 面试官:“那Spring Boot是如何简化Spring开发的?” 王铁牛:“它有自动配置,能快速搭建项目,减少很多配置文件。” 面试官:“MyBatis中#{}和{}有什么区别?” **王铁牛**:“#{}是预编译处理,{}是字符串替换,#{}更安全,能防止SQL注入。”
面试官:“第三轮。Dubbo的服务调用流程是怎样的?” 王铁牛:“嗯……就是,呃,服务提供者注册服务,消费者去调用,中间好像还有个注册中心啥的。” 面试官:“RabbitMQ的消息确认机制是怎么回事?” 王铁牛:“就是发消息后要确认对方收到了,具体咋确认我不太清楚。” 面试官:“xxl - job的核心原理是什么?” 王铁牛:“好像是做任务调度的,具体原理我没深入研究过。” 面试官:“Redis有哪些数据类型,ZSet的应用场景举例说下。” 王铁牛:“有字符串、哈希、列表、集合、有序集合。ZSet应用场景,呃,比如排行榜吧。”
面试官思索片刻后说道:“今天的面试就到这里,后续我们会综合评估,你回家等通知吧。感谢你今天来参加面试。”
答案:
- ArrayList和HashMap底层数据结构:
- ArrayList:底层是数组结构,它可以动态扩容。数组为其提供了快速的随机访问能力,因为可以通过索引直接定位到元素位置。
- HashMap:JDK1.8之前,底层是数组加链表结构。数组的每个位置是一个链表的头节点,当发生哈希碰撞时,新的元素会以链表的形式存储在该位置。JDK1.8 后,当链表长度大于8且数组长度大于64时,链表会转为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,能保证在最坏情况下的查找时间复杂度为O(log n)。
- ArrayList扩容操作:
- ArrayList有一个容量(capacity)和实际元素个数(size)。当size达到capacity时,会触发扩容。扩容时,会创建一个新的数组,新数组的大小一般是原数组大小的1.5倍(通过位运算实现,
newCapacity = oldCapacity + (oldCapacity >> 1))。然后通过System.arraycopy方法将原数组的元素复制到新数组中。这样就完成了扩容操作,使得ArrayList可以继续添加新元素。
- ArrayList有一个容量(capacity)和实际元素个数(size)。当size达到capacity时,会触发扩容。扩容时,会创建一个新的数组,新数组的大小一般是原数组大小的1.5倍(通过位运算实现,
- HashMap哈希碰撞及解决:
- 哈希碰撞:由于哈希函数的特性,不同的键可能会计算出相同的哈希值,这就导致了哈希碰撞。例如,两个不同的字符串通过哈希函数计算出的哈希值可能相同。
- 解决方法:JDK1.8之前,采用链地址法,即数组的每个位置是一个链表,当发生碰撞时,新元素添加到链表尾部。JDK1.8后,当链表长度大于8且数组长度大于64时,链表会转为红黑树。因为链表在查找元素时时间复杂度为O(n),而红黑树在最坏情况下时间复杂度为O(log n),这样可以大大提高查找效率。当链表长度小于6时,又会从红黑树转回链表,以减少树结构带来的维护成本。
- Spring的IOC和AOP:
- IOC(控制反转):传统开发中,对象的创建和管理由应用程序自身负责,而在Spring中,IOC将对象的创建和管理交给Spring容器。例如,一个Service类需要依赖另一个Dao类,在传统开发中,Service类内部会自己创建Dao类的实例。而在Spring中,通过配置或注解,由Spring容器创建Dao实例并注入到Service中。这样使得对象之间的依赖关系更加清晰,也提高了代码的可维护性和可测试性。
- AOP(面向切面编程):AOP是在不修改原有业务代码的基础上,将一些通用功能(如日志记录、事务管理、权限控制等)抽取出来,形成一个个切面。这些切面可以在目标方法执行前、执行后、异常时等不同时机切入,增强目标方法的功能。例如,在一个业务方法执行前记录日志,在方法执行后进行事务提交等操作。
- Spring Boot简化Spring开发:
- 自动配置:Spring Boot基于约定大于配置的原则,能根据项目的依赖自动配置Spring应用。例如,当项目引入了MySQL和MyBatis的依赖,Spring Boot会自动配置好数据源、MyBatis的相关配置等,开发者无需手动编写大量的XML配置文件。
- 起步依赖:提供了一系列的起步依赖(starter),开发者只需要引入相关的starter,就可以快速集成相应的功能。比如引入
spring - boot - starter - web就可以快速搭建一个Web应用,它会自动引入Spring MVC等相关依赖。
- MyBatis中#{}和${}区别:
- #{}:是预编译处理,MyBatis在处理#{}时,会将SQL中的#{}替换为?,然后使用PreparedStatement的set方法来设置参数值。这样可以有效防止SQL注入攻击,因为参数值不会直接拼接到SQL语句中。例如,
select * from user where username = #{username},在执行时会变为select * from user where username =?,然后通过PreparedStatement.setString(1, usernameValue)设置参数值。 - **{}时,会直接将{username}'
,如果username值为“admin' or '1'='1”,就会导致SQL注入,因为SQL语句会变为select * from user where username = 'admin' or '1'='1'`,这样会查询出所有用户数据。
- #{}:是预编译处理,MyBatis在处理#{}时,会将SQL中的#{}替换为?,然后使用PreparedStatement的set方法来设置参数值。这样可以有效防止SQL注入攻击,因为参数值不会直接拼接到SQL语句中。例如,
- Dubbo服务调用流程:
- 服务注册:服务提供者启动时,会向注册中心(如Zookeeper)注册自己提供的服务。注册中心记录了服务提供者的地址、端口等信息。
- 服务订阅:服务消费者启动时,会向注册中心订阅自己需要的服务。注册中心会将服务提供者的信息返回给消费者。
- 服务调用:消费者根据从注册中心获取的服务提供者信息,通过网络调用服务提供者的接口。在调用过程中,Dubbo会进行负载均衡,选择合适的服务提供者实例进行调用。例如,采用随机、轮询、权重等负载均衡策略。同时,Dubbo还支持集群容错,当某个服务提供者实例出现故障时,能自动切换到其他实例继续调用。
- RabbitMQ消息确认机制:
- 生产者确认(publisher confirm):生产者将消息发送到RabbitMQ服务器后,服务器会给生产者发送一个确认消息,告知生产者消息已成功接收。生产者可以通过
channel.confirmSelect()开启确认模式,然后通过channel.addConfirmListener()添加确认监听器,在监听器中处理确认结果。如果消息发送失败,生产者可以进行重试等操作。 - 消费者确认(consumer ack):消费者从RabbitMQ服务器获取消息并处理完成后,需要向服务器发送一个确认消息(ACK),告知服务器该消息已被成功处理。如果消费者没有发送ACK,RabbitMQ会认为消息没有被成功处理,可能会将消息重新发送给其他消费者或者在一定时间后重新发送给该消费者。消费者可以通过
channel.basicAck(deliveryTag, multiple)方法发送ACK,deliveryTag是消息的唯一标识,multiple表示是否批量确认。
- 生产者确认(publisher confirm):生产者将消息发送到RabbitMQ服务器后,服务器会给生产者发送一个确认消息,告知生产者消息已成功接收。生产者可以通过
- xxl - job核心原理:
- 调度中心:是xxl - job的核心组件,负责任务的管理、调度触发等。它基于数据库存储任务信息,包括任务的执行时间、执行参数、执行器地址等。调度中心通过定时任务(如Quartz)按照设定的时间规则触发任务执行。
- 执行器:部署在各个业务服务器上,负责实际执行任务。执行器启动时会向调度中心注册自己,调度中心会记录执行器的地址等信息。当调度中心触发任务执行时,会根据任务配置选择合适的执行器,通过网络调用执行器的接口来执行任务。执行器执行完任务后,会将执行结果返回给调度中心。
- Redis数据类型及ZSet应用场景:
- Redis数据类型:
- 字符串(string):最基本的数据类型,可以存储字符串、数字等。例如,可以存储用户的昵称、年龄等简单信息。
- 哈希(hash):用于存储对象,以键值对的形式存储,每个键值对中的值又可以是一个键值对。适合存储用户信息,如
hset user:1 name "张三" age 20。 - 列表(list):是一个有序的字符串链表,可以从链表的两端进行插入和删除操作。常用于实现消息队列,如生产者向列表的一端插入消息,消费者从另一端取出消息。
- 集合(set):无序的字符串集合,不允许重复元素。可以用于实现标签功能,比如一个文章可以有多个标签,这些标签存储在集合中。
- 有序集合(ZSet):在集合的基础上,每个元素都关联一个分数(score),根据分数进行排序。
- ZSet应用场景:
- 排行榜:例如游戏中的玩家积分排行榜,以玩家ID为元素,积分作为score,通过ZSet的排序功能可以很方便地获取积分排名靠前的玩家。
- 时间序列数据:如网站的访问量按时间记录,时间作为score,访问量作为元素,可以方便地查询某个时间段内的访问量数据。