《揭秘互联网大厂Java面试:从基础到进阶的核心知识点大考察》

44 阅读10分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 之后还有红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:嗯……好像是当元素个数达到容量的一定比例,就会扩容,新容量大概是原来的 1.5 倍,然后把旧数组的元素复制到新数组。 面试官:回答得可以。HashMap 在 JDK1.8 中引入红黑树的目的是什么? 王铁牛:好像是为了优化查询性能,链表太长时,查询效率低,红黑树能让查询更高效。

第二轮面试 面试官:接下来聊聊多线程和线程池。创建线程有几种方式? 王铁牛:有继承 Thread 类,实现 Runnable 接口,还有实现 Callable 接口。 面试官:很好。那线程池有哪些核心参数,分别有什么作用? 王铁牛:呃……有核心线程数,最大线程数,还有队列容量。核心线程数就是一开始创建的线程数,最大线程数是最多能创建的线程数,队列容量就是存放任务的队列大小。 面试官:那线程池的拒绝策略有哪些? 王铁牛:有 AbortPolicy,直接抛出异常;还有 DiscardPolicy,丢弃任务不抛出异常,其他的……我不太记得了。

第三轮面试 面试官:谈谈 Spring 和 Spring Boot。Spring 中的 IOC 是什么,有什么作用? 王铁牛:IOC 就是控制反转,把对象的创建和管理交给 Spring 容器,这样代码耦合度更低。 面试官:Spring Boot 相对于 Spring 有什么优势? 王铁牛:Spring Boot 能快速搭建项目,自动配置很多东西,让开发更简单。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 是直接替换,可能有 SQL 注入风险。 面试官:最后问下分布式相关的,Dubbo 是什么,有什么特点? 王铁牛:Dubbo 是分布式服务框架,能实现服务治理,好像有高性能、轻量级这些特点。 面试官:好的,今天的面试就到这里。你回去等通知吧,我们会综合评估所有候选人后,再决定是否录用你。感谢你今天来参加面试。

