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

85 阅读13分钟

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

王铁牛怀揣着忐忑与期待,走进了互联网大厂的面试会议室。严肃的面试官早已坐在那里,面前摆放着王铁牛的简历,一场考验即将开始。

第一轮提问 面试官:首先问几个 Java 核心知识的问题。Java 中基本数据类型有哪些? 王铁牛:Java 的基本数据类型有 byte、short、int、long、float、double、char、boolean。 面试官:回答得不错。那说说面向对象的四大特性是什么? 王铁牛:面向对象的四大特性是封装、继承、多态和抽象。 面试官:很好。在 Java 中,接口和抽象类有什么区别? 王铁牛:接口里全是抽象方法,类实现接口要实现它所有方法;抽象类可以有抽象方法也可以有普通方法,子类继承抽象类时,要实现抽象方法。

第二轮提问 面试官:接下来考察一下 JUC 和多线程相关的内容。什么是线程安全? 王铁牛:线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。 面试官:不错。那讲讲线程池的工作原理。 王铁牛:线程池就像一个池子里面有很多线程,当有任务来的时候,就从池子里拿线程去执行任务,任务执行完线程又回到池子里。 面试官:虽然大致意思对了,但不够准确。那说说 Java 中创建线程有几种方式? 王铁牛:有继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池这几种方式。

第三轮提问 面试官:现在来问一些框架和中间件的问题。Spring 框架中 IOC 和 AOP 是什么? 王铁牛:IOC 是控制反转,把对象的创建和管理交给 Spring 容器;AOP 是面向切面编程,可以在不修改原有代码的基础上增强功能。 面试官:回答得很好。那 Spring Boot 相对于 Spring 有什么优势? 王铁牛:Spring Boot 简化了 Spring 配置,有很多默认配置,能快速搭建项目。 面试官:不错。再说说 MyBatis 的一级缓存和二级缓存。 王铁牛:这个嘛……一级缓存好像是在会话层面的,二级缓存是在全局层面的,具体怎么工作我有点不太清楚。

面试接近尾声,面试官放下手中的笔,看着王铁牛说:“今天的面试就到这里。你对一些基础的 Java 知识和常见概念回答得还可以,像 Java 基本数据类型、面向对象特性、线程创建方式以及 Spring 的 IOC 和 AOP 等都回答得比较准确,说明你有一定的知识储备。但是在一些更深入的技术点上,比如线程池的工作原理和 MyBatis 缓存的详细工作机制,回答得不够清晰和完整,理解还不够深入。我们后续会综合评估,你先回家等通知吧。”

