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

56 阅读14分钟

第一轮面试 面试官:先从基础的 Java 核心知识问起。Java 中重载和重写的区别是什么? 王铁牛:重载是在一个类中,方法名相同但参数列表不同;重写是子类重写父类的方法,方法名、参数列表和返回类型都要一样,不过返回类型可以是父类返回类型的子类。 面试官:回答得不错。那说说 ArrayList 和 LinkedList 的区别。 王铁牛:ArrayList 基于数组实现,查询快,增删慢;LinkedList 基于链表实现,增删快,查询慢。 面试官:很好。HashMap 在 JDK1.7 和 JDK1.8 中有什么主要区别? 王铁牛:1.8 中引入了红黑树,当链表长度超过阈值会转成红黑树,提高查找效率,而且 1.8 是尾插法,1.7 是头插法。

第二轮面试 面试官:接下来聊聊多线程和 JUC。线程池的核心参数有哪些,分别有什么作用? 王铁牛:嗯……有核心线程数,就是线程池一开始创建的线程数量,还有最大线程数,就是最多能创建的线程数,还有队列,用来存放任务。 面试官:好,那说说线程的几种状态以及状态转换。 王铁牛:有新建、就绪、运行、阻塞、死亡,新建就是刚创建,就绪就是等待 CPU 调度,运行就是正在执行,阻塞就是被暂停,死亡就是执行完了。 面试官:那在多线程环境下,如何保证数据的一致性? 王铁牛:可以用锁,像 synchronized 关键字,还有……嗯……好像还有其他的锁。

第三轮面试 面试官:我们谈谈框架相关。Spring 中 IOC 和 AOP 分别是什么,有什么作用? 王铁牛:IOC 是控制反转,把对象创建和管理交给 Spring 容器;AOP 是面向切面编程,能在不修改原有代码的情况下增加功能,比如日志记录。 面试官:Spring Boot 相对于 Spring 有什么优势? 王铁牛:Spring Boot 能快速搭建项目,自动配置很多东西,减少了配置的麻烦。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 是字符串替换,可能有 SQL 注入风险。 面试官:Dubbo 是什么,在分布式系统中有什么作用? 王铁牛:Dubbo 是个分布式服务框架,能实现服务治理,好像能做服务注册、发现这些。 面试官:RabbitMQ 有哪些常见的应用场景? 王铁牛:可以做异步处理,还有消息的可靠传递,好像能解耦系统。 面试官:xxl - job 是什么,有什么特点? 王铁牛:它是个分布式任务调度平台,能做任务调度,特点嘛……好像是简单易用。 面试官:Redis 有哪些数据类型,分别适用于什么场景? 王铁牛:有字符串、哈希、列表、集合、有序集合,字符串存简单数据,哈希存对象,列表存队列,集合去重,有序集合可以排序。

面试官:今天的面试就到这里,你回去等通知吧。我们会综合评估你的表现,无论结果如何,都会尽快给你回复。感谢你今天来参加面试。

