第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 LinkedList 的区别是什么?
王铁牛:ArrayList 是基于数组实现的,查询快,增删慢;LinkedList 是基于链表实现的,增删快,查询慢。
面试官:不错,回答得很准确。那 HashMap 在 JDK1.7 和 JDK1.8 中有什么主要区别?
王铁牛:1.8 之后引入了红黑树,当链表长度达到 8 且数组长度达到 64 时,链表会转成红黑树,提高查找效率。1.7 是单纯的链表结构。
面试官:回答得很好。那 Spring 框架中 IOC 和 AOP 分别是什么?
王铁牛:IOC 是控制反转,把对象创建和管理的控制权交给 Spring 容器。AOP 是面向切面编程,在不修改原有代码的基础上,对业务进行增强。
第二轮面试 面试官:刚刚基础回答得不错,现在深入点。多线程中线程池的核心参数有哪些,分别有什么作用?
王铁牛:嗯……有核心线程数,好像是一直存活的线程数量,还有最大线程数,就是最多能创建的线程数,还有……还有队列容量,用来存放任务的。
面试官:回答得还行,不过不够全面。那 JUC 包下的 CountDownLatch 是做什么的?
王铁牛:这个……好像是用来控制线程等待的,等一些条件满足了再继续执行。
面试官:表述得不太清晰。JVM 的垃圾回收机制了解吗,说下常用的垃圾回收算法。
王铁牛:有标记 - 清除算法,标记出要回收的对象然后清除;还有复制算法,把存活对象复制到另一块空间;还有标记 - 整理算法,标记后把存活对象整理到一起。
第三轮面试 面试官:看来基础知识还行,现在问些更实际应用的。在 Spring Boot 项目中,如何实现自定义的 Starter?
王铁牛:呃……好像要创建一个配置类,然后在 META - INF 下面写一些文件,具体不太清楚了。
面试官:回答得比较模糊。MyBatis 中 #{} 和 ${} 的区别是什么,在实际业务场景中怎么选择?
王铁牛:#{} 是预编译,{} 是字符串替换,#{} 能防止 SQL 注入,一般用 #{},但有时候要动态获取表名之类的就用 {}。
面试官:勉强算对。Dubbo 服务治理中,如何进行服务降级?
王铁牛:这个……好像是设置一些规则,当服务调用失败或者响应时间过长时,返回一个默认值或者执行备用逻辑。
面试官:今天的面试就到这里,你回去等通知吧。我们会综合评估所有候选人,有结果会尽快联系你。感谢你今天来参加面试。
问题答案:
- ArrayList 和 LinkedList 的区别:
- 数据结构:ArrayList 基于动态数组实现,内存地址连续;LinkedList 基于双向链表实现,每个节点包含前驱和后继指针。
- 查询效率:ArrayList 支持随机访问,通过索引直接定位元素,时间复杂度为 O(1);LinkedList 需要从头或尾遍历链表,时间复杂度为 O(n)。
- 增删效率:ArrayList 在中间或开头插入、删除元素时,需要移动大量元素,时间复杂度为 O(n);在尾部插入删除效率高,时间复杂度为 O(1)。LinkedList 在任意位置增删元素只需修改指针,时间复杂度为 O(1)。
- HashMap 在 JDK1.7 和 JDK1.8 中的区别:
- 数据结构:JDK1.7 采用数组 + 链表结构;JDK1.8 采用数组 + 链表 + 红黑树结构。当链表长度大于 8 且数组长度大于等于 64 时,链表转换为红黑树,以提高查找效率。
- 插入方式:JDK1.7 采用头插法,新元素插入到链表头部;JDK1.8 采用尾插法,新元素插入到链表尾部,避免多线程环境下的环形链表问题。
- 扩容机制:JDK1.7 扩容时,需要重新计算每个元素的哈希值并重新插入到新数组;JDK1.8 优化了扩容机制,部分元素在扩容时位置不变,减少了重新计算哈希值和插入的开销。
- Spring 框架中 IOC 和 AOP:
- IOC(控制反转):将对象的创建和管理控制权从应用程序代码转移到 Spring 容器。通过依赖注入(DI)实现,有构造器注入、Setter 方法注入等方式。这样可以降低组件之间的耦合度,提高代码的可维护性和可测试性。
- AOP(面向切面编程):将横切关注点(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,以切面的形式进行统一管理。通过代理模式实现,有 JDK 动态代理和 CGLIB 代理两种方式。可以在不修改原有业务代码的基础上,对业务进行增强。
- 线程池的核心参数:
- 核心线程数(corePoolSize):线程池中一直存活的线程数量,即使这些线程处于空闲状态,也不会被销毁。当有新任务提交且线程池中的线程数小于核心线程数时,会创建新的线程来处理任务。
- 最大线程数(maximumPoolSize):线程池中允许创建的最大线程数量。当任务队列已满且线程数小于最大线程数时,会创建新的线程来处理任务。
- 队列容量(workQueue):用于存放等待执行任务的队列。常用的队列类型有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)、SynchronousQueue(同步队列)等。
- 线程存活时间(keepAliveTime):当线程池中的线程数大于核心线程数时,多余的空闲线程在存活时间内没有新任务可执行,就会被销毁。
- 时间单位(unit):线程存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
- CountDownLatch:
- 是 JUC 包下的一个同步工具类,用于控制线程的等待。它允许一个或多个线程等待其他一组线程完成操作后再继续执行。
- 构造函数接收一个整数参数,表示需要等待的操作数。线程调用
await()方法进入等待状态,其他线程完成操作后调用countDown()方法,当操作数减为 0 时,等待的线程被唤醒继续执行。
- JVM 的垃圾回收算法:
- 标记 - 清除算法:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象。缺点是会产生大量不连续的内存碎片,可能导致后续大对象无法分配内存。
- 复制算法:将内存分为两块,每次只使用其中一块。当这块内存满时,将存活对象复制到另一块内存,然后清除原内存块。优点是实现简单,不会产生内存碎片;缺点是内存利用率低,只能使用一半的内存空间。
- 标记 - 整理算法:先标记出所有需要回收的对象,然后将存活对象向一端移动,最后清除边界以外的内存。解决了标记 - 清除算法产生内存碎片的问题,同时避免了复制算法内存利用率低的缺点。
- Spring Boot 自定义 Starter:
- 创建自动配置类:在
src/main/java下创建一个配置类,使用@Configuration注解标记为配置类,在类中定义需要注入的 Bean。 - 创建 META - INF/spring.factories 文件:在
src/main/resources/META - INF目录下创建spring.factories文件,在文件中指定自动配置类,格式为org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 自定义自动配置类全限定名。 - 打包发布:将项目打包成 Jar 包发布,其他项目引入该 Jar 包后,Spring Boot 会自动加载自定义 Starter 的配置。
- 创建自动配置类:在
- MyBatis 中 #{} 和 ${} 的区别及应用场景:
- 区别:
- #{}:是预编译处理,MyBatis 会将 SQL 中的 #{} 替换为?,并使用 PreparedStatement 进行参数设置,能有效防止 SQL 注入。
- **{} 中的内容替换到 SQL 中,不进行预编译。
- 应用场景:
- #{}:适用于大多数参数传递场景,如普通查询条件、插入数据等。
- ${}:适用于需要动态获取表名、列名等场景,但使用时要注意 SQL 注入风险,需手动对参数进行校验和过滤。
- 区别:
- Dubbo 服务降级:
- 配置方式:在 Dubbo 配置文件或注解中设置服务降级规则。例如,使用
@Service注解时,可以通过mock属性指定降级逻辑。 - 实现原理:当服务调用失败(如网络异常、服务端故障等)或响应时间过长时,Dubbo 会根据配置的降级规则执行备用逻辑,如返回一个默认值、执行本地模拟方法等,避免因服务不可用导致整个系统崩溃。
- 配置方式:在 Dubbo 配置文件或注解中设置服务降级规则。例如,使用