《互联网大厂面试:Java 核心、并发、框架与中间件知识大考验》

39 阅读9分钟

第一轮面试:Java 基础与并发编程考验

面试官:你好,先问几个基础问题。Java 中多态的实现方式有哪些? 王铁牛:多态的实现方式主要有方法重载和方法重写。方法重载是在一个类中,方法名相同但参数列表不同;方法重写是子类重写父类的方法。 面试官:回答得不错。那说说 JUC 包下 CountDownLatch 的作用是什么? 王铁牛:CountDownLatch 可以让一个或多个线程等待其他线程完成操作。就像比赛时,运动员都准备好(线程完成操作),裁判才开始(主线程继续执行)。 面试官:很好。那线程池有哪些创建方式? 王铁牛:可以通过 Executors 工具类创建,比如 newFixedThreadPool 创建固定大小的线程池,newCachedThreadPool 创建可缓存的线程池,还有通过 ThreadPoolExecutor 自定义创建。 面试官:回答得很全面,看来基础掌握得不错。

第二轮面试:数据结构与框架深入探究

面试官:接着聊聊数据结构,HashMap 在 JDK 1.8 有哪些优化? 王铁牛:呃……好像是增加了红黑树吧,当链表长度超过 8 就会转成红黑树。 面试官:对,那 ArrayList 是如何实现动态扩容的? 王铁牛:这个……大概就是容量不够的时候就扩大容量,具体咋扩我有点不太确定。 面试官:好吧。那说说 Spring 的核心特性有哪些? 王铁牛:Spring 核心特性有 IOC(控制反转)和 AOP(面向切面编程)。IOC 就是把对象的创建和依赖关系的管理交给 Spring 容器;AOP 可以在不修改源代码的情况下增强功能。 面试官:基本正确,不过关于 ArrayList 扩容这块还得再巩固下。

第三轮面试:中间件与分布式知识挑战

面试官:我们来谈谈中间件,MyBatis 是如何实现 SQL 语句和 Java 代码的映射的? 王铁牛:好像是通过 XML 配置文件或者注解,具体咋弄我有点说不清楚。 面试官:那 Dubbo 的服务发现机制是怎样的? 王铁牛:Dubbo 服务发现……我记得和注册中心有关,具体啥原理我不太知道。 面试官:RabbitMQ 如何保证消息的可靠性传输? 王铁牛:这个……好像有确认机制,具体咋实现我不太了解。 面试官:你对中间件这块的知识掌握得不够扎实,后续还需要加强学习。

面试结束,面试官严肃地说:“今天的面试就到这里,你整体基础还可以,但在一些深入的技术点上还有所欠缺,尤其是中间件和分布式相关知识。你先回家等通知吧,我们会综合评估后给你答复。”

答案详解

  1. Java 中多态的实现方式有哪些
    • 方法重载(Overloading):在同一个类中,方法名相同但参数列表不同(参数的类型、个数、顺序不同),与返回值类型无关。编译器会根据调用方法时传入的实际参数来决定调用哪个方法。例如:
class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    public double add(double a, double b) {
        return a + b;
    }
}
- 方法重写(Overriding):子类继承父类后,重写父类中的方法。要求方法名、参数列表和返回值类型都相同(返回值类型可以是父类方法返回值类型的子类,这称为协变返回类型)。重写的方法不能比父类中被重写的方法有更严格的访问权限。例如:
class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}
  1. JUC 包下 CountDownLatch 的作用是什么: CountDownLatch 是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。它使用一个计数器来实现,初始化时设置计数器的值,每当一个线程完成任务时,计数器的值减 1,当计数器的值为 0 时,等待的线程可以继续执行。例如,在一个多线程的任务中,主线程需要等待所有子线程完成任务后再继续执行:
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 模拟线程执行任务
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " completed");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 线程完成任务,计数器减 1
                    latch.countDown();
                }
            }).start();
        }

        // 主线程等待所有子线程完成任务
        latch.await();
        System.out.println("All threads completed, main thread continues");
    }
}
  1. 线程池有哪些创建方式
    • 通过 Executors 工具类创建
      • newFixedThreadPool(int nThreads):创建一个固定大小的线程池,线程池中的线程数量始终保持不变。当有新任务提交时,如果线程池中有空闲线程,则立即执行;如果没有空闲线程,则将任务放入队列中等待。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}
    - `newCachedThreadPool()`:创建一个可缓存的线程池,线程池中的线程数量可以动态变化。如果有新任务提交,且线程池中有空闲线程,则立即执行;如果没有空闲线程,则创建新线程执行任务。当线程空闲时间超过 60 秒时,会被回收。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}
- **通过 ThreadPoolExecutor 自定义创建**