问题答案详细解析

  1. Java 中基本数据类型有哪些? Java 的基本数据类型分为四大类:

    • 整数类型
      • byte:占 1 个字节,取值范围 -128 到 127。
      • short:占 2 个字节,取值范围 -32768 到 32767。
      • int:占 4 个字节,取值范围 -2147483648 到 2147483647。
      • long:占 8 个字节,取值范围 -9223372036854775808 到 9223372036854775807,定义 long 类型变量时,需要在数字后面加 L 或 l。
    • 浮点类型
      • float:占 4 个字节,单精度浮点数,定义 float 类型变量时,需要在数字后面加 F 或 f。
      • double:占 8 个字节,双精度浮点数。
    • 字符类型
      • char:占 2 个字节,用于表示单个字符,用单引号括起来,例如 'A'。
    • 布尔类型
      • boolean:只有两个值,true 和 false,占 1 位。
  2. 面向对象的四大特性是什么?

    • 封装:将数据和操作数据的方法捆绑在一起,隐藏对象的属性和实现细节,仅对外提供公共访问方式。这样可以提高代码的安全性和可维护性。例如,一个类的属性可以设置为私有(private),通过公共的 getter 和 setter 方法来访问和修改这些属性。
    • 继承:允许一个类继承另一个类的属性和方法,被继承的类称为父类(基类),继承的类称为子类(派生类)。子类可以扩展父类的功能,也可以重写父类的方法。继承提高了代码的复用性。例如,猫类和狗类可以继承动物类的一些通用属性和方法。
    • 多态:指同一个行为具有多个不同表现形式或形态的能力。多态通过继承和方法重写或接口实现来实现。例如,父类的引用可以指向子类的对象,在调用方法时,会根据实际对象的类型来执行相应的方法。
    • 抽象:抽象是指从具体的事物中提取出共同的特征和行为,形成抽象类或接口。抽象类不能实例化,它可以包含抽象方法和非抽象方法,子类必须实现抽象类中的抽象方法。接口是一种完全抽象的类型,只包含抽象方法,类实现接口时需要实现接口中的所有方法。抽象可以帮助我们更好地设计和组织代码。
  3. 在 Java 中,接口和抽象类有什么区别?

    • 定义
      • 接口使用 interface 关键字定义,接口中的方法默认是 public abstract 的,变量默认是 public static final 的。
      • 抽象类使用 abstract class 关键字定义,抽象类中可以有抽象方法(使用 abstract 关键字修饰的方法,没有方法体),也可以有普通方法。
    • 实现方式
      • 一个类可以实现多个接口,使用 implements 关键字。
      • 一个类只能继承一个抽象类,使用 extends 关键字。
    • 用途
      • 接口主要用于定义一组规范,实现类需要遵循这些规范来实现具体的功能,适用于不同类之间的功能扩展和约束。
      • 抽象类主要用于抽取子类的公共代码和行为,为子类提供一个通用的模板,适用于具有相同父类的子类之间的继承和扩展。
  4. 什么是线程安全? 线程安全是指在多线程环境下,对共享资源的访问和操作不会出现数据不一致或其他异常情况。当多个线程同时访问一个共享资源时,如果没有进行适当的同步控制,可能会导致数据竞争、脏读、幻读等问题。为了保证线程安全,可以采用以下几种方式:

    • 使用同步机制:例如使用 synchronized 关键字或 ReentrantLock 类来对共享资源进行加锁,保证同一时间只有一个线程可以访问该资源。
    • 使用线程安全的数据结构:例如 ConcurrentHashMap、CopyOnWriteArrayList 等,这些数据结构内部已经实现了同步机制,可以在多线程环境下安全使用。
    • 避免共享资源:尽量减少多个线程对共享资源的访问,例如使用线程局部变量(ThreadLocal)来存储每个线程独立的数据。
  5. 讲讲线程池的工作原理。 线程池的工作原理主要分为以下几个步骤:

    • 创建线程池:使用 Executors 类的静态方法或 ThreadPoolExecutor 类的构造函数来创建线程池。线程池包含核心线程数、最大线程数、线程空闲时间、任务队列等参数。
    • 提交任务:当有新的任务提交到线程池时,线程池会根据当前的状态来决定如何处理该任务。
    • 处理任务
      • 如果当前线程数小于核心线程数,线程池会创建一个新的线程来执行该任务。
      • 如果当前线程数大于等于核心线程数,且任务队列未满,任务会被放入任务队列中等待执行。
      • 如果当前线程数大于等于核心线程数,且任务队列已满,但线程数小于最大线程数,线程池会创建一个新的线程来执行该任务。
      • 如果当前线程数大于等于最大线程数,且任务队列已满,线程池会根据拒绝策略来处理该任务,常见的拒绝策略有抛出异常、直接丢弃任务、丢弃队列中最旧的任务等。
    • 线程回收:当线程执行完任务后,如果线程空闲时间超过了设置的线程空闲时间,且当前线程数大于核心线程数,线程会被回收。
  6. Java 中创建线程有几种方式?

    • 继承 Thread 类:创建一个类继承自 Thread 类,重写 run() 方法,在 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() 方法,在 run() 方法中定义线程要执行的任务。然后创建该类的对象,将其作为参数传递给 Thread 类的构造函数,创建 Thread 对象,调用 start() 方法启动线程。示例代码如下:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread is running.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}
- **实现 Callable 接口**:创建一个类实现 Callable 接口,实现 call() 方法,在 call() 方法中定义线程要执行的任务,并可以返回结果。然后使用 FutureTask 类来包装 Callable 对象,将 FutureTask 对象作为参数传递给 Thread 类的构造函数,创建 Thread 对象,调用 start() 方法启动线程。可以通过 FutureTask 对象的 get() 方法获取线程执行的结果。示例代码如下:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 1 + 2;
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        Integer result = futureTask.get();
        System.out.println("Result: " + result);
    }
}
- **使用线程池**:使用 Executors 类的静态方法或 ThreadPoolExecutor 类的构造函数来创建线程池,然后将实现了 Runnable 接口或 Callable 接口的任务提交给线程池执行。示例代码如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

