Java面试总结(三)
1、购物车实现流程
1.1、购物车与用户的关系?
- 一个用户必须对应一个购物车。一个用户不管买多少商品,都会存放在属于自己的购物车中
- 单点登录一定在开发购物车之前。
1.2、跟购物车有关的操作有哪些?
添加购物车
- 用户未登录状态的添加
存放在
Redis
中(京东商城,可以以UUID作为唯一标识)存放在
Cookie
中存放在浏览器的
localstorage
- 用户登录状态的添加(两份)
- 存在
Redis
缓存中【读写数据快】 - 为了保障数据安全性,我们需要将数据存储在数据库中。
展示购物车
- 未登录状态的展示
直接从
Cookie
、Redis
、localstorage
中获取数据展示即可。- 登录状态的展示
用户一旦登录,我们必须获取数据库【
Redis
】与该用户未登录之前添加的全部商品信息。
2、Java类加载器及双亲委派机制
2.1、Java类加载器
JDK自带三个类加载器,从上而下分别是
BootStrapClassLoader
(C++实现)、ExtClassLoader
和AppClassLoader
。
BootStrapClassLoader
是ExtClassLoader
的父类加载器,默认负责加载%JAVA_HOME%lib
包下的jar包和class文件。(rt.jar由此类加载器加载)
ExtClassLoader
是AppClassLoader
的父类加载器,负责加载%JAVA_HOME% lib/ext
文件夹下的jar包和class类
AppClassLoader
是自定义类加载器的父类,负责加载classpath
下的类文件。(加载程序员写的代码、引入的jar由此类加载器加载),可以由ClassLoader.getSystemClassLoader()
获取继承
ClassLoader
实现自定义类加载器。
2.2、双亲委派机制
如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;
只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
2.3、双亲委派机制工作流程
- 当
Application ClassLoader
收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader
去完成。 - 当
Extension ClassLoader
收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader
去完成。 - 如果
Bootstrap ClassLoader
加载失败(在\lib中未找到所需类),就会让 Extension ClassLoader
尝试加载。 - 如果
Extension ClassLoader
也加载失败,就会使用Application ClassLoader
加载。 - 如果
Application ClassLoader
也加载失败,就会使用自定义加载器去尝试加载。 - 如果均加载失败,就会抛出
ClassNotFoundException
异常。
2.4、双亲委派机制作用
- 避免类的重复加载。当父类加载器加载了该类时,子类加载器就不会再加载一次。
- 防止java核心API被随意替换。
我们可以编写一个
java.lang.String
类替换原有的String类吗?不行,由于有双亲委派机制的存在,
Application ClassLoader
不会直接加载我们自定义的类,而是会委托其父类加载器Extension ClassLoader
加载,而Extension ClassLoader
又会委托其父类加载器,即Bootstrap ClassLoader
加载,由于java.lang.String
是rt.jar
下的类,即系统中的类,所以Bootstrap ClassLoader
会直接加载系统中的java.lang.String
,而且由于 String 类此时已经被引导加载器记载过,所以不会再加载我们自定义的String类
3、BeanFactory
和ApplicationContext
有什么区别?
ApplicationContext
是BeanFactory
的子接口ApplicationContext
提供了更完整的功能
- 继承
MessageSource
类,因此支持国际化 - 统一的资源文件访问方式。
- 提供在监听器中注册Bean的事件
- 同时加载多个配置文件
- 载入多个上下文
BeanFactroy
采用的是延迟加载形式来注入Bean
的,即只有在使用到某个Bean时(调用 get bean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的 Spring的配置问题。如果Bean的某一个隅性没有注入,BeanFacotry
加载后,直至第一次使用调用getBean
方法才会抛出异常
ApplicationContext
,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicationContext
启动后预载入所有的单实例Bean,通过预载入单实例bean确保当你需要的时候,你就不用等待,因为它们已经创建好了。相对于
BeanFactory
,ApplicationContext
唯一的缺点是占用内存空间。当应用程序配置较多 Bean 时,程序启动较慢。
4、简述 Mybatis
的插件运行原理,如何编写一个插件?
4.1、原理
Mybatis
只支持针对ParameterHandler
(设置预编译参数用的) 、ResultSetHandler
(处理结果集) 、StatementHandler
(处理SQL预编译,设置参数等相关工作)和Executor
(执行增删改查操作) 这四个接口的插件,使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这四个接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler
的invoke
方法,拦截哪些你指定需要拦截的方法。
4.2、编写插件
实现
Mybatis
的Interceptor
接口,并重写其中的intercept()
方法,然后再给插件编写注解,指定要拦截哪一种接口的哪些方法即可,需要在配置文件中配置编写的插件。
MyBatis
定义插件要实现Interceptor接口,这个接口只声明了三个方法:
- intercept:定义拦截的时候要执行的方法
setProperties
: 在MyBatis
进行配置插件的时候可以配置自定义相关属性- plugin: 插件用于封装目标对象,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理.
Mybatis
官方推荐使用@Intercepts
注解来实现拦截器插件的定义,例如
1 |
- 分页插件思路
拦截并获取查询的原始SQL,然后拼装成
countSQL
和pageSQL
,再进行查找取值。而如何获取查询的SQL,就需要使用我们上面分析的拦截器来获取了。
5、缓存雪崩、缓存穿透和缓存击穿
5.1、缓存雪崩
缓存雪崩是指缓存在同一时间内大面积地失效,所以,后面的请求都会落在数据库上,造成数据库在短时间内承受大量请求而崩溃。
除以上原因,缓存重启也可能造成缓存雪崩。
解决方案
- 缓存数据的过期时间设置为随机,防止同一时间内大量数据过期现象发生。
- 给每一个缓存数据增加响应的缓存标记,记录缓存是否失效,如果缓存标记失效,那么更新缓存。
- 缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。
这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
5.2、缓存穿透
缓存穿透是指缓存和数据库中都没有请求需要的数据,导致所有的请求都落在数据库上,造成数据库短时间内承受大量的请求而崩溃
解决方案
- 在接口层增加校验,如用户鉴权校验或者参数校验,不合法参数直接不予请求。
- 从缓存中拿不到的数据,在数据库中也没有正常取到,这是可以将
key-value
设置为key-null
,缓存有效时间可以设置短点,这样可以防止攻击用户反复用一个ID暴力攻击
5.3、缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
- 设置热点数据永不过期
- 加互斥锁(互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性)
6、深拷贝与浅拷贝
在Java中,当我们需要拷贝一个Java对象时,常见的会有两种方式的拷贝:深拷贝和浅拷贝。
6.1、浅拷贝
浅拷贝只是拷贝了源对象的地址,所以源对象的任何值发生变化时,拷贝对象的值也会随之发生变化。给了你一把原来房子的钥匙,你对这房子做的一切工作都会改变原来的房子。
浅拷贝带走的仅仅是这个对象的地址
下面演示一下浅拷贝
- 创建一个User类
1 |
|
- 进行浅拷贝,可以看到此时源对象和拷贝对象的地址一致
1 | User user = new User(); |
查看结果
- 此时改变
copyUser
的值,然后分别输出以上两个对象的值
1 | User user = new User(); |
查看结果,可以看到拷贝对象的值发生变化后,源对象的值也跟着发生变化。
6.2、深拷贝
深拷贝则是拷贝了源对象的所有值而不是地址,所以即使源对象的值发生任何变化时,拷贝对象的值也不会改变。给你建了一座新房子,你对新房子做什么,原来房子里的东西都不会发生改变。
深拷贝中,拷贝对象和源对象不再关联,而是彻底隔离。
- 常用的几种深拷贝范式
- 构造函数方式
- 重写clone方法
- Apache Commons Lang 序列化
Gson
序列化- Jackson 序列化
- 使用 Object 类下的
clone
方法深拷贝对象
让
User
类实现Cloneable
接口,并重写 Object 类下的 clone 方法,将方法修饰符修改为 public
1 |
|
使用 clone 方法进行深拷贝
1 | User user = new User(); |
此时输出的结果为:
7、接口和抽象类
1、抽象类
- 抽象类可以有抽象方法,也可以有普通方法
抽象类中不一定要有抽象方法,但有抽象方法的一定是抽象类。
- 继承抽象类,那么必须实现抽象父类的所有抽象方法,否则需要将子类也声明为抽象类。
2、接口
- 接口中定义的方法都是抽象方法,均以
public
修饰,在JDK8后,可以在接口中编写有方法体的方法,但此方法必须以default
关键字修饰,实现此接口的类会自动实现该default
方法
1 | public interface TestInteface { |
- 接口中还可以有静态方法,具体使用与类的静态方法一致。
1 | public interface TestInteface { |
- 接口中定义的变量均为 静态常量
3、二者区别
- 抽象类属于类,是单继承的。
- 接口可以多继承,且一个类可以实现多个接口。
- 接口可以看为一个特殊的抽象类
8、Spring 事务的传播行为
传播行为 | 意义 |
---|---|
PROPERGATION_MANDATORY(强制的) | 表示方法必须运行在一个事务中,如果当前事务不存在,就抛出异常 |
PROPAGATION_NESTED | 表示如果当前事务存在,则方法应该运行在一个嵌套事务中。否则,它看起来和 PROPAGATION_REQUIRED 看起来没什么俩样 |
PROPAGATION_NEVER | 表示方法不能运行在一个事务中,否则抛出异常 |
PROPAGATION_NOT_SUPPORTED | 表示方法不能运行在一个事务中,如果当前存在一个事务,则该方法将被挂起 |
PROPAGATION_SUPPORTS | 表示当前方法不需要运行在一个是事务中,但如果有一个事务已经存在,该方法也可以运行在这个事务中 |
PROPAGATION_REQUIRED | 表示当前方法必须运行在一个事务中,如果当前存在一个事务,那么该方法运行在这个事务中,否则,将创建一个新的事务 |
PROPAGATION_REQUIRES_NEW | 表示当前方法必须运行在自己的事务中,如果当前存在一个事务,那么这个事务将在该方法运行期间被挂起 |