java基础面经--下

24、反射的实现与作用

Java语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类类中的方法以及属性等。反射的实现主要借助以下四个类:

Class:类的对象

Constructor:类的构造方法

Field:类中的属性对象

Method:类中的方法对象

作用:反射机制指的是程序在运行时能够获取自身的信息。在java中,只要给定类的名字,那么就可以通过反射机制来获取类的所有信息。

反射的优点和缺点:优点就是增加灵活性,可以在运行时动态获取对象实例。缺点是反射的效率很低,而且会破坏封装,通过反射可以访问类的私有方法,不安全。

什么是代理模式? 中介

 代购,中介的意思

定义:代理模式是指,为其他对象提供一种代理以控制对这个对象的访问。

目的:为了在不修改目标对象的基础上,增强主业务逻辑

举个例子来说明:假如说我现在想买一辆二手车,虽然我可以自己去找车源,做质量检测等一系列的车辆过户流程,但是这确实太浪费我得时间和精力了。我只是想买一辆车而已为什么我还要额外做这么多事呢?于是我就通过中介公司来买车,他们来给我找车源,帮我办理车辆过户流程,我只是负责选择自己喜欢的车,然后付钱就可以了。用图表示如下:

为什么要用代理模式?

  • 控制访问:代理类不允许你访问目标,例如商家不让用户访问厂家。
  • 开闭原则,增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成(aop),而没必要打开已经封装好的委托类。

什么是静态代理和动态代理?优缺点?

按照代理创建的时期来进行分类的话, 可以分为两种:静态代理、动态代理。

静态代理是指代理类在程序运行之前就已经定义好了.java源文件,其与目标类的关系在程序运行之前就已经确立。在程序运行前代理类已经编译为.class文件。

动态代理在程序运行时通过反射机制动态创建的。

1.静态代理     

 一步:创建服务类接口

 1 package main.java.proxy;
 2 
 3 /**
 4  * @Auther: dan gao
 5  * @Description:
 6  * @Date: 22:40 2018/1/9 0009
 7  */
 8 public interface BuyHouse {
 9     void buyHosue();
10 }

第二步:实现服务接口

 1 import main.java.proxy.BuyHouse;
 2 
 3 /**
 4  * @Auther: dan gao
 5  * @Description:
 6  * @Date: 22:42 2018/1/9 0009
 7  */
 8 public class BuyHouseImpl implements BuyHouse {
 9 
10     @Override
11     public void buyHosue() {
12         System.out.println("我要买房");
13     }
14 }

第三步:创建代理类

 1 package main.java.proxy.impl;
 2 
 3 import main.java.proxy.BuyHouse;
 4 
 5 /**
 6  * @Auther: dan gao
 7  * @Description:
 8  * @Date: 22:43 2018/1/9 0009
 9  */
10 public class BuyHouseProxy implements BuyHouse {
11 
12     private BuyHouse buyHouse;
13 
14     public BuyHouseProxy(final BuyHouse buyHouse) {
15         this.buyHouse = buyHouse;
16     }
17 
18     @Override
19     public void buyHosue() {
20         System.out.println("买房前准备");
21         buyHouse.buyHosue();
22         System.out.println("买房后装修");
23 
24     }
25 }

第四步:编写测试类

import main.java.proxy.impl.BuyHouseImpl;
import main.java.proxy.impl.BuyHouseProxy;

/**
 * @Auther: dan gao
 * @Description:
 * @Date: 22:43 2018/1/9 0009
 */
public class ProxyTest {
    public static void main(String[] args) {
        BuyHouse buyHouse = new BuyHouseImpl();
        buyHouse.buyHosue();
        BuyHouseProxy buyHouseProxy = new BuyHouseProxy(buyHouse);
        buyHouseProxy.buyHosue();
    }
}

静态代理总结:

优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。

缺点:我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。                                             

2.动态代理

  在动态代理中我们不再需要再手动的创建代理类,我们只需要编写一个动态处理器就可以了。真正的代理对象由JDK再运行时为我们动态的来创建

第一步:编写动态处理器

 1 package main.java.proxy.impl;
 2 
 3 import java.lang.reflect.InvocationHandler;
 4 import java.lang.reflect.Method;
 5 
 6 /**
 7  * @Auther: dan gao
 8  * @Description:
 9  * @Date: 20:34 2018/1/12 0012
10  */
11 public class DynamicProxyHandler implements InvocationHandler {
12 
13     private Object object;
14 
15     public DynamicProxyHandler(final Object object) {
16         this.object = object;
17     }
18 
19     @Override
20     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
21         System.out.println("买房前准备");
22         Object result = method.invoke(object, args);
23         System.out.println("买房后装修");
24         return result;
25     }
26 }

第二步:编写测试类

 1 package main.java.proxy.test;
 2 
 3 import main.java.proxy.BuyHouse;
 4 import main.java.proxy.impl.BuyHouseImpl;
 5 import main.java.proxy.impl.DynamicProxyHandler;
 6 
 7 import java.lang.reflect.Proxy;
 8 
 9 /**
10  * @Auther: dan gao
11  * @Description:
12  * @Date: 20:38 2018/1/12 0012
13  */
14 public class DynamicProxyTest {
15     public static void main(String[] args) {
16         BuyHouse buyHouse = new BuyHouseImpl();
17         BuyHouse proxyBuyHouse = (BuyHouse) Proxy.newProxyInstance(BuyHouse.class.getClassLoader(), new
18                 Class[]{BuyHouse.class}, new DynamicProxyHandler(buyHouse));
19         proxyBuyHouse.buyHosue();
20     }
21 }

 注意Proxy.newProxyInstance()方法接受三个参数:

  • ClassLoader loader:指定当前目标对象使用的类加载器,获取加载器的方法是固定的
  • Class<?>[] interfaces:指定目标对象实现的接口的类型,使用泛型方式确认类型
  • InvocationHandler:指定动态处理器,执行目标对象的方法时,会触发事件处理器的方法

动态代理总结:

优点:相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。

缺点:但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。回想一下那些动态生成的代理类的继承关系图,它们已经注定有一个共同的父类叫ProxyJava的继承机制注定了这些动态代理类们无法实现对class的动态代理,原因是多继承在Java中本质上就行不通。有很多条理由,人们可以否定对 class代理的必要性,但是同样有一些理由,相信支持class动态代理会更美好。接口和类的划分,本就不是很明显,只是到了Java中才变得如此的细化。如果只从方法的声明及是否被定义来考量,有一种两者的混合体,它的名字叫抽象类。实现对抽象类的动态代理,相信也有其内在的价值。此外,还有一些历史遗留的类,它们将因为没有实现任何接口而从此与动态代理永世无缘。如此种种,不得不说是一个小小的遗憾。但是,不完美并不等于不伟大,伟大是一种本质,Java动态代理就是佐例。

3.CGLIB代理

       JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。

第一步:创建CGLIB代理类

 1 package dan.proxy.impl;
 2 
 3 import net.sf.cglib.proxy.Enhancer;
 4 import net.sf.cglib.proxy.MethodInterceptor;
 5 import net.sf.cglib.proxy.MethodProxy;
 6 
 7 import java.lang.reflect.Method;
 8 
 9 /**
10  * @Auther: dan gao
11  * @Description:
12  * @Date: 20:38 2018/1/16 0016
13  */
14 public class CglibProxy implements MethodInterceptor {
15     private Object target;
16     public Object getInstance(final Object target) {
17         this.target = target;
18         Enhancer enhancer = new Enhancer();
19         enhancer.setSuperclass(this.target.getClass());
20         enhancer.setCallback(this);
21         return enhancer.create();
22     }
23 
24     public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
25         System.out.println("买房前准备");
26         Object result = methodProxy.invoke(object, args);
27         System.out.println("买房后装修");
28         return result;
29     }
30 }

第二步:创建测试类

 1 package dan.proxy.test;
 2 
 3 import dan.proxy.BuyHouse;
 4 import dan.proxy.impl.BuyHouseImpl;
 5 import dan.proxy.impl.CglibProxy;
 6 
 7 /**
 8  * @Auther: dan gao
 9  * @Description:
10  * @Date: 20:52 2018/1/16 0016
11  */
12 public class CglibProxyTest {
13     public static void main(String[] args){
14         BuyHouse buyHouse = new BuyHouseImpl();
15         CglibProxy cglibProxy = new CglibProxy();
16         BuyHouseImpl buyHouseCglibProxy = (BuyHouseImpl) cglibProxy.getInstance(buyHouse);
17         buyHouseCglibProxy.buyHosue();
18     }
19 }

CGLIB代理总结: CGLIB创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。同时由于CGLib由于是采用动态创建子类的方法,对于final修饰的方法无法进行代理。

JDK动态代理和CGLIB代理有什么区别?

JDK动态代理主要针对类现了某个接口AOP则会使用JDK动态代理(spring的代理模式)。他基于反射机制实现,生成一个实现同样接口的代理类,然后通过重写的方式,实现读代码的增强。

而如果某个类没有实现接口AOP则会使用CGLIB代理CGLIBd的原理是继承,底层是基于ASM第三方框架,通过修改字节码,生成一个子类,然后重写父类的方法,实现对代码的增强。

25、注解的原理

注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。

26、Integer和int=5的区别:

Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是Integer,从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。

Java 为每个原始类型提供了包装类型:

  • - 原始类型: boolean,char,byte,short,int,long,float,double
  • - 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double

如:

class AutoUnboxingTest {

    public static void main(String[] args) {

        Integer a = new Integer(3);

        Integer b = 3;                  // 将3自动装箱成Integer类型

        int c = 3;

        System.out.println(a == b);     // false 两个引用没有引用同一对象

        System.out.println(a == c);     // true a自动拆箱成int类型再和c比较

    }

}

区别:

  1. Integer是int的包装类,int则是java的一种基本数据类型。
  2. Integer变量必须实例化后才能使用,而int变量不需要。
  3. Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象,而int则是直接存储数据值。
  4. Integer的默认值是null,int的默认值是0.

面试:为什么使用包装类型?

包装类型可以解决一些基本类型解决不了的问题:

1.集合不允许存放基本数据类型。add(Object o)

2.函数需要传递进去的参数为Object类型,传入基本数据类型就不可行。

3.基本类型可以和包装类型之间相互转换,自动装箱拆箱。

4.通过包装类型的parse方法可以实现基本数据类型和String类型之间的相互转换。

假设有这样一个场景,我接收到一个String型的数据想把它转换为整型,如果没有包装类这个操作是无法完成的,有了包装类我们可以这样做:

String num1 = "123";
int num2 = Integer.parseInt(num1);

parseInt就是Integer包装类提供的一个将字符串转成int型的方法。

27、equals和== 有什么区别?

1. 对于字符串的比较“==”比较的是两个字符串的地址

2. 对于字符串的比较 “equals”比较的是两个字符串的内容

<1>先说”==”:

对于基本数据类型 (byte,short,char,int,long,float,double,boolean)的变量”==”比较的是两个变量的值是否相等。

比如:int a = 3; int b = 3; a==b;返回就是true

对于引用类型如,则比较的是该变量所指向的地址.

拿我们最常用的String型来举例:

比如:String a = “abc”; String b = “abc”;

在这种情况下 字符串直接赋值给变量,该字符串会进入到常量池中,当第一次将 “abc”赋值给a的时候,会去常量池中找看有没有”abc”这个字符串,如果有的话,就将a指向该字符串在常量池中的地址,如果没有则在常量池中创建,第二次赋值 将 “abc”赋值给b的时候同样去常量池中找”abc”这个字符串,然后将他的地址赋值给b.

所以我们在做 a==b操作的时候返回的为true

再来看另一种情况:String a = new String(“abc”) ; String b = new String(“abc”);

这种情况下我们在做 a==b操作的时候返回的为false。

当我们使用 String a = new String(“abc”)创建一个字符串对象时在内存里面是这样分配空间的,首先会去常量池中找”abc”如果找到了再创建一个”abc”对象存到堆中,并且他的值指向堆中的”abc”,如果没有找到则先在常量池中创建”abc”,再在堆内存开辟一块内存空间创建”abc”,并且他的值指向堆中的”abc”。

<2>再来看”equals”:

Equals方法是在Object类中定义的,所有的类都继承于Object类,所以所有的类都有equals方法

我们来看看equals方法的源码:

可以看到在Object类的equals方法中也是用的”==”来进行比较,所以在进行比较时它和”==”应该时等价的,但是为什么我们在做 字符串比较的时候 两者比较出来的结果不一样呢?

原因就是 String类型对equals方法进行了重写。我们来看源码:

从源码我们可以看出,在String的equals方法中对字符串的字符进行了逐一比较如果都相同则返回true.所以对于String中的equals方法比较的是两个字符串的内容对于:

String a = new String(“abc”) ; String b = new String(“abc”);

由于a和b的内容相同,返回true.

28、final、finally、finalize的区别

  • final 可以修饰类、变量、方法,修饰类表示该类不能被继承,修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
  • finally一般作用在try-catch代码块中,在处理异常时,通常我们将一定到执行的代码放在finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
  • finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System的gc()方法的时候,由垃圾回收器调用finalize(),回收垃圾。

