《互联网大厂 Java 面试:从基础到进阶的核心技术大考察》

28 阅读11分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 之后有红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:呃,好像是当元素个数达到容量的一定比例,就会扩容,新容量好像是原来的 1.5 倍,然后把旧数组的数据复制到新数组。 面试官:嗯,基本思路对。那 HashMap 在什么情况下会发生扩容? 王铁牛:当元素个数达到负载因子乘以容量的时候就扩容。

第二轮面试 面试官:接下来问问多线程相关。讲讲线程池的核心参数有哪些? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位,还有任务队列。 面试官:很好。那线程池处理任务的流程是怎样的? 王铁牛:呃,任务来了先看核心线程有没有满,没满就创建核心线程处理,满了就放队列里,队列满了就看最大线程数,没达到就创建非核心线程处理,要是都满了就触发拒绝策略。 面试官:还行。那在高并发场景下,使用线程池可能会遇到什么问题? 王铁牛:嗯……可能会出现线程资源耗尽吧,还有……还有可能任务处理不过来。

第三轮面试 面试官:聊聊框架相关,Spring 中 Bean 的生命周期是怎样的? 王铁牛:呃,先实例化,然后属性注入,接着初始化,最后销毁。 面试官:好。那 Spring Boot 相对于 Spring 有什么优势? 王铁牛:Spring Boot 自动配置,能快速搭建项目,减少配置。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,{} 是字符串替换,#{} 能防止 SQL 注入。 面试官:Dubbo 主要解决了什么问题? 王铁牛:嗯……好像是服务治理,服务之间的调用啥的。 面试官:RabbitMQ 在项目中有什么作用? 王铁牛:可以做消息队列,异步处理,削峰填谷。 面试官:xxl - job 是什么,有什么应用场景? 王铁牛:是个分布式任务调度平台,能做定时任务啥的。 面试官:Redis 有哪些数据类型,在缓存场景下怎么选择合适的数据类型? 王铁牛:有字符串、哈希、列表、集合、有序集合。缓存场景下,简单的键值对就用字符串,复杂点的对象用哈希。

面试总结:从整体面试情况来看,你对于一些基础的知识点掌握得还可以,像 ArrayList、HashMap 的底层结构,线程池的核心参数等回答得不错。但在一些稍微深入和复杂的问题上,比如高并发场景下线程池的问题,回答得不是特别清晰全面。对于框架方面,虽然能说出一些基本概念,但对于它们在实际业务场景中的深度应用和细节,还需要进一步加强理解。我们会综合评估所有面试者的情况,你回去等通知吧,无论结果如何,希望你在技术学习上能继续保持热情,不断提升自己。

