面试官:请简要介绍一下 Java 核心知识中,面向对象编程的三大特性及其含义。
王铁牛:这我知道,封装、继承、多态嘛。封装就是把数据和操作数据的方法封装在一起;继承就是子类继承父类的属性和方法;多态就是同一个方法可以根据对象的不同类型而表现出不同的行为。
面试官:不错,回答得很准确。那说说 JUC 包中常用的几个类及其作用。
王铁牛:比如说 CountDownLatch,它可以让一个线程等待其他多个线程完成任务后再执行;还有 CyclicBarrier,能让一组线程互相等待,直到所有线程都到达某个屏障点。
面试官:很好。接下来谈谈 JVM 的内存结构以及各部分的功能。
王铁牛:JVM 内存结构包括堆、栈、方法区等。堆是存放对象实例的地方;栈主要存放局部变量和方法调用的上下文;方法区存储类信息、常量、静态变量等。
第一轮结束。
面试官:讲讲多线程中线程同步的几种方式。
王铁牛:有 synchronized 关键字,还有 Lock 接口及其实现类。
面试官:那线程池的工作原理是什么?
王铁牛:线程池就是预先创建一定数量的线程,当有任务来的时候,从线程池中获取线程去执行任务。
面试官:再说说 HashMap 的底层实现原理。
王铁牛:它是基于数组和链表实现的,当链表长度超过一定阈值时会转换为红黑树。
第二轮结束。
面试官:Spring 框架中依赖注入的方式有哪些?
王铁牛:有构造器注入、setter 方法注入、基于注解的注入。
面试官:Spring Boot 的自动配置原理是什么?
王铁牛:这个……就是自动配置一些默认的配置类啥的吧。
面试官:MyBatis 的缓存机制是怎样的?
王铁牛:有一级缓存和二级缓存,一级缓存是 SqlSession 级别的,二级缓存是 mapper 级别的。
第三轮结束。
面试结束,面试官表示会让王铁牛回家等通知。王铁牛整体表现就是对于简单问题回答得还不错,能展现出一定的基础知识储备,但对于复杂问题回答得比较模糊,不够深入和准确。在面试过程中,虽然前两轮能较好应对,但到第三轮面对一些有深度的问题时就有些吃力,反映出其对知识的掌握还不够扎实全面,可能还需要进一步加强对一些技术细节的理解和钻研,才能更好地应对大厂面试的挑战。
答案:
- 面向对象编程的三大特性:
- 封装:将数据和操作数据的方法封装在一起,对外提供统一的接口。这样可以隐藏内部实现细节,提高数据的安全性和程序的可维护性。例如,在一个类中定义私有成员变量,并通过公共的方法来访问和修改这些变量。
- 继承:子类继承父类的属性和方法。通过继承可以实现代码的复用,减少重复代码。比如,定义一个父类 Animal,子类 Dog 和 Cat 可以继承 Animal 的属性(如名字、年龄)和方法(如 eat)。
- 多态:同一个方法可以根据对象的不同类型而表现出不同的行为。这使得程序具有更好的扩展性和灵活性。例如,定义一个父类引用指向子类对象,调用同一个方法时,会根据实际对象的类型执行不同的操作。
- JUC 包中常用的类及其作用:
- CountDownLatch:允许一个或多个线程等待其他线程完成操作。例如,有一个主线程需要等待多个子线程都完成任务后再继续执行,可以使用 CountDownLatch。通过调用 countDown 方法来减少计数,当计数为 0 时,等待的线程会被唤醒。
- CyclicBarrier:让一组线程互相等待,直到所有线程都到达某个屏障点。比如,在一个多线程协作的场景中,需要所有线程都准备好后再一起执行后续操作,就可以使用 CyclicBarrier。当所有线程调用 await 方法到达屏障点时,会继续执行后续代码。
- JVM 的内存结构以及各部分的功能:
- 堆:是 JVM 中最大的一块内存区域,用于存放对象实例。所有 new 出来的对象都存放在堆中。堆可以分为新生代、老年代和永久代(Java8 后为元空间)。新生代主要存放新创建的对象,老年代存放经过多次垃圾回收后仍然存活的对象,永久代(元空间)存储类信息、常量、静态变量等。
- 栈:主要存放局部变量和方法调用的上下文。每个线程都有自己独立的栈空间。当一个方法被调用时,会在栈中创建一个栈帧,用于存储该方法的局部变量和返回地址等信息。
- 方法区:存储类信息、常量、静态变量等。它是各个线程共享的内存区域。方法区中的数据在程序运行期间不会被频繁销毁和创建,所以它的垃圾回收频率相对较低。
- 多线程中线程同步的几种方式:
- synchronized 关键字:可以修饰方法或代码块。当一个线程访问被 synchronized 修饰的方法或代码块时,会先获取对象的锁。如果锁已经被其他线程持有,那么该线程会进入等待状态,直到锁被释放。例如,在一个银行账户类中,使用 synchronized 修饰取款方法,确保在同一时间只有一个线程可以进行取款操作,保证数据的一致性。
- Lock 接口及其实现类:如 ReentrantLock。Lock 接口提供了比 synchronized 更灵活的锁控制。可以通过 lock 方法手动获取锁,通过 unlock 方法手动释放锁,还可以实现公平锁、可中断锁等特性。例如,在一个高并发场景中,需要更精细地控制锁的获取和释放时机,就可以使用 Lock 接口。
- 线程池的工作原理:
线程池预先创建一定数量的线程,当有任务提交时,从线程池中获取线程去执行任务。线程池主要有以下几个组成部分:
- 线程池管理器:负责创建、销毁和管理线程池。
- 工作线程:线程池中的线程,负责执行任务。
- 任务队列:用于存放提交的任务,当线程池中的线程都在忙碌时,任务会被放入任务队列中等待执行。
- 拒绝策略:当任务队列已满且线程池中的线程数达到最大时,需要采取的策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程处理任务)、DiscardPolicy(丢弃新提交的任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
- HashMap 的底层实现原理:
HashMap 是基于数组和链表实现的。它有一个默认长度为 16 的数组,当向 HashMap 中插入键值对时,会根据键的哈希值计算出在数组中的索引位置。
- 如果该位置为空,则直接插入新的键值对。
- 如果该位置不为空,且键相同,则覆盖原来的值。
- 如果该位置不为空,且键不同,则形成链表。当链表长度超过一定阈值(默认 8)时,链表会转换为红黑树,以提高查询效率。在进行查询时,同样根据键的哈希值计算索引位置,然后在对应的链表或红黑树中查找键值对。
- Spring 框架中依赖注入的方式:
- 构造器注入:通过构造函数来注入依赖。优点是注入的依赖在对象创建时就已经确定,并且不能为空。例如,在一个类的构造函数中传入另一个类的实例,实现依赖注入。
- setter 方法注入:通过 set 方法来注入依赖。这种方式比较灵活,可以在对象创建后再设置依赖。比如,定义一个 set 方法,在外部调用时传入需要注入的对象。
- 基于注解的注入:使用如 @Autowired、@Resource 等注解来实现依赖注入。@Autowired 可以自动根据类型匹配需要注入的对象,@Resource 既可以根据类型也可以根据名称进行注入。例如,在一个类的属性上添加 @Autowired 注解,Spring 会自动将匹配类型的对象注入到该属性中。
- Spring Boot 的自动配置原理: Spring Boot 的自动配置是基于条件注解实现的。它会根据应用的类路径、配置文件等信息,自动配置一些默认的配置类。这些配置类会根据条件判断是否生效。例如,当类路径下存在某个特定的库时,会自动配置相关的组件;或者根据配置文件中的属性值来决定是否启用某些功能。通过自动配置,大大简化了 Spring 应用的开发,开发者只需要关注自己的业务逻辑,而不需要手动配置大量的基础组件。
- MyBatis 的缓存机制:
- 一级缓存:是 SqlSession 级别的缓存。当一个 SqlSession 执行查询操作时,会先从一级缓存中查找,如果找到则直接返回结果;如果没找到,则执行数据库查询,并将查询结果存入一级缓存。在同一个 SqlSession 中,对相同的查询会复用一级缓存。例如,在一个 Service 方法中,多次调用同一个 MyBatis 的 mapper 方法查询数据,只要在同一个 SqlSession 内,只会执行一次数据库查询。
- 二级缓存:是 mapper 级别的缓存。多个 SqlSession 可以共享二级缓存。当一个 SqlSession 执行查询操作时,会先从一级缓存中查找,一级缓存未命中则从二级缓存中查找,若二级缓存也未命中,则执行数据库查询,并将结果存入一级缓存和二级缓存。二级缓存的开启需要在 mapper 配置文件中进行配置。例如,在多个 Service 方法中,不同的 SqlSession 都可以复用二级缓存中的数据,前提是对应的 mapper 配置了二级缓存。