《互联网大厂面试:揭秘 Java 核心、框架与中间件考察风暴》

35 阅读10分钟

《互联网大厂面试:揭秘 Java 核心、框架与中间件考察风暴》

在互联网大厂的一间严肃的面试室内,面试官正襟危坐,对面坐着略显紧张的求职者王铁牛。面试开始了。

第一轮面试 面试官:“我们先从 Java 核心知识开始。你能说一下 Java 中多态的实现方式有哪些吗?” 王铁牛:“多态的实现方式主要有两种,一种是方法重载,在同一个类中,多个方法有相同的名字,但参数列表不同;另一种是方法重写,子类重写父类的方法。” 面试官:“回答得不错。那在 Java 中,final 关键字有什么作用?” 王铁牛:“final 关键字可以修饰类、方法和变量。修饰类时,这个类不能被继承;修饰方法时,这个方法不能被重写;修饰变量时,这个变量就变成了常量,一旦赋值就不能再改变。” 面试官:“很好,看来你对基础掌握得挺扎实。那你说说 StringStringBuilderStringBuffer 的区别。” 王铁牛:“String 是不可变的,每次对 String 的操作都会生成一个新的 String 对象。StringBuilderStringBuffer 是可变的,StringBuilder 是非线程安全的,效率较高;StringBuffer 是线程安全的,效率相对较低。” 面试官:“非常棒,你的回答很准确。接下来进入第二轮。”

第二轮面试 面试官:“我们聊聊 JUC(Java 并发工具包)。CountDownLatchCyclicBarrier 有什么区别?” 王铁牛:“嗯……好像都和线程同步有关吧,具体区别我有点不太确定。” 面试官:“没关系,那再问你,ReentrantLocksynchronized 有什么不同?” 王铁牛:“我知道它们都能实现线程同步,ReentrantLock 好像更灵活,但具体怎么个灵活法我也说不太清楚。” 面试官:“看来你对 JUC 的掌握还不够深入。那说说线程池,线程池有哪几种创建方式?” 王铁牛:“我记得有 Executors 工具类可以创建,像 newFixedThreadPoolnewCachedThreadPool 这些,但具体它们有啥区别我不太懂。” 面试官:“这一轮你回答得不太理想,希望下一轮能有更好的表现。”

第三轮面试 面试官:“现在我们来谈谈框架。Spring 框架中 IOC(控制反转)和 AOP(面向切面编程)的原理是什么?” 王铁牛:“IOC 好像就是把对象的创建和管理交给 Spring 容器,AOP 是可以在不修改原有代码的情况下增加一些功能,但具体原理我讲不明白。” 面试官:“那 Spring Boot 有什么优点,它是如何实现自动配置的?” 王铁牛:“Spring Boot 能简化开发,好像是通过一些注解实现自动配置,但具体细节我不太清楚。” 面试官:“再问你,MyBatis 是如何实现数据库操作的?” 王铁牛:“MyBatis 可以通过 XML 文件或者注解来映射 SQL 语句,然后执行数据库操作,但具体过程我不太懂。” 面试官:“综合这三轮面试来看,你对 Java 核心基础知识掌握得还可以,但在 JUC、线程池以及框架方面的理解还不够深入。我们会综合评估这次面试情况,你先回家等通知吧。”

问题答案

  1. Java 中多态的实现方式有哪些?
    • 方法重载(Overloading):在同一个类中,多个方法可以有相同的名字,但参数列表必须不同(参数的类型、个数或顺序不同)。方法重载是一种编译时多态,编译器根据调用方法时传递的参数类型和数量来决定调用哪个方法。例如:
public class OverloadingExample {
    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. 在 Java 中,final 关键字有什么作用?
    • 修饰类:当一个类被 final 修饰时,这个类不能被继承。例如:
final class FinalClass {
    // 类的内容
}
// 以下代码会编译错误,因为 FinalClass 不能被继承
// class SubClass extends FinalClass {}
- **修饰方法**:当一个方法被 `final` 修饰时,这个方法不能被重写。例如:
class Parent {
    public final void finalMethod() {
        System.out.println("This is a final method");
    }
}
class Child extends Parent {
    // 以下代码会编译错误,因为 finalMethod 不能被重写
    // @Override
    // public void finalMethod() {}
}
- **修饰变量**:当一个变量被 `final` 修饰时,这个变量就变成了常量,一旦赋值就不能再改变。对于基本数据类型,其值不能改变;对于引用数据类型,其引用不能改变,但对象的内容可以改变。例如:
final int CONSTANT = 10;
// 以下代码会编译错误,因为 CONSTANT 是常量,不能再赋值
// CONSTANT = 20;

final StringBuilder sb = new StringBuilder("Hello");
// 可以修改对象的内容
sb.append(" World");
// 但不能改变引用
// sb = new StringBuilder("New"); // 编译错误
  1. StringStringBuilderStringBuffer 的区别
    • StringString 类是不可变的,一旦创建,其内容不能被修改。每次对 String 进行操作(如拼接、替换等),都会创建一个新的 String 对象。这会导致大量的内存开销,尤其是在频繁进行字符串操作的场景下。例如:
String str = "Hello";
str = str + " World"; // 这里创建了新的 String 对象
- **`StringBuilder`**`StringBuilder` 是可变的,它是非线程安全的,适用于单线程环境下的字符串拼接等操作。它的操作效率较高,因为不会频繁创建新的对象。例如:
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 直接在原对象上进行操作
- **`StringBuffer`**`StringBuffer` 也是可变的,它是线程安全的,适用于多线程环境下的字符串操作。由于要保证线程安全,它的操作效率相对较低。例如:
StringBuffer sb = new StringBuffer("Hello");
sb.append(" World"); // 线程安全的操作
  1. CountDownLatchCyclicBarrier 有什么区别?
    • CountDownLatchCountDownLatch 是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。它有一个计数器,在创建 CountDownLatch 对象时需要指定计数器的初始值。线程调用 countDown() 方法会将计数器减 1,当计数器的值变为 0 时,等待的线程会被唤醒继续执行。CountDownLatch 的计数器只能使用一次,不能重置。例如:
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println("Thread is working");
                latch.countDown();
            }).start();
        }
        latch.await();
        System.out.println("All threads have finished working");
    }
}
- **`CyclicBarrier`**`CyclicBarrier` 也是一个同步辅助类,它允许一组线程相互等待,直到所有线程都到达一个屏障点,然后所有线程可以继续执行。`CyclicBarrier` 有一个计数器,在创建 `CyclicBarrier` 对象时需要指定参与的线程数量。线程调用 `await()` 方法会将计数器减 1,当计数器的值变为 0 时,所有等待的线程会被唤醒继续执行,并且计数器可以重置,以便下次使用。例如:
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("All threads have reached the barrier");
        });
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    System.out.println("Thread is working");
                    barrier.await();
                    System.out.println("Thread continues after the barrier");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  1. ReentrantLocksynchronized 有什么不同?
    • 语法层面synchronized 是 Java 的关键字,是基于 JVM 实现的,使用起来比较简单,可以修饰方法或代码块。例如:
public class SynchronizedExample {
    public synchronized void method() {
        // 同步方法
    }
    public void anotherMethod() {
        synchronized (this) {
            // 同步代码块
        }
    }
}
- **`ReentrantLock`**`ReentrantLock` 是一个类,是基于 Java 代码实现的,使用时需要手动加锁和解锁。例如:
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    public void method() {
        lock.lock();
        try {
            // 执行同步操作
        } finally {
            lock.unlock();
        }
    }
}
- **灵活性**`ReentrantLock``synchronized` 更灵活,它可以实现公平锁(`ReentrantLock(true)`),即线程按照请求锁的顺序获取锁;还可以实现可中断锁,通过 `lockInterruptibly()` 方法,线程在等待锁的过程中可以被中断;另外,`ReentrantLock` 可以尝试获取锁(`tryLock()`),如果锁不可用,线程可以立即返回,而不会一直等待。
- **性能**:在 JDK 1.6 之前,`synchronized` 的性能较差,因为它是重量级锁。但在 JDK 1.6 及以后,JVM 对 `synchronized` 进行了优化,引入了偏向锁、轻量级锁等机制,性能有了很大提升,与 `ReentrantLock` 相差不大。

6. 线程池有哪几种创建方式? - 使用 Executors 工具类Executors 是 Java 提供的一个线程池工具类,它提供了几种静态方法来创建不同类型的线程池。 - newFixedThreadPool:创建一个固定大小的线程池,线程池中的线程数量是固定的。例如:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                System.out.println("Task 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("Task is running");
            });
        }
        executor.shutdown();
    }
}
    - **`newSingleThreadExecutor`**:创建一个单线程的线程池,线程池只有一个线程,任务会按照提交的顺序依次执行。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                System.out.println("Task is running");
            });
        }
        executor.shutdown();
    }
}
    - **`newScheduledThreadPool`**:创建一个可以定时执行任务的线程池。例如:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolExample {
    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.schedule(() -> {
            System.out.println("Task is running after 2 seconds");
        }, 2, TimeUnit.SECONDS);
        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("Task is running");
            });
        }
        executor.shutdown();
    }
}
  1. Spring 框架中 IOC(控制反转)和 AOP(面向切面编程)的原理是什么?
    • IOC(控制反转):IOC 的核心思想是将对象的创建和管理交给 Spring 容器,而不是由对象本身来创建和管理依赖对象。Spring 容器通过配置文件(如 XML 配置文件)或注解(如 @Component@Autowired 等)来管理对象的创建和依赖注入。当一个对象需要依赖另一个对象时,Spring 容器会自动将依赖对象注入到需要的对象中。例如:
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;
    }
    public void doSomething() {
        serviceB.doAnotherThing();
    }
}

@Component
class ServiceB {
    public void doAnotherThing() {
        System.out.println("Doing another thing");
    }
}
- **AOP(面向切面编程)**:AOP 是一种编程范式,它允许在不修改原有代码的情况下,对程序的某些部分进行增强。AOP 的核心概念包括切面(Aspect)、连接点(Join Point)、切入点(Pointcut)、通知(Advice)和织入(Weaving)。Spring AOP 是基于动态代理实现的,对于实现了接口的类,使用 JDK 动态代理;对于没有实现接口的类,使用 CGLIB 动态代理。例如:
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
class LoggingAspect {
    @Pointcut("execution(* com.example.Service.*(..))")
    public void serviceMethods() {}
    @Before("serviceMethods()")
    public void beforeServiceMethod() {
        System.out.println("Before service method");
    }
    @After("serviceMethods()")
    public void afterServiceMethod() {
        System.out.println("After service method");
    }