《互联网大厂 Java 面试:核心知识、并发与框架的深度考验》

94 阅读3分钟

互联网大厂 Java 面试:核心知识、并发与框架的深度考验

王铁牛怀揣着对互联网大厂的憧憬,走进了面试室。严肃的面试官坐在桌前,一场激烈的技术交锋即将开始。

第一轮提问

  • 面试官:首先问几个 Java 核心知识的问题。Java 中基本数据类型有哪些?
  • 王铁牛:这个我知道,有 byte、short、int、long、float、double、char、boolean。
  • 面试官:回答正确,不错。那 Java 中类和对象的区别是什么?
  • 王铁牛:类是对象的抽象模板,对象是类的具体实例。就好比汽车设计图是类,按照设计图造出来的汽车就是对象。
  • 面试官:非常好。那在 Java 中,什么是方法重载和方法重写?
  • 王铁牛:方法重载是在一个类中,有多个方法名相同,但参数列表不同的情况。方法重写是子类对父类中允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。

第二轮提问

  • 面试官:接下来问一些并发相关的问题。JUC 里的 CountDownLatch 是做什么用的?
  • 王铁牛:嗯……它好像是和线程有关的,能让线程等待,具体怎么用我有点记不清了。
  • 面试官:那多线程有哪些实现方式?
  • 王铁牛:有继承 Thread 类和实现 Runnable 接口,还有实现 Callable 接口。
  • 面试官:不错。那线程池有哪些常用的创建方式?
  • 王铁牛:我记得有 Executors 工具类可以创建,像 FixedThreadPool、CachedThreadPool 这些,具体细节我有点模糊了。
  • 面试官:稍微有点印象,但是不够清晰。那线程池的核心参数有哪些?
  • 王铁牛:好像有最大线程数、核心线程数,其他的我就不太确定了。

第三轮提问

  • 面试官:现在来问一些框架相关的问题。Spring 的核心特性有哪些?
  • 王铁牛:有 IoC 和 AOP,IoC 是控制反转,AOP 是面向切面编程。
  • 面试官:回答正确。那 Spring Boot 的自动配置原理是什么?
  • 王铁牛:这个……我知道是自动配置,但具体原理我说不太清楚。
  • 面试官:那 MyBatis 中 #{} 和 ${} 的区别是什么?
  • 王铁牛:我感觉它们都是用来传参数的,具体区别我不太能说准确。
  • 面试官:最后问一下,Dubbo、RabbitMq、xxl - job、Redis 这些技术,你能简单说一下它们的应用场景吗?
  • 王铁牛:Dubbo 好像是做分布式服务调用的,RabbitMq 是消息队列,xxl - job 是分布式任务调度,Redis 可以做缓存,但是具体细节我不太清楚。

面试官:今天的面试就到这里,你回去等通知吧。整体来看,你对一些基础的 Java 核心知识掌握得还可以,在回答简单问题时表现不错,展现出了一定的知识储备。但在并发编程和框架的深入原理方面,回答得不够清晰和准确,很多问题只是有个大概印象,缺乏深入的理解。我们后续会综合评估,有结果会及时通知你。

问题答案

  1. Java 中基本数据类型有哪些?
    • Java 中有 8 种基本数据类型,可分为 4 类:
      • 整数类型:byte(1 字节,范围 -128 到 127)、short(2 字节,范围 -32768 到 32767)、int(4 字节,范围 -2147483648 到 2147483647)、long(8 字节,范围 -2^63 到 2^63 - 1)。
      • 浮点类型:float(4 字节,单精度浮点数)、double(8 字节,双精度浮点数)。
      • 字符类型:char(2 字节,用于表示单个字符)。
      • 布尔类型:boolean(只有两个值,true 和 false)。
  2. Java 中类和对象的区别是什么?
    • 类是一种抽象的概念,是对一类事物的共同特征和行为的描述。它定义了对象的属性(成员变量)和方法。例如,定义一个“汽车”类,包含颜色、品牌等属性,以及启动、行驶等方法。
    • 对象是类的具体实例,是根据类创建出来的具体个体。通过类可以创建多个不同的对象,每个对象都有自己独立的属性值。比如根据“汽车”类创建出一辆红色的宝马汽车对象。
  3. 在 Java 中,什么是方法重载和方法重写?
    • 方法重载(Overloading):在同一个类中,多个方法具有相同的方法名,但参数列表不同(参数的类型、个数或顺序不同)。方法重载主要是为了方便调用者,根据不同的参数传递可以调用不同的实现。例如:
public class OverloadExample {
    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 是 Java 并发包(JUC)中的一个同步工具类,它允许一个或多个线程等待其他线程完成操作。它使用一个计数器来实现,初始化时设置计数器的值,每当一个线程完成任务时,计数器的值减 1,当计数器的值为 0 时,等待的线程将被唤醒继续执行。例如,主线程需要等待多个子线程完成数据加载后再进行后续操作,就可以使用 CountDownLatch。示例代码如下:
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 {
                    latch.countDown();
                }
            }).start();
        }
        latch.await();
        System.out.println("All threads completed, main thread continues");
    }
}
  1. 多线程有哪些实现方式?
    • 继承 Thread 类:创建一个类继承自 Thread 类,重写 run 方法,在 run 方法中定义线程要执行的任务。然后创建该类的对象并调用 start 方法启动线程。示例代码如下:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}