29、数据库中char与varchar的区别:

char是一种固定长度的类型,varchar是一种可变长度的类型。

  • char如果存入数据的实际长度比指定长度要小,会补空格至指定长度,如果存入的数据的实际长度大于指定长度,低版本会被截取,高版本会报错。
  • varchar类型的数据如果存入的数据的实际长度比指定的长度小,会缩短到实际长度,如果存入数据的实际长度大于指定长度,低版本会被截取,高版本会报错。

char效率会更高,但varchar更节省空间。

30、String、StringBuilder、StringBuffer的区别:

1.String对象一旦创建之后就是不可变的!StringBuilder,StringBuffer类提供的字符串进行修改。当你知道字符数据要改变的时候你就可以使用。

2.StringBuilder是一个可变的字符串类!可以把它看作一个容器

  • StringBuilder 可以通过 toString()方法转换成 String
  • String 可以通过 StringBuilder 的构造方法,转换成 StringBuilder
String string = new StringBuffer().toString();
StringBulider stringBuilder = new StringBulider(new String());

StringBuilder 拼接字符串的效率较高,但是它不是线程安全的!

3.StringBuffer 同样是一个可变的字符串类!也可以被看作是一个容器。

  • StringBuffer 可以通过 toString()方法转换成 String
  • String 可以通过 StringBuffer 的构造方法,转换成 StringBuffer
String string = new StringBuffer().toString();
StringBuffer stringBuffer = new StringBuffer(new String());

Stringbuffer拼接字符串的效率相对于 StringBuilder 较低,但是它是线程安全的!(加了synchronized)
 

31、Java的容器:

容器是一个java所编写的程序,原先必须自行编写程序以管理对象关系,容器会自动帮您做好。

  • 常用的容器:WebSphere,Weblogic,Resinm,Tomcat,Glassfish,spring
  • Java容器类包含:数组和集合容器:list ,Arraylist,Vector及map, hashTable, HashMap, HashSet

 32、怎么快速的把一个list集合中的元素去重?

https://blog.csdn.net/Mcdull__/article/details/118493866

33、二叉树,多叉树,B树,B+树,B*树,二叉搜索树,红黑树

索引是怎么实现的?为什么是B+树?为什么不用二叉树?为什么不用平衡二叉树?

二叉树的操作效率较高,但存在问题。

  1. 二叉树需要加载到内存,如果二叉树的节点少,那没什么问题,但是如果二叉树的节点很多(1亿),就会出现问题。
  2. 问题1:在构建二叉树时,需要多次进行I/O操作(海量数据存在数据库或者文件中),节点海量,构建二叉树时,速度有影响。
  3. 问题2:节点海量,也会造成二叉树的高度很大,会降低操作速度。

多叉树:在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。(2-3树,2-3-4树)

B树(B-tree/B-树)(就是多叉树的一种):B树通过重新组织节点,降低了树的高度,广泛应用于文件系统和数据库系统中。

B即Balanced,平衡的意思。

特点:在叶子节点和非叶子节点都存放数据。

B树为什么要设置成多路?

答:为了进一步降低树的高度。但不能设计成无限多路,因为如果不限制路数,B树就退化成一个有序数组了,而文件系统和数据库索引都是存在硬盘上的,并且数据量大的话,不一定能一次性加载到内存中。

B树做文件系统的索引比较多,MongoDB数据库索引用B树实现,MySQL的Innodb 存储引擎用B+树存放索引。

B+树(B树的变体)(也是多一种多路搜索树):

特点:(1所有的数据(关键字)都放在叶子节点上。

          

B+树做数据库的索引比较多。

这是和业务场景相关的,数据库中select数据,不一定只选一条,很多时候会选多条,比如按照id排序后选10条。使用B树需要做局部的中序遍历,可能要跨层访问。而B+树由于所有数据都在叶子结点,不用跨层,同时由于有链表结构,只需要找到首尾,通过链表就能把所有数据取出来了。

面试题:B+树查询的时间和树的高度有关,大概是log(n),而使用hash存储索引,查询的平均时间是O(1),既然hash比B+树更快,为啥mysql还用b+树来存索引呢?

答:这和业务场景有关。如果只选一个数据,那确实是hash更快。但是数据库中经常会选择多条,这时候由于B+树索引有序,并且又有链表相连,它的查询效率比hash就快很多了。而且数据库中的索引一般是在磁盘上,数据量大的情况可能无法一次装入内存,B+树的设计可以允许数据分批加载,同时树的高度较低,提高查找效率。

B*树(B+树的变体)(在B+树的非根和非叶子结点再增加指向兄弟的指针):

二叉搜索树、红黑树

https://blog.csdn.net/Mcdull__/article/details/118493781

34、ThreadLocal是什么?有哪些使用场景?

定义:ThreadLocal 类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程的变量。

作用:减少同一个线程内多个函数或组件之间公共变量传递的复杂度。

应用场景:ThreadLocal的应用场合,最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

特点:

 ThreadLocal和Synchronized的区别:

虽然ThreadLocal和Synchronized关键字都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同

 内部结构:

  • JDK 早期设计:每个ThreadLocal都创建一个Map, 然后用Thread(线程) 作为Map的key, 要存储的局部变量作为Map的value, 这样就能达到各个线程的局部变量隔离的效果, 这是最简单的设计方法。
  • JDK8 优化设计(现在的设计):每个Thread维护一个ThreadLocalMap, 这个Map的keyThreadLocal实例本身,value才是真正要存储的值Object

如今设计的好处:

  1. 每个Map存储的Entry数量变少
  2. 当Thread销毁的时候, THreadLocalMap也会随之销毁, 减少内存的使用.(之前以Thread为key会导致ThreadLocalMap的生命周期很长)

 面试题:强弱引用和内存泄漏

(ThreadLocal) key是弱引用, 其目的就是将ThreadLocal对象的生命周期和和线程的生命周期解绑, 减少内存使用。

内存泄漏相关概念
内存溢出: Memory overflow 没有足够的内存提供申请者使用.
内存泄漏: Memory Leak 程序中已经动态分配的堆内存由于某种原因, 程序未释放或者无法释放, 造成系统内部的浪费, 导致程序运行速度减缓甚至系统崩溃等严重结果. 内存泄漏的堆积终将导致内存溢出。

面试题1:假设ThreadLocalMap中的key使用了强引用, 那么会出现内存泄漏吗?

 面试题2:假设ThreadLocalMap中的key使用了弱引用, 那么会出现内存泄漏吗?

面试题3: 内存泄漏的真实原因

 面试题4:那为什么key还要使用弱引用呢?

无论 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

​ 要避免内存泄漏有两种方式:
​ 1 .使用完 ThreadLocal ,调用其 remove 方法删除对应的 Entry
​ 2 .使用完 ThreadLocal ,当前 Thread 也随之运行结束

相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的.

​ 也就是说,只要记得在使用完ThreadLocal 及时的调用 remove ,无论 key 是强引用还是弱引用都不会有问题.

那么为什么 key 要用弱引用呢

 事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null (也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么是会对 value 置为 null 的.

这就意味着使用完 ThreadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障弱引用的 ThreadLocal 会被回收对应value在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏.(弱引用的特点:如果一个对象持有弱引用,那么下一次垃圾回收的时候必然会被清理掉。)

面试题:如何使用?

 1)存储用户Session

​ 2)解决线程安全的问题

ThreadLocal具体源码讲解:

https://csp1999.blog.csdn.net/article/details/116604283

https://csp1999.blog.csdn.net/article/details/116721091

面试题小结:

面试题1:请你说一说你对ThreadLocal的理解?

ThreadLocal是一个全局对象,ThreadLocal是线程范围内变量共享的解决方案,ThreadLocal可以看做是一个map集合,key是当前线程,value是要存放的变量。

TheradLocal对象可以给每个线程分配一份属于自己的局部变量副本,多个线程之间可以互不干扰。一般我们会重写initalValue()方法来给当前ThreadLocal对象赋初始值。

面试题2:简单描述一下JDK1.8中,ThreadLocal原理?
JDK1.8中,每个线程对象thread类内部都有一个成员属性threadlocals(即ThreadLocalMap,它是一个Entry[]数组,而不是map集合),各个线程在调用同一个TheradLocal对象的set(value)方法设置值的时候,就是往各自的ThreadLocalMap对象数组中新增值。ThreadLocalMap(Entry[]数组)中存放的是一个个的Entry节点,它有两个属性字段,弱引用key(ThreadLocal对象),和强引用value(当前线程变量副本的值)。

面试题3:ThreadLocal是怎样做到线程相互不干扰的呢(线程隔离)?

首先每个线程Thread都有属于一份自己的ThreadLocalMap用于存储数据。当线程访问某个ThreadLocal对象的get()方法时,方法内部会检测该线程的ThreadLocalMap数组(Entry[])内是否存在key为当前ThreadLocal对象的Entry节点。如果数组内没有对应的节点,那么当前ThreadLocal对象就调用其内部的initialValue()方法创建一个Entry节点存放到ThreadLocalMap中去。

面试题4:ThreadLocal使用的hash是怎样计算得来的?

首先,ThreadLocal使用的hash并不是重写自Object的hashCode()方法,而是通过自身的nextHashCode();计算得来的,代码入下:

// threadLocalHashCode ---> 用于threadLocals的桶位寻址:
// 1.线程获取threadLocal.get()时:
// 		如果是第一次在某个threadLocal对象上get,那么就会给当前线程分配一个value,
// 		这个value 和 当前的threadLocal对象被包装成为一个 entry 
// 		其中entry的 key 是threadLocal对象,value 是threadLocal对象给当前线程生成的value
// 2.这个entry存放到当前线程 threadLocals 这个map的哪个桶位呢? 
//		桶位寻址与当前 threadLocal对象的 threadLocalHashCode有关系:
// 		使用 threadLocalHashCode & (table.length - 1) 计算结果得到的位置就是当前 entry 需要存放的位置。
private final int threadLocalHashCode = nextHashCode();

// nextHashCode: 表示hash值
// 创建ThreadLocal对象时会使用到该属性:
// 每创建一个threadLocal对象时,就会使用 nextHashCode 分配一个hash值给这个对象。
private static AtomicInteger nextHashCode = new AtomicInteger();

// HASH_INCREMENT: 表示hash值的增量~
// 每创建一个ThreadLocal对象,ThreadLocal.nextHashCode的值就会增长HASH_INCREMENT(0x61c88647)。
// 这个值很特殊,它是斐波那契数也叫黄金分割数。
// hash增量为这个数字,带来的好处就是hash分布非常均匀。
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * 返回一个nextHashCode的hash值:
 * 创建新的ThreadLocal对象时,使用这个方法,会给当前对象分配一个hash值。
 */
