《互联网大厂面试:全方位考察 Java 核心、框架及中间件知识》

55 阅读11分钟

互联网大厂面试:全方位考察 Java 核心、框架及中间件知识

在互联网大厂的一间严肃的面试室内,一位严肃的面试官正对面坐着略显紧张的求职者王铁牛,一场关于 Java 技术的面试拉开了帷幕。

第一轮提问 面试官:首先问你几个基础问题。Java 里的多态是怎么实现的呢? 王铁牛:多态主要有两种实现方式,一种是方法重载,在一个类中可以有多个同名但参数列表不同的方法;另一种是方法重写,子类可以重写父类的方法,然后通过父类引用指向子类对象,调用重写后的方法。 面试官:不错,回答得很清晰。那说说 JVM 的内存区域划分。 王铁牛:JVM 内存主要分为堆、栈、方法区、本地方法栈和程序计数器。堆是存储对象实例的地方;栈主要存储局部变量和方法调用信息;方法区存储类的信息、常量、静态变量等;本地方法栈为本地方法服务;程序计数器记录当前线程执行的字节码行号。 面试官:很好。那 ArrayList 是如何扩容的呢? 王铁牛:ArrayList 有一个初始容量,当添加元素时,如果容量不够了,会进行扩容。它会创建一个新的数组,新数组的容量一般是原来的 1.5 倍,然后把原来数组的元素复制到新数组中。

第二轮提问 面试官:接下来深入一点。线程池有哪些核心参数,它们分别有什么作用? 王铁牛:线程池的核心参数有 corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory 和 handler。corePoolSize 是核心线程数,线程池会一直保留这些线程;maximumPoolSize 是最大线程数;keepAliveTime 是线程空闲的存活时间;unit 是时间单位;workQueue 是任务队列,用来存储待执行的任务;threadFactory 是线程工厂,用于创建线程;handler 是任务拒绝策略,当任务队列满且线程数达到最大线程数时的处理方式。 面试官:回答得挺全面。那说说 Spring 的 AOP 原理。 王铁牛:Spring 的 AOP 主要基于代理模式实现,有 JDK 动态代理和 CGLIB 代理。JDK 动态代理是基于接口的,通过反射机制在运行时创建代理对象;CGLIB 代理是通过继承目标类生成子类来实现代理。 面试官:不错。MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,会把参数当成一个占位符,能防止 SQL 注入;{} 是直接替换,会把参数直接拼接到 SQL 语句中,可能存在 SQL 注入风险。

第三轮提问 面试官:现在问些关于中间件的问题。Dubbo 的负载均衡策略有哪些? 王铁牛:嗯……这个我有点不太确定,好像有随机和轮询吧,具体的不太清楚了。 面试官:那 RabbitMQ 的消息确认机制是怎样的? 王铁牛:我记得有生产者确认和消费者确认,但是具体怎么实现的我有点模糊了。 面试官:xxl - job 的执行原理能说一下吗? 王铁牛:这个我就知道它是一个分布式任务调度平台,具体原理不太会说。

面试官总结:王铁牛,通过这三轮的面试,我能看出你对 Java 核心知识、JVM、多线程、线程池、ArrayList 这些基础部分掌握得还不错,回答得很有条理,说明你有一定的知识储备。在 Spring、MyBatis 这些框架方面,你也能准确说出关键的技术点,表现良好。然而,在一些中间件知识上,像 Dubbo 的负载均衡策略、RabbitMQ 的消息确认机制以及 xxl - job 的执行原理,你回答得不太清晰,反映出你在这些领域的知识还不够深入和扎实。我们公司对技术的要求比较全面和深入,希望员工能对各种技术都有较好的理解和掌握。你先回家等通知吧,后续如果有进一步的消息,我们会及时联系你。

问题答案详解

  1. Java 多态的实现方式
    • 方法重载:在同一个类中,方法名相同但参数列表不同(参数的类型、个数、顺序不同),通过不同的参数调用不同的方法实现多态。例如:
public class OverloadExample {
    public int add(int a, int b) {
        return a + b;
    }
    public double add(double a, double b) {
        return a + b;
    }
}
- **方法重写**:子类继承父类后,重写父类的方法,通过父类引用指向子类对象,调用重写后的方法。例如:
class Animal {
    public void makeSound() {
        System.out.println("Animal makes sound");
    }
}
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}
public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.makeSound();
    }
}
  1. JVM 内存区域划分
    • 堆(Heap):是 JVM 中最大的一块内存区域,所有对象实例和数组都在这里分配内存。它是线程共享的,垃圾回收主要就是针对堆进行的。
    • 栈(Stack):每个线程都有自己的栈,栈中存储局部变量、方法调用信息等。每个方法在执行时会创建一个栈帧,栈帧包含局部变量表、操作数栈、动态链接、方法出口等信息。方法执行结束后,栈帧会出栈。
    • 方法区(Method Area):用于存储类的信息、常量、静态变量等。在 JDK 1.8 之前,方法区也被称为永久代,JDK 1.8 及以后使用元空间(Metaspace)代替永久代。
    • 本地方法栈(Native Method Stack):与栈类似,不过它是为本地方法服务的,本地方法是使用其他语言(如 C、C++)实现的方法。
    • 程序计数器(Program Counter Register):是一块较小的内存区域,它记录当前线程执行的字节码行号。每个线程都有自己独立的程序计数器。
  2. ArrayList 扩容机制 ArrayList 底层是基于数组实现的。当创建 ArrayList 对象时,如果没有指定初始容量,默认初始容量为 10。当添加元素时,如果当前数组容量不够,会进行扩容。扩容步骤如下:
    • 计算新的容量,一般是原来容量的 1.5 倍(oldCapacity + (oldCapacity >> 1))。
    • 创建一个新的数组,新数组的容量为计算得到的新容量。
    • 把原来数组的元素复制到新数组中。 例如:
import java.util.ArrayList;

public class ArrayListExample {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            list.add(i);
        }
    }
}

当添加第 11 个元素时,ArrayList 就会进行扩容。 4. 线程池核心参数及作用 - corePoolSize:核心线程数,线程池会一直保留这些线程,即使它们处于空闲状态。当有新任务提交时,会优先使用核心线程来执行任务。 - maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。当任务队列满且核心线程都在执行任务时,会创建新的线程直到达到最大线程数。 - keepAliveTime:线程空闲的存活时间,当线程空闲时间超过这个值时,线程会被销毁,直到线程数量减少到核心线程数。 - unit:时间单位,用于指定 keepAliveTime 的时间单位,如 TimeUnit.SECONDS 表示秒。 - workQueue:任务队列,用于存储待执行的任务。常见的任务队列有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。 - threadFactory:线程工厂,用于创建线程。可以通过自定义线程工厂来设置线程的名称、优先级等属性。 - handler:任务拒绝策略,当任务队列满且线程数达到最大线程数时,会调用任务拒绝策略来处理新提交的任务。常见的任务拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(让提交任务的线程执行任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃任务队列中最老的任务)。 例如:

import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // corePoolSize
                5, // maximumPoolSize
                60, // keepAliveTime
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(10),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
        for (int i = 0; i < 20; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}
  1. Spring AOP 原理
    • JDK 动态代理:基于接口实现,通过 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口来创建代理对象。代理对象实现了目标接口,在调用代理对象的方法时,会调用 InvocationHandler 的 invoke 方法,在 invoke 方法中可以添加额外的逻辑。例如:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

interface Subject {
    void request();
}
class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("RealSubject is handling request");
    }
}
class ProxyHandler implements InvocationHandler {
    private Object target;
    public ProxyHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method call");
        Object result = method.invoke(target, args);
        System.out.println("After method call");
        return result;
    }
}
public class JdkProxyExample {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        ProxyHandler proxyHandler = new ProxyHandler(realSubject);
        Subject proxySubject = (Subject) Proxy.newProxyInstance(
                RealSubject.class.getClassLoader(),
                RealSubject.class.getInterfaces(),
                proxyHandler
        );
        proxySubject.request();
    }
}
- **CGLIB 代理**:通过继承目标类生成子类来实现代理。CGLIB 会在运行时生成一个子类,并重写目标类的方法,在重写的方法中添加额外的逻辑。例如:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