public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
- **实现 Runnable 接口**:创建一个类实现 Runnable 接口,实现 run 方法,然后将该类的对象作为参数传递给 Thread 类的构造函数,再调用 start 方法启动线程。示例代码如下:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable thread is running");
    }
}
public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}
- **实现 Callable 接口**:Callable 接口与 Runnable 接口类似,但它的 call 方法有返回值,并且可以抛出异常。通常需要配合 FutureTask 类来获取返回值。示例代码如下:
import java.util.concurrent.*;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 1 + 2;
    }
}
public class CallableExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();
        Integer result = futureTask.get();
        System.out.println("Result: " + result);
    }
}
  1. 线程池有哪些常用的创建方式?
    • 使用 Executors 工具类
      • FixedThreadPool:创建一个固定大小的线程池,线程数量固定,当有新任务提交时,如果线程池中有空闲线程,则立即执行任务,否则任务会被放入队列等待。示例代码如下:
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(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}
    - **CachedThreadPool**:创建一个可缓存的线程池,如果线程池中的线程数量超过了处理任务所需的数量,空闲线程会在一定时间后被回收;如果有新任务提交,而线程池中没有空闲线程,则会创建新的线程来执行任务。示例代码如下:
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();
    }
}
    - **SingleThreadExecutor**:创建一个单线程的线程池,所有任务会按照提交的顺序依次执行。示例代码如下:
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(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}
    - **ScheduledThreadPool**:创建一个可以定时执行任务的线程池。示例代码如下:
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 executed after 2 seconds");
        }, 2, TimeUnit.SECONDS);
        executor.shutdown();
    }
}
- **使用 ThreadPoolExecutor 手动创建**:通过 ThreadPoolExecutor 的构造函数可以更灵活地配置线程池的参数。示例代码如下:
import java.util.concurrent.*;

public class ThreadPoolExecutorExample {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                5, // 最大线程数
                60, // 空闲线程存活时间
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(10) // 任务队列
        );
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}
  1. 线程池的核心参数有哪些?
    • corePoolSize:核心线程数,线程池在初始化时会创建的线程数量,当有新任务提交时,会优先使用核心线程来执行任务。
    • maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。当任务队列已满,且核心线程都在执行任务时,会创建新的线程直到达到最大线程数。
    • keepAliveTime:空闲线程存活时间,当线程池中的线程数量超过核心线程数时,空闲线程在经过 keepAliveTime 时间后会被回收。
    • unit:keepAliveTime 的时间单位,如 TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。
    • workQueue:任务队列,用于存储等待执行的任务。常见的任务队列有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。
    • threadFactory:线程工厂,用于创建线程。可以自定义线程工厂来设置线程的名称、优先级等属性。
    • handler:拒绝策略,当任务队列已满,且线程池中的线程数量达到最大线程数时,新提交的任务会触发拒绝策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程来执行任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
  2. Spring 的核心特性有哪些?
    • IoC(Inversion of Control,控制反转):将对象的创建和依赖关系的管理从代码中转移到 Spring 容器中。通过 Spring 容器来创建和管理对象,对象之间的依赖关系由容器注入,从而降低了代码的耦合度。例如,一个 Service 类依赖一个 DAO 类,在传统方式中,Service 类需要自己创建 DAO 类的对象,而在 Spring 中,Service 类只需要声明对 DAO 类的依赖,Spring 容器会负责创建和注入 DAO 类的对象。
    • AOP(Aspect - Oriented Programming,面向切面编程):允许在不修改原有业务逻辑的基础上,对程序进行增强。AOP 通过将横切关注点(如日志记录、事务管理等)与业务逻辑分离,提高了代码的可维护性和可复用性。例如,在方法执行前后添加日志记录,或者在方法执行过程中进行事务管理。
  3. Spring Boot 的自动配置原理是什么?
    • Spring Boot 的自动配置基于 Spring 的条件注解和类路径扫描机制。当 Spring Boot 应用启动时,会加载 META - INF/spring.factories 文件,该文件中定义了一系列自动配置类。然后根据类路径中是否存在某些类、配置文件中是否存在某些配置项等条件,使用条件注解(如 @ConditionalOnClass、@ConditionalOnMissingBean 等)来判断是否需要启用某个自动配置类。如果条件满足,则将该自动配置类中的 Bean 定义注册到 Spring 容器中,从而实现自动配置。例如,当类路径中存在 Hibernate 和 Spring Data JPA 相关的类时,Spring Boot 会自动配置数据源、EntityManagerFactory 等。
  4. MyBatis 中 #{} 和 ${} 的区别是什么?
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ? 占位符,然后使用 PreparedStatement 进行预编译,再将参数值设置到占位符中。这样可以有效防止 SQL 注入攻击,因为参数值会被自动进行转义处理。例如:
<select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>
- **${}**:是字符串替换,MyBatis 在处理 ${} 时,会直接将 ${} 中的内容替换为参数值,不会进行预编译和转义处理。因此,使用 ${} 可能会导致 SQL 注入攻击。通常 ${} 用于动态传入表名、列名等场景。例如:
<select id="getUsersByTable" parameterType="String" resultType="User">
    SELECT * FROM ${tableName}
</select>
  1. Dubbo、RabbitMq、xxl - job、Redis 这些技术的应用场景分别是什么?
    • Dubbo:是一个高性能的分布式服务框架,主要用于构建分布式系统中的服务调用。适用于大型的分布式系统,当系统拆分成多个服务模块后