ThreadPoolExecutor 是线程池的核心实现类,通过它可以自定义线程池的各种参数,如核心线程数、最大线程数、线程空闲时间、任务队列等。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                5, // 最大线程数
                60, // 线程空闲时间
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10) // 任务队列
        );
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}
  1. HashMap 在 JDK 1.8 有哪些优化
    • 数据结构:JDK 1.8 之前,HashMap 采用数组 + 链表的结构;JDK 1.8 引入了红黑树,当链表长度超过 8 且数组长度大于 64 时,链表会转换为红黑树,以提高查找效率。当红黑树节点数小于 6 时,会转换回链表。
    • 插入方式:JDK 1.8 之前采用头插法,多线程环境下可能会导致链表成环的问题;JDK 1.8 采用尾插法,避免了这个问题。
    • 哈希算法:JDK 1.8 对哈希算法进行了优化,减少了哈希冲突的概率。
  2. ArrayList 是如何实现动态扩容的: ArrayList 底层使用数组存储元素,当添加元素时,如果数组容量不足,会进行扩容操作。具体步骤如下:
    • 当调用 add 方法添加元素时,会先检查当前数组的容量是否足够,如果不够,则调用 grow 方法进行扩容。
    • grow 方法会计算新的容量,新容量为旧容量的 1.5 倍(oldCapacity + (oldCapacity >> 1))。
    • 然后使用 Arrays.copyOf 方法将旧数组中的元素复制到新数组中。
import java.util.ArrayList;

public class ArrayListResizeExample {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            list.add(i);
        }
    }
}
  1. Spring 的核心特性有哪些
    • IOC(控制反转):控制反转是指将对象的创建和依赖关系的管理从代码中转移到 Spring 容器中。Spring 容器负责创建对象、管理对象的生命周期和依赖关系。通过 IOC,降低了代码的耦合度,提高了可维护性和可测试性。例如,通过 XML 配置文件或注解来定义 Bean 和它们之间的依赖关系:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
class ServiceA {
    private ServiceB serviceB;

    @Autowired
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
class ServiceB {
    // ...
}
- **AOP(面向切面编程)**:面向切面编程是指在不修改源代码的情况下,对程序的功能进行增强。AOP 可以将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,提高代码的复用性和可维护性。Spring AOP 基于代理模式实现,有两种代理方式:JDK 动态代理和 CGLIB 代理。例如,使用注解定义切面和通知:
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
class LoggingAspect {
    @After("execution(* com.example.service.*.*(..))")
    public void logAfterMethod() {
        System.out.println("Method executed");
    }
}
  1. MyBatis 是如何实现 SQL 语句和 Java 代码的映射的
    • XML 配置方式:通过编写 XML 配置文件,在文件中定义 SQL 语句和 Java 方法的映射关系。例如:
<mapper namespace="com.example.dao.UserDao">
    <select id="getUserById" parameterType="int" resultType="com.example.entity.User">
        SELECT * FROM users WHERE id = #{id}
    </select>
</mapper>

在 Java 代码中,通过 Mapper 接口调用相应的方法:

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserDao {
    User getUserById(int id);
}
- **注解方式**:在 Mapper 接口的方法上使用注解来定义 SQL 语句。例如:
import org.apache.ibatis.annotations.Select;

@Mapper
public interface UserDao {
    @Select("SELECT * FROM users WHERE id = #{id}")
    User getUserById(int id);
}
  1. Dubbo 的服务发现机制是怎样的: Dubbo 的服务发现机制依赖于注册中心,常见的注册中心有 ZooKeeper、Nacos 等。服务发现的过程如下:
    • 服务提供者:在启动时,将自己提供的服务信息(服务接口、服务地址等)注册到注册中心。
    • 服务消费者:在启动时,从注册中心订阅所需的服务信息,并缓存到本地。当需要调用服务时,直接从本地缓存中获取服务提供者的地址。
    • 注册中心:负责管理服务提供者的注册信息和服务消费者的订阅信息,当服务提供者的信息发生变化时,会及时通知服务消费者。 例如,使用 ZooKeeper 作为注册中心,在 Dubbo 配置文件中配置注册中心地址:
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
  1. RabbitMQ 如何保证消息的可靠性传输
    • 生产者确认机制:生产者将消息发送到 RabbitMQ 后,RabbitMQ 会返回一个确认信号给生产者,告知消息是否成功接收。有两种确认模式:
      • 同步确认:生产者发送消息后,等待 RabbitMQ 的确认信号,只有收到确认信号后才继续发送下一条消息。
      • 异步确认:生产者发送消息后,继续发送其他消息,RabbitMQ 异步返回确认信号,生产者通过回调函数处理确认结果。
    • 消息持久化:将队列和消息都设置为持久化,这样即使 RabbitMQ 服务器重启,消息也不会丢失。在创建队列和发送消息时,设置相应的参数:
// 创建持久化队列
channel.queueDeclare("queue_name", true, false, false, null);
// 发送持久化消息
channel.basicPublish("", "queue_name", MessageProperties.PERSISTENT_TEXT_PLAIN, "message".getBytes());
- **消费者确认机制**:消费者从队列中获取消息后,需要向 RabbitMQ 发送确认信号,告知消息已经成功处理。有两种确认模式:
    - **自动确认**:消费者获取消息后,RabbitMQ 自动认为消息已经处理成功。
    - **手动确认**:消费者处理完消息后,手动向 RabbitMQ 发送确认信号。如果消费者在处理消息过程中出现异常,可以拒绝消息,让 RabbitMQ 重新发送。
// 手动确认模式
channel.basicConsume("queue_name", false, (consumerTag, delivery) -> {
    String message = new String(delivery.getBody(), "UTF-8");
    try {
        // 处理消息
        System.out.println("Received message: " + message);
        // 手动确认消息
        channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
    } catch (Exception e) {
        // 拒绝消息,让 RabbitMQ 重新发送
        channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true);
    }
}, consumerTag -> {});