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

31 阅读2分钟

在互联网大厂的一间严肃面试室内,一场针对 Java 技术的面试正在紧张进行。严肃的面试官坐在桌前,对面是略显紧张的求职者王铁牛。

第一轮面试开始 面试官:我们先从 Java 核心知识问起,Java 中基本数据类型有哪些? 王铁牛:有 byte、short、int、long、float、double、char、boolean。 面试官:回答正确,不错。那 Java 中重载和重写的区别是什么? 王铁牛:重载是在一个类中,方法名相同但参数列表不同;重写是子类重写父类的方法,方法名、参数列表和返回值类型都相同。 面试官:非常好。那 Java 中的多态是如何实现的? 王铁牛:多态主要通过继承、接口实现和方法重写来实现。父类引用指向子类对象,调用重写的方法时会根据实际对象类型来执行相应的方法。 面试官:回答得很清晰,看来你对 Java 核心知识掌握得不错。

第二轮面试开始 面试官:接下来聊聊 JUC 和多线程。线程有哪些状态? 王铁牛:有新建、就绪、运行、阻塞和死亡五种状态。 面试官:很好。那如何创建一个线程,有几种方式? 王铁牛:有三种方式,继承 Thread 类、实现 Runnable 接口和实现 Callable 接口。 面试官:回答正确。那线程池有什么作用,如何创建一个线程池? 王铁牛:线程池可以复用线程,减少线程创建和销毁的开销。可以通过 Executors 工具类创建,比如 newFixedThreadPool、newCachedThreadPool 等。 面试官:不错,对线程和线程池有一定的了解。

第三轮面试开始 面试官:现在问一些框架相关的问题。Spring 中的 IOC 和 AOP 是什么? 王铁牛:IOC 是控制反转,把对象的创建和依赖关系的管理交给 Spring 容器;AOP 是面向切面编程,用于在不修改原有代码的情况下增加额外的功能。 面试官:回答得还行。那 Spring Boot 有什么优势,如何快速搭建一个 Spring Boot 项目? 王铁牛:Spring Boot 简化了 Spring 开发,有自动配置等优势。可以通过 Spring Initializr 网站或者 IDE 的插件来快速搭建项目。 面试官:那 MyBatis 中 #{} 和 ${} 的区别是什么? 王铁牛:这个……嗯……好像一个是预编译,一个不是,但具体我有点说不太清。 面试官:看来你对一些复杂的细节掌握得不够扎实。

面试结束,面试官看着王铁牛说:“今天的面试就到这里,你整体对一些基础的 Java 知识和简单的框架概念有一定的了解,但对于一些复杂的技术细节和深入的业务场景理解还有所欠缺。你先回家等通知吧,后续如果有进一步消息会及时联系你。”

问题答案详细解析

  1. Java 中基本数据类型有哪些?
    • Java 中有 8 种基本数据类型,分为 4 类:
      • 整数类型:byte(1 字节,-128 到 127)、short(2 字节,-32768 到 32767)、int(4 字节,-2147483648 到 2147483647)、long(8 字节,范围更大)。
      • 浮点类型:float(4 字节,单精度浮点数)、double(8 字节,双精度浮点数)。
      • 字符类型:char(2 字节,用于表示单个字符,采用 Unicode 编码)。
      • 布尔类型:boolean(只有 true 和 false 两个值)。
  2. 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 eat() {
        System.out.println("Animal is eating");
    }
}
class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating");
    }
}
  1. Java 中的多态是如何实现的?
    • 多态主要通过以下几种方式实现:
      • 继承:子类继承父类,子类可以重写父类的方法。
      • 接口实现:类实现接口,实现接口中的方法。
      • 方法重写:子类重写父类的方法,在运行时根据实际对象的类型来调用相应的方法。例如:
class Shape {
    public void draw() {
        System.out.println("Drawing a shape");
    }
}
class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}
public class PolymorphismExample {
    public static void main(String[] args) {
        Shape shape = new Circle();
        shape.draw(); // 输出 "Drawing a circle"
    }
}
  1. 线程有哪些状态?
    • 线程在 Java 中有 5 种状态:
      • 新建(New):线程对象被创建,但还没有调用 start() 方法。
      • 就绪(Runnable):线程调用了 start() 方法,等待获取 CPU 时间片,处于可运行状态。
      • 运行(Running):线程获得 CPU 时间片,正在执行 run() 方法。
      • 阻塞(Blocked):线程由于某些原因放弃 CPU 时间片,暂时停止执行。阻塞状态分为三种:等待阻塞(调用 wait() 方法)、同步阻塞(获取同步锁失败)和其他阻塞(调用 sleep()、join() 等方法)。
      • 死亡(Terminated):线程执行完 run() 方法或者因异常退出,生命周期结束。
  2. 如何创建一个线程,有几种方式?
    • 有三种方式创建线程:
      • 继承 Thread 类:创建一个类继承 Thread 类,重写 run() 方法,然后创建该类的对象并调用 start() 方法。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}