问题答案

  1. ArrayList 和 HashMap 的底层数据结构
    • ArrayList:底层是数组结构,它通过数组来存储元素,支持随机访问,因为数组的内存地址是连续的。例如,当我们创建一个 ArrayList<Integer> list = new ArrayList<>(); 时,在底层会先初始化一个默认容量(通常是 10)的数组来存储 Integer 类型的数据。
    • HashMap:JDK1.8 之前底层是数组 + 链表结构,JDK1.8 之后是数组 + 链表 + 红黑树结构。数组的每个位置称为桶(bucket),通过哈希算法计算键的哈希值,然后对数组长度取模得到桶的索引位置。如果该位置没有元素,则直接放入;如果有元素,就形成链表(当链表长度大于 8 且数组容量大于 64 时,链表会转换为红黑树)。比如 HashMap<String, Integer> map = new HashMap<>();,当我们 map.put("key", 10); 时,会先计算 “key” 的哈希值,确定桶的位置进行存储。
  2. ArrayList 扩容操作
    • 当 ArrayList 中的元素个数达到其容量(capacity)的一定比例(默认是 0.9)时,会触发扩容。新容量是原来容量的 1.5 倍(newCapacity = oldCapacity + (oldCapacity >> 1))。然后会创建一个新的更大的数组,通过 System.arraycopy() 方法将旧数组中的元素复制到新数组中。例如,初始容量为 10 的 ArrayList,当添加第 11 个元素时,就会扩容为 15。
  3. HashMap 扩容情况
    • 当 HashMap 中的元素个数(size)达到负载因子(loadFactor,默认是 0.75)乘以容量(capacity)时,就会触发扩容。扩容时,新容量是原来容量的 2 倍。例如,初始容量为 16 的 HashMap,负载因子为 0.75,当元素个数达到 16 * 0.75 = 12 时,就会扩容为 32。扩容后需要重新计算每个元素在新数组中的位置,因为哈希值对新容量取模的结果可能会改变。
  4. 线程池核心参数
    • 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOuttrue
    • 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数,当任务队列满了且核心线程都在忙碌时,会创建新的线程,直到达到最大线程数。
    • 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程存活的最长时间。超过这个时间,多余的线程会被销毁。
    • 时间单位(unit):存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 任务队列(workQueue):用于存放等待执行的任务,常见的任务队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列,默认使用)、SynchronousQueue(不存储任务,直接交给线程处理)等。
  5. 线程池处理任务流程
    • 当有新任务提交到线程池时,首先判断核心线程数是否已满。如果未满,创建新的核心线程来处理任务。
    • 如果核心线程数已满,任务会被放入任务队列中。
    • 如果任务队列也已满,再判断当前线程数是否达到最大线程数。若未达到,创建新的非核心线程来处理任务。
    • 如果线程数已经达到最大线程数,此时会触发拒绝策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(将任务交给提交任务的线程处理)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交当前任务)。
  6. 高并发场景下线程池可能遇到的问题
    • 线程资源耗尽:如果任务提交速度过快,超过了线程池的处理能力,且任务队列也已满,不断创建新线程直到达到最大线程数,可能会导致系统资源耗尽,如内存溢出等问题。
    • 任务处理延迟:当任务队列过长,任务在队列中等待时间过长,会导致任务处理延迟,影响系统的响应时间。
    • 线程上下文切换开销:过多的线程会导致频繁的线程上下文切换,消耗 CPU 资源,降低系统性能。
  7. Spring 中 Bean 的生命周期
    • 实例化(Instantiation):通过构造函数或者工厂方法创建 Bean 的实例对象。例如,对于一个简单的 @Component 注解的类 UserService,Spring 容器会在启动时为其创建实例。
    • 属性注入(Populate):将 Bean 依赖的其他 Bean 通过 setter 方法、构造函数或者字段注入的方式进行赋值。比如 UserService 依赖 UserDao,可以通过 @Autowired 注解将 UserDao 注入到 UserService 中。
    • 初始化(Initialization):调用 Bean 的初始化方法(可以通过 @PostConstruct 注解标注的方法,或者实现 InitializingBean 接口的 afterPropertiesSet 方法,或者在配置文件中指定的 init - method),进行一些初始化操作,如数据库连接的建立等。
    • 销毁(Destruction):当 Bean 不再需要时,Spring 容器会调用其销毁方法(可以通过 @PreDestroy 注解标注的方法,或者实现 DisposableBean 接口的 destroy 方法,或者在配置文件中指定的 destroy - method),进行资源释放等操作,如关闭数据库连接等。
  8. Spring Boot 相对于 Spring 的优势
    • 自动配置:Spring Boot 基于约定大于配置的原则,能根据项目的依赖自动配置 Spring 应用,大大减少了手动配置的工作量。例如,引入 spring - boot - starter - jdbc 依赖,Spring Boot 就能自动配置好数据源、JdbcTemplate 等相关组件。
    • 快速搭建项目:使用 Spring Boot 可以快速创建一个可运行的 Spring 项目,内置了 Tomcat、Jetty 等服务器,直接打包成可执行的 jar 或 war 文件,方便部署。
    • 简化依赖管理:Spring Boot 的 parent 项目定义了一系列常用依赖的版本号,开发者无需手动指定版本,减少了版本冲突的问题。
  9. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ?,然后使用 PreparedStatement 进行设置参数值,这样可以有效防止 SQL 注入。例如,select * from user where username = #{username},在执行时会变成 select * from user where username =?,然后通过 PreparedStatement.setString(1, "具体用户名") 来设置参数。
    • **:是字符串替换方式,MyBatis在处理{}**:是字符串替换方式,MyBatis 在处理 `{} 时,会直接将 中的内容替换到SQL中。例如,selectfromuserwhereusername={} `中的内容替换到 SQL 中。例如,`select * from user where username = '{username}',如果 username变量值为 “admin' or '1'='1”,就会导致 SQL 注入,因为最终的 SQL 会变成select * from user where username = 'admin' or '1'='1'`。
  10. Dubbo 主要解决的问题
  • 服务治理:Dubbo 是一个分布式服务框架,主要解决了微服务架构中服务之间的调用、注册、发现、负载均衡、容错等问题。它提供了服务注册中心(如 Zookeeper),服务提供者将自己的服务注册到注册中心,服务消费者从注册中心获取服务提供者的地址,然后进行远程调用。通过负载均衡算法(如随机、轮询等),可以将请求均匀分配到多个服务提供者上,提高系统的可用性和性能。同时,Dubbo 还支持多种容错机制,如失败重试、快速失败等,保证服务调用的可靠性。
  1. RabbitMQ 在项目中的作用
  • 异步处理:在一些业务场景中,比如用户注册成功后需要发送邮件通知、生成用户积分等操作,这些操作可以通过 RabbitMQ 异步处理,将任务发送到消息队列中,主线程继续执行其他业务逻辑,提高系统的响应速度。
  • 削峰填谷:在高并发场景下,如电商的秒杀活动,大量的请求同时到达服务器,可能会导致服务器压力过大甚至崩溃。通过 RabbitMQ 作为消息队列,可以将请求先放入队列中,服务器按照自己的处理能力从队列中依次取出任务进行处理,避免瞬间高并发对系统造成冲击。
  1. xxl - job 是什么及应用场景
  • xxl - job:是一个轻量级分布式任务调度平台,它提供了简单易用的任务调度管理功能。
  • 应用场景
    • 定时任务:比如每天凌晨执行数据备份、统计报表生成等定时任务。
    • 分布式任务:在分布式系统中,有些任务需要在多个节点上同时执行,xxl - job 可以方便地管理这些分布式任务的调度和执行。
  1. Redis 数据类型及缓存场景选择
  • 字符串(String):最基本的数据类型,可以存储任何类型的数据,如字符串、数字等。在缓存简单的键值对数据时非常适用,比如缓存用户的基本信息(用户名、年龄等),可以将用户 ID 作为键,用户信息的 JSON 字符串作为值存储。
  • 哈希(Hash):用于存储对象,它将对象的每个字段和值存储为一个键值对。适用于缓存复杂对象,例如缓存用户详细信息,每个字段(如姓名、性别、地址等)作为哈希的一个字段,值作为对应字段的值,这样可以方便地对对象的部分字段进行更新,而不需要重新缓存整个对象。
  • 列表(List):是一个有序的字符串列表,可以从两端插入和删除元素。适用于实现消息队列(如简单的生产者 - 消费者模型),或者存储一些有序的数据,如最新的文章列表等。
  • 集合(Set):无序的字符串集合,并且集合中的元素是唯一的。可以用于去重场景,比如统计网站的独立访客,将访客的 IP 地址存储在集合中,由于集合的唯一性,重复的 IP 不会被存储。
  • 有序集合(Sorted Set):和集合类似,但每个元素都关联一个分数(score),根据分数进行排序。适用于排行榜场景,比如游戏玩家的积分排行榜,将玩家 ID 作为元素,积分作为分数存储在有序集合中,通过分数可以方便地获取排名信息。