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

62 阅读11分钟

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

王铁牛怀揣着对互联网大厂的向往,走进了这场决定命运的 Java 面试。严肃的面试官坐在桌前,眼神犀利,一场激烈的技术交锋即将开始。

第一轮提问 面试官:“首先问几个 Java 核心知识的问题。Java 中基本数据类型有哪些?” 王铁牛:“这个我知道,有 byte、short、int、long、float、double、char、boolean。” 面试官:“回答得不错。那说说面向对象的四大特性是什么?” 王铁牛:“是封装、继承、多态和抽象。” 面试官:“很好。再问一下,String 类为什么是不可变的?” 王铁牛:“因为 String 类内部是用 final 修饰的字符数组存储字符串的,所以它是不可变的。” 面试官:“非常棒,基础知识很扎实。”

第二轮提问 面试官:“接下来聊聊 JUC 和多线程。线程有哪些状态?” 王铁牛:“有新建、就绪、运行、阻塞和死亡这几种状态。” 面试官:“不错。那线程池有哪些创建方式?” 王铁牛:“可以通过 Executors 工具类创建,像 newFixedThreadPool、newCachedThreadPool 等。” 面试官:“那使用线程池有什么好处?” 王铁牛:“可以复用线程,减少线程创建和销毁的开销,提高性能。” 面试官:“很好,对多线程和线程池有一定的理解。”

第三轮提问 面试官:“现在问一些框架相关的问题。Spring 的 IOC 是什么?” 王铁牛:“IOC 就是控制反转,把对象的创建和依赖关系的管理交给 Spring 容器。” 面试官:“那 Spring Boot 的自动配置原理是什么?” 王铁牛:“嗯……就是……好像是根据类路径下的依赖自动配置一些组件。”(回答得有些含糊) 面试官:“再说说 MyBatis 的一级缓存和二级缓存。” 王铁牛:“一级缓存是会话级别的,二级缓存是全局的,但是具体怎么实现的我有点记不清了。”(回答不清晰) 面试官:“看来你对框架的了解还不够深入。”

面试结束 面试官扶了扶眼镜,说道:“今天的面试就到这里,你对于一些基础的 Java 知识和简单的概念回答得还可以,说明有一定的基础。但在一些复杂的框架原理和中间件的知识点上,回答得不够清晰和深入,需要进一步加强学习。你先回家等通知吧,如果有后续消息,我们会及时联系你。”