public class ThreadCreationExample {
    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 is running");
    }
}
public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}
    - 实现 Callable 接口:创建一个类实现 Callable 接口,实现 call() 方法,该方法有返回值。通过 FutureTask 类来包装 Callable 对象,再将 FutureTask 对象作为参数传递给 Thread 类的构造方法,调用 start() 方法启动线程,最后可以通过 FutureTask 的 get() 方法获取返回值。例如:
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 工具类创建:
        • newFixedThreadPool:创建一个固定大小的线程池,线程数量固定,当有任务提交时,如果线程池中有空闲线程,则立即执行任务,否则任务会被放入队列中等待。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

public class CachedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executorService.shutdown();
    }
}
    - 通过 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 < 10; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}
  1. Spring 中的 IOC 和 AOP 是什么?
    • IOC(Inversion of Control,控制反转):是一种设计思想,将对象的创建和依赖关系的管理从代码中转移到 Spring 容器中。在传统的编程中,对象的创建和依赖关系的维护由程序员手动完成,而在 IOC 中,这些工作由 Spring 容器负责。通过 IOC,降低了代码之间的耦合度,提高了代码的可维护性和可测试性。例如,在 Spring 中可以通过 XML 配置文件或注解来定义对象和它们之间的依赖关系。
// 定义一个接口
interface UserService {
    void addUser();
}
// 实现类
class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        System.out.println("Adding user");
    }
}
// 使用 Spring 注解注入
@Component
class UserController {
    @Autowired
    private UserService userService;

    public void doAddUser() {
        userService.addUser();
    }
}
- AOP(Aspect-Oriented Programming,面向切面编程):是一种编程范式,用于在不修改原有代码的情况下,对程序的某些功能进行增强。AOP 主要通过将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,形成独立的切面,然后在适当的时机将切面织入到目标对象中。在 Spring 中,AOP 可以通过 XML 配置或注解来实现。例如:
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
public 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");
    }
}
  1. Spring Boot 有什么优势,如何快速搭建一个 Spring Boot 项目?
    • Spring Boot 的优势:
      • 简化配置:Spring Boot 提供了自动配置机制,能够根据项目的依赖自动配置 Spring 应用,减少了大量的 XML 配置文件。
      • 快速开发:通过 Starter 依赖,开发者可以快速集成各种功能,如 Web 开发、数据库访问等。
      • 嵌入式服务器:Spring Boot 内置了 Tomcat、Jetty 等嵌入式服务器,无需手动部署到外部服务器。
      • 生产就绪:提供了健康检查、监控等生产级特性,方便应用的部署和运维。
    • 快速搭建 Spring Boot 项目的方式:
      • 使用 Spring Initializr 网站:访问 start.spring.io/,选择项目的基本信息(… Boot 版本等)和依赖,然后点击生成按钮下载项目压缩包,解压后导入到 IDE 中即可。
      • 使用 IDE 插件:在 IntelliJ IDEA 或 Eclipse 等 IDE 中,可以使用 Spring Initializr 插件来创建 Spring Boot 项目,操作步骤类似。
  2. MyBatis 中 #{} 和 ${} 的区别是什么?
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ? 占位符,然后使用 PreparedStatement 来执行 SQL 语句。这样可以防止 SQL 注入攻击,因为参数会被自动进行转义处理。例如:
<select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>
- ${}:是字符串替换,MyBatis 在处理 ${} 时,会直接将 ${} 中的内容替换到 SQL 语句中。这种方式存在 SQL 注入风险,因为参数不会进行转义处理。通常用于需要动态传入表名、列名等情况。例如:
<select id="getUserByColumn" parameterType="map" resultType="User">
    SELECT * FROM users WHERE ${column} = #{value}
</select>

在实际开发中,尽量使用 #{} 来避免 SQL 注入问题。