面经来源于牛客:https://www.nowcoder.com/feed/main/detail/020b0bf924c04dd8b26383ac712ea163
答案解析
1.自我介绍
在自我介绍时需要讲清楚以下 4 点:
- 你是谁?
- 你会什么技能?
- 使用这些技能做出过什么项目或成绩?(如果有编程大赛的经历更好)
- 你的优势是啥?为什么我们要用你?(可以讲解你的技术栈和企业技术栈的比较匹配)
2.静态方法和普通方法的区别?
静态方法和普通方法的区别主要体现在以下 2 点:
- 静态方法是属于类的,而普通方法是属于对象的。因此,静态方法可以在不创建对象的情况下直接调用,而普通方法则需要先创建对象,然后通过对象来调用。
- 静态方法不能访问非静态成员变量,而普通方法即可访问静态成员变量有可以访问非静态成员变量。
3.了解bio/nio/aio吗?
BIO、NIO、AIO 三者都是用来实现 IO(Input/Output)操作的,它们的区别如下:
- BIO 传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。它的优点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
- NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
- AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,因此人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
简单来说:BIO 就是传统 IO 包,产生的最早;NIO 是对 BIO 的改进提供了多路复用的同步非阻塞 IO,而 AIO 是 NIO 的升级,提供了异步非阻塞 IO。
4.Spring中创建Bean的方法?
注意这里说的是 Spring 中创建 Bean 的方法,而不是 Spring Boot 中创建 Bean 的方法,所以千万不要答错了。 Spring 中创建 Bean 最常见的 2 种方法如下:
- 通过 Spring XML 中配置 <bean> 标签来实现。
- 通过 @Component/@Service/@Controller/@Repository 等注解来实现。
具体实现如下。
4.1 通过Spring XML实现
在 Spring XML 中添加如下配置:
<bean id="user" class="site.javacn.entity"/>
4.2 通过注解实现
比如以下代码实现:
@Component
public class User{
}
需要注意的是,使用注解的方式需要在 Spring XML 中配置扫描路径才行,如下所示:
<content:component-scan base-package="site.javacn.demo"></content:component-scan>
5.讲讲集合类?
Java 中的集合类主要有两大部分:Collection 和 Map,而 Collection 中有包含了 Set、Queue 和 List,如下图所示:
![](/image/1686898882620-f398c21c-1f34-47bd-874a-9f372ffd8bcc.png)
其中最常用类是 List、Set 和 Map。
- List(列表):List 是一个有序集合,允许重复元素。常见的 List 实现类包括 ArrayList、LinkedList 和 Vector。ArrayList 基于动态数组实现,它支持随机访问和快速查找;LinkedList 基于链表实现,支持高效的插入和删除操作;Vector 是一个线程安全的 List,性能较差,通常不推荐使用。
- Set(集):Set 是一个不允许重复元素的集合。常见的 Set 实现类包括 HashSet、LinkedHashSet 和 TreeSet。HashSet 是基于哈希表实现,它提供了 O(1) 的插入、删除和查找操作;LinkedHashSet 在 HashSet 的基础上保持了元素的插入顺序;TreeSet 是基于红黑树实现,它可以保持元素的自然顺序或者通过 Comparator 进行排序。
- Map(映射):Map 是一种键值对(key-value)的映射表。常见的 Map 实现类包括 HashMap、LinkedHashMap 和 TreeMap。HashMap 是基于哈希表实现,它提供了 O(1) 的插入、删除和查找操作;LinkedHashMap 在 HashMap 的基础上保持了键值对的插入顺序;TreeMap 是基于红黑树实现,可以按照键的自然顺序或者通过 Comparator 进行排序。
除了 List、Set 和 Map,Java 还提供了一些其他的集合类,例如 Queue(队列)、Deque(双端队列)和 PriorityQueue(优先队列)等。这些集合类都提供了一组通用的操作方法,如添加元素、删除元素、查找元素、遍历集合等。此外,Java 8 引入了函数式接口和流式操作(Stream API),使得集合的处理更加简洁和高效。
6.说说ThreadLocal?
ThreadLocal 是 Java 中的一个线程本地变量类。它提供了一种简单的方式来维护线程的私有变量,每个线程都可以独立访问自己的变量副本,互不干扰。
ThreadLocal 类的主要作用是解决多线程环境下共享变量的线程安全问题。 在多线程应用程序中,如果多个线程共享同一个变量,可能会出现线程安全问题,例如竞态条件(Race Condition)和数据争用(Data Contention)。通过使用 ThreadLocal,可以将变量绑定到当前线程,确保每个线程都拥有自己的变量副本,从而避免线程间的冲突。
ThreadLocal 的使用方式相对简单。首先,需要创建一个 ThreadLocal 对象,并使用泛型指定变量的类型。然后,可以通过调用 ThreadLocal 对象的方法来访问和修改变量副本。每个线程通过 ThreadLocal 对象访问的变量都是线程私有的,不会受到其他线程的干扰。 ThreadLocal 的常用方法包括:
- get():获取当前线程的变量副本。
- set(value):设置当前线程的变量副本为指定的值。
- remove():移除当前线程的变量副本。
- initialValue():提供一个初始值的方法,可以通过子类重写该方法来自定义初始值。
需要注意的是,ThreadLocal 只能在当前线程内部访问变量,其他线程无法直接访问。每个线程都需要通过 ThreadLocal 对象来获取和修改变量副本。此外,为了避免内存泄漏,使用完 ThreadLocal 后应该及时调用其 remove() 方法,清理当前线程的变量副本。
ThreadLocal 在很多场景下都非常有用,例如在 Web 应用程序中存储用户的会话信息、在数据库连接池中维护每个线程的数据库连接等。它为线程之间的数据隔离提供了一种简洁而有效的解决方案。
7.Redis淘汰策略有哪些?
Redis 淘汰策略是指, 当 Redis(运行)内存被使用完时,也就是当 Redis 的运行内存,已经超过 Redis 设置的最大内存之后,Redis 将采用内存淘汰机制来删除符合条件的键值对,以此来保障 Redis 的正常运行。
Redis 4.0 之前有以下 6 种淘汰机制(也叫做内存淘汰策略):
- noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- allkeys-random:随机淘汰任意键值;
- volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值;
- volatile-random:随机淘汰设置了过期时间的任意键值;
- volatile-ttl:优先淘汰更早过期的键值。
在 Redis 4.0 版本中又新增了 2 种淘汰机制:
- volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
- allkeys-lfu:淘汰整个键值中最少使用的键值。
其中 allkeys-xxx 表示从所有的键值中淘汰数据,而 volatile-xxx 表示从设置了过期键的键值中淘汰数据。 所以,现在 Redis 的版本中有 8 种内存淘汰策略。
更多详情请查看:https://www.javacn.site/interview/redis/redis_memory.html
8.说下对JVM的了解?
JVM(Java Virtual Machine,Java虚拟机)是 Java 程序的运行环境,它负责将 Java 字节码翻译成机器代码并执行。也就是说 Java 代码之所以能够运行,主要是依靠 JVM 来实现的。
JVM 整体的大概执行流程是这样的:
- 程序在执行之前先要把 Java 代码转换成字节码(class 文件),JVM 首先需要把字节码通过一定的方式类加载器(ClassLoader) 把文件加载到内存中运行时数据区(Runtime Data Area);
- 但字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器,也就是 JVM 的执行引擎(Execution Engine)会将字节码翻译成底层系统指令再交由 CPU 去执行;
- 在执行的过程中,也需要调用其他语言的接口,如通过调用本地库接口(Native Interface) 来实现整个程序的运行,如下图所示:
![](/image/1686899402734-86e4d3a2-62ee-4e74-a8d6-3f05abfd04c2.png)
所以,整体来看, JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
更多详情请查看:https://www.javacn.site/interview/jvm/jvm.html
9.详细说下堆区?
在 JVM(Java虚拟机)中,堆区(Heap)是用于存储对象实例的一块内存区域。它是 Java 程序运行时内存的一部分,用于动态分配和管理对象的内存。 堆区具有以下特点:
- 对象实例存储:堆区主要用于存储 Java 程序中创建的对象实例。每当使用 new 关键字创建一个对象时,对象将被分配到堆区中。堆区中的对象可以动态地创建和销毁,不需要程序员手动管理内存。
- 动态内存分配:堆区采用动态内存分配方式,可以在程序运行时根据需要动态地分配内存空间给对象。这使得程序能够灵活地创建和使用对象,无需静态预先分配内存。
- 垃圾回收:堆区是垃圾回收器的主要工作区域。当对象不再被引用或不可达时,垃圾回收器会自动回收该对象所占用的内存空间。垃圾回收器负责在堆区中识别不再使用的对象,并释放它们占用的内存,从而回收资源。
- 分代结构:堆区通常被划分为不同的代(Generation)。通常,堆区分为新生代(Young Generation)和老年代(Old Generation)。新生代用于存放新创建的对象,而老年代用于存放存活时间较长的对象。这种分代结构可以提高垃圾回收的效率。
- 内存管理:堆区的内存管理由垃圾回收器负责。垃圾回收器会根据需要进行内存分配和回收,以保证堆区的可用内存能够满足对象的创建和存储需求。程序员不需要显式地管理堆区的内存,但可以通过调整垃圾回收器的参数来优化垃圾回收的性能。
需要注意的是,堆区是 Java 程序中最大的一块内存区域,也是所有线程共享的。因此,在多线程环境下,对堆区的访问需要考虑线程安全性。此外,堆区的大小可以通过 JVM 的启动参数进行配置,例如 -Xms 和 -Xmx 参数用于指定堆区的初始大小和最大大小。
堆区在 Java 程序中扮演着重要的角色,它提供了灵活的对象存储和垃圾回收机制,使得 Java 程序能够有效地管理内存并提供动态的对象创建与销毁。
10.Nacos中有一个服务挂了再调用它会怎么样?
Nacos(全称为"Naming and Configuration Service")是一个开源的分布式服务发现和配置管理平台,由阿里巴巴集团开发和维护。它旨在帮助开发人员构建和管理微服务架构中的服务注册、发现和配置的需求。
Nacos 提供的主要功能是注册中心(服务注册与发现)和配置中心。
在 Nacos 中,当一个服务挂了(即不可用)后,如果其他服务仍然尝试调用它,会出现以下情况:
- 超时等待:调用方可能会在一定的超时时间内等待响应。如果超时时间内没有收到响应,调用方可以选择放弃等待或进行重试。
- 请求失败:如果调用方无法建立与挂掉服务之间的连接,请求将失败。这可能是因为挂掉的服务无法响应请求或网络连接问题。
- 异常处理:当调用方收到来自挂掉的服务的错误响应或异常时,可以根据具体的业务逻辑进行处理。通常,调用方可以选择进行重试、降级处理或返回错误信息给上层调用者。
为了应对服务挂掉的情况,Nacos 提供了一些解决方案:
- 健康检查:Nacos 具备健康检查的功能,可以定期检测服务的健康状态。当服务挂掉时,Nacos 可以及时发现并将其标记为不可用,从而避免将请求发送给不可用的服务。
- 负载均衡:Nacos 可以与负载均衡器(如Ribbon)等组件结合使用,通过负载均衡算法将请求均匀分发给可用的服务实例。当一个服务挂掉时,负载均衡器可以自动将请求转发给其他可用的服务实例。
- 服务降级:通过在 Nacos 配置中心配置服务降级规则,可以在服务不可用时,返回一个预设的默认值或执行一个备选逻辑。这样可以避免整个系统的崩溃,并提供一定程度的可用性。
所以,在 Nacos 中,当一个服务挂掉后,具体的行为和后续处理取决于调用方的实现和配置。可以通过设置合适的超时时间、使用负载均衡、配置健康检查和服务降级等机制,来提高系统的可用性和容错性。
11.讲讲SpringBoot自动装配的原理?
Spring Boot 的自动装配(Auto-configuration)是 Spring Boot 框架的核心特性之一。通过自动装配可以自动配置和加载 Spring Boot 所需的各种组件和功能,从而大大的减少开发人员手动配置的工作。
在传统的 Spring 应用程序中,我们需要手动配置各种组件,如数据源、Web 容器、事务管理器等。这些配置需要编写大量的 XML 配置文件或 Java 配置类,增加了开发的工作量和复杂性。而 Spring Boot 的自动装配通过约定大于配置的原则,根据项目的依赖和配置信息,自动进行配置,使得开发人员无需进行大量的手动配置。
自动装配原理
Spring Boot 在启动时,会检索所有的 Spring 模块,找到符合条件的配置并应用到应用上下文中。这个过程发生在 SpringApplication 这个类中。 Spring Boot 自动装配主要依靠两部分:
- SpringFactoriesLoader 驱动:在启动过程中会加载 META-INF/spring.factories 配置文件,获取自动装配相关的配置类信息。
- 条件装配:Spring Boot 不会永远都自动装配,它会根据类路径下是否存在某个名称符合命名规则的自动装配类来决定是否进行自动装配。这就是条件装配,通过 @Conditional 条件注解完成。
自动装配的流程请参考:https://www.javacn.site/interview/spring/springboot_autowired.html
12.讲讲索引的结构?
这里所说的索引结构是指 MySQL 索引实现的数据结构。 MySQL 中默认的存储引擎 InnoDB 的索引是使用 B+ 树实现的。 B+ 树是一种多路搜索树,它的叶子节点存储了所有的数据行信息,叶子节点之间使用指针连接,方便范围查询和排序等操作,非叶子节点存储的是索引字段的值,这样就可以通过非叶子节点的索引值快速定位到叶子节点的数据了,索引的实现如下图所示:
B+ 树优点分析
1.查询效率
B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比即存索引又存记录的 B 树,B+ 树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O 次数会更少。
2.插入和删除效率
B+ 树有大量的冗余节点,这样使得删除一个节点的时候,可以直接从叶子节点中删除,甚至可以不动非叶子节点,这样删除非常快。B 树则不同,B 树没有冗余节点,删除节点的时候非常复杂,比如删除根节点中的数据,可能涉及复杂的树的变形。 B+ 树的插入也是一样,有冗余节点,插入可能存在节点的分裂(如果节点饱和),但是最多只涉及树的一条路径。而且 B+ 树会自动平衡,不需要像更多复杂的算法,类似红黑树的旋转操作等。因此,B+ 树的插入和删除效率更高。
3.范围查询
因为 B+ 树所有叶子节点间还有一个链表进行连接,这种设计对范围查找非常有帮助,比如说我们想知道 12 月 1 日和 12 月 12 日之间的订单,这个时候可以先查找到 12 月 1 日所在的叶子节点,然后利用链表向右遍历,直到找到 12 月12 日的节点,这样就不需要从根节点查询了,进一步节省查询需要的时间。 而 B 树没有将所有叶子节点用链表串联起来的结构,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。 因此,存在大量范围检索的场景,适合使用 B+树。
更多详情请查看:https://www.javacn.site/interview/mysql/indeximpl.html
13.讲讲线程池方面内容?
线程池是一种管理和复用线程资源的机制,它由一个线程池管理器和一组工作线程组成。线程池管理器负责创建和销毁线程池,以及管理线程池中的工作线程。工作线程则负责执行具体的任务。线程池的主要作用是管理和复用线程资源,避免了线程的频繁创建和销毁所带来的开销。
Java中线程池的创建方式主要有以下几种:
- 使用 ThreadPoolExecutor 类手动创建:通过 ThreadPoolExecutor 类的构造函数自定义线程池的参数,包括核心线程数、最大线程数、线程存活时间、任务队列等。
- 使用 Executors 类提供的工厂方法创建:通过 Executors 类提供的一些静态工厂方法创建线程池,例如 newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool 等。
- 使用 Spring 框架提供的 ThreadPoolTaskExecutor 类:在 Spring 框架中可以通过 ThreadPoolTaskExecutor 类来创建线程池。
不同的创建方式适用于不同的场景,通常可以根据实际情况选择合适的方式创建线程池。手动创建 ThreadPoolExecutor 类可以灵活地配置线程池参数,但需要对线程池的各项参数有一定的了解;使用 Executors 工厂方法可以快速创建线程池,但可能无法满足特定的需求,且容易出现内存溢出的情况;而 Spring 框架提供的 ThreadPoolTaskExecutor 类则只能在 Spring 框架中使用。
更多详情请查看:https://www.javacn.site/interview/thread/threadpool.html
14.未来的规划
当在面试中被问到“未来的规划”时,你从以下方面回答:
- 深入学习和应用 Java 生态系统:表达你对 Java 技术的热情,强调你计划深入学习 Java 的各种框架、库和工具。提到你对新的 Java 版本和功能的关注,并计划在工作中应用它们。
- 提高编码技能和代码质量:强调你的意愿提高编码技能,遵循最佳实践和设计模式,编写高质量的可维护代码。提到你的兴趣学习新的编码技术和工具,并将其应用到你的工作中。
- 深入理解系统设计和架构:表达你对系统设计和架构的兴趣,并表示你的意愿学习如何构建可扩展、可靠和高性能的Java应用程序。提到你计划阅读相关书籍、参与培训或者加入项目,以提高自己在这个领域的技能。
- 探索云计算和分布式系统:提到你对云计算和分布式系统的兴趣,并表示你计划学习使用云平台和分布式技术来构建可扩展的 Java 应用程序。强调你的意愿了解和使用云原生技术,如 Docker 和 Kubernetes。
- 提升沟通和团队合作能力:强调你的意愿与团队成员和其他利益相关者合作,分享知识和经验,并以有效的方式与他们沟通。表达你的意愿参与代码审查、团队讨论和知识分享会,以提高整个团队的效率和质量。
- 持续学习和追求专业发展:强调你对终身学习的承诺,并提到你计划通过参加培训、研讨会和技术会议来不断更新自己的技术知识。提到你的意愿积极参与开源项目、编写博客或者给技术社区做贡献。
请记住,这只是一些建议,你可以根据自己的兴趣、目标和面试的具体情况进行适当调整。重点在于表达你对 Java 技术的热情和对自我发展的承诺。
15.平时学习中碰到的难处
当在面试中被问到“平时学习中碰到的难处”时,你可以考虑以下回答:
- 遇到复杂的概念或技术:提到你在学习过程中可能会遇到一些抽象或复杂的概念,解释这些概念需要更多的时间和精力去理解和消化。重要的是表达你的解决问题的能力,比如你会通过反复阅读文档、查找相关资源、尝试示例代码或与他人讨论来克服这些困难。
- 遇到错误和调试问题:承认在编程学习中遇到错误和调试问题是很常见的。强调你的耐心和决心,解释你会使用调试工具和日志信息来定位和解决问题。还可以提到你在阅读错误消息、查看堆栈跟踪和进行逐步调试时的技巧。
- 学习新技术和工具的挑战:提到你在学习新技术和工具时可能会遇到挑战,因为它们可能有陡峭的学习曲线或复杂的配置过程。强调你的自学能力和适应能力,解释你如何利用官方文档、在线教程、视频课程或社区论坛来克服这些挑战。
- 时间管理和优先级设置:承认在学习过程中管理时间和设置优先级可能是一个挑战。解释你如何使用时间管理工具、制定学习计划和任务列表,以及如何将重点放在关键概念和技能上。强调你的组织能力和决策能力,以确保你能够高效地学习和掌握新知识。
- 与他人合作和解决问题:提到你可能会在学习过程中与他人合作,并且合作中可能会遇到一些挑战,比如沟通问题、合作冲突或技术差异。强调你的团队合作精神和解决问题的能力,解释你如何积极参与团队讨论、分享知识和经验,并与他人合作解决学习中的问题。
无论你遇到的困难是什么,重要的是展示你的积极态度、解决问题的能力和持续学习的动力。面试官更关心你如何应对和克服困难,而不仅仅关注你遇到了什么困难。
16.讲讲你对设计模式的了解
当校招中聊到设计模式时,可以举一些常见的设计模式,以及这些设计模式的具体应用,比如以下这些:
- 工厂模式(Factory Pattern): 工厂模式是一种创建型设计模式,它提供了一种创建对象的方式,使得应用程序可以更加灵活和可维护。比如在 Spring 中,FactoryBean 就是一个工厂模式的实现,使用它的工厂模式就可以创建出来其他的 Bean 对象。
- 单例模式(Singleton Pattern):单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供了一个全局访问点。比如在 Spring 中,所以的 Bean 默认是单例的,这意味着每个 Bean 只会被创建一次,并且可以在整个应用程序中共享。
- 代理模式模式(Proxy Pattern): 代理模式是一种结构型设计模式,它允许开发人员在不修改原有代码的情况下,向应用程序中添加新的功能。比如在 Spring AOP(面向切面编程)就是使用代理模式的实现,它允许开发人员在方法调用前后执行一些自定义的操作,比如日志记录、性能监控等。
- 模板方法模式(Template Pattern):模板方法模式是最常用的设计模式之一,它是指定义一个操作算法的骨架,而将一些步骤的实现延迟到子类中去实现,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。此模式是基于继承的思想实现代码复用的。比如在 MyBatis 中的典型代表 BaseExecutor,在 MyBatis 中 BaseExecutor 实现了大部分SQL 执行的逻辑。
- 观察者模式(Observer Pattern):定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。比如事件驱动、消息传递等功能时,可以使用观察者模式,例如 Spring Event 事件机制。
- 适配器模式(Adapter Pattern):适配器模式是一种结构型设计模式,它允许开发人员将一个类的接口转换成另一个类的接口,以满足客户端的需求。在 Spring 中,适配器模式常用于将不同类型的对象转换成统一的接口,比如将 Servlet API 转换成 Spring MVC 的控制器接口。
小结
e签宝面试问的技术细节不多,但涉及的知识点比较广,因此也很考验应聘者的综合技术能力,冰冻三尺非一日之寒,只有日常不断的积累,才能在面试的那一刻发挥出耀眼的光芒,兄弟们,跟着磊哥学起来。
以上内容来自我的 《Java 面试突击训练营》,这门课程是 有着 14 年工作经验(前 360 开发工程师),9 年面试官经验的我,花费 4 年时间打磨完成的一门视频面试课。
整个课程从 Java 基础到微服务 Spring Cloud、从实际开发问题到场景题应有尽有,如下图所示:
![](/image/camp.png)
全程通过视频直播 + 录播的方式,把 Java 常见的面试题系统的过一遍,遇到一个问题,把这个问题相关的内容都给大家讲明白,并且视频支持永久更新和观看。
上完训练营的课程之后,基本可以应对目前市面上绝大部分公司的面试了,想要了解详情,加我微信:vipStone【备注:训练营】
![](/image/weixin-2.png)