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

44 阅读2分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那 HashMap 在什么情况下会发生扩容? 王铁牛:当 HashMap 中的元素个数达到负载因子(默认 0.75)乘以当前容量的时候,就会进行扩容。 面试官:很好。再说说 ArrayList 扩容的机制是怎样的? 王铁牛:ArrayList 扩容时,会创建一个新的数组,新数组的大小是原数组大小的 1.5 倍,然后将原数组的元素复制到新数组中。

第二轮面试 面试官:接下来聊聊多线程和线程池。线程池有哪些核心参数? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位,还有任务队列。 面试官:嗯,还行。那线程池的拒绝策略有哪些? 王铁牛:有 AbortPolicy,直接抛出异常;还有 DiscardPolicy,丢弃任务不抛异常,好像还有 DiscardOldestPolicy 和 CallerRunsPolicy 吧。 面试官:那你讲讲 CallerRunsPolicy 策略具体是怎么处理任务的? 王铁牛:呃……就是……好像是让调用者线程来执行任务。

第三轮面试 面试官:我们谈谈框架相关的,Spring 中 Bean 的作用域有哪些? 王铁牛:有 singleton,单例模式,整个应用就一个实例;还有 prototype,每次请求都会创建新的实例;好像还有 request、session 这些。 面试官:好。那 Spring Boot 自动配置的原理是什么? 王铁牛:就是……它根据类路径下的依赖,自动配置一些 Bean 吧,具体不太清楚。 面试官:最后问下 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 能防止 SQL 注入,${} 可能有 SQL 注入风险。

面试官:今天的面试就到这里,你的表现有亮点也有不足。我们后续会综合评估所有候选人,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。

问题答案

  1. ArrayList 和 HashMap 的底层数据结构
    • ArrayList:底层是数组结构,它允许以数组下标的方式快速访问元素。例如 list.get(0) 可以直接获取到第一个元素,这是因为数组在内存中是连续存储的,通过计算偏移量可以快速定位元素。
    • HashMap:JDK1.8 之前底层是数组加链表,数组的每个位置是一个链表的头节点。当发生哈希冲突时(不同的 key 计算出相同的哈希值),就会将新的键值对以链表的形式挂在对应数组位置的链表上。JDK1.8 后,当链表长度大于 8 且数组容量大于 64 时,链表会转化为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,它能保证在最坏情况下,查找、插入和删除操作的时间复杂度为 O(log n)。
  2. HashMap 扩容机制
    • HashMap 有两个重要参数,容量(capacity)和负载因子(loadFactor)。默认初始容量是 16,负载因子默认是 0.75。当 HashMap 中的元素个数(size)达到 capacity * loadFactor 时,就会触发扩容。扩容时,会创建一个新的数组,新数组的大小是原数组大小的 2 倍。然后将原数组中的所有键值对重新计算哈希值并放入新数组中。这是因为扩容后数组大小改变,哈希值的计算结果可能会改变,所以需要重新分配位置。
  3. ArrayList 扩容机制
    • ArrayList 初始化时,如果没有指定容量,默认容量是 10。当向 ArrayList 中添加元素,且当前元素个数达到数组容量时,就会触发扩容。扩容时,会创建一个新的数组,新数组的大小是原数组大小的 1.5 倍(通过 oldCapacity + (oldCapacity >> 1) 计算得到,oldCapacity >> 1 相当于 oldCapacity / 2)。然后通过 System.arraycopy 方法将原数组的元素复制到新数组中。这样做是为了在尽量减少内存开销的同时,满足动态添加元素的需求。
  4. 线程池核心参数
    • 核心线程数(corePoolSize):线程池中会一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut 为 true。
    • 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数。当任务队列已满,且当前线程数小于最大线程数时,线程池会创建新的线程来处理任务。
    • 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程存活的最长时间。超过这个时间,多余的线程会被销毁。
    • 时间单位(unit):存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 任务队列(workQueue):用于存放等待执行的任务。常见的任务队列有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)、SynchronousQueue(同步队列,不存储任务,直接交给线程处理)等。
  5. 线程池拒绝策略
    • AbortPolicy:这是默认的拒绝策略,当任务无法提交到线程池(队列已满且线程数达到最大线程数)时,直接抛出 RejectedExecutionException 异常。
    • DiscardPolicy:丢弃无法处理的任务,不抛出任何异常。这种策略比较“安静”,可能会导致任务丢失,适用于对任务丢失不敏感的场景。
    • DiscardOldestPolicy:丢弃队列中最老的任务(队头任务),然后尝试提交新任务。如果队列是无界队列,可能会一直丢弃队头任务,新任务也无法提交成功。
    • CallerRunsPolicy:当任务无法提交到线程池时,由提交任务的线程(调用者线程)来执行该任务。这样做可以降低新任务的提交速度,同时也能保证任务不会被丢弃。
  6. Spring Bean 的作用域
    • singleton:单例模式,在整个 Spring 应用中,只会创建一个该 Bean 的实例。例如,配置一个数据库连接池的 Bean 为 singleton 作用域,这样整个应用都共享这一个数据库连接池实例,避免了资源浪费。
    • prototype:原型模式,每次请求获取该 Bean 时,都会创建一个新的实例。比如,一个处理用户请求的 Bean,如果设置为 prototype 作用域,每个用户请求都会创建一个新的实例来处理,保证不同请求之间的处理相互隔离。
    • request:在一次 HTTP 请求中,只会创建一个该 Bean 的实例。适用于 Web 应用中,与当前请求相关的 Bean,比如记录当前请求的一些属性的 Bean。
    • session:在一个 HTTP Session 中,只会创建一个该 Bean 的实例。用于在用户会话期间共享数据的 Bean,比如用户登录信息相关的 Bean。
  7. Spring Boot 自动配置原理
    • Spring Boot 依赖于 Spring 框架的条件化配置(@Conditional)机制。它在启动过程中,会扫描 META - INF/spring.factories 文件,该文件中定义了各种自动配置类。例如,当项目中引入了 spring - boot - starter - web 依赖时,其中包含了 Tomcat 相关的依赖,Spring Boot 会根据条件判断(如类路径下是否存在 Tomcat 相关的类),自动配置 Tomcat 作为 Web 服务器,并配置好相关的 Servlet 容器初始化等操作。自动配置类会根据项目的依赖情况,有条件地创建和配置 Bean,大大简化了 Spring 应用的配置过程。
  8. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 来执行 SQL。例如,SQL 语句 SELECT * FROM user WHERE username = #{username},实际执行时会变为 SELECT * FROM user WHERE username =?,然后通过 PreparedStatement 的 set 方法设置参数值。这种方式能有效防止 SQL 注入,因为参数值是作为字符串传入的,不会被解析为 SQL 语句的一部分。
    • **:是字符串替换,MyBatis在处理{}**:是字符串替换,MyBatis 在处理 {} 时,会直接将 替换为变量的值。例如,SQL语句SELECTFROMuserWHEREusername={} 替换为变量的值。例如,SQL 语句 `SELECT * FROM user WHERE username = '{username}',如果 username变量值为admin' OR '1'='1,那么实际执行的 SQL 就变为 SELECT * FROM user WHERE username = 'admin' OR '1'='1'`,这就导致了 SQL 注入漏洞。所以 ${} 一般用于传入数据库对象,如表名、列名等,但使用时要特别小心,确保传入的值是安全的。