问题答案

  1. ArrayList 和 HashMap 的底层数据结构
    • ArrayList:底层是动态数组。它基于数组实现,通过数组来存储元素。这样的结构使得它在随机访问元素时效率很高,因为可以通过数组下标直接定位到元素位置。例如 list.get(0) 可以直接获取到第一个元素。
    • HashMap:JDK1.8 之前底层是数组加链表。数组的每个位置是一个链表的头节点,当发生哈希冲突时(即不同的键计算出相同的哈希值),会将新的键值对以链表的形式挂在该位置。JDK1.8 之后,当链表长度大于 8 且数组容量大于 64 时,链表会转换为红黑树,以提高查询效率。因为链表在长度较长时,查询一个元素需要遍历链表,时间复杂度为 O(n),而红黑树的查询时间复杂度为 O(logn)。
  2. ArrayList 扩容操作
    • ArrayList 有一个容量的概念,当向 ArrayList 中添加元素时,如果当前元素个数达到了容量的大小,就会触发扩容。
    • 扩容时,新的容量是原来容量的 1.5 倍(通过位运算 oldCapacity + (oldCapacity >> 1) 实现)。
    • 然后会创建一个新的更大的数组,将旧数组中的元素复制到新数组中。这一过程涉及到数组的复制操作,在 Java 中可以使用 System.arraycopy() 方法。例如:
    Object[] newElementData = new Object[newCapacity];
    System.arraycopy(elementData, 0, newElementData, 0, Math.min(size, newCapacity));
    elementData = newElementData;
    
  3. HashMap 在 JDK1.8 中引入红黑树的目的
    • 在 JDK1.8 之前,HashMap 处理哈希冲突主要依靠链表。当哈希冲突严重时,链表会变得很长,此时查询一个元素的时间复杂度会退化为 O(n),效率很低。
    • 引入红黑树后,当链表长度大于 8 且数组容量大于 64 时,链表会转换为红黑树。红黑树是一种自平衡的二叉查找树,它的查询、插入和删除操作的时间复杂度都是 O(logn),相比链表的 O(n) 有了很大提升,从而优化了 HashMap 在哈希冲突严重时的查询性能。
  4. 创建线程的方式
    • 继承 Thread 类:通过继承 Thread 类,并重写 run() 方法来定义线程执行的逻辑。例如:
    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Thread is running");
        }
    }
    MyThread thread = new MyThread();
    thread.start();
    
    • 实现 Runnable 接口:实现 Runnable 接口,并重写 run() 方法。然后将实现了 Runnable 接口的对象作为参数传递给 Thread 类的构造函数来创建线程。例如:
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("Runnable is running");
        }
    }
    MyRunnable runnable = new MyRunnable();
    Thread thread = new Thread(runnable);
    thread.start();
    
    • 实现 Callable 接口:实现 Callable 接口,并重写 call() 方法。call() 方法可以有返回值,并且可以抛出异常。通过 FutureTask 类来包装 Callable 对象,再将 FutureTask 对象作为参数传递给 Thread 类的构造函数来创建线程。例如:
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            return 100;
        }
    }
    
    public class Main {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            MyCallable callable = new MyCallable();
            FutureTask<Integer> futureTask = new FutureTask<>(callable);
            Thread thread = new Thread(futureTask);
            thread.start();
            Integer result = futureTask.get();
            System.out.println("Result: " + result);
        }
    }
    
  5. 线程池的核心参数及作用
    • 核心线程数(corePoolSize):线程池在初始化后,会创建 corePoolSize 数量的线程来处理任务。这些线程即使在空闲状态下也不会被销毁(除非设置了 allowCoreThreadTimeOut 为 true)。例如,当有任务提交到线程池时,首先会创建核心线程来处理任务,直到核心线程数达到 corePoolSize。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列已满,且核心线程数都在忙碌时,线程池会继续创建新的线程,直到线程数达到 maximumPoolSize。但如果此时任务队列已满且线程数已经达到 maximumPoolSize,就会触发拒绝策略。
    • 队列容量(workQueue):用于存放等待处理任务的队列。当核心线程都在忙碌时,新提交的任务会被放入这个队列中等待处理。常见的队列类型有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(可选择有界或无界)、SynchronousQueue(不存储任务,直接提交给线程处理)等。
    • 线程存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程在等待新任务到来的时间超过 keepAliveTime 后,会被销毁。例如,如果设置 keepAliveTime 为 60 秒,那么当一个非核心线程空闲 60 秒后,就会被销毁。
    • 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)等。
  6. 线程池的拒绝策略
    • AbortPolicy:这是默认的拒绝策略。当任务无法被执行(队列已满且线程数达到最大线程数)时,直接抛出 RejectedExecutionException 异常。
    • DiscardPolicy:丢弃无法处理的任务,不抛出任何异常。这种策略比较“安静”,可能会导致任务丢失,适用于对任务可靠性要求不高的场景。
    • DiscardOldestPolicy:丢弃队列中最老的任务(即队列头部的任务),然后尝试将新任务加入队列。如果此时队列仍然满,则可能会再次触发拒绝策略。
    • CallerRunsPolicy:当任务被拒绝时,由提交任务的线程(即调用 execute() 方法的线程)来执行该任务。这样可以降低新任务的提交速度,缓解线程池的压力。
  7. Spring 中的 IOC 及作用
    • IOC(Inversion of Control):即控制反转,它是 Spring 框架的核心概念之一。传统的应用程序中,对象的创建和管理由应用程序自身负责,对象之间的依赖关系也由应用程序代码来维护。而在 Spring 中,IOC 把对象的创建、初始化、销毁等控制权从应用程序代码转移到了 Spring 容器。
    • 作用
      • 降低耦合度:通过 IOC,对象之间不需要直接创建和依赖对方,而是由 Spring 容器来管理对象及其依赖关系。例如,一个 UserService 依赖于 UserDao,在传统方式下,UserService 中需要自己创建 UserDao 对象。而在 Spring 中,可以通过配置(如 XML 配置或注解)让 Spring 容器创建 UserDao 对象,并注入到 UserService 中,这样 UserServiceUserDao 的耦合度就降低了。
      • 提高可维护性和可测试性:由于对象的创建和依赖关系由 Spring 容器管理,当需要更换 UserDao 的实现类时,只需要在 Spring 配置中修改,而不需要修改 UserService 的代码。在测试 UserService 时,也可以方便地通过 Spring 容器注入模拟的 UserDao 对象,提高测试的便利性。
  8. Spring Boot 相对于 Spring 的优势
    • 快速搭建项目:Spring Boot 提供了大量的 Starter 依赖,通过引入这些 Starter,Spring Boot 可以自动配置很多常用的组件,如数据库连接、Web 服务器等。例如,引入 spring - boot - starter - web 依赖,就可以快速搭建一个基于 Spring MVC 的 Web 项目,而在传统 Spring 项目中,需要手动配置大量的 XML 文件或 Java 配置类来完成这些配置。
    • 简化配置:Spring Boot 采用了约定大于配置的原则,很多配置都有默认值,开发者只需要在必要时进行少量的配置修改。例如,对于数据库连接,Spring Boot 可以根据引入的数据库驱动自动配置数据源,开发者只需要在 application.properties 文件中配置数据库的 URL、用户名和密码等基本信息即可。
    • 内置服务器:Spring Boot 可以内置 Tomcat、Jetty 等 Web 服务器,使得项目可以直接以 Jar 包的形式运行,无需像传统 Spring 项目那样部署到外部服务器。例如,通过 mvn spring - boot:run 命令就可以直接启动一个包含内置 Tomcat 服务器的 Spring Boot 项目。
  9. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译方式。在 SQL 语句中,#{} 会被解析为一个占位符 ?。例如,SELECT * FROM user WHERE username = #{username},在执行 SQL 时,会将 #{username} 替换为 ?,然后通过 PreparedStatement 设置参数值。这种方式可以有效防止 SQL 注入攻击,因为参数值是作为字符串传递的,不会被解析为 SQL 语句的一部分。
    • **:是字符串替换方式。在SQL语句中,{}**:是字符串替换方式。在 SQL 语句中,{} 会直接将其内容替换为实际的值。例如,SELECT * FROM user WHERE username = '${username}',如果 username 的值为 'admin' OR '1'='1',那么最终执行的 SQL 语句就是 SELECT * FROM user WHERE username = 'admin' OR '1'='1',这样就会导致 SQL 注入漏洞。所以 ${} 一般用于传入数据库对象,如表名、列名等,但使用时要特别小心,确保传入的值是可信的。
  10. Dubbo 是什么及特点
  • Dubbo:是阿里巴巴开源的一款高性能、轻量级的分布式服务框架,致力于提供高性能和透明化的 RPC(Remote Procedure Call,远程过程调用)服务调用方案,以及 SOA(Service - Oriented Architecture,面向服务的架构)服务治理方案。
  • 特点
    • 高性能:Dubbo 采用了多种优化技术来提高性能。例如,它支持多种序列化协议(如 Hessian2、JSON 等),可以根据不同的场景选择合适的序列化方式,提高数据传输效率。在网络通信方面,它支持 Netty 等高性能网络框架,能够处理大量的并发请求。
    • 轻量级:Dubbo 的设计理念是轻量级,它不依赖于复杂的容器环境,只需要引入相关的依赖即可使用。相比于一些重量级的分布式框架,Dubbo 的部署和使用更加简单,对系统资源的消耗也相对较少。
    • 服务治理能力强:Dubbo 提供了丰富的服务治理功能,如服务注册与发现、负载均衡、容错机制等。通过服务注册与发现,服务提供者可以将自己的服务注册到注册中心(如 Zookeeper),服务消费者可以从注册中心获取服务提供者的地址信息。负载均衡功能可以在多个服务提供者之间合理分配请求,提高系统的整体性能。容错机制可以在服务调用失败时采取相应的策略,如重试、快速失败等,保证系统的稳定性。