面试题目来源于牛客,如下图所示:
文章地址:https://www.nowcoder.com/feed/main/detail/fc9c93e2dbbb4a1cb9e7f1d2135f6644
答案解析
1.自我介绍
在自我介绍时需要讲清楚以下 4 点:
- 你是谁?
- 你会什么技能?
- 使用这些技能做出过什么项目或成绩?(如果有编程大赛的经历更好)
- 你的优势是啥?为什么我们要用你?(可以讲解你的技术栈和企业技术栈的比较匹配)
2.Java基本数据类型?
基本数据类型是指不可再分的原子数据类型,内存中直接存储此类型的值,通过内存地址即可直接访问到数据,并且此内存区域只能存放这种类型的值。
在 Java 中,一共有 8 种基本类型(primitive type),其中有 4 种整型、2 种浮点类型、1 种用于表示 Unicode 编码的字符类型 char 和 1 种用于表示真假值的 boolean 类型。
- 4 种整型:int、short、long、byte
- 2 种浮点类型:float、double
- 字符类型:char
- 真假类型:boolean
3.对象三大特征?
对象的三大特征是:封装、继承和多态。
- 封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
- 继承:子类可以重用父类的属性和方法,同时还可以添加自己的属性和方法。
- 多态:同一个接口可以有不同的实现方式,即同一种行为可以有多种不同的表现形式。
4.多态和封装的含义及作用?
多态是指同一个接口可以有不同的实现方式,即同一种行为可以有多种不同的表现形式。多态的作用是为了实现另一个目的是接口重用,多态可以提高代码的灵活性和可扩展性,使得程序更加健壮、易于维护。
封装是指将数据和操作数据的方法包装在一起,对外隐藏内部实现细节,只提供有限的公共接口。封装的作用是为了保护对象的内部状态,防止外部对其进行非法访问和修改,同时也可以通过封装来实现代码的复用。
5.方法重写和方法重载的区别?
方法重写和方法重载是Java中多态性的不同表现。
- 重写是指子类中的方法与父类中继承的方法有完全相同的返回值类型、方法名、参数个数以及参数类型,这样就可以实现对父类方法的覆盖。
- 方法重载发生在同一个类中,方法名相同,参数列表不同。方法返回类型、权限修饰符不作为方法重载的依据。
6.String/StringBuffer和StringBuilder区别?
String、StringBuffer 和 StringBuilder 都是 Java 中用来操作字符串的。
- String:不可变的字符串,因此在动态字符串拼接的场景下不适合使用,效率很低(每次都会生成新的字符串)。
- StringBuffer:提供了 append 和 insert 方法可用于字符串的拼接,解决了 String 字符串拼接场景下的性能问题,并且它是线程安全的,它使用 synchronized 来保证线程安全。
- StringBuilder:和 StringBuffer 类似,拥有相同的操作 API,但它可以更高效的拼接字符串,但不能保证线程安全。
7.String常用方法有哪些?
String 类的常见方法有以下这些:
- length():返回字符串的长度。
- charAt(int index):返回指定索引处的字符。
- indexOf(String str):返回指定字符串在当前字符串中第一次出现的位置,如果未找到则返回 -1。
- substring(int beginIndex):返回从指定索引开始的子字符串。
- substring(int beginIndex, int endIndex):返回从指定索引开始到指定索引结束的子字符串。
- toLowerCase():将所有字符转换为小写。
- toUpperCase():将所有字符转换为大写。
- trim():去除字符串两端的空格。
8.==和equals有什么区别?
== 对于基本数据类型来说,是用于比较 “值”是否相等的;而对于引用类型来说,是用于比较引用地址是否相同的;而 equals 本质上就是 ==,只不过很多类,比如 String 和 Integer 等类中都重写了 equals 方法,把它变成了值比较。
9.两个对象hashCode相同equals为true吗?
不一定。如果对象在重写 hashCode 方法时,重写了 equals 方法的话,那么 hashCode 相同,它们进行 equals 对比时,结果是有可能为 true 的;但如果重写 hashCode 方法时,没有重写 equals 方法,那么结果一定是 false,如下图所示:
10.ArrayList和LinkedList有什么区别?
ArrayList 和 LinkedList 都是 Java 中的 List 接口的实现类。 但它们有以下不同:
- 底层实现不同:ArrayList 是基于动态数组的数据结构,而 LinkedList 是基于链表的数据结构。
- 随机访问性能不同:ArrayList 优于 LinkedList,因为 ArrayList 可以根据下标以 O(1) 时间复杂度对元素进行随机访问。而 LinkedList 的访问时间复杂度为 O(n),因为它需要遍历整个链表才能找到指定的元素。
- 插入和删除性能不同:LinkedList 优于 ArrayList,因为 LinkedList 的插入和删除操作时间复杂度为 O(1),而 ArrayList 的时间复杂度为 O(n)。
更多内容,请参考:https://www.javacn.site/interview/basic/arraylist-linkedlist.html
11.ArrayList 1.6和1.7有什么区别?
ArrayList 在 JDK 1.6 和 JDK 1.7 的区别主要有两方面不同:
- 扩容实现上:JDK 1.6 扩容是原来的 1.5 倍,而 JDK 1.7 利用位运算进行扩容,所以 JDK 1.7 的扩容效率更高;
- 最多容量上:JDK 1.6 没有最大容量设置,而 JDK 1.7 添加了 MAX_ARRAY_SIZE 变量,设置了ArrayList 的最大容量。
12.HashMap底层实现?
HashMap 底层是实用数组 + 链表或红黑树的方式实现的,如下图所示:
更多内容请参考:https://www.javacn.site/interview/basic/hashmap-impl.html
13.为什么要使用红黑树?
因为当链表的数据很多时,查询的效率会很低,链表的查询时间复杂度是 O(n),但如果将链表转换成红黑树,那么查询的时间复杂度就变成了 O(log n),提高了HashMap 的查询效率。
14.为什么会退化成链表?
因为当数据比较少时,查询不会再有效率问题,因为节点很少,查询的效率问题几乎可以忽略不计。而继续使用红黑树的话,因为红黑树在添加或删除节点时,需要自平衡,那么这些操作的效率就远远低于链表,所以为了综合考虑 HashMap 的性能,当数据比较少时就会从红黑树退化成链表。
15.HashMap负载因子?调大有什么影响?
:HashMap 有两个重要的参数:容量(Capacity)和加载因子(LoadFactor)。
- 容量(Capacity):是指 HashMap 中桶的数量,默认的初始值为 16。
- 加载因子(LoadFactor):也被称为装载因子,或负载因子,LoadFactor 是用来判定 HashMap 是否扩容的依据,默认值为 0.75f,装载因子的计算公式 = HashMap 存放的 KV 总和(size)/ Capacity。
负载因子的默认值为 0.75,当负载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生 Hash 冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低。
16. 说一下使用过的线程池?
线程池可以分为以下两类:
- 通过 ThreadPoolExecutor 手动创建线程池。
- 通过 Executors 执行器自动创建线程池。
而以上两类创建线程池的方式,又有 7 种具体实现方法,这 7 种实现方法分别是:
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序。
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池。
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池。
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
- ThreadPoolExecutor:手动创建线程池的方式,它创建时最多可以设置 7 个参数。
不过通常在开发中大多会使用 ThreadPoolExecutor 手动创建线程池的方式,因为这种创建方式更可控,语义有更明确。
17.拦截器和过滤器区别?
拦截器和过滤器的区别主要体现在以下 5 点:
- 出身不同:过滤器来自于 Servlet,而拦截器来自于 Spring 框架;
- 触发时机不同:请求的执行顺序是:请求进入容器 > 进入过滤器 > 进入 Servlet > 进入拦截器 > 执行控制器(Controller),所以过滤器和拦截器的执行时机,是过滤器会先执行,然后才会执行拦截器,最后才会进入真正的要调用的方法;
- 底层实现不同:过滤器是基于方法回调实现的,拦截器是基于动态代理(底层是反射)实现的;
- 支持的项目类型不同:过滤器是 Servlet 规范中定义的,所以过滤器要依赖 Servlet 容器,它只能用在 Web 项目中;而拦截器是 Spring 中的一个组件,因此拦截器既可以用在 Web 项目中,同时还可以用在 Application 或 Swing 程序中;
- 使用的场景不同:因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务;而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能。
18.Spring核心思想?
Spring 是一款顶级开源框架,它是包含了众多工具方法的 IoC 容器。Spring 最核心的思想是 IoC 和 DI,其中:
- IoC:Inversion of Control 的缩写,翻译成中文是“控制反转”的意思,它不是一个具体的技术,而是一个实现对象解耦的思想。
- DI: Dependency Injection 的缩写,翻译成中文是“依赖注入”的意思。依赖注入不是一种设计实现,而是一种具体的技术,它是在 IoC 容器运行期间,动态地将某个依赖对象注入到当前对象的技术就叫做 DI(依赖注入)。
所以 IoC 是 Spring 的核心思想,而 DI 是实现了 IoC 思想的具体技术。
19.AOP动态代理模式?
AOP(Aspect-OrientedProgramming,面向切面编程)可以说是 OOP(Object-Oriented Programing,面向对象编程)的补充和完善,OOP 引入封装、继承和多态性等概念来建立一种公共对象处理的能力,当我们需要处理公共行为的时候,OOP 就会显得无能为力,而 AOP 的出现正好解决了这个问题。比如统一的日志处理模块、授权验证模块等都可以使用 AOP 很轻松的处理。
Spring AOP 底层是通过动态代理实现的,动态代理的常见实现方法有两种:
- JDK 动态代理: JDK 动态代理是一种使用 Java 标准库中的 java.lang.reflect.Proxy 类来实现动态代理的技术。在 JDK 动态代理中,被代理类必须实现一个或多个接口,并通过 InvocationHandler 接口来实现代理类的具体逻辑。
- CG Lib 动态代理: CGLIB 动态代理是一种使用 CGLIB 库来实现动态代理的技术。在 CGLIB 动态代理中,代理类不需要实现接口,而是通过继承被代理类来实现代理。 具体来说,当使用 CGLIB 动态代理时,需要定义一个继承被代理类的子类,并在该子类中实现代理类的具体逻辑。
20.Spring框架常用设计模式?
Spring 中包含的常见设计模式有:工厂模式、单例模式、代理模式、观察者模式、模板方法模式、适配器模式和策略模式等。
设计模式的定义以及具体应用场景请参考:https://www.javacn.site/interview/spring/design_pattern.html
21.Bean有几种注入方式?
在 Spring 中实现依赖注入的常见方式有以下 3 种:
- 属性注入(Field Injection)
- Setter 注入(Setter Injection)
- 构造方法注入(Constructor Injection)
官方推荐的注入方式是构造方法注入,这几种注入方式的区别和特点详见:https://www.javacn.site/interview/spring/inject.html
22.Bean的作用范围?
在 Spring 中,Bean 的作用域指的是 Bean 实例的生命周期和可见范围。 Spring 中的 Bean 作用域主要有以下 6 种:
- singleton:单例模式
- prototype:原型模式
- request:请求模式
- session:会话模式
- application:ApplicationContext 模式
- websocket:websocket 模式
其中 3-5 只能用于 Spring MVC 项目中,而 webscoket 则只能应用在 websocket 项目中。 更多细节实现请参考:https://www.javacn.site/interview/spring/scope.html
23.Controller多线程下会有问题吗?
Controller 默认是单例的,它是有可能存在线程安全问题的。解决方案可以考虑,不要在 Controller 中定义变量,或使用 ThreadLocal 来为每个线程创建独享的变量,或升级 Controller 的作用域为原型模式,都可以解决 Controller 默认单例模式的线程安全问题。
24.事务四大特性、四种隔离级别?
MySQL 事务具备 ACID 四大特性,如下所述:
- 原子性(Atomicity):事务中的所有操作要么全部执行成功,要么全部失败回滚,不能只执行其中一部分操作。
- 一致性(Consistency):事务执行前后,数据库的完整性约束没有被破坏,数据总是从一个一致性状态转移到另一个一致性状态。
- 隔离性(Isolation):事务之间是相互隔离的,每个事务对其他事务的操作是透明的,一个事务的中间结果对其他事务是不可见的。
- 持久性(Durability):事务完成后,对数据库的修改将永久保存在数据库中,即使系统故障也不会丢失。
其中,隔离性是实现事务的重要特征之一。
MySQL 中提供了四种事务隔离级别:
- 读未提交(Read Uncommitted)
- 读已提交(Read Committed)
- 可重复读(Repeatable Read)
- 串行化(Serializable)
不同的事务隔离解决可以解决不同的问题,如下图所示:
25.默认隔离级别?
MySQL 默认的事务隔离级别是可重复读,而 Oracle 数据库默认的事务隔离级别是读已提交。
26.说一下MySQL索引?
索引是一种数据结构,使用它可以快速的查询和检索数据。它可以看做是一本书的目录介绍,使用它可以快速地定位到需要查询的内容。 索引一般可以分为以下几类:
- 主键索引:主键索引是一种特殊的索引类型,它是用于唯一标识每一行数据的索引,每个表只能有一个主键索引。
- 唯一索引:唯一索引是用来保证列的唯一性的索引,一个表可以有多个唯一索引。
- 普通索引:普通索引也叫非唯一索引,它是最常见的一种索引类型,可以加速查询和排序操作。
- 全文索引:全文索引是一种用于全文搜索的索引类型,能够对文本数据进行快速的模糊搜索和关键字搜索。
- 复合索引:复合索引也叫多列索引或联合索引,它是包含多个列的索引类型,能够加速多列查询和排序操作。
- 哈希索引:哈希索引是基于哈希表实现的索引类型,能够对等值查询进行高效的处理,但不支持范围查询和排序,MySQL 中 Memory 引擎中支持哈希索引。
不同的索引类型对应着不同的业务场景,更多内容细节,请访问:https://www.javacn.site/interview/mysql/mysqlindex.html
27.解释一下原子性是什么?
原子性是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 比如银行账户转账问题:从账户 A 向账户 B 转 1000 元,那么必然包括 2 个操作:
- 从账户 A 减去 1000 元;
- 往账户 B 加上 1000 元。
试想一下,如果这 2 个操作不具备原子性,会造成什么样的后果。假如从账户 A 减去 1000 元之后,操作突然中止,B 账号也没有加钱,这就导致账户 A 减去的 1000 元凭空消失了,而原子性就是保证这两个操作,要么一起成功,要么一起失败,不会出现执行一半的情况。
28.一人一单怎么实现?
可以通过添加分布式锁的方式来实现,每个人对应一个唯一的分布式锁。如果一个人已经添加了分布式锁,说明它已经购买了,之后就不能重复购买了。分布式锁的具体实现请参考:https://www.javacn.site/interview/redis/redis_lock.html
29.Redis缓存穿透、击穿、雪崩?怎么解决?
缓存穿透
缓存穿透是指在缓存系统中,大量的请求查询不存在于缓存和数据库中的数据,导致这些请求直接访问数据库,占用数据库资源,而缓存无法发挥作用的现象。
解决缓存穿透问题,可以采取以下策略:
- 布隆过滤器(Bloom Filter):布隆过滤器是一种高效的数据结构,可以用于快速判断一个元素是否存在于集合中。在缓存层引入布隆过滤器,可以在查询请求到达时,首先通过布隆过滤器判断该请求对应的数据是否存在于缓存或数据库中,从而避免无效的查询操作。
- 缓存空值处理:对于查询数据库返回的空结果,也可以将空结果缓存起来,设置一个较短的过期时间,避免频繁查询数据库。这样在下次查询相同的数据时,可以直接从缓存中获取空结果,而不需要再次查询数据库。
- 异步加载缓存:当缓存未命中时,可以异步加载数据到缓存中,避免在高并发场景下直接访问数据库。在异步加载过程中,可以通过互斥锁或分布式锁来保证只有一个线程去加载数据,避免重复加载。
- 设置热点数据永不过期:对于一些热点数据,可以将其设置为永不过期,或者过期时间较长,以保证这部分数据始终在缓存中可用。
- 限制恶意请求:通过访问频率控制、验证码等手段,限制对缓存的恶意请求,防止攻击者通过查询不存在的数据来触发缓存穿透。
缓存击穿
缓存击穿是指在缓存系统中,某个热点数据过期或失效时,同时有大量的请求访问该数据,导致请求直接访问数据库或后端服务,给数据库或后端服务造成巨大压力,导致系统性能下降甚至崩溃的现象。 解决缓存击穿问题,可以采取以下策略:
- 设置热点数据永不过期或过期时间较长:对于一些热点数据,可以将其设置为永不过期,或者设置一个较长的过期时间,确保热点数据在缓存中可用,减少因为过期而触发的缓存击穿。
- 加互斥锁或分布式锁:在访问热点数据时,可以引入互斥锁或分布式锁,保证只有一个线程去访问后端服务或数据库,其他线程等待结果。当第一个线程获取到数据后,其他线程可以直接从缓存获取,避免多个线程同时访问后端服务,减轻压力。
- 限制并发访问:通过限制并发访问热点数据的请求量,可以控制请求的流量,避免过多请求同时访问热点数据。
缓存雪崩
缓存雪崩是指在缓存中大量的键同时过期或失效,导致请求直接访问数据库或后端服务,给数据库或后端服务造成巨大压力,导致系统性能下降甚至崩溃的现象。
解决缓存雪崩问题,可以采取以下策略:
- 设置随机过期时间:为缓存键设置随机的过期时间,避免大量键同时过期的情况发生,减少缓存雪崩的概率。
- 实现缓存预热:在系统启动或缓存失效前,提前加载热门数据到缓存中,避免在关键时刻大量请求直接访问后端服务。
- 使用分布式缓存:将缓存数据分布在多个缓存节点上,通过分散请求负载来减少单个缓存节点的压力,提高系统的可用性和抗压能力。
- 设置熔断机制:在缓存失效的情况下,通过设置熔断机制,直接返回默认值或错误信息,避免请求直接访问后端服务,减轻后端服务的压力。
- 实时监控和报警:监控缓存系统的状态和性能指标,及时发现异常情况,并通过报警机制通知运维人员进行处理,减少缓存雪崩的影响。
31.本地缓存使用过吗?
使用过,本地缓存是指和应用程序在同一个进程内的内存空间去存储数据,数据的读写都是在同一个进程内完成的。
本地缓存优点: 读取速度快,但是不能进行大数据量存储。 本地缓存不需要远程网络请求去操作内存空间,没有额外的性能消耗,所以读取速度快。 但是由于本地缓存占用了应用进程的内存空间,比如 Java 进程的 JVM 内存空间,故不能进行大数据量存储。
本地缓存缺点 :应用程序集群部署时,会存在数据更新问题(数据更新不一致)本地缓存一般只能被同一个应用进程的程序访问,不能被其他应用程序进程访问。 在单体应用集群部署时,如果数据库有数据需要更新,就要同步更新不同服务器节点上的本地缓存的数据来保证数据的一致性,但是这种操作的复杂度高,容易出错。
Java 程序中的所有集合都可以看作为本地缓存,Spring 中的 Spring Cache 也是本地缓存,以及 MyBatis 中的二级缓存也是本地缓存。
32.本地缓存框架?
Java 中的本地缓存框架有很多,常用的有 Guava Cache、Caffeine、Ehcache 等。
Guava 官方文档:https://guava.dev/
Caffeine 官方文档:https://github.com/ben-manes/caffeine/wiki
Ehcache 官方文档:https://www.ehcache.org/documentation/
小结
亚信的面试题整体都不难,但题量是真的多。
以上内容来自我的 《Java 面试突击训练营》,这门课程是 有着 14 年工作经验(前 360 开发工程师),9 年面试官经验的我,花费 4 年时间打磨完成的一门视频面试课。
整个课程从 Java 基础到微服务 Spring Cloud、从实际开发问题到场景题应有尽有,如下图所示:
全程通过视频直播 + 录播的方式,把 Java 常见的面试题系统的过一遍,遇到一个问题,把这个问题相关的内容都给大家讲明白,并且视频支持永久更新和观看。
上完训练营的课程之后,基本可以应对目前市面上绝大部分公司的面试了,想要了解详情,加我微信:vipStone【备注:训练营】