public class Main {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        MyRunnable myRunnable = new MyRunnable();
        executorService.submit(myRunnable);
        executorService.shutdown();
    }
}
  1. Spring 框架中 IOC 和 AOP 是什么?

    • IOC(控制反转):是一种设计原则,将对象的创建和管理控制权从代码中转移到 Spring 容器中。在传统的编程方式中,对象的创建和依赖关系的管理由代码本身负责,而在 Spring 中,对象的创建、初始化、销毁等操作都由 Spring 容器来完成。通过 IOC,代码的耦合度降低,提高了代码的可维护性和可测试性。例如,一个类需要依赖另一个类的对象,不需要在类内部手动创建该对象,而是通过 Spring 容器注入该对象。
    • AOP(面向切面编程):是一种编程范式,允许在不修改原有代码的基础上,对程序的功能进行增强。AOP 将程序中的一些通用的、横切性的功能(如日志记录、事务管理、权限验证等)从业务逻辑中分离出来,形成独立的切面。在程序运行时,AOP 会在特定的切入点(如方法调用、异常抛出等)将切面的功能织入到业务逻辑中。AOP 可以提高代码的复用性和可维护性。在 Spring 中,AOP 主要通过动态代理实现,有基于接口的 JDK 动态代理和基于类的 CGLIB 动态代理。
  2. Spring Boot 相对于 Spring 有什么优势?

    • 简化配置:Spring Boot 提供了大量的默认配置,减少了开发者手动配置的工作量。例如,在 Spring 中配置一个 Web 项目需要编写大量的 XML 配置文件或 Java 配置类,而在 Spring Boot 中,只需要添加相应的依赖,就可以快速搭建一个 Web 项目。
    • 快速搭建项目:Spring Boot 提供了 Spring Initializr 工具,可以通过网页或命令行快速生成项目骨架,包含了项目所需的依赖和基本配置。开发者可以直接在生成的项目基础上进行开发,提高了开发效率。
    • 嵌入式服务器:Spring Boot 内置了嵌入式服务器(如 Tomcat、Jetty 等),不需要手动部署到外部服务器,直接运行项目的主类就可以启动服务器,方便开发和测试。
    • 自动配置:Spring Boot 根据项目中添加的依赖自动进行配置,例如,如果项目中添加了 Spring Data JPA 的依赖,Spring Boot 会自动配置数据源、JPA 实体管理器等。
    • 监控和管理:Spring Boot Actuator 提供了丰富的监控和管理功能,如健康检查、性能指标监控、日志管理等,方便开发者对项目进行监控和管理。
  3. MyBatis 的一级缓存和二级缓存。

    • 一级缓存:是会话(SqlSession)级别的缓存,默认是开启的。当同一个 SqlSession 执行相同的 SQL 查询时,第一次查询会将结果存储在一级缓存中,后续相同的查询会直接从缓存中获取结果,而不需要再次访问数据库。一级缓存的生命周期与 SqlSession 相同,当 SqlSession 关闭或调用了 insert、update、delete 等操作时,一级缓存会被清空。一级缓存可以减少数据库的访问次数,提高查询性能。
    • 二级缓存:是全局级别的缓存,需要手动开启。二级缓存是多个 SqlSession 共享的,不同的 SqlSession 可以访问同一个二级缓存。开启二级缓存后,当一个 SqlSession 执行查询时,会先从二级缓存中查找结果,如果没有找到,再从数据库中查询,并将结果存储在二级缓存中。二级缓存的生命周期比一级缓存长,当执行 insert、update、delete 等操作时,会清空二级缓存。二级缓存可以进一步减少数据库的访问次数,提高系统的整体性能。在 MyBatis 中,可以通过在映射文件中添加 <cache> 标签或在 Java 代码中配置 Cache 接口的实现类来开启二级缓存。