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

28 阅读9分钟

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

在互联网大厂的一间安静的面试室内,严肃的面试官正对面坐着紧张的求职者王铁牛,一场决定命运的面试即将展开。

第一轮提问

  • 面试官:我们先从 Java 核心知识开始。你能说说 Java 中多态的实现方式有哪些吗?
  • 王铁牛:嗯,多态的实现方式主要有两种,一种是方法重载,在同一个类中,方法名相同但参数列表不同;另一种是方法重写,子类重写父类的方法。
  • 面试官:回答得不错。那再问你,抽象类和接口有什么区别呢?
  • 王铁牛:抽象类可以有构造方法,也可以有普通方法和成员变量,而接口不能有构造方法,只能有抽象方法和常量。
  • 面试官:很好。最后问你,Java 中的自动装箱和拆箱是怎么回事?
  • 王铁牛:自动装箱就是把基本数据类型自动转换为对应的包装类对象,拆箱则相反,是把包装类对象自动转换为基本数据类型。

第二轮提问

  • 面试官:接下来聊聊 JUC 相关的。你知道 CountDownLatch 是做什么用的吗?
  • 王铁牛:CountDownLatch 可以让一个或多个线程等待其他线程完成操作后再继续执行,通过一个计数器来实现。
  • 面试官:那 CyclicBarrier 呢,它和 CountDownLatch 有什么不同?
  • 王铁牛:这个……好像也是和线程同步有关吧,不同嘛,我不太确定。
  • 面试官:没关系,再问你,在 JUC 中,Semaphore 是如何使用的?
  • 王铁牛:Semaphore 好像是用来控制并发线程数量的,但具体怎么用我有点模糊了。
  • 面试官:看来你对部分概念有了解,但还不够深入。那说下 Fork/Join 框架是干什么的?
  • 王铁牛:这个我听说过,好像是用于并行计算的,但不太清楚细节。

第三轮提问

  • 面试官:我们再谈谈 Spring 相关的。Spring 的 IOC 容器的工作原理是什么?
  • 王铁牛:IOC 就是控制反转,把对象的创建和依赖关系的管理交给 Spring 容器,具体原理我有点说不清楚。
  • 面试官:那 Spring 的 AOP 有哪些应用场景呢?
  • 王铁牛:AOP 好像可以用于日志记录、事务管理这些,具体怎么实现我不太懂。
  • 面试官:Spring Boot 是如何实现自动配置的?
  • 王铁牛:它好像是根据类路径下的依赖和配置文件自动配置 Spring 应用,但具体过程我讲不明白。
  • 面试官:最后问你,MyBatis 的一级缓存和二级缓存有什么区别?
  • 王铁牛:我知道有这两级缓存,但具体区别不太清楚。

面试官推了推眼镜,表情严肃地说:“王铁牛,从这次面试来看,你对一些基础的 Java 知识有一定的掌握,回答简单问题时表现还可以。但对于一些相对复杂的知识点,像 JUC 里的高级工具、Spring 和 Spring Boot 的核心原理,你的理解还不够深入和清晰。我们会综合考虑这次面试情况,你先回家等通知吧。”

问题答案

  1. Java 中多态的实现方式有哪些
    • 方法重载:在同一个类中,方法名相同但参数列表不同(参数的类型、个数、顺序不同)。编译器根据调用方法时传递的参数来决定具体调用哪个方法。例如:
class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    public double add(double a, double b) {
        return a + b;
    }
}
- 方法重写:子类重写父类的方法,要求方法名、参数列表和返回值类型都相同(返回值类型可以是子类类型,这是协变返回类型)。运行时根据对象的实际类型来决定调用哪个类的方法。例如:
class Animal {
    public void sound() {
        System.out.println("Animal makes a sound");
    }
}
class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("Dog barks");
    }
}
  1. 抽象类和接口有什么区别
    • 抽象类:可以有构造方法,用于初始化抽象类的成员变量;可以有普通方法和成员变量;一个类只能继承一个抽象类。例如:
abstract class Shape {
    protected int side;
    public Shape(int side) {
        this.side = side;
    }
    public abstract int area();
    public void printInfo() {
        System.out.println("This is a shape");
    }
}
- 接口:不能有构造方法;只能有抽象方法(Java 8 及以后可以有默认方法和静态方法)和常量(public static final);一个类可以实现多个接口。例如:
interface Drawable {
    void draw();
    default void print() {
        System.out.println("Printing...");
    }
}
  1. Java 中的自动装箱和拆箱是怎么回事
    • 自动装箱:把基本数据类型自动转换为对应的包装类对象。例如:
Integer i = 10; // 自动装箱,相当于 Integer i = Integer.valueOf(10);
- 自动拆箱:把包装类对象自动转换为基本数据类型。例如:
Integer i = 10;
int j = i; // 自动拆箱,相当于 int j = i.intValue();
  1. 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 {
                    System.out.println(Thread.currentThread().getName() + " is working");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            }).start();
        }

        latch.await();
        System.out.println("All threads have finished their work");
    }
}
  1. CyclicBarrier 呢,它和 CountDownLatch 有什么不同
    • CyclicBarrier 也是一个同步工具类,它允许一组线程相互等待,直到所有线程都到达一个屏障点后再继续执行。与 CountDownLatch 不同的是,CyclicBarrier 可以重复使用,当所有线程都到达屏障点后,计数器会重置,可以再次使用。例如:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("All threads have reached the barrier");
        });

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " is waiting at the barrier");
                    barrier.await();
                    System.out.println(Thread.currentThread().getName() + " has passed the barrier");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
