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

29 阅读9分钟

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

严肃的面试官端坐在办公桌后,面前放着王铁牛的简历。王铁牛则有些紧张地坐在对面,双手不自觉地捏着衣角。面试正式开始。

第一轮提问 面试官:“我们先从 Java 核心知识开始。你能说一下 Java 中多态的实现方式有哪些吗?” 王铁牛:“嗯,多态的实现方式主要有两种,一种是方法重载,在同一个类中,方法名相同但参数列表不同;另一种是方法重写,子类重写父类的方法。” 面试官:“回答得不错。那在 Java 中,抽象类和接口有什么区别呢?” 王铁牛:“抽象类可以有构造方法,接口没有构造方法;抽象类可以有普通方法,接口在 Java 8 之前只能有抽象方法,Java 8 之后可以有默认方法和静态方法;一个类只能继承一个抽象类,但可以实现多个接口。” 面试官:“很好。那你说说 Java 中的异常处理机制。” 王铁牛:“Java 中的异常分为受检查异常和非受检查异常。受检查异常在编译时必须处理,比如 IOException;非受检查异常一般是运行时异常,像 NullPointerException,不强制要求处理。可以使用 try - catch - finally 语句来捕获和处理异常。” 面试官:“非常棒,基础很扎实。”

第二轮提问 面试官:“接下来我们聊聊 JUC 和多线程。你知道 Java 中创建线程有哪几种方式吗?” 王铁牛:“有四种方式,继承 Thread 类,实现 Runnable 接口,实现 Callable 接口,使用线程池。” 面试官:“那你说说线程池的好处有哪些?” 王铁牛:“线程池可以降低资源消耗,重复利用已创建的线程,减少线程创建和销毁造成的开销;还可以提高响应速度,当任务到达时,不需要等待线程创建就能立即执行;也方便线程的管理,可以设置线程池的参数,如核心线程数、最大线程数等。” 面试官:“不错。那在多线程环境下,如何保证数据的安全性呢?” 王铁牛:“可以使用 synchronized 关键字来实现同步,它可以修饰方法或者代码块;也可以使用 Lock 接口的实现类,如 ReentrantLock;还可以使用原子类,像 AtomicInteger 等。” 面试官:“回答得很全面。”

第三轮提问 面试官:“现在我们来谈谈框架和中间件。你使用过 Spring 框架,那 Spring 的核心特性有哪些?” 王铁牛:“Spring 的核心特性主要有依赖注入(DI)和面向切面编程(AOP)。依赖注入可以降低组件之间的耦合度,AOP 可以实现一些横切关注点,比如日志记录、事务管理等。” 面试官:“那 Spring Boot 相比 Spring 有什么优势呢?” 王铁牛:“Spring Boot 简化了 Spring 应用的开发,它提供了自动配置,减少了大量的配置文件;还内置了嵌入式服务器,如 Tomcat、Jetty 等,方便开发和部署。” 面试官:“你了解 MyBatis 吗?说说 MyBatis 是如何实现数据库操作的。” 王铁牛:“MyBatis 通过映射文件或者注解来定义 SQL 语句,它有一个 SqlSessionFactory 来创建 SqlSession,通过 SqlSession 来执行 SQL 语句,最后返回结果。” 面试官:“看起来你对这些框架有一定的了解。”

面试接近尾声,面试官整理了一下桌上的资料,看着王铁牛说:“今天的面试就到这里了,我们会综合评估你的表现。你回家等通知吧,后续无论结果如何,我们都会及时反馈给你。”

问题答案详解

第一轮问题答案

  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;
    }
}
- **方法重写**:子类重写父类的方法,要求方法名、参数列表和返回值类型都相同(返回值类型在 Java 5 之后可以是子类类型)。重写的方法不能比被重写的方法有更严格的访问权限。例如:
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 8 之前只能有抽象方法,Java 8 之后可以有默认方法(使用 default 关键字修饰)和静态方法。
    • 继承和实现:一个类只能继承一个抽象类,但可以实现多个接口,这体现了 Java 的单继承多实现原则。
  2. Java 中的异常处理机制
    • 异常分类:Java 中的异常分为受检查异常(Checked Exception)和非受检查异常(Unchecked Exception)。受检查异常是指在编译时必须处理的异常,如 IOException、SQLException 等;非受检查异常一般是运行时异常(RuntimeException)及其子类,如 NullPointerException、ArrayIndexOutOfBoundsException 等,不强制要求在编译时处理。
    • 异常处理语句:使用 try - catch - finally 语句来捕获和处理异常。try 块中放置可能会抛出异常的代码,catch 块用于捕获并处理异常,finally 块中的代码无论是否发生异常都会执行。例如:
try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("除数不能为零:" + e.getMessage());
} finally {
    System.out.println("finally 块一定会执行");
}

第二轮问题答案

  1. Java 中创建线程的方式
    • 继承 Thread 类:创建一个类继承 Thread 类,重写 run 方法,然后创建该类的实例并调用 start 方法启动线程。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}

public class Main {
    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 Main {
    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 Main {
    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);
    }
}
- **使用线程池**:通过 Executors 工具类创建线程池,将任务提交给线程池执行。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Task is running");
    }
}

public class Main {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        MyTask task = new MyTask();
        executorService.submit(task);
        executorService.shutdown();
    }
}
  1. 线程池的好处
    • 降低资源消耗:线程的创建和销毁需要消耗系统资源,线程池可以重复利用已创建的线程,减少了线程创建和销毁的开销。
    • 提高响应速度:当任务到达时,不需要等待线程创建就能立即执行,因为线程池中有空闲的线程。
    • 方便线程的管理:可以设置线程池的参数,如核心线程数、最大线程数、线程空闲时间等,对线程进行有效的管理。
  2. 多线程环境下保证数据安全性的方法
    • synchronized 关键字:可以修饰方法或者代码块,保证同一时刻只有一个线程可以访问被修饰的方法或代码块。例如:
public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}
- **Lock 接口的实现类**:如 ReentrantLock,通过 lock() 方法获取锁,unlock() 方法释放锁。相比 synchronized 关键字,Lock 接口提供了更灵活的锁机制。例如:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}
- **原子类**:如 AtomicInteger、AtomicLong 等,这些类使用 CAS(Compare - And - Swap)算法实现原子操作,在多线程环境下可以保证数据的原子性。例如:
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }
}

第三轮问题答案

  1. Spring 的核心特性
    • 依赖注入(DI):通过将对象的依赖关系的创建和管理交给 Spring 容器来完成,降低了组件之间的耦合度。例如,一个类需要另一个类的实例,可以通过构造方法、Setter 方法或者字段注入的方式将实例注入到该类中。
public class UserService {
    private UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}
- **面向切面编程(AOP)**:将一些横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,通过代理的方式在目标方法执行前后插入相应的逻辑。Spring AOP 可以使用 XML 配置或者注解的方式来实现。例如:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void beforeAdvice() {
        System.out.println("Before method execution");
    }
}
  1. Spring Boot 相比 Spring 的优势
    • 自动配置:Spring Boot 提供了大量的自动配置类,根据项目中引入的依赖自动配置 Spring 应用,减少了大量的配置文件。例如,引入 Spring Boot Starter Web 依赖后,Spring Boot 会自动配置嵌入式服务器和 Spring MVC。
    • 嵌入式服务器:Spring Boot 内置了嵌入式服务器,如 Tomcat、Jetty 等,不需要手动部署到外部服务器,方便开发和部署。可以通过配置文件或者注解来选择使用的服务器。
  2. MyBatis 实现数据库操作的方式
    • 映射文件或注解:MyBatis 可以通过 XML 映射文件或者注解来定义 SQL 语句。例如,使用 XML 映射文件:
<mapper namespace="com.example.dao.UserDao">
    <select id="getUserById" parameterType="int" resultType="com.example.entity.User">
        SELECT * FROM users WHERE id = #{id}
    </select>
</mapper>
- **SqlSessionFactory**:通过 SqlSessionFactoryBuilder 解析配置文件创建 SqlSessionFactory,SqlSessionFactory 是创建 SqlSession 的工厂。例如:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
- **SqlSession**:通过 SqlSessionFactory 创建 SqlSession,SqlSession 用于执行 SQL 语句。例如:
SqlSession session = sqlSessionFactory.openSession();
User user = session.selectOne("com.example.dao.UserDao.getUserById", 1);
session.close();