class TargetClass {
    public void method() {
        System.out.println("TargetClass method is called");
    }
}
class CglibInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Before method call");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("After method call");
        return result;
    }
}
public class CglibProxyExample {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(TargetClass.class);
        enhancer.setCallback(new CglibInterceptor());
        TargetClass proxy = (TargetClass) enhancer.create();
        proxy.method();
    }
}
  1. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译处理,MyBatis 会把 #{} 替换成一个占位符(?),然后使用 PreparedStatement 来执行 SQL 语句。这样可以防止 SQL 注入,因为参数会以安全的方式传递。例如:
<select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>
- **${}**:是直接替换,MyBatis 会把 ${} 直接替换成参数的值,然后拼接成 SQL 语句。这种方式可能存在 SQL 注入风险,因为参数直接拼接到 SQL 语句中。例如:
<select id="getUserByUsername" parameterType="String" resultType="User">
    SELECT * FROM users WHERE username = '${username}'
</select>
  1. Dubbo 的负载均衡策略
    • RandomLoadBalance:随机负载均衡策略,根据权重随机选择一个服务提供者。默认情况下,每个服务提供者的权重是相同的,可以通过配置来设置不同的权重。
    • RoundRobinLoadBalance:轮询负载均衡策略,按照顺序依次选择服务提供者。可以通过配置设置每个服务提供者的权重,权重高的服务提供者会被更频繁地选择。
    • LeastActiveLoadBalance:最少活跃调用数负载均衡策略,选择当前活跃调用数最少的服务提供者。如果有多个服务提供者的活跃调用数相同,则根据权重随机选择一个。
    • ConsistentHashLoadBalance:一致性哈希负载均衡策略,根据请求的参数计算哈希值,然后根据哈希值选择服务提供者。这样可以保证相同的请求总是发送到同一个服务提供者。
  2. RabbitMQ 的消息确认机制
    • 生产者确认:生产者可以通过两种方式确认消息是否发送到 RabbitMQ 服务器。
      • 事务模式:生产者开启事务,发送消息后提交事务。如果事务提交成功,则表示消息发送成功;如果事务提交失败,则表示消息发送失败。但是事务模式会影响性能。
      • 确认模式:生产者开启确认模式,发送消息后,RabbitMQ 服务器会返回一个确认消息给生产者。确认消息有两种类型:ACK(确认成功)和 NACK(确认失败)。生产者可以根据确认消息来处理消息发送结果。例如:
import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class ProducerConfirmExample {
    private static final String QUEUE_NAME = "test_queue";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 开启确认模式
        channel.confirmSelect();

        String message = "Hello, RabbitMQ!";
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

        // 等待确认消息
        if (channel.waitForConfirms()) {
            System.out.println("Message sent successfully");
        } else {
            System.out.println("Message sent failed");
        }

        channel.close();
        connection.close();
    }
}
- **消费者确认**:消费者从 RabbitMQ 服务器接收消息后,需要向服务器发送确认消息,表示消息已经处理完毕。消费者确认也有两种模式:
    - **自动确认**:消费者接收到消息后,RabbitMQ 服务器会自动认为消息已经处理完毕,会立即从队列中删除该消息。
    - **手动确认**:消费者接收到消息后,需要手动调用 basicAck 方法向服务器发送确认消息。只有在消费者发送确认消息后,RabbitMQ 服务器才会从队列中删除该消息。例如:
import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class ConsumerConfirmExample {
    private static final String QUEUE_NAME = "test_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 关闭自动确认
        boolean autoAck = false;

        channel.basicConsume(QUEUE_NAME, autoAck, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("Received message: " + message);

                // 手动确认消息
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });
    }
}
  1. xxl - job 的执行原理 xxl - job 是一个分布式任务调度平台,其执行原理主要包括以下几个部分:
    • 调度中心:负责任务的调度和管理。调度中心会根据任务的配置信息(如执行时间、执行周期等),在合适的时间点触发任务执行。
    • 执行器:负责执行具体的任务。执行器会注册到调度中心,调度