答案解析

  1. Java 中重载和重写的区别
    • 重载(Overloading):发生在同一个类中,方法名必须相同,参数列表不同(参数个数、类型、顺序至少有一个不同),与方法的返回类型、访问修饰符无关。例如,一个类中有 void add(int a, int b)int add(int a, int b, int c) 就是重载关系。重载主要用于在一个类中提供多种相似功能但参数不同的方法,方便调用者根据不同的参数情况选择合适的方法。
    • 重写(Overriding):发生在子类与父类之间,子类重写父类的方法。方法名、参数列表、返回类型(JDK1.5 后可以是父类返回类型的子类)必须相同,访问修饰符不能比父类更严格(可以相同或更宽松)。例如,父类有 public void run(),子类重写时方法签名必须一样。重写主要用于子类根据自身需求对父类的方法进行不同的实现,体现了多态性。
  2. ArrayList 和 LinkedList 的区别
    • 数据结构:ArrayList 基于动态数组实现,内部维护一个数组,当元素数量超过数组容量时会进行扩容;LinkedList 基于双向链表实现,每个节点包含前驱节点、后继节点和数据。
    • 查询效率:ArrayList 支持随机访问,通过索引直接定位元素,时间复杂度为 O(1),所以查询效率高;LinkedList 需要从头或尾开始遍历链表找到目标元素,时间复杂度为 O(n),查询效率低。
    • 增删效率:ArrayList 在数组中间或开头插入、删除元素时,需要移动大量元素,时间复杂度为 O(n);在尾部插入删除效率较高,时间复杂度为 O(1)。LinkedList 在链表任意位置增删元素只需修改相邻节点的指针,时间复杂度为 O(1)。
  3. HashMap 在 JDK1.7 和 JDK1.8 中的主要区别
    • 数据结构:JDK1.7 中 HashMap 由数组 + 链表组成;JDK1.8 中当链表长度超过阈值(默认为 8)且数组长度大于 64 时,链表会转换为红黑树,即数据结构变为数组 + 链表 + 红黑树。红黑树的引入提高了哈希冲突严重时的查找效率,时间复杂度从链表的 O(n) 变为红黑树的 O(logn)。
    • 插入方式:JDK1.7 采用头插法,新元素会插入到链表头部;JDK1.8 采用尾插法,新元素插入到链表尾部。头插法在多线程环境下可能会形成环形链表导致死循环,尾插法避免了这个问题。
  4. 线程池的核心参数
    • 核心线程数(corePoolSize):线程池初始化时创建的线程数量,这些线程会一直存活,即使处于空闲状态也不会被销毁(除非设置了 allowCoreThreadTimeOut 为 true)。例如,一个线程池设置 corePoolSize 为 5,那么一开始就会创建 5 个线程。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数量。当任务队列已满且核心线程都在忙碌时,线程池会创建新的线程,直到达到最大线程数。如果此时任务队列和线程池都已满,新任务会根据拒绝策略处理。
    • 阻塞队列(workQueue):用于存放等待执行的任务。常见的阻塞队列有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列,也可指定容量变为有界)、SynchronousQueue(不存储任务,直接提交给线程处理)等。不同的队列特性会影响线程池的性能和行为。
    • 线程存活时间(keepAliveTime):当线程数量超过核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。例如,设置 keepAliveTime 为 10 秒,那么超过核心线程数的空闲线程等待 10 秒后若没有新任务就会被销毁。
    • 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 拒绝策略(RejectedExecutionHandler):当任务队列已满且线程数达到最大线程数时,新任务的处理策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(将任务交给调用者线程处理)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交新任务)。
  5. 线程的几种状态以及状态转换
    • 新建(New):当线程对象被创建但还未调用 start() 方法时,线程处于新建状态。例如 Thread thread = new Thread(); 此时线程就处于新建状态。
    • 就绪(Runnable):调用 start() 方法后,线程进入就绪状态,等待 CPU 调度执行。处于这个状态的线程在可运行线程池中等待分配 CPU 时间片。
    • 运行(Running):当线程获得 CPU 时间片开始执行时,进入运行状态,执行 run() 方法中的代码。
    • 阻塞(Blocked):线程因为某些原因暂时停止执行,让出 CPU 资源。常见的阻塞原因有:等待获取锁(synchronized 同步块)、调用了 wait() 方法、执行 I/O 操作等。例如,线程 A 进入 synchronized 同步块,线程 B 也想进入该同步块,此时线程 B 就会进入阻塞状态,直到线程 A 释放锁。
    • 死亡(Terminated):线程执行完 run() 方法或者因异常退出,就进入死亡状态,此时线程生命周期结束。
    • 状态转换:新建状态调用 start() 方法进入就绪状态;就绪状态获得 CPU 调度进入运行状态;运行状态调用 wait() 方法、执行 I/O 操作、等待获取锁等进入阻塞状态;阻塞状态满足相应条件(如获取到锁、I/O 操作完成、被 notify() 唤醒等)进入就绪状态;运行状态执行完 run() 方法或因异常退出进入死亡状态。
  6. 在多线程环境下保证数据一致性的方法
    • 锁机制
      • synchronized:是 Java 内置的关键字,用于实现同步。可以修饰方法或代码块。修饰实例方法时,锁对象是当前实例;修饰静态方法时,锁对象是当前类的 Class 对象;修饰代码块时,可以指定锁对象。例如 synchronized(this) { // 同步代码块 },同一时刻只有一个线程能进入同步代码块,保证了数据的一致性。
      • ReentrantLock:是 JUC 包下的可重入锁,功能比 synchronized 更强大。它支持公平锁和非公平锁(默认非公平),可以中断等待锁的线程,还可以实现读写锁等功能。例如 ReentrantLock lock = new ReentrantLock(); lock.lock(); try { // 业务代码 } finally { lock.unlock(); },使用时必须手动释放锁,放在 finally 块中确保无论是否发生异常都能释放锁。
    • 原子类:JUC 包下提供了一系列原子类,如 AtomicInteger、AtomicLong 等。这些类通过 CAS(Compare - And - Swap)操作保证对数据的原子性操作,不需要使用锁就能保证多线程环境下数据的一致性。例如 AtomicInteger atomicInteger = new AtomicInteger(0); atomicInteger.incrementAndGet(); 能原子性地对值进行加 1 操作。
    • ThreadLocal:提供线程局部变量,每个线程都有自己独立的变量副本,互不干扰,从而保证数据一致性。常用于解决多线程环境下的并发访问问题,如数据库连接、Session 管理等。例如 ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0); 每个线程访问 threadLocal.get() 得到的是自己线程独有的变量值。
  7. Spring 中 IOC 和 AOP
    • IOC(Inversion of Control,控制反转):也叫依赖注入(Dependency Injection,DI)。传统方式是在类内部通过 new 关键字创建依赖对象,而 IOC 是把对象的创建和管理交给 Spring 容器。Spring 容器负责创建对象、管理对象的生命周期以及对象之间的依赖关系。例如,一个 Service 类依赖一个 Dao 类,在 IOC 模式下,Spring 容器会创建 Dao 对象并注入到 Service 类中,而不是在 Service 类内部自己创建 Dao 对象。IOC 提高了代码的可维护性和可测试性,降低了组件之间的耦合度。
    • AOP(Aspect - Oriented Programming,面向切面编程):是一种编程范式,用于将横切关注点(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,以提高代码的可维护性和复用性。在 Spring 中,通过切面(Aspect)、切点(Pointcut)、通知(Advice)等概念实现 AOP。切面定义了横切逻辑,切点定义了在哪些连接点(Joinpoint,如方法调用、异常抛出等)应用切面,通知定义了在切点处执行的具体操作(如前置通知、后置通知、环绕通知等)。例如,通过 AOP 可以在方法执行前记录日志,在方法执行后进行事务提交,而不需要在每个业务方法中重复编写这些代码。
  8. Spring Boot 相对于 Spring 的优势
    • 快速搭建项目:Spring Boot 提供了大量的 Starter 依赖,通过简单的配置就能快速搭建一个完整的项目,减少了手动配置各种依赖和组件的繁琐过程。例如,引入 spring - boot - starter - web 依赖就能快速搭建一个 Web 项目,自动配置好 Tomcat、Spring MVC 等相关组件。
    • 自动配置:Spring Boot 基于约定大于配置的原则,能根据项目的依赖自动配置 Spring 应用。开发者不需要像在 Spring 中那样手动配置大量的 XML 文件或 Java 配置类。例如,只要引入了数据库相关的依赖,Spring Boot 就能自动配置好数据源、JdbcTemplate 等。
    • 内置服务器:Spring Boot 内置了 Tomcat、Jetty 等服务器,可直接打包成可执行的 Jar 或 War 文件,方便部署和运行。不需要像 Spring 项目那样部署到外部服务器,简化了部署流程。
    • 监控和管理:Spring Boot Actuator 提供了对应用的监控和管理功能,如查看应用的健康状态、性能指标等,方便运维和调试。
  9. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译方式,MyBatis 会将 SQL 中的 #{} 替换为?,并使用 PreparedStatement 进行参数设置。这种方式能有效防止 SQL 注入攻击,因为参数会被当作字符串处理,而不是直接嵌入 SQL 语句。例如 select * from user where username = #{username},在执行时会将 #{username} 替换为?,然后通过 PreparedStatement 的 setString 方法设置参数值。
    • **:是字符串替换方式,MyBatis会直接将{}**:是字符串替换方式,MyBatis 会直接将 {} 中的内容替换到 SQL 语句中。如果参数值来自用户输入且未经过严格校验,可能会导致 SQL 注入风险。例如 select * from user where username = '${username}',若用户输入 ' or '1'='1,则会拼接成恶意的 SQL 语句 select * from user where username = '' or '1'='1',从而获取所有用户数据。一般在需要动态生成 SQL 语句的某些部分(如表名、列名等)时使用 ${},但要确保参数值的安全性。
  10. Dubbo 是什么,在分布式系统中的作用
    • Dubbo 是阿里巴巴开源的高性能、轻量级的分布式服务框架:致力于解决微服务架构下的服务治理与通信问题。
    • 在分布式系统中的作用
      • 服务注册与发现:Dubbo 提供了服务注册中心(如 Zookeeper、Nacos 等),服务提供者将自己的服务注册到注册中心,服务消费者从注册中心获取服务地址。这样实现了服务的自动发现,降低了服务之间的耦合度。例如,多个服务提供者提供相同的用户服务,服务消费者只需要从注册中心获取可用的服务地址,而不需要关心具体的提供者实例。
      • 负载均衡:当有多个服务提供者提供相同服务时,Dubbo 支持多种负载均衡策略(如随机、轮询、最少活跃调用数等),能将请求均匀分配到各个服务提供者上,提高系统的整体性能和可用性。例如,采用随机负载均衡策略,Dubbo 会随机选择一个服务提供者处理请求。
      • 服务调用:Dubbo 支持多种远程调用协议(如 Dubbo 协议、HTTP 协议等),服务消费者可以像调用本地方法一样调用远程服务,隐藏了远程调用的复杂性。例如,在服务消费者代码中,通过 Dubbo 的配置和代理机制,调用远程服务就像调用本地接口方法一样简单。
      • 服务治理:Dubbo 提供了丰富的服务治理功能,如服务降级、服务容错、服务限流等。在服务提供者出现故障或压力过大时,通过服务降级可以返回默认值或错误提示,保证系统的基本可用性;服务容错机制可以在调用失败时进行重试等操作;服务限流可以控制单位时间内的请求量,防止系统被大量请求压垮。
  11. RabbitMQ 常见的应用场景
    • 异步处理:在一些业务场景中,某些操作不需要立即得到结果,可以将这些操作封装成消息发送到 RabbitMQ 队列中,由消费者异步处理。例如,用户注册成功后发送欢迎邮件,邮件发送操作可以异步处理,将邮件发送任务封装成消息发送到队列,这样用户注册的主流程不会因为邮件发送而阻塞,提高了系统的响应速度。
    • 解耦系统:不同的系统或模块之间通过 RabbitMQ 进行消息传递,降低了系统之间的耦合度。例如,电商系统中的订单模块和库存模块,订单创建后通过消息通知库存模块扣减库存,订单模块不需要直接调用