面试题
Java基础知识
1.jvm、jdk、jre
jvm:
jvm是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
jdk:
jdk 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序
jre:
jre是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
2.成员变量与局部变量的区别
- 语法形式 :从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰
- 存储方式: 从变量在内存中的存储方式来看,如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存 - 生存时间: 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
- 默认值 : 从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
3.静态方法和实例方法有何不同?
- 调用方式
在外部调用静态方法时,可以使用 类名.方法名
的方式,也可以使用 对象.方法名
的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象
- 访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制
4.重载和重写有什么区别?
- 重载
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同
- 重写
-
方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类
-
如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法,但是被static
修饰的方法能够被再次声明。 -
构造方法无法被重写
5. Java 中的几种基本数据类型?
Java 中有 8 种基本数据类型,分别为:
-
6 种数字类型:
- 4 种整数型:
byte
、short
、int
、long
- 2 种浮点型:
float
、double
- 4 种整数型:
-
1 种字符类型:
char
-
1 种布尔型:
boolean
6.基本类型和包装类型的区别
-
成员变量包装类型不赋值就是
null
,而基本类型有默认值且不是null
。 -
包装类型可用于泛型,而基本类型不可以。
-
基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被
static
修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。 -
相比于对象类型, 基本数据类型占用的空间非常小。
7.包装类型的缓存机制
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
两种浮点数类型的包装类 Float
,Double
并没有实现缓存机制。
8. 自动装箱与拆箱了解吗?原理是什么?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型
9. 为什么浮点数运算的时候会有精度丢失的风险
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示
10. 如何解决浮点数运算的精度丢失问题?
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal
来做的
11.面向对象和面向过程的区别
两者的主要区别在于解决问题的方式不同:
- 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
- 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
另外,面向对象开发的程序一般更易维护、易复用、易扩展
12.对象的相等和引用相等的区别
- 对象的相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是他们指向的内存地址是否相等。
13.类的构造方法的作用是什么
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作
14.面向对象三大特征
- 封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性
- 继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率
-
子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
-
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
-
子类可以用自己的方式实现父类的方法
- 多态
多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
15. 接口和抽象类有什么共同点和区别
共同点 :
- 都不能被实例化。
- 都可以包含抽象方法。
区别 :
- 接口可以多实现,而抽象类只能单继承
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的,并且必须赋值,否则通不过编译。
- 接口不能包含构造器,抽象类可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作
- 接口里只能包含抽象方法,静态方法和默认方法(加default),不能为普通方法提供方法实现,抽象类则完全可以包含普通方法,接口中的普通方法默认为抽象方法
- 抽象类中可以拥有静态代码块的,接口中不能拥有静态代码块
16. 深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存, 使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误
17.== 和 equals() 的区别
- 对于基本数据类型来说,
==
比较的是值。 - 对于引用数据类型来说,
==
比较的是对象的内存地址
equals()
方法存在两种使用情况:
- 类没有重写
equals()
方法 :通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object
类equals()
方法。 - 类重写了
equals()
方法 :一般我们都重写equals()
方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
18.String、StringBuffer、StringBuilder 的区别
final类型的字符数组,所引用的字符串不能被改变,每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作。StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
19. try-catch-finally 如何使用
try
块 : 用于捕获异常。其后可接零个或多个 catch
块,如果没有 catch
块,则必须跟一个 finally
块。
catch
块 : 用于处理 try 捕获到的异常。
finally
块 : 无论是否捕获或处理异常,finally
块里的语句都会被执行。当在 try
块或 catch
块中遇到 return
语句时,finally
语句块将在方法返回之前被执行。
20.什么是序列化?什么是反序列化
我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
- 序列化: 将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
21. 如果有些字段不想进行序列化怎么办
对于不想进行序列化的变量,使用 transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
关于 transient
还有几点注意:
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化
22.获取 Class 对象的四种方式
1. 知道具体类的情况下可以使用:
Class alunbarClass = TargetObject.class;
2. 通过 Class.forName()
传入类的全路径获取:
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");
3. 通过对象实例instance.getClass()
获取:
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
4. 通过类加载器xxxClassLoader.loadClass()
传入类路径获取:
ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");
/************************/
//4.类加载器
ClassLoader classLoader = Practice0.class.getClassLoader();
Class aClass2 = classLoader.loadClass("org.xiyou.util.Practice.one");
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException {
/**
* 获取 TargetObject 类的 Class 对象并且创建 TargetObject 类实例
*/
Class<?> targetClass = Class.forName("cn.javaguide.TargetObject");
TargetObject targetObject = (TargetObject) targetClass.newInstance();
/**
* 获取 TargetObject 类中定义的所有方法
*/
Method[] methods = targetClass.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
/**
* 获取指定方法并调用
*/
Method publicMethod = targetClass.getDeclaredMethod("publicMethod",
String.class);
publicMethod.invoke(targetObject, "JavaGuide");
/**
* 获取指定参数并对参数进行修改
*/
Field field = targetClass.getDeclaredField("value");
//为了对类中的参数进行修改我们取消安全检查
field.setAccessible(true);
field.set(targetObject, "JavaGuide");
/**
* 调用 private 方法
*/
Method privateMethod = targetClass.getDeclaredMethod("privateMethod");
//为了调用private方法我们取消安全检查
privateMethod.setAccessible(true);
privateMethod.invoke(targetObject);
}
}
23.List, Set, Queue, Map 四者的区别
-
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。 -
Set
(注重独一无二的性质): 存储的元素是无序的、不可重复的。 -
Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。 -
Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
24. 集合框架底层数据结构总结
-
List
ArrayList
:Object[]
数组Vector
:Object[]
数组LinkedList
: 双向链表
-
Set
HashSet
(无序,唯一): 基于HashMap
实现的,底层采用HashMap
来保存元素LinkedHashSet
:LinkedHashSet
是HashSet
的子类,并且其内部是通过LinkedHashMap
来实现的。有点类似于我们之前说的LinkedHashMap
其内部是基于HashMap
实现一样,不过还是有一点点区别的TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树)
-
Map
HashMap
: JDK1.8 之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间LinkedHashMap
:LinkedHashMap
继承自HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》open in new windowHashtable
: 数组+链表组成的,数组是Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的TreeMap
: 红黑树(自平衡的排序二叉树)
25. 如何选用集合
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map
接口下的集合,需要排序时选择 TreeMap
,不需要排序时就选择 HashMap
,需要保证线程安全就选用 ConcurrentHashMap
。
当我们只需要存放元素值时,就选择实现Collection
接口的集合,需要保证元素唯一时选择实现 Set
接口的集合比如 TreeSet
或 HashSet
,不需要就选择实现 List
接口的比如 ArrayList
或 LinkedList
,然后再根据实现这些接口的集合的特点来选用
26. 为什么要使用集合
当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端, 因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。
数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。 但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据。
27.ArrayList的扩容机制
当创建Arraylist对象时,如果使用无参构造器,则初始elementData容量为0,第一次添加,则扩容elementData为10,当添加的数据达到10个后会触发扩容机制,再创建一个数组容量为原来的1.5倍,然后把之前数组的数据拷贝进新数组。
28.HashMap
-
hashmap如何解决hash冲突的:
hashmap底层采用了数组+链表+红黑树的数据结构,数组的默认长度是16,当用Put方法添加数据的时候hashmap会根据key的hash取模运算,最终把值放到数组中的指定位置,但是难免会出现hash冲突,hash值不同的两个key取模后落到同一个数组下标,hashmap采用链地址法,把产生hash冲突的Key组成一个单向链表,用尾插法保存到链表尾部。为了使链表长度过长导致查询效率下降,当链表长度大于等于8并且数组长度大于等于64的时候,hashmap会把当前链表转成红黑树从而减少链表查询的时间复杂度的问题。
29.解决hash冲突的方法
开放地址法:我们在遇到哈希冲突时,去寻找一个新的空闲的哈希地址。
(1)线性探测法
(2)平方探测法(二次探测)
链地址法:将所有哈希地址相同的记录都链接在同一链表中
再哈希法:同时构造多个不同的哈希函数,等发生哈希冲突时就使用第二个、第三个……等其他的哈希函数计算地址,直到不发生冲突为止。虽然不易发生聚集,但是增加了计算时间
建立公共溢出区:将哈希表分为基本表和溢出表,将发生冲突的都存放在溢出表中
30. hashmap和hashtable的区别
-
功能特性
- hashtable线程是安全的,而hashmap不是
- hashmap的性能要比hashtable要好,因为hashtable采用了全局同步锁保证安全性,对性能影响较大
-
内部实现
- Hashtable使用数组+链表,hashmap采用了数组+链表+红黑树
- Hashmap初始容量是16,hashtable初始容量是11
- hashmap可以使用null作为key而hashtable不允许
-
散列算法不同
- Hashtable直接是使用key的hashcode对数组长度做取模。而hashmap对key的hashcode做了二次散列,避免Key分布不均匀问题影响查询性能。
31. HashMap 的长度为什么是 2 的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
32.sleep() 方法和 wait() 方法对比
-
相同点:
- 一旦执行方法,都可以使得当前线程进入阻塞状态。
-
不同点:
- wait()是object类中的方法,而sleep()是线程thread类中的方法
- 如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁
- 唤醒方式不同,wait()依靠notify或者notifyAll、中断、达到指定时间来唤醒,而sleep()到达指定时间被唤醒
- 调用wait()需要先获取对象锁,sleep()不需要
33.为什么wait()方法不定义在Thread中
wait()
是让获得对象锁的线程实现等待,然后别的线程调用同一个对象上的 notify()
来唤醒。每个对象(Object
)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object
)而非当前的线程(Thread
)
为什么 sleep()
方法定义在 Thread
中
因为 sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁
34.MyISAM 和 InnoDB 有什么区别
-
InnoDB 支持行级别的锁粒度,MyISAM 不支持,只支持表级别的锁粒度。
-
MyISAM 不提供事务支持。InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别。
-
MyISAM 不支持外键,而 InnoDB 支持。
-
MyISAM 不支持 MVVC,而 InnoDB 支持。
-
虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。
-
MyISAM 不支持数据库异常崩溃后的安全恢复,而 InnoDB 支持。
-
InnoDB 的性能比 MyISAM 更强大。
35. 什么是事务?事务的特性?
多条sql语句执行,要么全部成功,要么全部失败
原子性(Atomicity
) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性(Consistency
): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
隔离性(Isolation
): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
持久性(Durability
): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
36.并发事务带来了哪些问题?
脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
丢失修改(Lost to modify)在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
不可重复读(Unrepeatable read)指在一个事务内多次读同一数据。在一个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读
幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务读取了几行数 据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会 发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
37.并发事务的控制方式有哪些
MySQL 中并发事务的控制方式无非就两种:锁 和 MVCC。锁可以看作是悲观控制的模式,多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式
MySQL 中主要是通过 读写锁 来实现并发控制
共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。
MVCC 是多版本并发控制方法,即对一份数据会存储多个版本,通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号,版本号是唯一的
38.SQL 标准定义了哪些事务隔离级别
READ-UNCOMMITTED(读取未提交) : 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交) : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
REPEATABLE-READ(可重复读) : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化) : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
39.MySQL 的默认隔离级别是什么?
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)
40.Redis 常用的数据结构有哪些
5 种基础数据结构 :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
3 种特殊数据结构 :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。
41.String 的应用场景有哪些
常规数据(比如 session、token、、序列化后的对象)的缓存;
计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数;
分布式锁(利用 SETNX key value
命令可以实现一个最简易的分布式锁);
42.购物车信息用 String 还是 Hash 存储更好呢
由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储:
- 用户 id 为 key
- 商品 id 为 field,商品数量为 value
那用户购物车信息的维护具体应该怎么操作呢?
- 用户添加商品就是往 Hash 里面增加新的 field 与 value;
- 查询购物车信息就是遍历对应的 Hash;
- 更改商品数量直接修改对应的 value 值(直接 set 或者做运算皆可);
- 删除商品就是删除 Hash 中对应的 field;
- 清空购物车直接删除对应的 key 即可
43.Redis 持久化机制
Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)
RDB持久化是以快照的方式在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件。恢复数据通过读取原始数据加载到内存恢复数据,数据恢复快,效率高,但是数据容易丢失(数据同步有时间间隔)
AOF是将所有的命令行记录以 Redis 命令请求协议的格式完全持久化存储,保存为 AOF 文件,恢复数据通过命令恢复数据,缓存一致性更高,牺牲性能
44.持久化有两种,那应该怎么选择呢
- 不要仅仅使用 RDB ,因为那样会导致你丢失很多数据。
- 也不要仅仅使用 AOF ,因为那样有两个问题,第一,你通过 AOF 做冷备没有 RDB 做冷备的恢 复速度更快; 第二, RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备 份和恢复机制的 bug 。
- Redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制, 用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。
- 如果同时使用 RDB 和 AOF 两种持久化机制,那么在 Redis 重启的时候,会使用 AOF 来重新 构建数据,因为 AOF 中的数据更加完整。
45.请描述 Redis 的缓存淘汰策略
当 Redis 使用的内存达到 maxmemory 参数配置的阈值的时候,Redis 就会根据配置的内存淘汰策略。 把访问频率不高的 key 从内存中移除
Redis 默认提供了 8 种缓存淘汰策略,这 8 种缓存淘汰策略总的来说,我认为可 以归类成五种
第一种, 采用 LRU 策略,就是把不经常使用的 key 淘汰掉。
第二种,采用 LFU 策略,它在 LRU 算法上做了优化,增加了数据访问次数,从 而确保淘汰的是非热点 key。
第三种,随机策略,也就是是随机删除一些 key。
第四种,ttl 策略,从设置了过期时间的 key 里面,挑选出过期时间最近的 key 进行优先淘汰
第五种,当内存不够的时候,直接报错,这是默认的策略。
这些策略可以在 redis.conf 文件中手动配置和修改,我们可以根据缓存的类型和 缓存使用的场景来选择合适的淘汰策略。 最后一个方面,我们在使用缓存的时候,建议是增加这些缓存的过期时间。 因为我们知道这些缓存大概的生命周期,从而更好的利用内存。
45.如何保证缓存和数据库数据的一致性
当应用程序需要去读取某个数据的时候,首先会先尝试去 Redis 里面加载,如果 命中就直接返回。如果没有命中,就从数据库查询,查询到数据后再把这个数据 缓存到 Redis 里面
在这样一个架构中,会出现一个问题,就是一份数据,同时保存在数据库和 Redis 里面,当数据发生变化的时候,需要同时更新 Redis 和 Mysql,由于更新是有先 后顺序的,并且它不像 Mysql 中的多表事务操作,可以满足 ACID 特性。所以就 会出现数据一致性问题
在这种情况下,能够选择的方法只有几种。
先更新数据库,再更新缓存
先删除缓存,再更新数据库
如果先更新数据库,再更新缓存,如果缓存更新失败,就会导致数据库和 Redis 中的数据不一致。
如果是先删除缓存,再更新数据库,理想情况是应用下次访问 Redis 的时候,发 现 Redis 里面的数据是空的,就从数据库加载保存到 Redis 里面,那么数据是 一致的。但是在极端情况下,由于删除 Redis 和更新数据库这两个操作并不是原 子的,所以这个过程如果有其他线程来访问,还是会存在数据不一致问题.
所以,如果需要在极端情况下仍然保证 Redis 和 Mysql 的数据一致性,就只能 采用最终一致性方案。 比如基于 RocketMQ 的可靠性消息通信,来实现最终一致性
还可以直接通过 Canal 组件,监控 Mysql 中 binlog 的日志,把更新后的数据同 步到 Redis 里面
因为这里是基于最终一致性来实现的,如果业务场景不能接受数据的短期不一致 性,那就不能使用这个方案来做
46.@Autowired 和 @Resource 的区别是什么
1、 共同点 两者都可以写在属性和setter方法上。两者如果都写在属性上,那么就不需要再写setter方法
2、autowired声明当前属性自动装配,默认byType,如果我们想使用按照名称(byName)来装配,可以结 合@Qualifier注解一起使用。
默认装配方式为byName,如果根据byName没有找到对应的bean,则继续根据byType寻找对应的bean,根据byType如果依然没有找到Bean或者找到不止一个类型匹配的bean,则抛出异常
47. Bean 的作用域有哪些
singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean()
两次,得到的是不同的 Bean 实例。
request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
application/global-session (仅 Web 应用可用): 每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
48.Bean 的生命周期
Spring 生命周期全过程大致分为五个阶段:创建前准备阶段、创建实例阶段、 依赖注入阶段、 容器缓存阶段和销毁实例阶段
一、创建前准备阶段
这个阶段主要的作用是,Bean 在开始加载之前,需要从上下文和相关配置中解析并查找 Bean 有关的扩展实现, 比如像容器在初始化 bean 时调用的方法init-method、容器在 销毁 bean 时调用的方法destory-method。 以及,BeanFactoryPostProcessor 这类的 bean 加载过程中的前置和后置处理。 这些类或者配置其实是 Spring 提供给开发者,用来实现 Bean 加载过程中的扩展机制,在很多和 Spring 集成的中间件中比较常见,比如 Dubb
二、创建实例阶段
这个阶段主要是通过反射来创建 Bean 的实例对象,并且扫描和解析 Bean声明的一些属性
三、依赖注入阶段
如果被实例化的 Bean 存在依赖其他 Bean 对象的情况,则需要对这些依赖 bean 进行对象注入。比如常见的@Autowired、setter 注入等依赖注入的配置形式。同时 ,在这个阶段会触发一些扩展的调用 ,比如常见的扩展类 : BeanPostProcessors(用来实现 bean 初始化前后的扩展回调)、 InitializingBean(这个类有一个afterPropertiesSet(),这个在工作中也比较常见)、 BeanFactoryAware 等等
四、容器缓存阶段
容器缓存阶段主要是把 bean 保存到容器以及 Spring 的缓存中,到了这个阶段, Bean 就可以被开发者使用了。 这个阶段涉及到的操作,常见的有,init-method 这个属性配置的方法, 会在这 个阶段调用。 以 及 像 BeanPostProcessors 方 法 中 的 后 置 处 理 器 方 法 如 : postProcessAfterInitialization,也会在这个阶段触发
五、销毁实例阶段
当 Spring 应用上下文关闭时,该上下文中的所有 bean 都会被销毁。 如果存在 Bean 实现了 DisposableBean 接口,或者配置了 destory-method 属性, 会在这个阶段被调用
49.Spring 是如何解决循环依赖问题的
如果在代码中,将两个或多个 Bean 互相之间持有对方的引用就会 发生循环依赖。循环的依赖将会导致注入死循环。
循环依赖有三种形态: 第一种互相依赖:A 依赖 B,B 又依赖 A,它们之间形成了循环依赖。
第二种三者间依赖:A 依赖 B,B 依赖 C,C 又依赖 A,形成了循环
第三种是自我依赖:A 依赖 A 形成了循环
而 Spring 中设计了三级缓存来解决循环依赖问题,当我们去调用 getBean()方法 的时候,Spring 会先从一级缓存中去找到目标 Bean,如果发现一级缓存中没有 便会去二级缓存中去找,而如果一、二级缓存中都没有找到,意味着该目标 Bean 还没有实例化。于是,Spring 容器会实例化目标 Bean(PS:刚初始化的 Bean 称为早期 Bean) 。然后,将目标 Bean 放入到二级缓存中,同时,加上标记是 否存在循环依赖。如果不存在循环依赖便会将目标 Bean 存入到二级缓存,否则, 便会标记该 Bean 存在循环依赖,然后将等待下一次轮询赋值,也就是解析 @Autowired 注解。等@Autowired 注解赋值完成后,会将目标 Bean 存入到一 级缓存。 Spring 一级缓存中存放所有的成熟 Bean,二级缓存中存放所有的早期 Bean, 先取一级缓存,再去二级缓存。
50. springboot自动装配
51.IOC和AOP的理解
IOC:控制反转。利用了工厂模式 将对象交给容器管理,你只需要在spring配置文件总配置相应的bean,以及设置相关的属性,让 spring容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件 中配置的bean都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你需要 调用这些bean的类
AOP(Aspect-Oriented Programming,面向切面编程)基于动态代理能够将那些与业务无关,却为业务模块所共同调用的逻辑(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。 Spring AOP是基于动态代理的,如果要代理的对象实现了某个接口,那么Spring AOP就会使用JDK 动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用JDK动态代理,转而使用CGlib 动态代理生成一个被代理对象的子类来作为代理
51.SpringMVC请求流程
52.Spring 管理事务的方式有几种
编程式事务 : 在代码中硬编码(不推荐使用) : 通过 TransactionTemplate
或者 TransactionManager
手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。
声明式事务 : 在 XML 配置文件中配置或者直接基于注解(推荐使用) : 实际是通过 AOP 实现(基于@Transactional
的全注解方式使用最多)
53.Spring 事务中的隔离级别有哪几种
54.#{}和${}的区别是什么
#{}是预编译处理,${}是字符串替换。
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理${}时,就是把${}替换成变量的值。 使用#{}可以有效的防止SQL注入,提高系统安全性
55.Spring 中有哪些方式可以把 Bean 注入 到 IOC 容器?
把 Bean 注入到 IOC 容器里面的方式有 7 种方式
使用 xml 的方式来声明 Bean 的定义,Spring 容器在启动的时候会加载并解析这 个 xml,把 bean 装载到 IOC 容器中。
使用@CompontScan 注解来扫描声明了@Controller、@Service、@Repository、 @Component 注解的类。
使用@Configuration 注解声明配置类,并使用@Bean 注解实现 Bean 的定义, 这种方式其实是 xml 配置方式的一种演变,是 Spring 迈入到无配置化时代的里 程碑。
使用@Import 注解,导入配置类或者普通的 Bean 使 用 FactoryBean 工 厂 bean , 动 态 构 建 一 个 Bean 实 例 , Spring Cloud OpenFeign 里面的动态代理实例就是使用 FactoryBean 来实现的。
实现 ImportBeanDefinitionRegistrar 接口,可以动态注入 Bean 实例。这个在 Spring Boot 里面的启动注解有用到。 实现 ImportSelector 接口,动态批量注入配置类或者 Bean 对象,这个在 Spring Boot 里面的自动装配机制里面有用到
在面试中我介绍了自己的经历和技术,我认为我之前的工作经历和做过的项目与归公司的要求很接近,如果能够入职我能够将我的经历,知识和资源引入到贵公司中,并且能快速适应,快速高效产出业绩,我希望能够再争取一下我的待遇。
面试官你好,我叫邱亚豪,来自河南驻马店,2020年本科毕业于南京理工大学紫金学院软件工程专业,目前在西安石油大学电子信息专业读研一,
之前已经在软件开发行业已经做两年了,在这两年里,我主要在南京海泰信息医疗系统有限公司从事java开发工作,
期间参与了武汉协和医院和中山大学附属第一医院医疗信息系统的开发。所负责的项目在多家医院上线并使用。负责医疗系统门诊相关功能模块的添加以及Bug的修复工作
熟悉医院业务和整个对接流程对医院门诊系统开发流程有较为深入理解