问题答案

  1. Java 中基本数据类型有哪些? Java 中有 8 种基本数据类型,分为 4 类:
    • 整数类型
      • byte:1 字节,范围是 -128 到 127。
      • short:2 字节,范围是 -32768 到 32767。
      • int:4 字节,范围是 -2147483648 到 2147483647。
      • long:8 字节,范围更大,定义时需要在数字后面加 L,如 long num = 100L。
    • 浮点类型
      • float:4 字节,单精度浮点数,定义时需要在数字后面加 F,如 float f = 3.14F。
      • double:8 字节,双精度浮点数,是小数的默认类型,如 double d = 3.14。
    • 字符类型
      • char:2 字节,用于表示单个字符,用单引号括起来,如 char c = 'A'。
    • 布尔类型
      • boolean:只有两个值,true 和 false,用于逻辑判断。
  2. 面向对象的四大特性是什么?
    • 封装:将数据和操作数据的方法绑定在一起,隐藏对象的内部实现细节,只对外提供必要的接口。通过访问修饰符(如 private、protected、public)来控制对类中成员的访问权限,提高了代码的安全性和可维护性。例如,一个类的属性可以设置为 private,然后通过 public 的 getter 和 setter 方法来访问和修改这些属性。
    • 继承:允许一个类继承另一个类的属性和方法,被继承的类称为父类(基类),继承的类称为子类(派生类)。子类可以扩展父类的功能,也可以重写父类的方法。继承实现了代码的复用和层次结构的建立。例如,Dog 类可以继承 Animal 类,继承 Animal 类的一些通用属性和方法,同时可以有自己特有的属性和方法。
    • 多态:指同一个行为具有多个不同表现形式或形态的能力。多态通过继承和方法重写、接口实现以及方法重载来实现。在运行时,根据对象的实际类型来调用相应的方法。例如,一个父类类型的引用可以指向不同的子类对象,调用相同的方法时会有不同的表现。
    • 抽象:抽象是指将一类对象的共同特征总结出来构造类的过程。抽象类和接口是实现抽象的两种方式。抽象类中可以包含抽象方法和具体方法,抽象方法只有声明没有实现,需要子类去实现。接口则只包含抽象方法和常量。抽象类和接口的使用可以提高代码的可扩展性和可维护性。
  3. String 类为什么是不可变的? String 类内部使用一个被 final 修饰的字符数组来存储字符串,即 private final char value[];。final 关键字修饰的数组,其引用不可变,也就是不能再指向其他数组对象。同时,String 类没有提供修改字符数组内容的方法,一旦创建了 String 对象,其内部的字符数组内容就不能被改变。如果对 String 对象进行拼接、替换等操作,实际上是创建了一个新的 String 对象。这种不可变的特性带来了很多好处,比如:
    • 线程安全:多个线程可以同时访问同一个 String 对象,而不用担心数据被修改。
    • 缓存哈希码:由于 String 的不可变性,其哈希码可以被缓存,提高了哈希表(如 HashMap)的性能。
    • 安全性:在一些场景中,如作为网络连接的 URL、数据库的用户名和密码等,不可变的 String 可以防止被恶意修改。
  4. 线程有哪些状态? 在 Java 中,线程有 6 种状态,定义在 Thread.State 枚举中:
    • 新建(NEW):当创建一个 Thread 对象时,线程处于新建状态,此时线程还没有开始执行。例如:Thread t = new Thread();
    • 就绪(RUNNABLE):调用线程的 start() 方法后,线程进入就绪状态,此时线程已经准备好执行,等待获取 CPU 时间片。
    • 运行(RUNNING):当线程获得 CPU 时间片后,开始执行 run() 方法中的代码,此时线程处于运行状态。
    • 阻塞(BLOCKED、WAITING、TIMED_WAITING)
      • BLOCKED:当线程试图获取一个被其他线程占用的锁时,会进入阻塞状态,直到获取到锁。
      • WAITING:调用 Object.wait()、Thread.join() 或 LockSupport.park() 方法后,线程会进入等待状态,需要其他线程唤醒。
      • TIMED_WAITING:调用带有超时参数的方法,如 Thread.sleep(long millis)、Object.wait(long timeout) 等,线程会进入计时等待状态,在超时时间到达后会自动唤醒。
    • 终止(TERMINATED):线程的 run() 方法执行完毕或者因异常终止时,线程进入终止状态,此时线程的生命周期结束。
  5. 线程池有哪些创建方式? 可以通过 Executors 工具类创建线程池,常见的创建方式有:
    • newFixedThreadPool:创建一个固定大小的线程池,线程数量固定,当有新任务提交时,如果线程池中有空闲线程,则立即执行任务;如果没有空闲线程,则任务会被放入阻塞队列中等待。例如:ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
    • newCachedThreadPool:创建一个可缓存的线程池,线程数量不固定,如果有新任务提交,且线程池中有空闲线程,则使用空闲线程执行任务;如果没有空闲线程,则创建新线程执行任务。当线程空闲时间超过 60 秒时,会被回收。例如:ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    • newSingleThreadExecutor:创建一个单线程的线程池,只有一个线程来执行任务,保证任务按照提交的顺序依次执行。例如:ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    • newScheduledThreadPool:创建一个可以定时执行任务的线程池,用于执行定时任务和周期性任务。例如:ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3); 不过,在实际开发中,不建议直接使用 Executors 工具类创建线程池,因为可能会导致 OOM(OutOfMemoryError)问题,建议使用 ThreadPoolExecutor 来手动创建线程池。
  6. 使用线程池有什么好处?
    • 复用线程:线程的创建和销毁需要消耗系统资源,使用线程池可以复用已经创建的线程,减少线程创建和销毁的开销,提高系统性能。
    • 提高响应速度:当有新任务提交时,线程池中如果有空闲线程,可以立即执行任务,而不需要等待新线程的创建,从而提高了系统的响应速度。
    • 便于管理:线程池可以对线程进行统一的管理,如设置线程的数量、任务队列的大小、线程的优先级等,方便对系统资源进行控制和监控。
    • 控制并发数量:通过设置线程池的核心线程数和最大线程数,可以控制同时执行的线程数量,避免过多的线程导致系统资源耗尽。
  7. Spring 的 IOC 是什么? IOC 即控制反转(Inversion of Control),是 Spring 框架的核心特性之一。在传统的编程中,对象的创建和依赖关系的管理是由对象本身负责的,而在 IOC 模式下,对象的创建和依赖关系的管理交给了 Spring 容器。Spring 容器通过配置文件(如 XML 配置文件)或注解(如 @Component、@Autowired 等)来创建和管理对象,并将对象之间的依赖关系注入到对象中。例如,一个 Service 类依赖一个 DAO 类,在传统编程中,Service 类需要自己创建 DAO 类的对象;而在 Spring 中,Spring 容器会创建 DAO 类的对象,并将其注入到 Service 类中。这样做的好处是降低了对象之间的耦合度,提高了代码的可维护性和可测试性。
  8. Spring Boot 的自动配置原理是什么? Spring Boot 的自动配置是基于 Spring 的条件注解和类路径扫描实现的。主要原理如下:
    • 启动类和 @SpringBootApplication 注解:Spring Boot 应用的启动类上通常会有 @SpringBootApplication 注解,该注解是一个组合注解,包含了 @SpringBootConfiguration、@EnableAutoConfiguration 和 @ComponentScan 注解。
    • @EnableAutoConfiguration 注解:该注解开启了自动配置功能,它会导入 AutoConfigurationImportSelector 类,该类会读取 META - INF/spring.factories 文件。
    • spring.factories 文件:该文件位于 Spring Boot 自动配置模块的类路径下,其中定义了一系列的自动配置类。AutoConfigurationImportSelector 会根据类路径下的依赖和条件注解来筛选出需要加载的自动配置类。
    • 条件注解:自动配置类中使用了大量的条件注解,如 @ConditionalOnClass、@ConditionalOnMissingBean 等。这些注解会根据类路径下是否存在某个类、是否已经存在某个 Bean 等条件来决定是否加载该自动配置类。例如,@ConditionalOnClass(DataSource.class) 表示只有当类路径下存在 DataSource 类时,才会加载该自动配置类。
    • 加载自动配置类:经过筛选后,符合条件的自动配置类会被加载到 Spring 容器中,这些自动配置类会根据配置属性和默认值来创建和配置各种 Bean。
  9. MyBatis 的一级缓存和二级缓存
    • 一级缓存
      • 作用域:一级缓存是会话(SqlSession)级别的缓存,同一个 SqlSession 中执行相同的 SQL 查询时,会先从一级缓存中查找,如果缓存中存在结果,则直接返回,不会再去数据库中查询。
      • 实现原理:MyBatis 在每个 SqlSession 中维护了一个 HashMap 来作为一级缓存,键是 SQL 查询语句和参数,值是查询结果。
      • 失效情况:当执行插入、更新、删除操作时,会清空当前 SqlSession 的一级缓存,以保证数据的一致性。
    • 二级缓存
      • 作用域:二级缓存是全局级别的缓存,多个 SqlSession 可以共享二级缓存。需要在映射文件中开启二级缓存,并且可以配置缓存的实现类(如 Ehcache、Redis 等)。
      • 实现原理:MyBatis 的二级缓存也是基于 HashMap 实现的,不同的是,二级缓存是在 Mapper 级别共享的。当一个 SqlSession 关闭时,会将一级缓存中的数据刷新到二级缓存中。
      • 失效情况:当执行插入、更新、删除操作时,会清空对应的 Mapper 的二级缓存。二级缓存可以提高系统的性能,尤其是在多次执行相同查询的场景下,但需要注意缓存的一致性问题。