一、设计模式
1、单例模式
(1)什么是单例模式?
这个是23种设计模式之一,设计模式就是前人总结的经验,而单例模式是指一个类只有一个实例对象,而且必须在自己类内创建。
(2)为什么用单例模式?
单例模式是让一个类只有一个对象,从而减少系统资源的消耗,比如说数据库连接池、工厂类和数据源等这种创建和销毁需要消耗很多资源的类就写成单例模式,还有Spring的bean默认作用域也是单例。
(3)怎么实现单例模式?
大体思路有三点:
A、私有化构造方法
B、私有化创建一个实例对象(属性)
C、给外界提供获取这个实例对象的静态方法
具体来说有三种方式:
**A、饿汉式:**利用类加载机制,线程安全,不需要加锁,但发射和序列化这两种场景不适合,为什么序列化不适合?因为静态变量序列化的时候不保存,所以反序列化的时候会重新创建实例,破坏了单例。
怎么防止反射攻击?进化为静态内部类模式,然后抛出异常;
怎么解决反序列化问题?加个方法来保存静态变量。
**B、懒汉式:**线程不安全,但不是所有线程地址都不一样,相反地址不一样概率很小;
怎么解决线程安全问题?加锁,用synchronized,可以加在方法上也可以在代码块;
但这种方法效率低,所以可以在外面加个if(instance == null)即双检锁,这样就不需要线程来一次同步一次,而是只同步一次;
还有一个问题:JVM指令重排,所谓指令重排是指单例类里静态方法中的return instance = new LockSingleton()的正常指令中变量初始化和变量指向内存空间顺序反了(给对象开辟空间这个第一步不会变),会出现线程不安全问题,所以在私有化静态属性里加上关键字volatile。
**C、ThreadLocal:**这个和饿汉式一样不需要加锁,它的实现是以空间换时间,为每个线程提供变量的独立副本,所以多线程之间不能相互修改,Spring的bean单例但线程安全就是因为用了这个,时间原因,这里深入写代码。
注:也可以用CAS实现单例模式,这个思想和可能会出现的问题理解,但代码比较难,以后再弄。
2、学习过程中遇到过什么设计模式和出现场景?
Java中设计模式有23种,大致可以分为三类:创建型、结构型和行为型
A、创建型有单例、工厂和建造者
(1)单例模式:Spring的bean对象创建默认是单例,采用ThreadLocal创建;
(2)工厂模式:Spring的bean对象创建默认是单例,但如果是有有参构造的类,则用工厂模式,分别用静态工厂、实例工厂和Spring自带工厂这三种;Executors创建五种线程池也是用工厂模式,因为真正创建线程池的是里面的ThreadPoolExecutor,只是因为这个类直接创建线程池需要指定很多参数,比较麻烦,所以用Executors这个工厂类;在MyBaits中有操作Sql之前需要获取SqlSession对象,而SqlSession对象是从SqlSessionFactory这个工厂类中获得的。
所以单例模式和工厂模式都可以创建对象,区别在构造方法是否有参,如果有参而且参数很多创建麻烦就用工厂模式,反之用单例模式。
(3)建造者模式
上面提到SqlSession是由工厂类SqlSessionFactory创建的,那SqlSessionFactory又是谁创建的?SqlSessionFactoryBuilder创建,这个就用到建造者模式,这个思想类似景区联票。
B、结构型有代理模式、装饰者模式
(1)代理模式
代理模式分为静态代理和动态代理,代理的目的是增强方法和实现软件的高内聚,静态代理解决了高内聚的目标,即把无关功能分开了,但代码还是存在大量冗余,所以一般用动态代理,动态代理的应用场景是Spring的AOP,AOP的意思是面向切面编程,AOP里用到两种动态代理方法,一种是JDK自带的Proxy类(p o 科 西),还有一种是第三方引入的cglib,二者区别在于Proxy创建的代理者必须继承目标对象的所有接口,那如果想要代理的目标对象没有实现接口的方法怎么办?用cglib,AOP默认用Proxy,如果用不了就用cglib,我们也可以自己配置强制用cglib。
(2)装饰者模式
继承父类可以重写父类的方法,但如果希望重写实例对象的方法怎么办?此时肯定不能用继承,可以考虑用装饰者模式,应用场景是JavaWeb中用过滤器实现全站乱码处理可以对request对象进行装饰,怎么装饰?创建一个类继承和目标对象同样的类和接口,然后创建一个有参构造方法把这个对象传入,再根据需求对方法进行改变或者不改变。
C、行为型有责任链模式
责任链模式的应用场景有JavaWeb中多个过滤器的过滤规则和SpringAOP的多个切面规则,类似一个里面嵌套着一个。
二、异常
1、Exception和Error(这些答案都可以看源码注释,考的是Java基础知识和编程经验)
(1)Exception和Error的区别
首先Exception和Error都是Throwable的子类,只有继承Throwable的子类才能被JVM抛出(throw)并且被程序捕获(catch),但Error这个子类不应该被catch,因为Error是程序正常运行的时候不大可能出现的情况,当Error出现的时候一般会导致程序崩溃,并且处于一个不可恢复的状态,比如内存溢出(OutOfMemoryError)、栈溢出(StackOverflowError)等情况。
而Exception这个子类推荐去捕获并处理,因为Exception是在程序运行过程中可以预见的一些意外情况,比如参数为空,参数类型不合法等情况,可以对其进行判断、捕获和处理。
(2)运行时异常和一般异常的区别
一般异常又称为受检查异常(源码注释翻译过来这个名字),这些异常需要在方法或者构造方法中声明,具体声明操作是在方法名或者构造方法名后面加上throws和受检查异常名,而调用这些方法的调用者需要对这些受检查异常进行处理,比如IO异常(IOException)、类没发现异常(ClassNotFoundException,Spring中bean创建经常出现这个异常)。
不受检查的异常又称为运行时异常,这些异常只有在运行的时候才会出现,所有的运行时异常都继承RuntimeException,比如空指针异常(NullPointerException)、类型转换异常(ClassCastException,web编程中拿到id这个字符串然后赋给一个整数,就会出现类型转换异常)和数组越界异常(IndexOutOfBoundException)等。运行时异常通常是在编码中可以避免的逻辑错误(所以比较靠经验,而一般异常在IDE中会提示。),比如避免空指针异常的方法:可以通过if判断对象是否为空,如果对象为空,就不去调用。
(3)写出几种常见的运行时异常
空指针异常(NullPointerException)、类型转换异常(ClassCastException)和数组越界异常(IndexOutOfBoundException)等。
2、ClassNotFoundException和NoClassDefFoundError区别?
(1)继承角度:ClassNotFoundException继承Exception,所以具有Exception的特征,即需要捕获处理;而NoClassDefFoundError继承Error,所以具有Error的特征,即不需要捕获处理。
(2)场景角度:ClassNotFoundException的场景是当程序运行的过程中尝试使用类加载器去加载Class文件的时候,没有在classpath中找到指定的类,JVM就会抛出这个Exception,典型例子就是发射,比如在JDBC第一步中用了反射的第三种方法Class.forname加载JDBC驱动类的时候,如果没有导入JDBC的jar包,那就会抛出这个异常;而NoClassDefFoundError的场景是程序编译的时候通过即生成了.class文件,但运行的时候没发现.class文件不见了JVM就会抛出这个Error,比如new一个对象,编译的时候通过并生成一个Class文件,但如果我们把它删了,然后再运行这个程序就会出现这个Error,还有一种情况是使用SpringMVC用到JSON,如果没有导入jar包,编译可以通过,但运行程序会出现这个Error,当然版本问题也会导致这个问题。
3、throw和throws的区别?
(1)出现位置不同:throw出现在方法体内;而throws出现在方法名后面。
(2)功能不同:throw是手动抛出一个异常,而且一次只能抛出一个,并且在方法体内捕获处理,如果不捕获处理则在方法上声明throws;而throws抛出这个方法可能出现的所有异常,不止一个,这些异常需要该方法调用者去捕获处理。
4、你对异常有什么了解?
(1)从理论上讲:
说下Exception和Error的区别、Exception的分类和一些常见的Exception
(2)从实践上讲:
尽量不要捕获处理Exception这样的通用异常,应该一个个捕获处理;
try-catch会产生额外的性能开销,所以不要用大的try包住大段代码;
实际开发中,要学会借助日志,比如订单出现异常不会去看控制台的。
三、基本数据类型和包装类
int和Integer的区别
int属性基本数据类型,而Integer是包装类;
Java是面向对象的语言,所以包装类的作用会比基本数据类型多,比如Integer除了能表示int的所有数字外,还有null,这个null可以表示每个参加考试的学科成绩;还有Integer可以作为泛型,但int不行。
Integer和String一样是final类,所以方法传参要注意值不会变化;
JDK1.5新特性:
(1)自动装箱和拆箱
int到Integer是装箱,Integer到int是拆箱,JDK1.5之后支持自动拆箱和装箱;
(2)valueOf()方法的缓存
JDK1.5之后valueOf方法里面有个缓存区,缓存区的范围默认是-128到127之间,即在这个范围内的Integer对象自动拆箱的所有对象的值一样时地址也一样,如果超过这个范围那就不能用==比较,而是用equals()方法或者intValue()方法。
// Integer num1 = new Integer(2);
// Integer num2 = new Integer(2);
// 不是上面这样,因为那个是有参构造,和valueOf无关,即没有缓存机制
Integer num1 = 2;
Integer num2 = 2;
System.out.println(num1 == num2);
四、String
1、String、StringBuffer和StringBuilder的区别
(1)String是一个final修饰的类,而且本质上是一个final修饰的char数组,所以String不能被继承,其属性也不可变,即如果对字符串进行拼接、剪切等操作都会产生新的String对象;
(2)StringBuffer本质也是一个char数组,只是没有用final修饰,而且所有方法都用synchronized修饰,所以是线程安全的,但这样性能会比较低;
(3)StringBuilder是JDK1.5出现的,其实它底层和StringBuffer一模一样,只是方法没有用synchronized修饰而已,所以线程不安全但效率更高;
需要注意的是,不管StringBuffer还是StringBuilder底层用append()方法拼接字符串的时候,如果拼接后的长度过大会产生扩容,扩容是很影响性能,所以如果可以预见拼接后的数组大小,那就一开始指定好大点的数组长度。
2、下面代码创建了几个对象,输出是什么
String s1 = new String(“abc”);
String s2 = new String(“abc”);
System.out.println(s1 == s2);
输出是false,因为有new肯定会创建新的对象,所以堆里有2个对象,而常量池里只有一个对象,即字符串常量abc,然后s1、s2分别引用堆里的2个对象。
五、集合
1、HashMap和Hashtable的区别?
(1)继承的父类不同:HashMap继承AbstractMap,Hashtable继承Dictionary,但二者都是Map接口的实现类。
(2) 线程是否安全:JDK1.8之前,HashMap和Hashtable底层实现基本一样,只是HashTable多了synchronized修饰,所以线程安全,而HashMap线程不安全。
(3)作用不同:JDK1.8之前,HashMap比Hashtable多个功能,key和value都可以用null表示,而Hashtable遇到null会抛出空指针异常。
2、为什么HashMap的容量必须是2的指数次?
因为put一个key进行hashcode计算出hash值后不是对当前数组长度取余,而是用位运算,那容量-1(2的指数次用二进制表示都是最高位为1,其他位全为0,那如果-1则最高位为0,其他位全为1,这样显然让值更均匀分布)可以让其和hash值位运算结果还是hash值本身,从而减少hash冲突。
3、为什么HashMap的扩容因子是0.75?
这个值是平衡空间和时间算出来的最佳值,如果扩容因子太小,则导致扩容太频繁;如果扩容因子太大,则扩容时数据量太大性能会降低。
4、HashMap的put()方法(为什么经常考put()方法?因为put()方法导致线程不安全)
(为什么要提下数组长度和扩容因子?因为线程不安全是扩容导致的,而扩容和这两个因素有关。)
(1)JDK1.8之前的HashMap:数组+链表
正常情况下是输入key后用hashcode()方法求出其hash值,然后再用位运算求出对应桶的位置,然后把嵌套类Entry所需的key、value、hash值和指向下一个元素的next放进去,如果存放的桶数超过扩容因子0.75对应的数量后就会扩容一倍,扩容的本质就是弄一个新数组,然后把旧数组的值放到新的里面,但放之前需要重新计算hash值,但可能计算后两个key所在的位置还是一样的,比如数组长度为8,则8-1对应的二进制是0111,然后来了两个key值7和23,对应的二进制数分别是0111和10111,这两个数和0111位运算后得到的值都是0111,所以在同一个桶中,那如果扩容了,则数组长度-1后变成15的二进制数01111,但和7、23的二进制位运算后还是0111,所以所在的桶数不变,但因为头插法,所以二者顺序变化了,但此时当前线程扩容到一半CPU时间片用完被挂起,然后来了其他线程进入扩容操作,操作完这个线程又回来继续操作剩下一半的扩容操作,则链表会形成一个闭环,即死循环,从而造成CPU的100%,所以线程不安全。
(2)JDK1.8之后的HashMap:数组+链表+红黑树
JDK1.8后,HashMap对链表进行了修改,当链表里存储的数量大于8之后链表会变成红黑树,但需要注意的是不是只有这个条件就会转红黑树,还有个数组长度必须大于64这个条件,因为它怕可能是数组长度太短,导致hash冲突太多所以都在一个桶里。还有如果我们删除了红黑树里的数字,从而让里面的数小于6这个阈值,则红黑树会变回链表,这些阈值都是根据数据结构时间和空间复杂度对比的最佳值。
JDK1.8链表采用尾插法,所以扩容不会有线程安全问题,但还有一种线程不安全的情况:当一个线程算出来的桶值在HashMap中是空的,此时准备放入自己的数据时突然CPU时间片切换给下一个线程,这个线程也刚好是这个桶值,发现里面也没有数据,所以把自己数据写入,然后之前那个线程获取CPU资源后会在之前判断为空的基础上写入,那就会出现数据覆盖的现象。
4、HashMap的get()方法
对key值进行hashCode运算得到hash值,然后和数组长度-1的二进制数进行位运算得到一个值,把这个值对应数组的索引找到当前索引的链表或者红黑树,如果是链表就根据嵌套类Entry里的key值一个个遍历,找到后就返回对应的value值;如果是红黑树,那就根据红黑树的规则查询。
5、ArrayList和LinkedList的区别?
(1)二者底层数据结构不同:ArrayList是动态数组实现(为什么动态数组?因为涉及扩容),而LinkedList是双向链表实现。
(2)二者都有add()、get()和remove()这三个操作方法,但效率不同:
A、add()方法
ArrayList的add方法分两种情况,如果是在头部或者尾部则直接插入即可,但如果是中间因为数组是连续空间,所以需要把该索引后面的数依次覆盖前一个,然后size减1,ArrayList的add()方法时间复杂度是O(n)(按最差情况算);
LinkedList的add方法只需要对指向上一个元素和指向下一个元素的引用进行相应转换即可,不需要涉及空间转换, LinkedList的add()时间复杂度是O(1);
所以针对add()方法,LinkedList效率比ArrayList高。
B、get()方法
ArrayList的get()方法比较简单,直接根据数组索引找到即可,时间复杂度是O(1);
LinkedList的get()方法分两种情况,如果是头部和尾部,因为LinkedList底层定义了这两个属性,所以可以直接获取,但如果是中间,则需要一个个遍历,时间复杂度是O(n);
所以针对get()方法,ArrayList效率比LinkedList高。
C、remove()方法
这个方法二者的底层原理和add()方法差不多,所以针对remove()方法,LinkedList的效率比ArrayList高。
六、运算符
1、&和&&的区别
二者结果都是一样的,但判断上会有不同,&是不管前面的表达式运算结果是真还是假都会对后面的表达式进行运算;&&是判断前面表达式运算结果是真还是假再决定是否对后面表达式进行计算,如果前面是真则运算后面的表达式,否则就不运算,因为结果肯定是假,这就是&&的短路功能,|和||的区别和这个一样。
2、==和equals的区别
首先是运算符,而equals是Object这个顶级父类的的方法;
然后是既可以比较基本数据类型的值是否相等也可以比较引用类型的地址是否相等,而equals()方法只能比较引用类型,但不一定比较地址,虽然默认是比较地址,但可以重写equals方法来比较内容,比如String类就重写了equals()方法来比较两个字符串内容是否相等。
七、final
1、final的作用
(1)修饰类:该类是最终类,不能被继承,比如String类和包装类;
(2)修饰方法:该方法是不能被重写的方法;
(3)修饰属性:该属性不能被更改。
2、final修饰的对象是否可变
这个要分两种情况讨论:
(1)修饰的是基本数据类型,那变量一旦被赋值就不可以被改变;
(2)修饰的是引用数据类型,那引用指向的地址不可变,但对象里的内容可以变化,比如修饰一个Studen对象,这个引用只能指向这个地址,不能指向新的Student对象,但可以改变Student里的name、age等属性。