【分享】Java后端常问面试题
本帖最后由 yunjl 于 2021-11-17 19:04 编辑适用于刚毕业的应届生,很多都是我面试时面试官问的问题。这些只是基础也就是说至少面试官问到你,你得知道他问的是哪方面的。
面试题还是要背的,但是面试官闻到你的时候尽量用自己的语言说出来会更好。
[*]int 和 integer 区别
Integer是int的包装类,int则是java的一种基本数据类型
Integer变量必须实例化后才能使用,而int变量不需要
Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
Integer的默认值是null,int的默认值是0
[*]== 与eqals 区别
==对于基本数据类型比较的是数据的值,对于引用数据类型比较的是地址值
equals在没有被重写的情况下和 == 一样,重写后(string)就是比较的数据的值
"=="比"equals"运行速度快,因为"=="只是比较引用。
equals是一个方法
[*]String、StringBuffer与StringBuilder区别
String是一组final修饰的char数组,值是不可变的,每次操作都会生成新的String对象
StringBuffer 和 StringBuilder 是可变的
StringBuffer效率比较低,是线程安全的
StringBuilder效率高,非线程安全的
[*]sleep、wait 的区别
sleep()方法是属于Thread类中的。
wait()方法,是属于Object类中的,会放弃对象锁,进入等待状态,只有调用notify()方法后才进入就绪状态,是实例方法,只能在同步方法内使用
sleep()方法线程不会释放对象锁,暂停当前线程,是静态方法
[*]当局部变量与XX变量重名时,如何区分:
局部变量与实例变量重名 在成员便令前面加 this.
局部变量与类变量重名 在类变量前面加 类名.
[*]常用的集合类有哪些?
Map接口和Collection接口是所有集合的父接口:
Collection接口的子接口包括:
Set接口和List接口Map接口的实现类主要有:HashMap、TreeMap、
HashtableSet接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。
常用的实现类有 ArrayList、LinkedList 和 Vector。
Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。
Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
Map 的常用实现类:HashMap、TreeMap、HashTable
[*]hashmap在jdk7和jdk8的区别
HashMap 类是散列映射,通过散列函数计算键对应的存储位置,因此可以快速地完成放置键值对、删除键值对、根据键获得值的操作。
1、jdk7中使用数组+链表来实现,jdk8使用的数组+链表(+红黑树)
jdk8 默认初始化大小16,加载因子0.75。还有一个默认的树化的大小8。非树化大小为6,也就是红黑树的元素小于6的时候,又会变回一个链表。中间有个差值7可以有效防止链表和树频繁转换
解决冲突 :链地址法,线性探查法,平方探查法
什么情况下转化为红黑树:链表长度大于8 ,散列表长度达到64
2、新节点插入到链表 jdk7插入在头部,jdk8插入在尾部
至于为什么jdk7会采用头插法,据说是考虑到热点数据的原因,即最近插入的元素也很可能最近会被使用到。所以为了缩短链表查找元素的时间,所以每次都会将新插入的元素放到表头。
jdk8中为什么新来的元素是放在节点的后面?我们知道jdk8链表大于8的时候就要树化,本身就要算这个链表的长度有多大,链表算大小就要一个个去遍历了,遍历到了才能知道数量,也可以直接把数据放到后面了,这样也是很方便,减少了把数据移动到头数组位置这一步,效率也提高了。而且,jdk7中出现的那个HashMap死循环bug不会有了,因为这里是只是平移,没有调换顺序。
3、jdk8的hash算法有所简化
jdk8中我们知道用的是链表过度到红黑树,效率会提高,所以jdk8提高查询效率的地方由红黑树去实现,没必要像jdk那样右移那么复杂。
[*]为什么重写equals时要重写hashcode
1、为什么重写对象equals方法的时候,要重写hashcode,跟hashmap有关系吗?
如果两个对象相等,则hashcode一定也是相同的
两个对象相等,对两个equals方法返回true
两个对象有相同的hashcode值,它们也不一定是相等的
综上,equals方法被重写,则hashCode方法也必须被重写对象谁有特定的hashcode值,如果没有重写hashCode(),则对象无论如何都不会相等(即使这两个对象指向相同的数据)。
[*]HashMap 和 Hashtable
Hashtable 类是散列表,其功能和 HashMap 相似。
HashMap 不是线程安全的,
Hashtable 的大多数方法用关键字 synchronized 修饰,因此 Hashtable 是线程安全的。
在不需要保证线程安全的情况下,HashMap 的效率高于 Hashtable。HashMap 允许键或值为 null,只能有一个键为 null,可以有一个或多个键对应的值为 null,Hashtable 不允许键或值为 null。(因为Hashtable支持并发)
这是因为ConcurrentHashMap和Hashtable都是支持并发的,这样会有一个问题,当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put(k,v)的时候value就为null,还是这个key从来没有做过映射。而HashMap是非并发的,可以通过contains(key)来进行校验。而支持并发的Map在调用m.contains(key)和m.get(key)时m可能已经不同了
从 JDK 1.8 开始,HashMap 在链表长度大于阈值(默认为 8)时,将链表转化为红黑树以减少搜索时间,Hashtable 没有这样的机制。
[*]list和set的区别
List有序有重复,
set无重复,map的key也和set一样。
List和Set都是接口继承于Collection接口。
最大的不同就是List是可以重复的。而Set是不能重复的。
List接口有三个实现类:LinkedList,ArrayList,Vector ,
Set接口有两个实现类:HashSet(底层由HashMap实现),LinkedHashSetList适合经常追加数据,插入,删除数据。但随机取数效率比较低。
Set适合经常地随机储存,插入,删除。但是在遍历时效率比较低。
Set : 存入Set的每个元素都必须是唯一的。加入Set的元素必须重写equals方法以确保对象的唯一性。
HashSet : 为快速查找设计的Set。存入HashSet的对象必须重写hashCode。
TreeSet : 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。
LinkedHashSet : 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。
[*]ArrayList和hashmap的扩容机制
ArrayList的初始容量为10.懒加载机制,当插入数据的长度大于10 时,所需要的容量大于本身的容量,他会调用扩容方法,扩容的大小时原来的1.5倍(原数值+原数值右移一位)
HashMap的初始化大小是16,是懒加载机制,在第一次put的时候才会创建初始空间,默认负载因子是75%阈值=容量*加载因子。默认12。当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的2倍。(原数值左移一位)
[*]Array 和 ArrayList 有何区别?
Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。
[*]TreeSet判断重复元素
TreeSet的底层是TreeMap的keySet(),而TreeMap是基于红黑树实现的,红黑树是一种平衡二叉查找树,TreeSet是用compareTo()来判断重复元素的
[*]堆和栈的区别
1、栈由系统自动分配,而堆是人为申请开辟;
2、栈获得的空间较小,而堆获得的空间较大;
3、栈由系统自动分配,速度较快,而堆一般速度比较慢;
4、栈是连续的空间,而堆是不连续的空间。
[*]索引失效的情况有哪些
当查询条件存在隐式转换时,索引会失效
查询条件存在null值条件,not条件,包含函数,
like条件 以%开头
组合索引未用左列字段
组合索引中使用了范围查询条件,后面的索引会失效
[*]SQL优化
首先可以判断业务的设计是否合理,通过慢查询日志定位到那些SQL语句的效率比较低,然后具体查看这些语句是根据什么查询的,比如全表数据,索引,接着可以读写分离,覆盖索引,根据数据量大小选择更为合适的数据库
[*]锁的种类
表锁:开销小,加锁速度快,不会出现死锁,粒度比较大,锁冲突概率高,并发多查询为主,少量按照索引更新
行锁:开销大,加锁速度慢,会出现死锁情况,粒度比较小,锁冲突的概率低,并发小大量按照索引更新,并发查询量大的情况
[*]删除delete pk truncate
1.delete 可以加where 条件,truncate不能加
2.truncate删除,效率高一丢丢
3.假如要删除的表中有自增长列,如果用delete删除后,再插入数据,自增长列的值从断点开始,而truncate删除后,再插入数据,自增长列的值从1开始。
4.truncate删除没有返回值,delete删除有返回值
5.truncate删除不能回滚,delete删除可以回滚.
[*]事务的特性
ACID(属性)
原子性(Atomicity):一个事务不可再分割,要么都执行要么都不执行
一致性(Consistency):一个事务执行会使数据从一个一致状态切换到另外一个一致状态
隔离性(Isolation):一个事务的执行不受其他事务的干扰
持久性(Durability):一个事务一旦提交,则会永久的改变数据库的数据.
[*]SpringMvc工作流程
第一步:发起请求到前端控制器(DispatcherServlet)
第二步:前端控制器请求HandlerMapping查找 Handler (可以根据xml配置、注解进行查找)
第三步:处理器映射器HandlerMapping向前端控制器返回Handler,HandlerMapping会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象,多个HandlerInterceptor拦截器对象),通过这种策略模式,很容易添加新的映射策略
第四步:前端控制器调用处理器适配器去执行Handler
第五步:处理器适配器HandlerAdapter将会根据适配的结果去执行Handler
第六步:Handler执行完成给适配器返回ModelAndView
第七步:处理器适配器向前端控制器返回ModelAndView (ModelAndView是springmvc框架的一个底层对象,包括 Model和view)
第八步:前端控制器请求视图解析器去进行视图解析 (根据逻辑视图名解析成真正的视图(jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可
第九步:视图解析器向前端控制器返回View
第十步:前端控制器进行视图渲染 (视图渲染将模型数据(在ModelAndView对象中)填充到request域)
第十一步:前端控制器向用户响应结果
[*]springmvc中的异常管理
采用aop的设计思想,把controller中异常抛出,创建自定义异常类和全局异常处理类,使用@ControllerAdvice注解,在处理方法上使用注解@ExceptionHandler(value = 自定义异常类.class
[*]throw 和 throws 的区别是什么?
throw用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常。
throws 用在方法声明上,可以抛出多个异常,标识该方法可能抛出的异常列表。
[*]get请求和post请求的区别
GET把参数包含在URL中,POST通过request传递参数
form表单提交时,原URL中有参数的情况下:
get:原参数列表会被忽视,后台无法接收到这个参数,只能得到表单中的参数;(表单中的数据通过URL拼接的方式覆盖了原参数)
post:后台可以获取原参数,参数分为两部分:一部分是原URL中的参数放在地址栏;另一部分是表单中的参数放在请求头中;(表单中的数据被封装到请求头中,不会改变原URL)
GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留GET请求在URL中传送的参数是有长度限制的,而POST没有。
对参数的数据类型,GET只接受ASCII字符,而POST没有限制。GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
[*]手撕代码题:写一个单例模式:
public class SingletonTest06 {
public static void main(String[] args) {
System.out.println("双重检查");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2); // true
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
// 懒汉式(线程安全,同步方法)
class Singleton {
private static volatile Singleton instance;
private Singleton() {}
//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题同时保证了效率, 推荐使用
public staticSingleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
优缺点说明:
1) Double-Check 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (singleton == null)检查,这样就可以保证线程安全了。
2) 这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步.
3) 线程安全;延迟加载;效率较高。
在实际开发中,推荐使用这种单例设计模式
数据库主从架构原理:
主从数据库中的数据是同步的,如果主数据库出现异常是可以使用从数据库(高可用)
当SQL语句对主数据库进行修改时,会吧SQL语句也存入到binlog文件中,并通知从数据库进行读取,从数据库通过中继日志进行同步数据
image.png
问题1:当主数据库修改后立马查询数据(从数据库还没来及更新)
解决:从数据库会强制转换成在主数据库中查询
问题2 :网络问题导致主从数据库查询异常
解决:半同步
如何解决post请求乱码的问题:
使用filter过滤器
web.xml
<!--使用filter过滤器解决post请求的中文乱码问题-->
<!--需要配置在配置文件的开头-->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!--设置请求的编码集-->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!--设置响应的编码集-->
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
[*]实体类的属性名和表中的字段不一样时如何解决:
1、写 SQL 语句的时候 写别名
2、在MyBatis的全局配置文件中开启驼峰命名规则。要求 数据库字段中含有下划线,切实体类属性名是相同的单词构成
3、在Mapper映射文件中使用 resultMap 自定义映射
4、可以接合@Results、@Result、@ResultMap实现开发
[*]说一说IOC:
IoC 为控制反转。就是创建对象的控制权,被反转到了Spring框架上。
通常,我们实例化一个对象时,都是使用类的构造方法来new一个对象,这个过程是由我们自己来控制的,而控制反转就把new对象的工交给了Spring容器。
IoC的主要实现方式有两种:
依赖查找、依赖注入。依赖注入是一种更可取的方式。
依赖查找,主要是容器为组件提供一个回调接口和上下文环境。这样一来,组件就必须自己使用容器提供的API来查找资源和协作对象,控制反转仅体现在那些回调方法上,容器调用这些回调方法,从而应用代码获取到资源。
依赖注入,组件不做定位查询,只提供标准的Java方法让容器去决定依赖关系。容器全权负责组件的装配,把符合依赖关系的对象通过Java Bean属性或构造方法传递给需要的对象。
Spring依赖注入的方式主要有四个,基于注解注入方式、set注入方式、构造器注入方式、静态工厂注入方式。
[*]@Autowired与@Resource的区别:
@Autowired与@Resource都可以用来装配Bean,都可以写在字段、setter方法上。
他们的区别是:
@Autowired默认按类型进行自动装配(该注解属于Spring),默认情况下要求依赖对象必须存在,如果要允许为null,需设置required属性为false,例:@Autowired(required=false)。如果要使用名称进行装配,可以与@Qualifier注解一起使用。
@Resource默认按照名称进行装配(该注解属于J2EE),名称可以通过name属性来指定。如果没有指定name属性,当注解写在字段上时,默认取字段名进行装配;如果注解写在setter方法上,默认取属性名进行装配。当找不到与名称相匹配的Bean时,会按照类型进行装配。但是,name属性一旦指定,就只会按照名称进行装配。
[*]如果cookie禁用了,session域里面的内容怎么使用;
使用url重定向方法(URL拼接sessionid)
使用矩阵变量对URL进行重写(有参数的情况,SpringBoot默认是禁用了矩阵变量的功能,需要手动开启)
[*]redis持久化有几种类型:
Redis 提供了 2 个不同形式的持久化方式
RDB ( Redis DataBase)在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
AOF (Append OF File)以日志的形式来记录每个写操作,只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一-次以完成数据的恢复工作。
[*]如何保证Redis与mysql中的数据的一致性
1、给缓存设置过期时间,所有的写操作以数据库为准,如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
2、设置一个定时执行的任务,一定时间后会读取数据库,把最新的数据传给Redis
3、可以使用异步更新缓存的机制,基于mysql的binlog同步更新redis,类似mysql的主从同步。
[*]什么是缓存穿透:
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。
解决:
对空值缓存并设置过期时间
设置可访问的名单(白名单)
bitmaps类型存储
进行实时监控
布隆过滤器
[*]缓存击穿
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回传到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决:
预先设置热门数据:
使用锁:
[*]缓存雪崩
缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key
解决:
构建多级缓存架构使用锁或队列:
设置过期标志更新缓存:
将缓存失效时间分散开:
[*]工厂模式主要分为三种形态:
简单工厂模式(Simple Factory)工厂方法模式(Factory Method)抽象工厂模式(Abstract Factory)
[*]表的索引
索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。
优点:
通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
缺点:
创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
[*]哪些情况需要创建索引:
主键自动建立唯 一 索引
频繁作为查询条件的字段应该创建索引
查询中与其它表关联的字段,外键关系建立索引
频繁更新的字段不适合创建索引,因为每次更新不单是更新了记录还会更新索引
单键组索引的选择问题,who? 在高并发下领向创建组合索引
查询中排序的字段,排序字段若通过索引法访问将大大提高排序速度
查询中统计或者分组字段 非常感谢,很实用
页:
[1]