private static int nextHashCode() {
    // 每创建一个对象,nextHashCode计算得到的hash值就增长HASH_INCREMENT(0x61c88647)
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

/*
 * 初始化一个起始value:
 * 默认返回null,一般情况下都是需要重写这个方法的。
 */
protected T initialValue() {
    return null;
}

面试题5:为什么ThreadLocalMap选择去重新设计"Map",而不直接使用JDK中的HashMap呢?

因为ThreadLocal自己重新设计的Map,它可以把自己的Key限定为特有类型(ThreadLocal),这个特定类型的Key使用的是弱引用WeakReference<ThreadLocal<?>>,而HashMap中的key采用的是强引用方式。

面试题6:ThreadLocalMap的Entry中的key为什么要设置成弱引用?

1、ThreadLocalMap存储的格式是Entry<ThreadLocal,T>,如果使用强引用,当key原来对象失效时,jvm不会回收map里面的ThreadLocal.

2、ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统GC的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value.

3、站在ThreadLocalMap角度就可以区分出哪些Entry是过期的,哪些Entry是非过期的:

  • 例如:在set()方法向下寻找可用 solt 桶位的过程中,如果碰到key == null 的情况,说明当前Entry 是过期数据,这个时候可以强行占用该桶位,通过replaceStaleEntry方法执行替换过期数据的逻辑。
  • 例如:cleanSomeSlots(int i, int n)方法通过遍历桶位,也会将 key == null 过期数据清理掉
     

面试题7:ThreadLocalMap对象是何时第一次被创建的?线程的ThreadLocalMap会被多次创建吗?

每个线程Thread对象的ThreadLocalMap都是延迟初始化的,当我们再调用ThreadLocal对象的set()或get()方法时,它会检测当前线程是否已经绑定了ThreadLocalMap,如果已经绑定,则继续执行get()或者set()方法的逻辑。而如果没有,则会先创建ThreadLocalMap并将其绑定给Thread对象。

而且下次你的ThreadLocalMap也不会被多次创建,在线程的生命周期内,ThreadLocalMap对象只会被初始化一次。

面试题8:ThreadLocalMap的初始化长度是多少呢?为什么初始容量要是2的N此次幂呢?

答:16,它的设计和hashMap是一样的,都是为了方便hash寻址时,得到index(桶位)更均匀分布,减少hash冲突。

寻址算法为:index = threadLocalHashCode & (table.length - 1)。这个算法实际就是取模运算:hash % tab.length,而计算机中直接求余运算效率不如位移运算。所以源码中做了优化,使用 hash & (tab.length- 1)来寻找桶位。而实际上 hash % length 等于 hash & ( length - 1) 的前提是 length 必须为 2 的 n 次幂。
例如,数组长度 tab.length = 8 的时候,3 & (8 - 1) = 3,2 & (8 - 1) = 2,桶的位置是(数组索引) 3 和 2,不同位置上,不发生 hash 碰撞。
 

面试题9:ThreadLocalMap的扩容阈值是多少?它的扩容机制是怎样的?

首先,ThreadLocalMap的扩容阈值为初始容量的2/3,当数组中存储Entry节点的个数大于等于2/3时,它并不会直接开始扩容,而是先调用rehash()方法,在该方法中,全面扫描整个数组,并将数组中过期的数据(key == null)给清理掉,重新整理数据。之后再次重新判断数组内的Entry节点的个数是否达到扩容阈值的3/4,如果达到再调用真正扩容的方法resize().

面试题10:那么你对resize()方法内部的扩容算法了解吗?

  • resize()方法在真正执行扩容时,内部逻辑是先创建一个新的数组,新数组长度是元才能数组长度的2倍。
  • 然后遍历旧数组,将旧数组中的数组重新按照hash算法迁移到新数组里面。
  • 接着重新计算出下次扩容的阈值threshold。
  • 最后更新thread对象的threadLocals字段引用,使其指向新数组。

面试题11:请你说一下ThreadLocal的get方法执行流程?

  • ① 首先 get() 方法中会先获取当前线程对象 t : Thread t = Thread.currentThread();
  • ② 接下来根据 t 获取其独有的 ThreadLocalMap 数组:ThreadLocalMap map = getMap(t);
  • ③ 如果 ② 获取的 map为空,则调用setInitialValue()方法,该方法内部调用 initialValue();方法获取 value,并根据 当前线程t 和 value 调用 createMap(t, value); 方法创建 ThradLocalMap。
  • ④ 如果 ② 获取的 map不为空,则直接调用 ThreadLocalMap.Entry e = map.getEntry(this); 方法通过 this(当前ThreadLocal对象)从 ThreadLocalMap 中获取对应封装数据的 Entry 节点。
  • ⑤ 最终通过 T result = (T)e.value; 得到要获取的线程变量副本的值。
     

注意:

第 ④ 步中,通过当前 ThreadLocal 对象从 ThreadLocalMap 中获取对应封装数据的 Entry 节点时,内部逻辑是需要涉及到桶位寻址 index = threadLocalHashCode & (table.length - 1),如果获取的 inde 桶位中没有目标数据,这时候会执行``nextIndex(int i, int len)方法,**线性的向前或者向后去寻找目标数据所在的桶位,直到遍历整个数组仍未找到,则返回null`**。

此外,在线性的向前、向后遍历数组寻找目标元素所在的桶位时,如果发现数据过期了(key == null),则需要调用expungeStaleEntry(i);方法进行一次探测式过期数据回收。
 

面试题12:请你说一下ThreadLocal的set方法执行流程?

① 首先,set()方法向 ThreadLocalMap 中添加数据时,也是需要根据 Key (ThreadLocal对象) 的去寻址找到要插入的桶位下标 i = key.threadLocalHashCode & (len-1);
② 根据桶位下标,获取对应桶中的Enety 对象Entry e = tab[i];,如果获取的 e 为 null ,则说明是空桶,直接将 Key 和 Value 包装成 Entry 放入桶中即可:tab[i] = new Entry(key, value);
③ 如果第 ② 步骤获取的 e 不为 null,说明不是空桶,则需要从以下三种情况考虑:
如果当前桶中 Entry 的 Key 不是当前 ThreadLocal 对象,且不为 null,则调用nextIndex(int i, int len)方法线性查找下一个空桶位,并将新数据放入。
如果当前桶中 Entry 的 Key 是当前 ThreadLocal 对象,则通过更新操作,将就 Entry 的 Value 值覆盖。
如果如果当前桶中 Entry 的 Key 是null,则说明当前 Entry 已经过期,需要执行 替换过期数据的逻辑: replaceStaleEntry(key, value, i);。
 

35、什么是守护线程(精灵线程)?

守护线程: 为所有非守护线程提供服务的线程。任何一个守护线程都是整个JVM中所有非守护线程的保姆。

 t.setDaemon(true)为守护线程,也叫精灵线程,若主线程启动t线程,则t线程是主线程的守护线程,当主线程执行完了,则守护线程也随之结束。

作用:

GC垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

应用场景:

(1)来为其他线程提供服务支持的情况。

(2)在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确的关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。

注意:

theread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。

在Daemon线程中产生的新线程也是Daemon的。

java自带的多线程框架,比如ExecutrorService,会将守护线程转换为用户线程,所以不能用java的线程池来创建后台线程。

36、什么是序列化和反序列化?反序列化失败的场景?

序列化:对象的状态转化成字节流,以后可以通过这些值再生成相同状态的对象。对象序列化是对象持久化的一种实现方法,它是将对象的属性和方法转化为一种序列化的形式用于存储和传输

反序列化:根据这些保存的信息重建对象的过程。

优点:

  • 实现了数据的持久化,通过序列化可以把数据永久的保存在硬盘上(通常存放在文件里)。
  • 实现了远程通信,即在网络上传送对象的字节序列。

反序列化失败的场景:

因为 Java 的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。

一般来说,定义serialVersionUID的方式有两种,分别为:

  • 采用默认的1L,具体为private static final long serialVersionUID = 1L;
  • 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,例如 private static final long serialVersionUID = XXXL;
     

37、ArrayList和LinkedList的区别和底层实现?如何实现线程安全?

1.ArrayList底层基于数组实现适合对元素进行快速随机访问和遍历,但从ArrayList的中间位置插入或者删除元素时因为要对数组进行复制、移动的代价比较高,因此不适合插入和删除

默认初始容量为10,当数组容量不够时,会触发扩容机制(

int newCapacity = oldCapacity + (oldCapacity >> 1);扩大到当前的1.5倍

)需要将原来的数据复制调新的数组中。

2.LinkedList底层基于双向链表实现,适合数据的动态插入和删除,内部提供了list接口中没有定义的方法,用于操作表头和表尾元素,可以当做堆栈、队列和双向队列使用。

区别:

都是线程不安全的,ArrayList适用于查找的场景,LinkedList适用于增加删除多的场景。

面试:如何实现线程安全?

​ 可以使用原生的Vector,或者是Collections.synchronizedList(List list)函数返回一个线程安全的ArrayList集合,或者使用concurrent并发包下的CopyOnWriteArrayList的。

​ ①Vector: 底层通过synchronize修饰保证线程安全,效率较差

​ ②Collections.synchronizedList(List list):

​ ③CopyOnWriteArrayList写时加锁,使用了一种叫写时复制的方法;读操作是可以不用加锁的

38、fail-fast是什么?fail-safe是什么?

fail-fast:快速失败

当异常产生时,直接抛出异常,程序终止。

 fail-fast主要是体现在当我们在遍历集合元素的时候,经常会使用迭代器,但在迭代器遍历元素的过程中,如果集合的结构被改变的话,就会抛出异常ConcurrentModificationException,防止继续遍历。这就是所谓的快速失败机制。这里要注意的这里说的结构被改变,是例如插入和删除这种操作,只是改变集合里的值的话并不会抛出异常。

fail-safe:安全失败

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。

缺点:基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

39、synchronized(this)和synchronized(class)之间的区别?

对象锁和类锁?

1、对象锁

在java中,每个对象都会有一个monitor对象,这个对象其实就是java对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。

2、类锁

在java中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的Class对象锁。每个类只有一个Class对象,所以每个类只有一个类锁。

synchronized 可以修饰方法和代码块

  • 修饰代码块

    • synchronized(this|object) {}

    • synchronized(类.class) {}

  • 修饰方法

    • 修饰非静态方法

    • 修饰静态方法

总结:

对于静态方法,由于此时对象还未生成,所以只能采用类锁,只要采用类锁,就会拦截所有线程,只能让一个线程访问。

对于对象锁(this),如果是同一个实例,就会按照顺序访问,但如果是不同实例,就可以同时访问。如果对象锁和访问的对象没有关系,那么就会同时访问。

40、list和array相互转换怎么转?

数组转换成list :采用java集合中自带的Arrays.asList(array)方法就可以了

String[] array = new String[] {"zhu", "wen", "tao"};
    // String数组转List集合
    List<String> mlist = Arrays.asList(array);
    // 输出List集合
    for (int i = 0; i < mlist.size(); i++) {
        System.out.println("mlist-->" + mlist.get(i));
    }

list转换成数组:采用集合的toArray()方法直接把List集合转换成数组,这里需要注意,不能这样写:
        String[] array = (String[]) mlist.toArray();
这样写的话,编译运行时会报类型无法转换java.lang.ClassCastException的错误,这是为何呢,这样写看起来没有问题啊,因为java中的强制类型转换是针对单个对象才有效果的,而List是多对象的集合,所以将整个List强制转换是不行的
正确的写法应该是这样的:
        String[] array = mlist.toArray(new String[0]);

List<String> mlist = new ArrayList<>();
    mlist.add("zhu");
    mlist.add("wen");
    mlist.add("tao");
    // List转成数组
    String[] array = mlist.toArray(new String[0]);
    // 输出数组
    for (int i = 0; i < array.length; i++) {
        System.out.println("array--> " + array[i]);
    }

41、线程的五种状态

1、新建状态(new):线程对象被创建后,就进入了新建状态 Thread thread = new Thread();

2、就绪状态(Runnable):调动了该对象的start()方法,启动该线程。thread.start();随时可以被CPU调度执行。

3、运行状态(Running):线程获取CPU权限进行执行。(注意:线程只能从就绪状态进入到运行状态)

4、阻塞状态(Blocked):线程因为某种原因放弃CPU执行权,暂时停止执行,直到线程进入到就绪状态,才有机会转到运行状态。

阻塞的情况分为三种:

(1)等待阻塞:调用wait()方法,让线程等待某工作的完成(释放锁

(2)同步阻塞:线程在获取synchnorized同步锁失败(锁被其他线程占用)

(3)其他阻塞:通过调用sleep()或join()或发出了I/O请求时,线程进入阻塞状态,当sleep()状态超时,join()等待线程终止或超时,或者I/O处理完毕时,线程重新进入就绪状态。(不释放锁

5、死亡状态(dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

wait和sleep区别:

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁

wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。调用wait会释放锁。
 

42、设计模式你知道哪些?线程安全的单例模式怎么写?

设计模式:https://blog.csdn.net/Mcdull__/article/details/114935147?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162702642116780357279222%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=162702642116780357279222&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_v2~rank_v29-1-114935147.pc_v2_rank_blog_default&utm_term=%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F&spm=1018.2226.3001.4450

43、JDK1.8的新特性:

https://blog.csdn.net/Mcdull__/article/details/112464042 

44、关于数组的时间复杂度

算法时间复杂度
线性查找O(N)
二分查找O(logN)
无序数组的插入O(1)
有序数组的插入O(N)
无序数组的删除O(N)
有序数组的删除O(N)

45、二分查找和八大排序算法

 二分查找:

也称折半查找(Binary Search),它是一种效率较高的查找方法,前提是数据结构必须先排好序,可以在数据规模的对数时间复杂度内完成查找。但是,二分查找要求线性表具有有随机访问的特点(例如数组),也要求线性表能够根据中间元素的特点推测它两侧元素的性质,以达到缩减问题规模的效果。
 

例:

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 :

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums中并且下标为 4
//二分法模板
class Solution {
    public int search(int[] nums, int target) {
        int l=0,r=nums.length-1,m=r/2; //l表示左边,r表示右边
        while(l<=r){
            if(nums[m]==target){
                return m;
            }
            if(nums[m]<target){
                l=m+1; 
            }else{
                r=m-1;
            }
            m=l+(r-l)/2;//注意这里是r-l,不是r-1
        }
        return -1;
    }
}

八大排序:

https://blog.csdn.net/Mcdull__/article/details/112206344

46、方法重写和方法重载

重写(Override):子类对父类的允许访问的方法的实现过程进行重新编写,形参和返回值都不能改变

重载(Overload):是在一个类里面,方法名字相同,而参数不同,返回类型可以相同也可以不同。

47、为什么更多的选择组合而不是继承?

继承是is-a ,组合是has-a

继承的缺点:

  1. 在java中,类只能单继承。无法通过继承的方式,重用多个类中的代码。
  2. 父类的属性和方法,子类无条件全部继承。这样很容易造成方法的污染。(如果人类继承于鸟类,我们希望拥有的是:鸟的翅膀和飞的行为。但是,鸟还有吃虫的行为,鸟还有下蛋的行为。这些是我们不希望拥有的。)
  3. 从父类继承而来的实现是静态的,不能在运行时发生改变,不够灵活。

而通过组合关系,可以解决继承的缺点,由于一个类可以建多个属性,也就是可以聚合多个类。所以通过组合关系,重用多个类中的代码。

48、java有哪些不好的设计?

  1. Java的泛型不支持基本类型,需要基本类型的话就要装箱拆箱,空间跟性能都比直接使用基本类型要糟糕。
  2. 单继承

49、递归的优缺点:

优点:

1、代码简洁

2、易于理解

如在树的前/中/后序遍历中,递归的实现明显比循环简单。

缺点:

1、时间和空间的消耗比较大

递归由于是函数调用自身,而函数的调用时消耗时间和空间的,每一次函数调用,都需要在内存栈中分配空间以保存参数,返回值和临时变量,而往栈中压入和弹出数据也都需要时间,所以降低了效率。

2、重复计算

递归中又很多计算都是重复的,递归的本质时把一个问题分解成两个或多个小 问题,多个小问题存在重叠的部分,即存在重复计算,如斐波那契数列的递归实现。

3、调用栈溢出

递归可能时调用栈溢出,每次调用时都会在内存栈中分配空间,而栈空间的容量是有限的,当调用的次数太多,就可能会超出栈的容量,进而造成调用栈溢出。
 

50、Integer类能否被继承?String类能否继承?

Integer:

public final class Integer extends Number implements Comparable<Integer>

final修饰,很显然,不可被继承,即不可被扩展。

String:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence 

不能被继承,因为String类有final修饰符,而final修饰的类是不能被继承的。

51、java线程中start()和run()方法的区别?

start():启动新线程,需要一个主线程来启动新的子线程,使其处于就绪状态(不能执行两次调用start()方法,否则会报错)

run():只是线程的一个普通方法(必须要重写)。

当用start()开始一个线程后,线程就进入就绪状态,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。但是这并不意味着线程就会立即运行。只有当cpu分配时间片时,这个线程获得时间片时,才开始执行run()方法。start()是方法,它调用run()方法.而run()方法是你必须重写的. run()方法中包含的是线程的主体(真正的逻辑)。

例:继承Thread类的启动方式

public class ThreadTest {
    public static void main(String[] args) {
        MyThread t =new MyThread();
        t.start();
    }
}
class MyThread extends Thread{
    @Override
         public void run() {
        System.out.println("Hello World!");
    }
}

52.一个四核CPU在一个时间片上能执行多少个线程?

CPU的核心数是指物理上,也就是硬件上存在着几个核心。比如,双核就是包括2个相对独立的CPU核心单元组,四核就包含4个相对独立的CPU核心单元组,等等,依次类推。

线程数是一种逻辑的概念,简单地说,就是模拟出的CPU核心数。比如,可以通过一个CPU核心数模拟出2线程的CPU,也就是说,这个单核心的CPU被模拟成了一个类似双核心CPU的功能。我们从任务管理器的性能标签页中看到的是两个CPU。

比如:

Intel 赛扬G460是单核心双线程的CPU,

Intel 酷睿i3 3220是双核心 四线程

Intel 酷睿i5 4570是四核心 四线程等等,

Intel 酷睿i7 4770K是四核心 八线程

53.Nginx除了负载均衡,还能做什么呢?

https://zhuanlan.zhihu.com/p/114594862

1.负载均衡:Nginx通过反向代理合一实现服务的负载均衡,避免了服务器单点故障,把请求按照一定的策略转发到不同的服务器上,达到负载的效果。

2.静态代理:Nginx擅长处理静态文件,是非常好的图片、文件服务器。把所有的静态资源放到nginx上,可以使应用动静分离,性能更好。

3.限流

4.缓存:浏览器缓存,静态资源缓存用expire;代理层缓存。

5.黑白名单

54、秒杀怎么解决超卖现象?

使用乐观锁(根据版本号)。在实际减库存的SQL操作中,首先判断version是否是我们查询库存时候的version,如果是,扣减库存,成功抢购。如果发现version变了,则不更新数据库,返回抢购失败。

55、谈谈你对多态的理解?

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。

现实生活中,同一个方法,具体实现会完全不同。 比如:同样是调用人的“休息”方法,张三是睡觉,李四是旅游,王五是敲代码,数学教授是做数学题; 同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。

多态的要点:

多态是方法的多态,不是属性的多态。

多态有三个必要条件:继承,方法重写,父类引用指向子类对象。

父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就有出现了。

作者:寻觅
链接:https://zhuanlan.zhihu.com/p/63878686
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

class Animal {
    public void shout() {
        System.out.println("叫了一声!");
    }
}
class Dog extends Animal {
    public void shout() {
        System.out.println("旺旺旺!");
    }
   public void seeDoor() {
        System.out.println("看门中....");
   }
}
class Cat extends Animal {
    public void shout() {
        System.out.println("喵喵喵喵!");
    }
}
public class TestPolym {
    public static void main(String[] args) {
        Animal a1 = new Cat(); // 向上可以自动转型
        //传的具体是哪一个类就调用哪一个类的方法。大大提高了程序的可扩展性。
        animalCry(a1);
        Animal a2 = new Dog();
        animalCry(a2);//a2为编译类型,Dog对象才是运行时类型。
       // a2.seeDoor();//编译错误,无法调用子类独有的方法
         
        //编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
        // 否则通不过编译器的检查。
        Dog dog = (Dog)a2;//向下需要强制类型转换
    }
 
    // 有了多态,只需要让增加的这个类继承Animal类就可以了。
    public static void animalCry(Animal a) {
        a.shout();
    }
 
    /* 如果没有多态,我们这里需要写很多重载的方法。
    public static void animalCry(Dog d) {
        d.shout();
    }
    public static void animalCry(Cat c) {
        c.shout();
    }
    */
}

上述是多态最为多见的一种用法,即父类引用做方法的形参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。

由此,我们可以看出多态的主要优势是提高了代码的可扩展性,符合开闭原则。但是多态也有弊端,就是无法调用子类特有的功能,比如,我不能使用父类的引用变量调用Dog类特有的seeDoor()方法。

56、接口和抽象类区别?

接口是对动作的抽象,抽象类是对根源的抽象。

抽象类表示的是,这个对象是什么,接口表示的是,这个对象能做什么。比如男人女人,这两个类,他们的抽象类 是人。人可以吃东西,狗也可以吃东西,就可以把“吃东西”定义成一个接口,然后让这些类去实现它。

所以,在高级语言上,一个类只能继承一个类(抽象类)(正如人不可能同时是生物和非生物),但是可以实现多个接口(吃饭接口、走路接口)。

  • 第一点. 接口是抽象类的变体,接口中所有的方法都是抽象的。而抽象类是声明方法的存在而不去实现它的类。
  • 第二点. 接口可以多继承,抽象类不行
  • 第三点. 接口定义方法,不能实现,而抽象类可以实现部分方法。
  • 第四点. 接口中基本数据类型为static 而抽类象不是的。

当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。


抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度的
 

57、java和python的区别?

1.Python比Java简单,学习成本低,开发效率高

2.Java运行效率高于Python,尤其是纯Python开发的程序,效率极低

3.Java相关资料多,尤其是中文资料

4.Java版本比较稳定,Python2和3不兼容导致大量类库失效

5.Java开发偏向于软件工程,团队协同,Python更适合小型开发

6.Java偏向于商业开发,Python适合于数据分析

7.Java是一种静态类型语言,Python是一种动态类型语言

8.Java中的所有变量需要先声明(类型)才能使用,Python中的变量不需要声明类型

9.Java编译以后才能运行,Python直接就可以运行;

10.JAVA 里的块用大括号对包括,Python 以冒号 + 四个空格缩进表示。

11.JAVA 的类型要声明,Python 的类型不需要。

12.JAVA 每行语句以分号结束,Python 可以不写分号。

13.实现同一功能时,JAVA 要敲的键盘次数一般要比 Python 多。

58、如何设计一个不可变类?

不可变类:实例创建后状态不能被修改的类。如String,基本类型的包装类等。他们的实例信息是由调用构造函数时就提供,不会提供对外的设值方法,保证他们的状态不会被改变。

设计原则:

  1.  不提供对外修改对象状态的设值方法。
  2. 声明的所有域都是final类型
  3. 保证类不会被扩展,正确的被创建。

需要注意:并不是所有的域都必须声明为final类型,具体可参考String类中的hash字段;即使对象中的所有的域都是final类型的,也不能保证对象是不可变的,因为在final类型的域中可能指向的是可变对象

59、深拷贝和浅拷贝的区别?怎么实现?

浅拷贝:java中Object下的clone方法是浅拷贝,针对引用类型执行的是同一个地址。

深拷贝:三种方法:

1.直接通过new对象创建新的对象,通过new出来的对象肯定是在内存中重新开辟一块空间存储所有可以实现深拷贝。

2.通过调用父类clone再进行重新复制,虽然调用父类Native修饰的clone方法比第一种方式速度快,此步骤如果继承类中有多个引用类型克隆相对麻烦。

3.通过序列化和反序列化使用流进行深拷贝(注意类都要实现Serializable接口),因为保存在流中的数据相当于新的,若要实现对象深拷贝,推荐使用此方法。

60.拦截器和过滤器的区别

61.双向链表如何插入?

 

62、Java中synchronized用在静态方法和非静态方法上面的区别

  在Java中,synchronized是用来表示同步的,我们可以synchronized来修饰一个方法。也可以synchronized来修饰方法里面的一个语句块。那么,在static方法和非static方法前面加synchronized到底有什么不同呢?大家都知道,static的方法属于类方法,它属于这个Class(注意:这里的Class不是指Class的某个具体对象),那么static获取到的锁,是属于类的锁。而非static方法获取到的锁,是属于当前对象的锁。所以,他们之间不会产生互斥。

63、hashTable的key和value为什么都不能为null?而hashMap可以?

源码:https://blog.csdn.net/u010791410/article/details/102495881

应用: 

这个问题还要从HashMap和HashTable的区别来说,HashTable内的方法是同步的,而HashMap不是;所以一般来讲,HashMap不是线程安全的,一般只用于单线程中;而HashTable则往往用于多线程中;

在允许key - value为null的情况下,考虑下面一个场景:

map.get(key) 的返回结果是null,那么是因为不存在对应的key是null呢,还是key对应的value就是null;

对于单线程来讲,这个问题是可以解决的,通过map.contains(key)就可以判断,但是对于多线程来讲,要解决这个问题就很复杂了,必须由外部保证contains 与 get操作的原子性,正是出于对这个问题考虑,所以不允许value为null;(实际上HashTable中并没有提供contains方法,也是因为这个原因)

那么为什么key也不能是null呢?

由于null不是对象,因此不能在其上调用.equals()或.hashCode(),因此Hashtable无法将其计算哈希值以用作键。但是HashMap对此做了特殊处理;


Spring 


1、Spring容器是什么?

从概念上讲:Spring 容器是 Spring 框架的核心,是用来管理对象的。容器将创建对象,把它们连接在一起,配置它们,并管理他们的整个生命周期从创建到销毁。

从具象化讲:通过概念的描述有些同学还是一脸懵逼,在我们的项目中哪个东西是Spring容器?在java项目中,我们使用实现了org.springframework.context.ApplicationContext接口的实现类。在web项目中,我们使用spring.xml——Spring的配置文件。

从代码上讲:一个Spring容器就是某个实现了ApplicationContext接口的类的实例。也就是说,从代码层面,Spring容器其实就是一个ApplicationContext(一个实例化对象)。

什么是Spring?

Spring是于2003年兴起的一个轻量级java开发框架,他是为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面编程(AOP)

Spring的作用就是为代码解耦合,降低代码的耦合度。就是让对象和对象(模块和模块)之间关系不是使用代码关联,而是通过配置来说明,

  • IOC又称自动注入,注入即赋值,IOC使的主业务在相互调用的过程中,不用再自己维护关系了,即不用自己再创建要使用的对象了,而是右Spring容器统一管理。
  • AOP是动态代理的规范化,AOP是Spring框架中的一个重要内容,利用AOP可以对业务逻辑的各个部分进行隔离,从而使业务逻辑的各个部分之间的耦合度降低,提高程序的可重复性,同时提高了开发的效率。

2、配置文件application和bootstrap的应用场景:

application:配置文件这个容易理解,主要用于SpringBoot项目的自动化配置。

bootstrap:配置文件有以下几个应用场景:

  • 使用SpringCloud Config配置中心时,这时需要在bootstrap配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
  • 一些固定的不能被覆盖的属性;
  • 一些加密/解密的场景。

application.yml 是用户级别的配置,而bootstrap.yml 是系统级别的配置

3、JDK动态代理和CGLIB代理有什么区别?

JDK动态代理主要针对类实现了某个接口AOP则会使用JDK动态代理(spring的代理模式)。他基于反射机制实现,生成一个实现同样接口的代理类,然后通过重写的方式,实现读代码的增强。

而如果某个类没有实现接口,AOP则会使用CGLIB代理。他的底层原理是基于ASM第三方框架,通过修改字节码生成一个子类,然后重写父类的方法,实现对代码的增强。

4、Spring Bean的生命周期?(五个阶段)

bean的实例化阶段:创建一个bean对象

bean实例的属性填充阶段:为bean实例的属性赋值

bean实例的初始化阶段:对bean实例进行初始化

bean实例的正常使用阶段

bean实例的销毁阶段:容器关闭后,将bean实例销毁

5、spring bean 的作用域之间有什么区别?

在spring的配置文件中,给bean加上scope属性来·指定bean的作用域如下:

  • singleton:默认值,当IOC容器一创建就会创建bean的实例,而且是单例的,每次得到的都是同一个。
  • prototype:原型的,当IOC容器一创建不在实例化该bean,每次调用getBean方法是再实例化该bean,而且每次得到的都不一样。
  • request:每次请求实例化一个bean。
  • session:在一次会话中共享一个bean。

6、请简单介绍Spring支持的常用数据库事务传播属性?隔离级别?

事务的传播属性:propagation,一个方法在运行了一个开启了事务的方法中,当前方法是使用原来的事务还是开启一个新的事务。

 主要用的是前两种。

事务的传播属性可以在@Transactional注解的propagation中定义。

7、BeanFactory、FactoryBean、ApplicationContext有什么区别?

  • BeanFactory是一个Bean工厂,使用简单工厂模式,是Spring IOC 容器顶级接口,是用于管理Bean的工厂,最核心的功能是通过getBean()方法加载Bean()对象,通常我们不会直接使用该接口,而是使用其子接口ApplicationContext。
  • FactoryBean:英文单词工厂在前,是一个工厂Bean,使用了工厂方法模式,实现该接口的类可以自己定义要创建的Bean实例,只需要实现它的getObject()方法即可。
  • ApplicationContext:是BeanFactory的子接口,扩展了BeanFactory的功能(高级IOC容器)

8、Spring MVC的工作流程?

第一步:发起请求到前端控制器(DispatcherServlet)

第二步:前端控制器请求HandlerMapping查找 Handler (可以根据xml配置、注解进行查找)

第三步:处理器映射器HandlerMapping向前端控制器返回Handler,HandlerMapping会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象,多个HandlerInterceptor拦截器对象),通过这种策略模式,很容易添加新的映射策略

第四步:前端控制器调用处理器适配器去执行Handler

第五步:处理器适配器HandlerAdapter将会根据适配的结果去执行Handler

第六步:Handler执行完成给适配器返回ModelAndView

第七步:处理器适配器向前端控制器返回ModelAndView (ModelAndView是springmvc框架的一个底层对象,包括 Model和view)

第八步:前端控制器请求视图解析器去进行视图解析 (根据逻辑视图名解析成真正的视图(jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可

第九步:视图解析器向前端控制器返回View

第十步:前端控制器进行视图渲染 (视图渲染将模型数据(在ModelAndView对象中)填充到request域)

第十一步:前端控制器向用户响应结果

8、面试题一:你肯定知道spring,那说说aop的全部通知顺序,springboot或springboot2对aop的执行顺序影响?说说你使用aop中碰到的坑?

AOP常用注解:

@Before 前置通知:目标方法之前执行

@After 后置通知:目标方法之后执行(始终执行,类似finally)

@AfterReturning 返回后通知:执行方法结束前执行(异常不执行)

@AfterThrowing 异常通知:出现异常时候执行

@Around 环绕通知:环绕目标方法执行
 

spring4:

    spring4下AOP执行顺序:

  • 正常情况下:@Before前置通知----->@After后置通知----->@AfterRunning正常返回
  • 异常情况下:@Before前置通知----->@After后置通知----->@AfterThrowing方法异常

spring5:

从5以后,最后执行的都是after

9、面试题二:spring循环依赖相关

你解释下spring中的三级缓存分别是什么?三个Map有什么异同?

什么是循环依赖?请你谈谈?看过spring源码吗?一般我们说的spring容器是什么?

如何检测是否存在循环依赖?实际开发中见过循环依赖的异常吗?

多例的情况下,循环依赖问题为什么无法解决?

什么是循环依赖?

多个bean之间相互依赖,形成了一个闭环。比如:A依赖于B,B依赖于C,C依赖于A.

通常来说,如果问Spring容器内部如何解决循环依赖,一定是指默认的单例Bean中,属性相互引用的场景。

如何检测是否存在循环依赖?

使用一个列表来记录正在创建中的bean,bean创建之前,先去纪录一下自己是否已经在列表中了,如果在,说明存在循环依赖,如果不在,则将其加入这个列表,bean创建完成之后,再将其从这个列表中移除。

源码方面来看一下,spring创建单例bean时候,会调用下面方法

protected void beforeSingletonCreation(String beanName) {
        if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
            throw new BeanCurrentlyInCreationException(beanName);
        }
    }

singletonsCurrentlyInCreation就是用来记录目前正在创建中的bean名称列表,this.singletonsCurrentlyInCreation.add(beanName)返回false,说明beanName已经在当前列表中了,此时会抛循环依赖的异常BeanCurrentlyInCreationException,这个异常对应的源码:

public BeanCurrentlyInCreationException(String beanName) {
        super(beanName,
                "Requested bean is currently in creation: Is there an unresolvable circular reference?");
    }

上面是单例bean检测循环依赖的源码,再来看看非单例bean的情况。

以prototype情况为例,源码位于org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean方法中,将主要代码列出来看一下:

//检查正在创建的bean列表中是否存在beanName,如果存在,说明存在循环依赖,抛出循环依赖的异常
if (isPrototypeCurrentlyInCreation(beanName)) {
    throw new BeanCurrentlyInCreationException(beanName);
}

//判断scope是否是prototype
if (mbd.isPrototype()) {
    Object prototypeInstance = null;
    try {
        //将beanName放入正在创建的列表中
        beforePrototypeCreation(beanName);
        prototypeInstance = createBean(beanName, mbd, args);
    }
    finally {
        //将beanName从正在创建的列表中移除
        afterPrototypeCreation(beanName);
    }
}

常见的2种注解 信息定义bean的方式:

  1. 类上标注@Compontent注解来定义一个bean

  2. 配置类中使用@Bean注解来定义bean

Spring创建bean主要的几个步骤:

  • 步骤1:实例化bean,即调用构造器创建bean实例
  • 步骤2:填充属性,注入依赖的bean,比如通过set方式,@Autowired注解的方式注入依赖的bean
  • 步骤3:bean的初始化,比如调用init方法等

从上面3个步骤中可以看出,注入依赖的对象,有2种情况:

  1. 通过步骤1中构造器的方式注入依赖

  2. 通过步骤2注入依赖

两种注入方式对循环依赖的影响

循环依赖现象在spring容器中注入依赖的对象,有2种情况

  • 构造器方式注入依赖(不可行)
  • 以set方式注入依赖(可行)

我们AB循环依赖问题只要A的注入方式是setter且singleton ,就不会有循环依赖问题,而使用构造器注入会产生循环依赖问题。

多例的情况下,循环依赖问题为什么无法解决?

只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,每次都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。

那就会有下面几种情况需要注意。

还是以2个bean相互依赖为例:serviceA和serviceB

情况1

条件

serviceA:多例

serviceB:多例

结果

此时不管是任何方式都是无法解决循环依赖的问题,最终都会报错,因为每次去获取依赖的bean都会重新创建。

情况2

条件

serviceA:单例

serviceB:多例

结果

若使用构造器的方式相互注入,是无法完成注入操作的,会报错。

若采用set方式注入,所有bean都还未创建的情况下,若去容器中获取serviceB,会报错,为什么?我们来看一下过程:

1.从容器中获取serviceB
2.serviceB由于是多例的,所以缓存中肯定是没有的
3.检查serviceB是在正在创建的bean名称列表中,没有
4.准备创建serviceB
5.将serviceB放入正在创建的bean名称列表中
6.实例化serviceB(由于serviceB是多例的,所以不会提前暴露,必须是单例的才会暴露)
7.准备填充serviceB属性,发现需要注入serviceA
8.从容器中查找serviceA
9.尝试从3级缓存中找serviceA,找不到
10.准备创建serviceA
11.将serviceA放入正在创建的bean名称列表中
12.实例化serviceA
13.由于serviceA是单例的,将早期serviceA暴露出去,丢到第3级缓存中
14.准备填充serviceA的属性,发现需要注入serviceB
15.从容器中获取serviceB
16.先从缓存中找serviceB,找不到
17.检查serviceB是在正在创建的bean名称列表中,发现已经存在了,抛出循环依赖的异常

例:spring循环依赖纯java代码验证案例

Spring容器循环依赖报错演示BeanCurrentlylnCreationException

1.构造器方式注入依赖(不可行)

@Component
public class ServiceB{
    private ServiceA serviceA;
    
    public ServiceB(ServiceA serviceA){ //有参构造
        this.serviceA = serviceA;
    }
}
@Component
public class ServiceA{
    private ServiceB serviceB;
    
    public ServiceA(ServiceB serviceB){//有参构造
        this.serviceB = serviceB;
    }
}
public class ClientConstructor{
    public static void main(String[] args){
        new ServiceA(new ServiceB(new ServiceA()));//这会抛出编译异常
    }
}
1.spring轮询准备创建2个bean:serviceA和serviceB
2.spring容器发现singletonObjects中没有serviceA
3.调用serviceA的构造器创建serviceA实例
4.serviceA准备注入依赖的对象,发现需要通过setServiceB注入serviceB
5.serviceA向spring容器查找serviceB
6.spring容器发现singletonObjects中没有serviceB
7.调用serviceB的构造器创建serviceB实例
8.serviceB准备注入依赖的对象,发现需要通过setServiceA注入serviceA
9.serviceB向spring容器查找serviceA
10.此时又进入步骤2了

卧槽,上面过程死循环了,怎么才能终结?

可以在第3步后加一个操作:将实例化好的serviceA丢到singletonObjects中,此时问题就解决了。

spring中也采用类似的方式,稍微有点区别,上面使用了一个缓存,而spring内部采用了3级缓存来解决这个问题,我们一起来细看一下。

2.以set方式注入依赖(可行)

@Component
public class ServiceBB{
    private ServiceAA serviceAA;
    
    public void setServiceAA(ServiceAA serviceAA){
        this.serviceAA = serviceAA;
        System.out.println("B里面设置了A");
    }
}
@Component
public class ServiceAA{
    private ServiceBB serviceBB;
    
    public void setServiceBB(ServiceBB serviceBB){
        this.serviceBB = serviceBB;
        System.out.println("A里面设置了B");
    }
}
public class ClientSet{
    public static void main(String[] args){
        //创建serviceAA
        ServiceAA a = new ServiceAA();
        //创建serviceBB
        ServiceBB b = new ServiceBB();
        //将serviceA入到serviceB中
        b.setServiceAA(a);
        //将serviceB法入到serviceA中
        a.setServiceBB(b);
    }
}

输出结果:

B里面设置了A
A里面设置了B

例:spring循环依赖bug演示

beans:A,B

public class A {

	private B b;

	public B getB() {
		return b;
	}

	public void setB(B b) {
		this.b = b;
        System.out.println("A call setB.");
	}
}
public class B {

	private A a;

	public A getA() {
		return a;
	}

	public void setA(A a) {
		this.a = a;
        System.out.println("B call setA.");
	}	
}

运行类

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ClientSpringContainer {

	public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
		A a = context.getBean("a", A.class);
		B b = context.getBean("b", B.class);
	}
}

默认的单例(Singleton)的场景是支持循环依赖的,不报错

beans.xml

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
    
    <bean id="a" class="com.lun.interview.circular.A">
    	<property name="b" ref="b"></property>
    </bean>
    <bean id="b" class="com.lun.interview.circular.B">
    	<property name="a" ref="a"></property>
    </bean>
    
</beans>

输出结果

00:00:25.649 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6d86b085
00:00:25.828 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [beans.xml]
00:00:25.859 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'a'
00:00:25.875 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'b'
B call setA.
A call setB.

原型(Prototype)的场景是不支持循环依赖的,会报错

beans.xml

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
    
    <bean id="a" class="com.lun.interview.circular.A" scope="prototype">
    	<property name="b" ref="b"></property>
    </bean>
    <bean id="b" class="com.lun.interview.circular.B" scope="prototype">
    	<property name="a" ref="a"></property>
    </bean>
    
</beans>

输出结果

00:01:39.904 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6d86b085
00:01:40.062 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [beans.xml]
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'a' defined in class path resource [beans.xml]: Cannot resolve reference to bean 'b' while setting bean property 'b'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'b' defined in class path resource [beans.xml]: Cannot resolve reference to bean 'a' while setting bean property 'a'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:342)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1697)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1442)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:593)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:342)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1115)
	at com.lun.interview.circular.ClientSpringContainer.main(ClientSpringContainer.java:10)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'b' defined in class path resource [beans.xml]: Cannot resolve reference to bean 'a' while setting bean property 'a'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:342)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1697)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1442)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:593)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:342)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330)
	... 9 more
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:268)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330)
	... 17 more

重要结论:

spring内部通过3级缓存来解决循环依赖   DefaultSingletonBeanRegister

只有单例的bean会通过3级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到3级缓存中。

第一级缓存(也叫单例池)singletonObjects:存放已经经历了完整生命周期的bean对象(成品)

第二级缓存:earlySingletonObjects,存放早期暴露出来的bean对象,Bean的生命周期未结束(属性还未填充完)  (半成品)

第三级缓存:sigletonFactories,存放可以生成bean的工厂。

package org.springframework.beans.factory.support;

...

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

	...

	/** Cache of singleton objects: bean name to bean instance. */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/** Cache of singleton factories: bean name to ObjectFactory. */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

	/** Cache of early singleton objects: bean name to bean instance. */
	private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
 
    ...
    
}

spring循环依赖debug前置知识

实例化和初始化:

实例化 - 内存中申请一块内存空间,如同租赁好房子,自己的家具还未搬来。

初始化-属性填充 - 完成属性的各种赋值,如同装修,家具,家电进场。

3个Map和四大方法,总体相关对象

  • 第一级缓存singletonObjects存放的是已经初始化好了的Bean,
  • 第二级缓存earlySingletonObjects存放的是实例化了,但是未初始化的Bean,
  • 第三级缓存singletonFactories存放的是FactoryBean。假如A类实现了FactoryBean,那么依赖注入的时候不是A类,而是A类产生的Bean
package org.springframework.beans.factory.support;

...

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

	...

	/** 
	单例对象的缓存:bean名称—bean实例,即:所谓的单例池。
	表示已经经历了完整生命周期的Bean对象
	第一级缓存
	*/
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/**
	早期的单例对象的高速缓存: bean名称—bean实例。
	表示 Bean的生命周期还没走完(Bean的属性还未填充)就把这个 Bean存入该缓存中也就是实例化但未初始化的 bean放入该缓存里
	第二级缓存
	*/
	private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

	/**
	单例工厂的高速缓存:bean名称—ObjectFactory
	表示存放生成 bean的工厂
	第三级缓存
	*/
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
 
    ...
    
}

A / B两对象在三级缓存中的迁移说明

B里面设置了A
A里面设置了B

  1. A创建过程中需要B,于是A将自己放到三级缓存里面,去实例化B。
  2. B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A。
  3. B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态),然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。
     

spring循环依赖debug源码(很难很重要)

Spring解决循环依赖依靠的是Bean的“中间态”这个概念,而这个中间态指的是已经实例化但还没初始化的状态-----半成品

实例化的过程又是通过构造器创建的,如果A还没有创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决、

Spring为了解决单例的循环依赖问题,使用了三级缓存。

其中一级缓存为单例池(singletonObjects)

二级缓存为提前曝光对象(earlySingletonObjects)

三级缓存为提前曝光对象工厂(singletonFactories)

假设A、B循环引用,实例化A的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了B,同样的流程将B也是实例化后放入三级缓存,接着去填充属性时又发现自己依赖A,这时候从缓存中查找到早期暴露的A,如果没有AOP代理的话,直接将A的原始对象注入B,完成B的初始化后,进行属性填充和初始化,这时候B完成后,就去完成剩下的A的步骤,如果有AOP代理,就进行AOP处理获取代理后的对象A,注入B,走剩下的流程。

小总结:

Spring创建bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化

每次创建bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个。

当我们创建 beanA的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了beanB,接着就又去创建beanB,同样的流程,创建完beanB填充属性时又发现它依赖了beanA又是同样的流程,
不同的是:这时候可以在三级缓存中查到刚放进去的原始对象beanA.所以不需要继续创建,用它注入 beanB,完成 beanB的创建

既然 beanB创建好了,所以 beanA就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成。

为什么要用3级缓存?

如果只使用2级缓存,直接将实例化好的bean暴露给二级缓存是否可以?

不行!!!

原因早期暴露给其他依赖者的bean和最终暴露的bean不一致的问题!!!

若将刚刚实例化好的bean直接丢到二级缓存中暴露出去,如果后期这个bean对象被更改了,比如可能在上面加了一些拦截器,将其包装为一个代理了,那么暴露出去的bean和最终的这个bean就不一样,将自己暴露出去的时候是一个原始对象,而自己最终却是一个代理对象,最终会导致被暴露出去的和最终的bean不是同一个bean的,将产生意想不到的效果,而三级缓存就可以发现这个问题,会报错。

下面我们通过代码来演示一下效果。

案例

下面来2个bean,相互依赖,通过set方法相互注入,并且其内部都有一个m1方法,用来输出一行日志。

Service1

package com.javacode2018.lesson003.demo2.test3;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Service1 {
    public void m1() {
        System.out.println("Service1 m1");
    }

    private Service2 service2;

    @Autowired
    public void setService2(Service2 service2) {
        this.service2 = service2;
    }

}

Service2

package com.javacode2018.lesson003.demo2.test3;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Service2 {

    public void m1() {
        System.out.println("Service2 m1");
        this.service1.m1();//@1
    }

    private Service1 service1;

    @Autowired
    public void setService1(Service1 service1) {
        this.service1 = service1;
    }

    public Service1 getService1() {
        return service1;
    }
}

注意上面的@1,service2的m1方法中会调用service1的m1方法。

需求

在service1上面加个拦截器,要求在调用service1的任何方法之前需要先输出一行日志

你好,service1

实现

新增一个Bean后置处理器来对service1对应的bean进行处理,将其封装为一个代理暴露出去。

package com.javacode2018.lesson003.demo2.test3;

import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
public class MethodBeforeInterceptor implements BeanPostProcessor {
    @Nullable
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if ("service1".equals(beanName)) {
            //代理创建工厂,需传入被代理的目标对象
            ProxyFactory proxyFactory = new ProxyFactory(bean);
            //添加一个方法前置通知,会在方法执行之前调用通知中的before方法
            proxyFactory.addAdvice(new MethodBeforeAdvice() {
                @Override
                public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
                    System.out.println("你好,service1");
                }
            });
            //返回代理对象
            return proxyFactory.getProxy();
        }
        return bean;
    }
}

上面的postProcessAfterInitialization方法内部会在service1初始化之后调用,内部会对service1这个bean进行处理,返回一个代理对象,通过代理来访问service1的方法,访问service1中的任何方法之前,会先输出:你好,service1

代码中使用了ProxyFactory

来个配置类

@ComponentScan
public class MainConfig3 {

}

来个测试用例

@Test
public void test3() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig3.class);
    context.refresh();
}

运行:报错了

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'service1': Bean with name 'service1' has been injected into other beans [service2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:624)

可以看出是AbstractAutowireCapableBeanFactory.java:624这个地方整出来的异常,将这块代码贴出来给大家看一下:

if (earlySingletonExposure) {
    //@1
    Object earlySingletonReference = getSingleton(beanName, false);
    if (earlySingletonReference != null) {
        //@2
        if (exposedObject == bean) {
            exposedObject = earlySingletonReference;
        }
        //@3
        else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
            String[] dependentBeans = getDependentBeans(beanName);
            Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
            for (String dependentBean : dependentBeans) {
                if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                    actualDependentBeans.add(dependentBean);
                }
            }
            if (!actualDependentBeans.isEmpty()) {
                throw new BeanCurrentlyInCreationException(beanName,
                                                           "Bean with name '" + beanName + "' has been injected into other beans [" +
                                                           StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                                                           "] in its raw version as part of a circular reference, but has eventually been " +
                                                           "wrapped. This means that said other beans do not use the final version of the " +
                                                           "bean. This is often the result of over-eager type matching - consider using " +
                                                           "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
            }
        }
    }
}

上面代码主要用来判断当有循环依赖的情况下,早期暴露给别人使用的bean是否和最终的bean不一样的情况下,会抛出一个异常。

我们再来通过代码级别的来解释上面代码:

@1:调用getSingleton(beanName, false)方法,这个方法用来从3个级别的缓存中获取bean,但是注意了,这个地方第二个参数是false,此时只会尝试从第1级和第2级缓存中获取bean,如果能够获取到,说明了什么?说明了第2级缓存中已经有这个bean了,而什么情况下第2级缓存中会有bean?说明这个bean从第3级缓存中已经被别人获取过,然后从第3级缓存移到了第2级缓存中,说明这个早期的bean被别人通过getSingleton(beanName, true)获取过

@2:这个地方用来判断早期暴露的bean和最终spring容器对这个bean走完创建过程之后是否还是同一个bean,上面我们的service1被代理了,所以这个地方会返回false,此时会走到@3

@3:allowRawInjectionDespiteWrapping这个参数用来控制是否允许循环依赖的情况下,早期暴露给被人使用的bean在后期是否可以被包装,通俗点理解就是:是否允许早期给别人使用的bean和最终bean不一致的情况,这个值默认是false,表示不允许,也就是说你暴露给别人的bean和你最终的bean需要是一直的,你给别人的是1,你后面不能将其修改成2了啊,不一样了,你给我用个鸟。

而上面代码注入到service2中的service1是早期的service1,而最终spring容器中的service1变成一个代理对象了,早期的和最终的不一致了,而allowRawInjectionDespiteWrapping又是false,所以报异常了。

那么如何解决这个问题:

很简单,allowRawInjectionDespiteWrapping设置为true就可以了,下面改一下代码如下:

@Test
public void test4() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    //创建一个BeanFactoryPostProcessor:BeanFactory后置处理器
    context.addBeanFactoryPostProcessor(beanFactory -> {
        if (beanFactory instanceof DefaultListableBeanFactory) {
            //将allowRawInjectionDespiteWrapping设置为true
            ((DefaultListableBeanFactory) beanFactory).setAllowRawInjectionDespiteWrapping(true);
        }
    });
    context.register(MainConfig3.class);
    context.refresh();

    System.out.println("容器初始化完毕");
}

上面代码中将allowRawInjectionDespiteWrapping设置为true了,是通过一个BeanFactoryPostProcessor来实现的,后面会有一篇文章来详解BeanFactoryPostProcessor,目前你只需要知道BeanFactoryPostProcessor可以在bean创建之前用来干预BeanFactory的创建过程,可以用来修改BeanFactory中的一些配置。

再次输出

容器初始化完毕

此时正常了,我们继续,看看我们加在service1上的拦截器起效了没有,上面代码中加入下面代码:

//获取service1
Service1 service1 = context.getBean(Service1.class);
//获取service2
Service2 service2 = context.getBean(Service2.class);

System.out.println("----A-----");
service2.m1(); //@1
System.out.println("----B-----");
service1.m1(); //@2
System.out.println("----C-----");
System.out.println(service2.getService1() == service1);

上面为了区分结果,使用了----格式的几行日志将输出结果分开了,来运行一下,输出:

容器初始化完毕
----A-----
Service2 m1
Service1 m1
----B-----
你好,service1
Service1 m1
----C-----
false

从输出中可以看出。

service2.m1()对应输出:

Service2 m1
Service1 m1

service1.m1()对应输出:

你好,service1
Service1 m1

而service2.m1方法中调用了service1.m1,这个里面拦截器没有起效啊,但是单独调用service1.m1方法,却起效了,说明service2中注入的service1不是代理对象,所以没有加上拦截器的功能,那是因为service2中注入的是早期的service1,注入的时候service1还不是一个代理对象,所以没有拦截器中的功能。

再看看最后一行输出为false,说明service2中的service1确实和spring容器中的service1不是一个对象了。

ok,那么这种情况是不是很诧异,如何解决这个问题?

既然最终service1是一个代理对象,那么你提前暴露出去的时候,注入到service2的时候,你也必须得是个代理对象啊,需要确保给别人和最终是同一个对象。

这个怎么整?继续看暴露早期bean的源码,注意了下面是重点:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

注意有个getEarlyBeanReference方法,来看一下这个方法是干什么的,源码如下:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

从3级缓存中获取bean的时候,会调用上面这个方法来获取bean,这个方法内部会看一下容器中是否有SmartInstantiationAwareBeanPostProcessor这种处理器,然后会依次调用这种处理器中的getEarlyBeanReference方法,那么思路来了,我们可以自定义一个SmartInstantiationAwareBeanPostProcessor,然后在其getEarlyBeanReference中来创建代理不就可以了,聪明,我们来试试,将MethodBeforeInterceptor代码改成下面这样:

@Component
public class MethodBeforeInterceptor implements SmartInstantiationAwareBeanPostProcessor {
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
        if ("service1".equals(beanName)) {
            //代理创建工厂,需传入被代理的目标对象
            ProxyFactory proxyFactory = new ProxyFactory(bean);
            //添加一个方法前置通知,会在方法执行之前调用通知中的before方法
            proxyFactory.addAdvice(new MethodBeforeAdvice() {
                @Override
                public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
                    System.out.println("你好,service1");
                }
            });
            //返回代理对象
            return proxyFactory.getProxy();
        }
        return bean;
    }
}

对应测试用例

@Test
public void test5() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig4.class);
    System.out.println("容器初始化完毕");

    //获取service1
    com.javacode2018.lesson003.demo2.test4.Service1  service1 = context.getBean(com.javacode2018.lesson003.demo2.test4.Service1.class);
    //获取service2
    com.javacode2018.lesson003.demo2.test4.Service2 service2 = context.getBean(com.javacode2018.lesson003.demo2.test4.Service2.class);

    System.out.println("----A-----");
    service2.m1(); //@1
    System.out.println("----B-----");
    service1.m1(); //@2
    System.out.println("----C-----");
    System.out.println(service2.getService1() == service1);
}

运行输出

容器初始化完毕
----A-----
Service2 m1
你好,service1
Service1 m1
----B-----
你好,service1
Service1 m1
----C-----
true

单例bean解决了循环依赖,还存在什么问题?

循环依赖的情况下,由于注入的是早期的bean,此时早期的bean中还未被填充属性,初始化等各种操作,也就是说此时bean并没有被完全初始化完毕,此时若直接拿去使用,可能存在有问题的风险。

10、SpringMVC谁调谁的问题

在SpringMVC中,强化了注解的使用,在Controller,Service,Dao层都可以使用注解。

  • 使用@Controller创建处理器对象
  • @Service创建业务对象
  • @Autowired或者@Resource在Controller类中注入Service,在Service类中注入Dao.

使用@Controller注解的处理器的处理器方法,其返回值类型常用的有四种类型: 

第一种:ModelAndView          (传递数据+传递视图(即跳转到其他资源))

第二种:String                         (视图)

第三种: 无返回值void            (处理Ajax)

第四种:返回自定义类型对象  (数据) 

区分返回值String是数据还是视图,看有没有@ResponseBody注解,如果有,则是数据,否则是视图。

11、springmvc和springboot的区别

Spring 框架就像一个家族,有众多衍生产品例如 boot、security、jpa等等。但他们的基础都是Spring 的 ioc和 aop ioc 提供了依赖注入的容器 aop ,解决了面向横切面的编程,然后在此两者的基础上实现了其他延伸产品的高级功能。Spring MVC是基于 Servlet 的一个 MVC 框架 主要解决 WEB 开发的问题,因为 Spring 的配置非常复杂,各种XML、 JavaConfig、hin处理起来比较繁琐。于是为了简化开发者的使用,从而创造性地推出了Spring boot,约定优于配置,简化了spring的配置流程。

说得更简便一些:Spring 最初利用“工厂模式”(DI)和“代理模式”(AOP)解耦应用组件。大家觉得挺好用,于是按照这种模式搞了一个 MVC框架(一些用Spring 解耦的组件),用开发 web 应用( SpringMVC )。然后有发现每次开发都写很多样板代码,为了简化工作流程,于是开发出了一些“懒人整合包”(starter),这套就是 Spring Boot。

Spring MVC的功能

Spring MVC提供了一种轻度耦合的方式来开发web应用。

Spring MVC是Spring的一个模块,式一个web框架。通过Dispatcher Servlet, ModelAndView 和 View Resolver,开发web应用变得很容易。解决的问题领域是网站应用程序或者服务开发——URL路由、Session、模板引擎、静态Web资源等等。

Spring Boot的功能

Spring Boot实现了自动配置,降低了项目搭建的复杂度。

众所周知Spring框架需要进行大量的配置,Spring Boot引入自动配置的概念,让项目设置变得很容易。Spring Boot本身并不提供Spring框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于Spring框架的应用程序。也就是说,它并不是用来替代Spring的解决方案,而是和Spring框架紧密结合用于提升Spring开发者体验的工具。同时它集成了大量常用的第三方库配置(例如Jackson, JDBC, Mongo, Redis, Mail等等),Spring Boot应用中这些第三方库几乎可以零配置的开箱即用(out-of-the-box),大部分的Spring Boot应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。

Spring Boot只是承载者,辅助你简化项目搭建过程的。如果承载的是WEB项目,使用Spring MVC作为MVC框架,那么工作流程和你上面描述的是完全一样的,因为这部分工作是Spring MVC做的而不是Spring Boot。

对使用者来说,换用Spring Boot以后,项目初始化方法变了,配置文件变了,另外就是不需要单独安装Tomcat这类容器服务器了,maven打出jar包直接跑起来就是个网站,但你最核心的业务逻辑实现与业务流程实现没有任何变化。

所以,用最简练的语言概括就是:

Spring 是一个“引擎”;

Spring MVC 是基于Spring的一个 MVC 框架 ;

Spring Boot 是基于Spring4的条件注册的一套快速开发整合包

12、Spring和SpringMVC有哪些常用的注解:

Spring部分:

声明bean的注解

@Component 通⽤的注解,可标注任意类为 Spring 组件

@Service 在业务逻辑层使用(service层)

@Repository 在数据访问层使用(dao层)

@Controller 在展现层使用,控制器的声明(controller层)

注入bean的注解

@Autowired:可以对类成员变量、方法、构造方法进行标注

​ 默认按照类型注入,若要按照名称注入,需要搭配@Qualifier注解一起使用

@Resource:默认按照名称来装配注入

Spring MVC部分:

@Controller 声明该类为SpringMVC中的Controller

@RequestMapping 用于映射Web请求

@ResponseBody 支持将返回值放在response内,而不是一个页面,通常用户返回json数据

@RequestBody 允许request的参数在request体中,而不是在直接连接在地址后面。

@PathVariable 用于接收路径参数,比如@RequestMapping("/hello/{name}")申明的路径,将注解放在参数中前,即可获取该值,通常作为Restful的接口实现方法。

13、application.properties和application.yml文件区别以及加载顺序

properties的优先级高于yml。即如果两个文件中都配置了端口号,只有properties中的端口号有效,而yml文件中端口配置无效。

两者都是配置文件,在使用上略有区别。

application.properties中

server.port=8801
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.serviceUrl.defaultZone=http\://localhost\:${server.port}/eureka/

 yml中:

server:
    port: 8801
 
eureka:
   client:
     registerWithEureka: false
     fetchRegistry: false
     serviceUrl:
      defaultZone: http://localhost:8801/eureka/

主要的区别:

1、在properties文件中是以”.”进行分割的, 在yml中是用”:”进行分割;
2、yml的数据格式和json的格式很像,都是K-V格式,并且通过”:”进行赋值;
3、在yml中缩进一定不能使用TAB,否则会报很奇怪的错误;(缩进只能用空格!!!!)
4、每个k的冒号后面一定都要加一个空格;
5、使用spring cloud的maven进行构造的项目,在把properties换成yml后,一定要进行mvn clean insatll
 

14、Spring Boot的自动配置原理和启动过程?

https://zhuanlan.zhihu.com/p/84551197

自动配置原理:

@SpringBootApplication  , SpringApplication.run

  @SpringBootApplication = (默认属性)@Configuration + @EnableAutoConfiguration + @ComponentScan

即 如果我们使用如下的SpringBoot启动类,整个SpringBoot应用依然可以与之前的启动类功能对等:

首先第一个:

@Configuration:提到@Configuration就要提到他的搭档@Bean。使用这两个注解就可以创建一个简单的spring配置类,可以用来替代相应的xml配置文件。Configuration的注解类标识这个类可以使用Spring IoC容器作为bean定义的来源。

接着第二个:

@ComponentScan这个注解在Spring中很重要,它对应XML配置中的元素,@ComponentScan的功能其实就是自动扫描并加载符合条件的组件(比如@Component@Repository等)或者bean定义,最终将这些bean定义加载到IoC容器中。

我们可以通过basePackages等属性来细粒度的定制@ComponentScan自动扫描的范围,如果不指定,则默认Spring框架实现会从声明@ComponentScan所在类的package进行扫描。

注:所以SpringBoot的启动类最好是放在root package下,因为默认不指定basePackages。

最后一个:

@EnableAutoConfiguration会根据类路径中的jar依赖为项目进行自动配置,如:添加了spring-boot-starter-web依赖,会自动添加Tomcat和Spring MVC的依赖,Spring Boot会对Tomcat和Spring MVC进行自动配置。

启动流程?

启动流程主要分为三个部分:

  • 第一部分进行SpringApplication的初始化模块,配置一些基本的环境变量、资源、构造器、监听器;
  • 第二部分实现了应用具体的启动方案,包括启动流程的监听模块、加载配置环境模块、及核心的创建上下文环境模块;
  • 第三部分是自动化配置模块,该模块作为springboot自动配置核心,在后面的分析中会详细讨论。在下面的启动程序中我们会串联起结构中的主要功能。

15.IOC的底层原理?

https://www.cnblogs.com/volvane/articles/9353124.html

  Spring中的IoC的实现原理就是工厂模式反射机制


SpringCloud


学习笔记https://blog.csdn.net/Mcdull__/article/details/111600717 

面试题:https://blog.csdn.net/Mcdull__/article/details/111773079


mybatis


Mybatis的介绍以及它的优缺点

MyBatis

(1)Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。

(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

(3)通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。

MyBatis的优点

(1)基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里解除sql与程序代码的耦合便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。

(2)与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;

(3)很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。

(4)能够与Spring很好的集成

(5)提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

MyBatis的缺点

(1)SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求

(2)SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库

Mybatis #{}和${}的区别,${}SQL语句中什么情况下用

<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">
select id, username, password, role
from user
where username = #{username,jdbcType=VARCHAR}
and password = #{password,jdbcType=VARCHAR}
</select>

<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">
select id, username, password, role
from user
where username = ${username,jdbcType=VARCHAR}
and password = ${password,jdbcType=VARCHAR}
</select>

mybatis中#和$的区别:

1.#将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。而$将传入的数据直接显示生成在sql中。

如:where username=#{username},如果传入的值是111,那么解析成sql时的值为where username="111", 如果传入的值是id,则解析成的sql为where username="id". 

如:where username=${username},如果传入的值是111,那么解析成sql时的值为where username=111;

如果传入的值是:drop table user;,则解析成的sql为:select id, username, password, role from user where username=;drop table user;

2.#方式能够很大程度防止sql注入,$方式无法防止sql注入

3.$对象一般用于传入数据库对象,例如传入表名列名。${xx}这样的参数会直接参与sql编译,从而不能避免注入。

4.一般能用#的就别用$,若不得不使用${xx}这样的参数,那么要手工的做好过滤工作,来防止sql注入。

什么是sql注入?

SQL注入,大家都不陌生,是一种常见的攻击方式。攻击者在界面的表单信息或URL上输入一些奇怪的SQL片段(例如“or ‘1’=’1’”这样的语句),有可能入侵参数检验不足的应用程序。所以,在我们的应用中需要做一些工作,来防备这样的攻击方式。在一些安全性要求很高的应用中(比如银行软件),经常使用将SQL语句全部替换为存储过程这样的方式,来防止SQL注入。这当然是一种很安全的方式,但我们平时开发中,可能不需要这种死板的方式。

作者:森屿还巷
链接:https://zhuanlan.zhihu.com/p/39408398
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

mybatis是如何做到防止sql注入的?

  MyBatis框架作为一款半自动化的持久层框架,其SQL语句都要我们自己手动编写,这个时候当然需要防止SQL注入。其实,MyBatis的SQL是一个具有“输入+输出”的功能,类似于函数的结构,参考上面的两个例子。其中,parameterType表示了输入的参数类型,resultType表示了输出的参数类型。回应上文,如果我们想防止SQL注入,理所当然地要在输入参数上下功夫。上面代码中使用#的即输入参数在SQL中拼接的部分,传入参数后,打印出执行的SQL语句,会看到SQL是这样的:

select id, username, password, role from user where username=? and password=?

  不管输入什么参数,打印出的SQL都是这样的。这是因为MyBatis启用了预编译功能,在SQL执行前,会先将上面的SQL发送给数据库进行编译;执行时,直接使用编译好的SQL,替换占位符“?”就可以了。因为SQL注入只能对编译过程起作用,所以这样的方式就很好地避免了SQL注入的问题。

  【底层实现原理】MyBatis是如何做到SQL预编译的呢?其实在框架底层,是JDBC中的PreparedStatement类在起作用,PreparedStatement是我们很熟悉的Statement的子类,它的对象包含了编译好的SQL语句。这种“准备好”的方式不仅能提高安全性,而且在多次执行同一个SQL时,能够提高效率。原因是SQL已编译好,再次执行时无需再编译。

总结:

#{}:相当于JDBC中的PreparedStatement

${}:  输出变量的值

简单说,#{}是经过预编译的,是安全的,${}是未经过预编译的,是非安全的,存在sql注入。

mybatis二级缓存

默认使用一级缓存,二级缓存需要手动开启。

区别:

一级缓存的作用域是一个sqlsession内

二级缓存的作用域是针对mapper进行缓存。

一级缓存:

在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用sqlSession第一次查询后,Mybatis会将其放在缓存中,以后再进行查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送Sql到数据库。

一级缓存时执行commit,close,增删改等操作,就会清空当前的一级缓存;当对SqlSession执行更新操作(update、delete、insert)后并执行commit时,不仅清空其自身的一级缓存(执行更新操作的效果),也清空二级缓存(执行commit()的效果)。

二级缓存:

 二级缓存指的就是同一个namespace下的mapper,二级缓存中,也有一个map结构,这个区域就是一级缓存区域。一级缓存中的key是由sql语句,条件,statement等信息组成一个唯一值。一级缓存中的value就是查询出的结果对象。

1、在配置文件中 开启二级缓存的总开关

<setting name="cacheEnabled" value="true" />

2、 在mapper映射文件中开启二级缓存

<cache eviction="FIFO" flushInterval="60000" size="512" 
readOnly="true"/>

参数名属性eviction收回策略flushInterval刷新间隔size引用数目readOnly只读


Maven依赖冲突的产生原因和解决方式

首先我们来看一下依赖冲突产生的原因:

  1. 如果项目的依赖A和依赖B同时引入了依赖C。
  2. 如果依赖C在A和B中的版本不一致就可能依赖冲突。
  3. 比如 项目 <- A, B, A <- C(1.0),B <- C(1.1)。
  4. 那么maven如果选择高版本C(1.1)来导入(这个选择maven会根据不等路径短路径原则同等路径第一声明原则选取),C(1.0)中的类c在C(1.1)中被修改而不存在了。
  5. 在编译期可能并不会报错,因为编译的目的只是把业务源代码编译成class文件,所以如果项目源代码中没有引入共有依赖C因升级而缺失的类c,就不会出现编译失败。除非源代码就引入了共有依赖C因升级而缺失的类c则会直接编译失败。
  6. 在运行期,很有可能出现依赖A在执行过程中调用C(1.0)以前有但是升级到C(1.1)就缺失的类c,导致运行期失败,出现很典型的依赖冲突时的NoClassDefFoundError错误。
  7. 如果是升级后出现原有的方法被修改而不存在的情况时,就会抛出NoSuchMethodError错误。

那么怎么来解决依赖冲突呢?

  1. 首先可以借助Maven查看依赖的依赖树来分析一下:mvn dependency:tree,或者使用IDEA的插件Dependency Analyzer插件来可视化地分析依赖关系。这个过程后可以明确哪些dependency引入了可能会冲突的依赖。
  2. 比如我们的项目引入A的依赖C为1.1版本,引入的B会在内部依赖C的1.0版本,那么Dependency Analyzer插件会出现依赖冲突提示,会提示B引入的C的1.0版本和当前选用的C的1.1版本冲突因而被忽略(1.0 omitted for conflict with 1.1)。
  3. 如果这时候打war包出来启动很有可能会遇到因依赖冲突而出现的NoClassDefFoundErrorNoSuchMethodError,导致编译期正常的代码无法在运行期跑起来。
  4. 由于A引入的C的版本高而B依赖的C版本低,我们优先会选择兼容高版本C的方案,即试图把B的版本调高以使得引入的依赖C可以和A引入的依赖A达到一致的版本,以此来解决依赖冲突。当然这是一个理想状况。
  5. 如果找到了目前已有的所有的B的版本,均发现其依赖的C没有与A一致的1.1版本,比如B是一个许久未升级的旧项目,那么也可以考虑把A的版本拉低以使得C的版本降到与B一致的1.0版本,当然这也可能会反过来导致A不能正常工作。
  6. 上面已经可以看出来解决依赖冲突这件事情并不简单,很难顾及两边,很多情况下引入不同版本依赖的很可能超过两方而是更多方。
  7. 那么来考虑一下妥协的方案,如果A引入的C使用的功能并不跟被抛弃的类或方法有关,而是其他在1.1版本中仍然没有改变的类或方法,那么可以考虑直接使用旧的1.0版本,那么可以使用exclusion标签来在A中排除掉对C的依赖,那么A在使用到C的功能时会使用B引入的1.0旧版本C。即A其实向B妥协使用了B依赖的C。


情景题


1.100亿黑名单URL,每个64B,问这个黑名单要怎么存?判断一个URL是否在黑名单中 

散列表:

​ 如果把黑名单看成一个集合,将其存在 hashmap 中,貌似太大了,需要 640G,明显不科学。

布隆过滤器:处理需要大约23G

​ 它实际上是一个很长的二进制矢量和一系列随机映射函数。 实际上,布隆过滤器广泛应用于网页黑名单系统、垃圾邮件过滤系统、爬虫网址判重系统等,但业务上要可以忍受判断失误率。

 它可以用来判断一个元素是否在一个集合中。它的优势是只需要占用很小的内存空间以及有着高效的查询效率。对于布隆过滤器而言,它的本质是一个位数组/位图(bitmap) (位数组就是数组的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。

​ 在数组中的每一位都是二进制位。布隆过滤器除了一个位数组,还有 K 个哈希函数。当一个元素加入布隆过滤器中的时候,会进行如下操作:

  • 使用 K 个哈希函数对元素值进行 K 次计算,得到 K 个哈希值。
  • 根据得到的哈希值,在位数组中把对应下标的值置为 1。

假设一种有k个哈希函数,且每个哈希函数的输出范围都大于m,接着将输出值对k取余(%m),就会得到k个[0, m-1]的值,由于每个哈希函数之间相互独立,因此这k个数也相互独立,最后将这k个数对应到bitarray上并标记为1(涂黑)。

等判断时,将输入对象经过这k个哈希函数计算得到k个值,然后判断对应bitarray的k个位置是否都为1(是否标黑),如果有一个不为黑,那么这个输入对象则不在这个集合中,也就不是黑名单了!如果都是黑,那说明在集合中,但有可能会误,由于当输入对象过多,而集合也就是bitarray过小,则会出现大部分为黑的情况,那样就容易发生误判!因此使用布隆过滤器是需要容忍错误率的,即使很低很低!

2.海量数据中找出前K大数(Top问题)

问题汇总:

(1)有10000000个记录,这些查询串的重复度比较高,如果除去重复后,不超过3000000个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。请统计最热门的10个查询串,要求使用的内存不能超过1GB。

(2)有10个文件,每个文件1GB,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。按照query的频度排序。

(3)有一个1GB大小的文件,里面的每一行是一个词,词的大小不超过16个字节,内存限制大小是1MB。返回频数最高的100个词。

(4)提取某日访问网站次数最多的那个IP。

(5)10亿个整数找出重复次数最多的100个整数。

(6)搜索的输入信息是一个字符串,统计300万条输入信息中最热门的前10条,每次输入的一个字符串为不超过255B,内存使用只有1GB。

(7)有1000万个身份证号以及他们对应的数据,身份证号可能重复,找出出现次数最多的身份证号。

https://www.zhihu.com/question/28874340/answer/735805680 

针对TopK类问题,通常比较好的方案是分治+Trie树/hash +小顶堆。即先将数据集按照hash方法分解成多个小数据集,然后使用Trie树或者Hash统计每个小数据集中的query词频,之后用小顶堆求出没个数据集中频率出现最高的前K个数,最后在所有top k中求出最终的Top k.
 

方法进阶:

1、最简单的方法就是快排,取topk

2、局部淘汰法。用一个容器保存前k个数,然后将剩余的所有数字——与容器内的最小数字相比,如果所有后续的元素都比容器内的k个数还小,那么容器内这k个数就是最大k个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完所有的数,得到的结果容器中保存的数即为最终结果了

3、分治法。将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的10000个,最后在剩下的100*10000个数据里面找出最大的10000个。100万个数据里面查找最大的10000个数据的方法如下:用快速排序的方法,将数据分为2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大堆个数N小于10000个,就在小的那堆里面快速排序一次,找第10000-n大的数字;递归以上过程,就可以找到第1w大的数。参考上面的找出第1w大数字,就可以类似的方法找到前10000大数字了。此种方法需要每次的内存空间为10^6*4=4MB,一共需要101次这样的比较。

4、采用最小堆。首先读入前10000个数来创建大小为10000的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为10000),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10000个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)。

3.给你100万个数据,怎么查出来占用内存的大小

如果想知道Mysql数据库中每个表占用的空间,表记录的行数的话,可以打开Mysql的 information_schema数据库。该库中有一个TABLES表,这个表主要的字段分别是:

TABLE_SCHEMA : 数据库名

TABLE_NAME:表名

ENGINE:所使用的存储引擎

TABLES_ROWS:记录数

DATA_LENGTH:数据大小

INDEX_LENGTH:索引大小

如果想知道一个表占用空间的大小,那就相当于是 数据大小+索引大小 即可。

SQL:

SELECT TABLE_NAME,DATA_LENGTH+INDEX_LENGTH,TABLE_ROWS FROM TABLES WHERE TABLE_SCHEMA='数据库名' AND TABLE_NAME='表名'

4、淘宝是通过什么手段获取信息做推荐的?

目前已知的,淘系的app,如手机淘宝,手机天猫,支付宝,淘票票,优酷之类,你一切在这些app上浏览习惯,收藏加购,交易,社交板块互动信息,都会被淘宝抓取并分析你需求。还有结合你实名制后,他得到年龄,性别,家乡,收货地,住址,工作地址,手机型号,手机号码等一切信息,分析你消费能力,近期消费意愿,在淘宝数据库里形成对你的人群画像!用大数据对用户进行分析,你最近搜索过的,经常搜索的,还有和你搜索相同产品的人搜索的,对你进行推荐。

5、手机的指纹识别是怎么实现的?

总指纹解锁的原理就是利用我们的手指指纹与下面的电容器相互作用,从而影响下面电容器的容量,检测电容器变化的装置能够通过电容的变化来计算出我们手指指纹的形态,通过与之前录入指纹的对比,从而达到解开我们的手机的目的。

6、聊天信息敏感词过滤的实现思路?

(1)字符串匹配算法(两层for或kmp算法):当玩家输入一段文字内容并发出时,通过字符串匹配算法检查是否包含敏感词,有的话就替换掉。假设我们有100W个敏感词,我们肯定是不希望玩家每说一句话,就把100W条数据都过滤一遍进行匹配的,为了提高效率,就要用到接下来说的(2)多模式串匹配算法了。

利用Trie(前缀树/字典树/单词查找树)。 https://www.cnblogs.com/bonelee/p/8830825.html  它最大的特点就是共享字符串的公共前缀来达到节省空间的目的了。例如,字符串 "abc"和"abd"构成的 trie 树如下:

应用:(1)trie 最大的特点就是利用了字符串的公共前缀,像我们有时候在百度、谷歌输入某个关键字的时候,它会给我们列举出很多相关的信息

(2)trie 树来实现敏感词过滤

例如我给你一段字符串“abcdefghi",以及三个敏感词"de", "bca", "bcf"

先把你给我的三个敏感词:"de", "bca", "bcf" 建立一颗 trie 树,如下:

接着我们可以采用三个指针来遍历,我直接用上面你给你例子来演示吧。

1、首先指针 p1 指向 root,指针 p2 和 p3 指向字符串第一个字符

2、然后从字符串的 a 开始,检测有没有以 a 作为前缀的敏感词,直接判断 p1 的孩子节点中是否有 a 这个节点就可以了,显然这里没有。接着把指针 p2 和 p3 向右移动一格。

3、然后从字符串 b 开始查找,看看是否有以 b 作为前缀的字符串,p1 的孩子节点中有 b,这时,我们把 p1 指向节点 b,p2 向右移动一格,不过,p3不动。

4、判断 p1 的孩子节点中是否存在 p2 指向的字符c,显然有。我们把 p1 指向节点 c,p2 向右移动一格,p3不动。

5、判断 p1 的孩子节点中是否存在 p2 指向的字符d,这里没有。这意味着,不存在以字符b作为前缀的敏感词。这时我们把p2和p3都移向字符c,p1 还是还原到最开始指向 root。

6、和前面的步骤一样,判断有没以 c 作为前缀的字符串,显然这里没有,所以把 p2 和 p3 移到字符 d。

7、然后从字符串 d 开始查找,看看是否有以 d 作为前缀的字符串,p1 的孩子节点中有 d,这时,我们把 p1 指向节点 b,p2 向右移动一格,不过,p3和刚才一样不动。(看到这里,我猜你已经懂了)

8、判断 p1 的孩子节点中是否存在 p2 指向的字符e,显然有。我们把 p1 指向节点 e,并且,这里e是最后一个节点了,查找结束,所以存在敏感词de,即 p3 和 p2 这个区间指向的就是敏感词了,把 p2 和 p3 指向的区间那些字符替换成 *。并且把 p2 和 p3 移向字符 f。如下:

9、接着还是重复同样的步骤,知道 p3 指向最后一个字符。

复杂度分析

面试官:可以说说时间复杂度吗?

小秋:如果敏感词的长度为 m,则每个敏感词的查找时间复杂度是 O(m),字符串的长度为 n,我们需要遍历 n 遍,所以敏感词查找这个过程的时间复杂度是 O(n * m)。如果有 t 个敏感词的话,构建 trie 树的时间复杂度是 O(t * m)。

这里我说明一下,在实际的应用中,构建 trie 树的时间复杂度我觉得可以忽略,因为 trie 树我们可以在一开始就构建了,以后可以无数次重复利用的了。而刚才的 kmp 算法时间复杂度是 t *(m+n),不过kmp需要维护 next 数组比较费空间,而且在实际情况中,敏感词的数量 t 是比较大,而 n 反而比较小的吧。

面试官:如果让你来 构建 trie 树,你会用什么数据结构来实现?

小秋:我一般使用 Java,我会采用 HashMap 来实现,因为一个节点的字节点个数未知,采用 HashMap 可以动态拓展,而且可以在 O(1) 复杂度内判断某个子节点是否存在。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值