- 主要区别:
    - CountDownLatch 是一个线程等待其他线程完成任务,计数器只能使用一次;CyclicBarrier 是一组线程相互等待,计数器可以重置重复使用。
    - CountDownLatch 的计数器是在其他线程中递减;CyclicBarrier 的计数器是在每个线程到达屏障点时自动递减。

6. 在 JUC 中,Semaphore 是如何使用的 - Semaphore 是一个计数信号量,用于控制同时访问某个资源的线程数量。它通过一个许可证的概念来实现,每个线程在访问资源前需要获取一个许可证,访问完后释放许可证。例如:

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        int permits = 2;
        Semaphore semaphore = new Semaphore(permits);

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " has acquired a permit");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " has released a permit");
                }
            }).start();
        }
    }
}
  1. Fork/Join 框架是干什么的
    • Fork/Join 框架是 Java 7 引入的一个用于并行计算的框架,它通过将一个大任务拆分成多个小任务,然后并行执行这些小任务,最后将结果合并。主要涉及两个类:ForkJoinPool 和 ForkJoinTask。例如:
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

class SumTask extends RecursiveTask<Integer> {
    private static final int THRESHOLD = 10;
    private int start;
    private int end;

    public SumTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if (end - start <= THRESHOLD) {
            int sum = 0;
            for (int i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            int mid = (start + end) / 2;
            SumTask leftTask = new SumTask(start, mid);
            SumTask rightTask = new SumTask(mid + 1, end);

            leftTask.fork();
            int rightResult = rightTask.compute();
            int leftResult = leftTask.join();

            return leftResult + rightResult;
        }
    }
}

public class ForkJoinExample {
    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        SumTask task = new SumTask(1, 100);
        int result = pool.invoke(task);
        System.out.println("The sum is: " + result);
    }
}
  1. Spring 的 IOC 容器的工作原理是什么
    • IOC(控制反转)是 Spring 的核心特性之一,它把对象的创建和依赖关系的管理交给 Spring 容器。工作原理如下:
      • 配置文件解析:Spring 容器读取配置文件(如 XML 或 Java 注解),解析其中的 Bean 定义信息。
      • Bean 定义注册:将解析得到的 Bean 定义信息注册到 BeanFactory 中,以 BeanDefinition 对象的形式存储。
      • Bean 创建:当需要使用某个 Bean 时,Spring 容器根据 BeanDefinition 信息创建 Bean 实例。在创建过程中,如果 Bean 有依赖关系,会自动注入依赖的 Bean。
      • 依赖注入:通过构造方法、Setter 方法或字段注入等方式,将依赖的 Bean 注入到目标 Bean 中。
  2. Spring 的 AOP 有哪些应用场景呢
    • AOP(面向切面编程)是 Spring 的另一个核心特性,它可以在不修改原有代码的情况下,对程序进行增强。应用场景包括:
      • 日志记录:在方法执行前后记录日志,方便调试和监控。
      • 事务管理:在方法执行前后进行事务的开启、提交或回滚操作。
      • 权限验证:在方法执行前进行权限验证,确保用户有访问权限。
      • 性能监控:统计方法的执行时间,分析性能瓶颈。
  3. Spring Boot 是如何实现自动配置的
    • Spring Boot 通过自动配置机制简化了 Spring 应用的开发。实现原理如下:
      • 启动类注解:Spring Boot 应用的启动类上有 @SpringBootApplication 注解,它包含了 @EnableAutoConfiguration 注解,用于开启自动配置功能。
      • 自动配置类:Spring Boot 提供了一系列的自动配置类,这些类位于 spring-boot-autoconfigure 模块中。每个自动配置类都有一个 @Conditional 注解,用于根据条件判断是否生效。
      • 类路径扫描:Spring Boot 会扫描类路径下的依赖,根据依赖的存在与否来决定哪些自动配置类生效。例如,如果类路径下存在 MySQL 驱动,那么会自动配置与 MySQL 相关的数据源。
      • 属性配置:用户可以通过 application.properties 或 application.yml 文件来覆盖自动配置的默认值。
  4. MyBatis 的一级缓存和二级缓存有什么区别
    • 一级缓存:是 SqlSession 级别的缓存,同一个 SqlSession 中执行相同的查询语句时,会先从一级缓存中查找结果,如果存在则直接返回,否则从数据库中查询并将结果存入缓存。一级缓存是默认开启的,并且不能关闭。
    • 二级缓存:是 Mapper 级别的缓存,多个 SqlSession 可以共享同一个 Mapper 的二级缓存。需要在配置文件中显式开启二级缓存,并且需要在 Mapper 接口或 XML 文件中配置。二级缓存的作用域更广,可以提高查询性能,但需要注意缓存的一致性问题。