Java 面试题

学习笔记 ·

1.Java 基础 1.什么是面向对象? 面向对象是一种编程思想,它不同与面向过程,面向过程强调的是事件的执行流程以及顺序,而面向对象则注重于事件有哪些参与者,以及这些参与者需要做些什么。相比而言,面向过程比较直接高效,而面向对象则更加易于扩展、复用和维护。 面向对象有三大基本特性: 封装、继承、多态 封装 :将内部…

1.Java 基础

1.什么是面向对象?

面向对象是一种编程思想,它不同与面向过程,面向过程强调的是事件的执行流程以及顺序,而面向对象则注重于事件有哪些参与者,以及这些参与者需要做些什么。相比而言,面向过程比较直接高效,而面向对象则更加易于扩展、复用和维护。

面向对象有三大基本特性:封装、继承、多态

封装:将内部细节封装起来,通过一些公用的方法来暴露该对象的功能,封装可以实现高内聚、低耦合

继承:继承主要用来实现重用代码,节省开发时间;当子类继承父类后,子类会直接获取父类的属性和方法;

多态:多态要有继承、方法重写以及父类的引用指向子类的对象。多态指的是子类对象可以直接赋值给父类变量,但运行时依然表现出子类的行为特征,这意味着,同一个类型的对象在执行同一个方法时,可以表现出多种行为特征。

2.String、StringBuffer、StingBuilder的区别

String 是被 final 关键字修饰的类,被 final 修饰的类不能够被继承,它的内部维护了一个常量字符数组,所以 String 类一旦被实例化,它的值就不能够再改变,所以每次操作都会产生一个新的对象;

StringBuffer 类是 JDK1.0 提供的类,与 String 不同的是,它不是 final 修饰的,所以SpringBuffer 实例化的值是可以改变的,并且它所提供的字符串的方法都是被 synchronized 修饰的,是同步的。

StringBuilder 类是 JDK1.5 提供的。这个类提供与 SpringBuffer 兼容的 API ,但它不是同步的。SpringBuilder 这个类设计的目的就是在单线程的环境下来做 SpringBuffer 的替代品。

总的来说,String 是 final 类,StringBuffer 和 StringBuilder 不是,StringBuffer 是线程安全的,而 SpringBuilder 不是线程安全的。

StringBuffer 与 StringBuilder 默认容量都为16,扩容大小为原来的两倍 + 2。

3.重载与重写的区别

重载发生在同一类中,方法名相同,参数列表不一致,与返回类型无关;

重写发生在父子类中,方法名以及参数列表必须一致,方法体不同,返回值的范围以及抛出的异常范围要小于等于父类,访问修饰符范围要大于等于父类,如果父类方法访问修饰符为private,那么子类将不能重写该方法。

4.== 和 equals 的区别

对于 ==,如果作用于基本数据类型的变量,则直接比较是否相等,而如果作用与引用类型的话,比较的就是两个对象的内存地址是否相等。

对于 equals 方法,如果没有对equals进行重写(不重写就是==),则比较的是两个对象的内存地址是否相等;但是例如 String、Data、Integer 这些重写了equals方法的类,比较的就是所指向对象的内容。

5.为什么重写 equals 必须重写 hashCode 方法?

Object规范约定 equals 相等的对象必须具有相等的哈希码,hashCode相等的两个对象,equals 不一定相等(比如散列冲突); String类的 hashCode() 是基于字符串中每个字符的 Unicode 值来计算的。具体来说,它按照下面的方式计算:

  • 对于字符串中的每一个字符,取其 Unicode 值(也就是其char值)。
  • 将每个字符值乘以 31 的 n 次方,其中 n 是字符在字符串中的位置(从尾部开始计数,即最后一个字符的位置为 0)。
  • 将所有的结果加起来。 如果两个字符串具有相同的字符但是顺序不同,它们的哈希码可能相同,尽管这是不常见的。然而,由于哈希函数的特性,哈希碰撞(即不同的对象具有相同的哈希码)是不可避免的。

Object中定义的HashCode方法生成的哈希码能保证同一个类的对象的哈希码一定是不同的,如果只重写了equals,而没有重写hashCode,可能会出现equals方法返回true,而hashCode方法返回false,也就是会导致两个相同的对象的 hashCode 的值不一样,这样的后果会导致在hashMap、hashSet等类中存储多个一模一样的对象,显然这种情况是不对的。

6.JDK、JRE、JVM 的区别

JDK 是Java开发工具包,它提供了编译、运行Java程序所需的各种工具和资源。它是包含JRE的,但是JRE可以单独安装。

JRE 是Java运行环境,用于解释执行Java的字节码文件。

JVM 是Java虚拟机,是JRE的一部分,是可运行Java字节码文件的虚拟机。他是整个Java实现跨平台的核心部分,负责解释执行字节码文件,是可运行Java字节码文件的虚拟计算机。

7.抽象类与接口的区别

语法层面上来看,抽象类与接口都不能够被实例化;

  1. 抽象类可以定义构造器,而接口不能;(抽象类定义构造器是为了让子类完成初始化操作而并非创建对象);
  2. 接口定义的方法只能够是抽象的,而抽象类中可以有抽象方法与具体方法;(在JDK8的时候接口可以有default方法);
  3. 抽象类中可以有普通的成员变量,而接口只能有静态变量;(因为接口默认是被public static final修饰的);
  4. 接口中不能有初始代码块,抽象类是可以的;
  5. 它们最大的区别还是一个类只能继承一个抽象类,但是可以实现多个接口。
  6. 接口可以继承多个接口,抽象类是不可以继承接口的,可以实现接口;

设计层面上来讲:

  1. 抽象类是对整个类的整体来进行抽象,描述的是事物的本质,而接口描述的是事物的功能;
  2. 如果一个类需要为一些类提供公共的代码和规范子类的行为时,应优先考虑抽象类;(因为抽象类中的非抽象方法可以被子类继承下来,使实现功能的代码更简单,并且必须实现抽象方法);
  3. 当注重代码的扩展性和可维护性的时候,应当优先采用接口;
  4. 如果要实现多继承,则必须使用接口。

8.Lock和Synchronized的区别?

  1. 从功能角度上看,Lock和Synchronized都是Java中用来解决线程安全问题的工具;
  2. Synchronized是Java中的关键字,而Lock是JUC包中提供的接口,这个接口有很多实现类(例如ReentrantLock重入锁);
  3. Lock比Synchronized的灵活性要高,因为Lock可以自主决定什么时候加锁,什么时候释放锁,只需要调用lock和unlock这两个方法就行,而Synchronized是关键字,Synchronized锁的释放是被动的,只有当同步代码块出现异常或者执行完毕后才会释放;
  4. 从性能上看,二者的区别不大。

9.volatile关键字用法

volatile是一个特征修饰符。volatile的作用是作为指令关键字,保证了线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。也保证了顺序一致性:具体体现在volatile禁止指令重排序。

线程安全需要考虑三方面:可见性、原子性、有序性。
1.可见性:一个线程对共享变量修改,另一个线程能够看见最新的结果;
2.有序性:一个线程内代码按照编写顺序执行;
3.原子性:一个线程内多行代码以一个整体运行,期间不能有其它线程代码的插队。

volatile能够保证共享变量的可见性(JIT优化不识别被volatile修饰的共享变量)和有序性(禁用指令重排序),但是不能保证原子性。(有序性:为被volatile修饰的共享变量加上内存屏障,使得其他变量不能越过屏障)。

10.为什么String要被final修饰

被final修饰的变量,无论是类属性、对象属性、形参还是局部变量,都需要进行初始化操作。 从内存角度来看,String被final修饰是字符串常量池的要求:当我们创建字符串的时候,如果该字符串已经存在常量池中,那么将返回现在的字符串引用,而不是创建新的对象,这样在大量使用字符串的情况下,可以节省内存空间,提高效率。 还可以缓存Hashcode;String的不变性保证了hashcode永远是相同的,从而不用每次使用hashcode就需要计算hashcode,这样更有效率。(当集合插入对象时,直接通过hashcode来判别是否存在该对象)
有一个原因就是安全性。String是几乎每个类都会去使用的类,如果对String某一处的改变不小心影响了该变量所有引用的改变,这样显然是不安全的。String被final修饰,即使在多线程环境下也是线程安全的。

11.深拷贝浅拷贝的区别

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存(分支)。
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象,是“值”而不是“引用”(不是分支)

12.异常与断言的区别

断言(Assert)是用来检查非法情况而不是错误情况的,用来帮开发者快速定位问题的位置。(断言在发布版不起作用、处理方式不够友好) 异常(Exception)处理用于对程序发生异常情况的处理,增强程序的健壮性和容错性。

13.Java命名规范

1.标识符只能使用数字、字母、下划线以及美元符号来进行命名。
2.标识符区分大小写且不能使用Java关键字或者数字开头来进行命名。
3.类名一般使用大驼峰,方法名以及变量使用小驼峰,常量一般全大写使用下划线进行分隔。

14.Java中常量与变量的区别

在Java中,常量是指用final关键字修饰的变量。

常量可分为:静态常量(被static修饰,类只要加载就可以访问)、成员常量、局部常量
1.值的可变性:常量的值一旦被定义后就不能够被修改,而变量是可以随时改变的。
2.类型的确定性:常量的类型在定义时就必须确认,而变量可以在定义后再进行赋值。

15.Java中的访问权限修饰符

Java语言为我们提供了三种访问修饰符,分别是public protected private,如果不加修饰符就是default访问权限。

在修饰成员变量或者成员方法的时候,private表示该成员只能在本类访问、default表示可以在同包下访问、protected表示可以在同包下其他类以及它的子类所访问、public表示可以在任意包、任意类所访问。

16.a = a + 1 与 a += 1 的区别

a += 1 存在隐式强制类型转换

17.Java中double(双精度浮点数)与float(单精度浮点数)的区别

Java中默认小数都是double类型的。

1.内存中占用的字节数不同:float 4个字节,double 8个字节。
2.有效数字位数不同:float 8位,double 16位。
3.数值取值范围不同

java中3 * 0.1 == 0.3将会返回什么?true还是false?
false,因为浮点数不能完全精确的表示出来,一般会损失精度。

java中float f = 3.4;是否正确?
不正确,3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于向下转型会造成精度损失,因此需要强制类型转换float f = (float)3.4;或者写成 float f = 3.4f;才可以。

18.Java是值传递还是引用传递

Java只有值传递

1.对于基本数据类型,传递的是他们实际值的副本。
2.对于引用类型,传递的是对象的引用的值的副本,即对象的内存地址,而不是对象本身。

值传递传递的是数据的拷贝,而不是数据本身,引用类型的传递虽然可以修改引用对象的内容,但不能修改引用本身(引用类型和内存地址),所以,引用传递其实就是值传递的一种形式。

19.Error与Exception的区别

Exception与Error都是继承了Throwable类,在Java中只有Throwable类型的实例才能被捕获或者抛出。
Exception是指程序在正常运行的情况下,可以预料到的意外情况,可以被捕获并作出相应处理;

而Error在正常情况下,绝大多数的Error都会导致程序崩溃(比如内存溢出),或者使Java虚拟机处于一个不正常的状态,而这些非正常的错误不能被捕获并进行相应处理。

值传递传递的是数据的拷贝,而不是数据本身,引用类型的传递虽然可以修改引用对象的内容,但不能修改引用本身(引用类型和内存地址),所以,引用传递其实就是值传递的一种形式。

20.String字符串如何实现编码转换

先通过getBytes方法来获取字节数组,再使用带编码的String构造器。

String str1 = "Criesa";
String str2 = new String(str1.getBytes(), "UTF-8");

21.什么是泛型

泛型本质上是为了将类型参数化,使用泛型可以增加代码的可读性以及稳定性,编译器可以对泛型参数进行检测,如果传入的参数不是指定的类型,程序就会报错。

Java泛型通过擦除的方式来实现,泛型擦除是指在Java编译期间,编译器会将泛型 T 擦除为 Object 类或者将 T extends xxx 擦除为 xxx ,泛型本质上是编译器的行为,为了保证引入泛型机制而不会创建出新的类型,从而减少虚拟机的开销,通过泛型擦除将泛型类转换为普通类。

22.在finally中执行return会导致什么?

在Java中,无论是否发生异常在finally中的代码块都会被执行。在finally中执行return会导致在try块与catch块中的return失效。

23.String创建对象的个数及其原理。

  • 字面量 + 字面量
String s1 = "abc" + "def";  

只创建一个对象:编译期已经将常量拼接为“abcdef”并放入到常量池中,顾只创建一个对象s1。

  • 字面量 + 对象 + 字面量
String s1 = "abc";
String s2 = "abc" + s1 + "def";  

创建三个对象(考虑StringBuilder就四个):其中常量池中abc、def,堆中abcdef。(字符串常量池主要用于存储编译期间可以确定的字符串字面量,而动态构建的字符串则是在程序运行时创建的,他们被分配到堆上,以保证每次执行该代码片段时都可以生成独立的对象实例)

  • new String("xx") + new String("xx")
String s = new String("abc") + new String("abc");

创建四个对象(考虑StringBuilder就五个):JVM首先在String池中创建1个String对象存储“abc”;new关键字会在堆上创建2个String对象;两个字符串相加,会先在内部创建1个StringBuilder对象,最终在堆上存储1个对象abcabc。

2.集合

1.Java有哪些集合

Java中的集合类主要是由Collection和Map这两个接口派生出的,collection又派生出三个子接口,分别是set、list、queue。

所有的Java集合类,都是set、list、map、queue这四个接口的实现类。

set代表无序的,不可重复的集合;

list表示有序的、可重复的集合;

queue表示先进先出的队列;

map表示以键值对的形式来存储数据的集合。

这些接口有着众多的实现类,常见的实现类由,hashset、treeset,hashMap、treeMap、ArrayList、LinkedList

2.Java常用的并发集合有哪些

1.ConcurrentHashMap
ConcurrentHashMap是HashMap的线程安全版本,它通过细粒度的锁来实现高效的并发读写。

2.ConcurrentLinkedQueue
ConcurrentLinkedQueue是一种基于单向链表结构的无界并发队列,他按照FIFO(先进先出)的顺序对元素进行操作,特别适用于“单生产,多消费”的场景。

3.ConcurrentLinkedDueue
ConcurrentLinkedDueue是一种基于双向链表的无界并发队列,与ConcurrentLinkedDueue不同,他同时支持FIFO(先进先出)和FILO(先进后出)两种操作方式,能够在队列的头部和尾部进行插入和删除操作,所以ConcurrentLinkedDueue更适合“多生产,多消费”的场景。

4.CopyOnWriteArrayList
CopyOnWriteArrayList是ArrayList的线程安全变体,他的实现原理是在添加元素的时候对方法进行加锁,先把原List复制一份,再添加新的元素,这样就实现了线程的安全性。

5.CopyOnWriteArraySet 同上,是HashSet的线程安全变体。

3.Java中哪类集合可对元素进行随机访问?

在Java中,允许对元素进行随机访问的集合类通常实现了RandomAccess接口。这个接口没有定义方法,仅仅用来表明某个集合支持快速的随机访问。

其中,ArrayList、Vector、CopyOnWriteArrayList均实现了RandomAccess接口。

4.Comparable与Comparator的区别

二者都可用来进行比较、排序,在Java中基本数据类型可以直接排序,而对于类的比较,需要人工定义逻辑。可以将Comparable比作内部比较器,Comparator比作外部比较器。

Comparable接口是在定义类的时候实现的,Comparator接口是在排序的时候实现的。相较而言,前者更加简单,是需要实现Comparable接口重写compateTo方法即可,而Comparator则需要另一个Comparator接口的实现类来作为比较器。

5.Collection与Collections的区别

Collection是Java中集合Set、List的顶级接口,Collections则是集合的一个工具类,提供了一系列静态方法,比如sort、isEmpty、containsKey等。

6.List、Set、Map的区别

List、Set、Map的区别主要在于数据结构、元素是否有序、是否可重复、键值是否能为null等。

7.为什么Map不继承Collection接口

首先Collection集合定义的是一组对象,代表的是一组独立元素的集合,操作的是单个元素;而Map是键值对形式的集合,操作的是键值对,而不是一组对象的概念,所以Map继承Collection接口毫无意义。

8.常见的线程安全的Map

1.Hashtable:Hashtable中所有关键方法都被syncronized关键字修饰,说明它是方法级别的阻塞,只能一个线程操作,效率低。

2.ConcurrentHashMap:ConcurrentHashMap是目前使用最为广泛的线程安全的Map实现,它提供了更高的并发级别,允许多个线程同时读取而不加锁,且支持一定数量上的并发更新。

在JDK7及以前,ConcurrentHashMap采用分段锁的技术,将整个哈希表分割成多个独立的部分,每个部分都有自己的锁,从而减少了锁竞争,允许更高的并发度。

在JDK8及以后,ConcurrentHashMap不再使用显式的分段锁结构,而是依赖于更细粒度的锁和无锁算法。

3.Collections.synchronizedMap():通过传递给这个静态方法可以将任何Map转换为线程安全的版本。

9.HashMap与Hashtable的区别

  • 共同点

1.首先他们都是以key-value键值对的形式来存储数据;
2.它们都实现了serializable接口,都支持可序列化;
3.它们都实现类cloneable接口,允许被克隆;

  • 不同点

1.HashMap允许出现空的键值,Hashtable不允许;
2.底层数据结构不一样,从JDK8开始,HashMap底层采用的数组+链表+红黑树,而Hashtable采用的数组+链表;
3.HashMap继承于AbstractMap类,Hashtable继承于Dictionary类(被废弃);
4.初始容量以及扩容方式不同,HashMap初始容量为16,Hashtable初始容量为11,他们的负载因子默认都为0.75(泊松分布),当现有容量大于总容量 * 负载因子时,HashMap扩容到当前的两倍,Hashtable则是两倍+1;
5.最主要的区别还是线程的安全性不一致,HashMap没有考虑线程同步,而Hashtable被Synchronized修饰,是线程安全的。

10.HashMap是如何解决Hash冲突的?HashMap为什么会引入红黑树?

1.HashMap采用的是链式寻址法,当发生哈希冲突时,这些键值对会形成一个单向链表,随着冲突数量的增加,链表长度也会增加,这会导致查询、插入删除时的时间复杂度由O(1)恶化为O(n)。

2.为了改善这一机制,Java8引入了红黑树:当链表长度大于8且HashMap的大小超过64时,链表就会转换为红黑树(O(logn)),当链表长度缩小到一定阈值时(默认为6),会转换为链表。

  • 链表长度为什么为8: 考虑最差的情况,理性情况下,链表长度符合泊松分布,长度很难到8。8 是一个综合考虑了性能优化、资源利用以及实际应用中哈希冲突频率等因素后的合理选择。当然,这个值是可以根据具体需求调整的,但它通常不需要修改,除非你有特定的理由和充分的测试支持你的新设定。

  • 为什么不直接使用红黑树: 因为树节点所占用的空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。最开始使用链表的时候,链表较短,查询性能也差不多,但是当链表越来越长时会导致查询效率降低,此时才会以空间换时间,舍弃链表转为红黑树。

11.HashMap和TreeMap如何选?

对于一般的键值对存储操作,HashMap是Map接口中最常用且高效的方法,HashMap基于哈希表实现,能够支持各种快速的常规操作。并且允许存储null的键值,不需要维护任何顺序,所以当我们不需要关心顺序的时候,HashMap是性能最优的。

当我们需要对Map中的键值对进行排序的时候,TreeMap更为合适。 TreeMap基于红黑树实现,自动维护键的自然顺序或通过自定义比较器定义的顺序,在需要顺序访问的场景中更为合适。

12.HashMap的键可以使用对象吗?

HashMap的键值是泛型,他们可以是任何类型,但是需要保证,当对象作为键的时候,确保HashMap能正常工作,需要重写equals和HashCode方法。当对象为可变对象时需要保证HashCode值不会改变。

13.描述一下Map put的过程(HashMap的扩容过程)

1.调用putVal方法将键值对插入到HashMap中;
2.计算键的Hash值,如果为null,则哈希值为0;
3.根据哈希值找到键值对应的位置:

  • 如果HashMap的table数组为null或者长度为0,进行第一次扩容(初始化容量为16);
  • 计算键值对在table数组的位置,如果该位置上没有其他键值对,直接插入;

4.如果该位置已经存在其他键值对,则需要进行链表或者红黑树插入动作:

  • 如果该位置存在相同的键,则直接用新值替换旧值;
  • 如果该位置上存在TreeNode节点,则将键值对插入到红黑树中;
  • 如果该位置上存在链表,则遍历链表到合适的位置插入键值对;
  • 如果链表超过了阈值,则将链表转换为红黑树;

5.插入成功,如果size超过阈值(数组长度 * 负载因子),进行扩容(每次扩容为2倍)。

HashMap扩容两倍的原因:目的是为了降低链表与红黑树转换的概率。
通过除留余数法方式获得桶号,因为Hash表的大小始终为2的n次幂,因此可以将取模转为位运算操作来提高效率,容量为2的n次幂会保证n-1(数组的最大索引值)的二进制全为1,位运算时会充分散列,避免不必要的哈希冲突而形成链表或红黑树。

14.HashMap get方法逻辑

get方法首先会调用key的hashCode方法计算出他的哈希值,再通过哈希值和哈希表的长度计算出它在哈希表中的位置。如果在该位置上的Entry链表不为空,则遍历链表,查找是否存在与key相等的Entry对象,如果存在则返回该对象的value值;如果不存在则返回null。如果该位置上的链表为空,直接返回null。

15.说一下对LinkedHashMap的理解

LinkedHashMap可以认为是HashMap+LinkedList,既使用HashMap操作数据结构,用使用LinkedList维护插入元素的先后顺序,它的底层通过维护一个双向链表保证了元素迭代的顺序。
默认情况下,LinkedHashMap是按照插入顺序进行排序的,如果在创建实例时指定第三个参数为true,那么每次访问某个键时,该键就会被移动到链表末尾。

16.说一下对TreeMap的理解

TreeMap是一个有序的key-value集合,他是通过红黑树实现的。

17.循环删除List可能会出现什么异常?

lndexOutOfBoundsException:动态删除元素导致数组下标越界;
ConcurrentModificationException:调用List.remove()方法时modCount和expectedModCount值不一致导致报错。list.remove() 没有对 expectedModCount 重新赋值,需要使用迭代器 iterator.remove();

3.IO

1.什么是IO?

Java中的IO主要是指使用Java进行输入、输出操作,包括但不限于:磁盘文件读写、网络数据读写、内存数据读写。
Java中的I/O操作核心库在Java.io和Java.nio包下面,他们提供了全面的IO操作接口和实现类,IO读写是以“流”为基础的进行输入输出的,当程序读写数据时,就会开启一个通向来源对象(读)的流,以及通向目标对象(写)的流,数据在输入和输出介质中“流”动,所以叫“流”。

2.如何从数据传输方式理解IO流?

1.按照流的方向

  • 输入流:只能从中读取数据(主要由InputStream和Reader作为基类)
  • 输出流:只能从中写入数据(主要由OutputStream和Writer作为基类)

2.按照数据传输单位

  • 字节流:以字节为单元,可以操作任何数据(文本文件、图片文件)(主要由InputStream和OutputStream作为基类)
  • 字符流:以字符为单元,只能操作纯字符数据(主要是文本文件的读写)(主要由Reader和Writer作为基类)

3.按照流的操作模式

  • 节点流:可以向一个特定的IO设备(磁盘、网络)读/写数据的流
  • 过滤流:用于对一个已存在的流进行连接和封装,通过封装后的流来实现数据的读/写功能

4.同步与异步

  • 同步IO:发送一个读/写请求后,调用方需要等待操作完成后才能继续执行
  • 异步IO:发送一个读/写请求后,调用方无需等待,可以在后台处理时继续执行其他任务

5.阻塞与非阻塞

  • 阻塞IO:当读写操作没有立即完成时,线程会被挂起,直到操作完成
  • 非阻塞IO:如果当前无法立即完成读写,不会阻塞线程

3.如何理解NIO、BIO、AIO

1.BIO

  • 同步并阻塞,服务端为每个客户端连接创建一个新的线程/进程来处理请求,如果整个连接不做任何事情会造成不必要的线程开销。

2.NIO

  • 同步非阻塞,服务实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接由I/O请求时才会启动一个线程进行处理。

3.AIO

  • 异步非阻塞,服务实现模式为一个有效请求一个线程,客户端的连接请求和发送数据的请求使用不同的线程来处理,它不是当I/O准备好时才通知线程,而是当I/O操作完成后才通知服务端程序启动线程去处理,相当于一个回调。

BIO是同步阻塞IO,其读、写、连接等IO所有操作都是阻塞的。NIO是同步非阻塞的多路复用IO,与BIO相比,Channel+Buffer的非阻塞操作可以提高读写性能,多路复用可以支持大量连接。AIO是异步非阻塞IO,与NIO相比,使用了完全异步的方式,没有多路复用器,因此简化了编程实现。

4.如何理解同步IO与异步IO

同步IO是指在读写数据时,应用程序必须等待操作系统完成数据读写操作后才继续执行下一步操作。
异步IO是指在读写数据时,应用程序不必等待操作系统完成数据读写操作,可以继续执行其他任务。在数据读写完成后,操作系统会通知应用程序,以便应用程序进行后续处理。

5.如何理解阻塞IO与非阻塞IO

阻塞IO是指在程序请求读写操作时,需等待操作完成后才能继续执行其他操作。
非阻塞IO是指在程序请求读写操作时,无需等待操作完成就能返回执行其他操作。

6.字节流如何转换为字符流

在Java中,字节流(OutputStream、IntputStream)与字符流(Reader、Writer)之间的转换可以通过InputStreamWriter和InputStreamReader来进行转换。

7.什么是Java的序列化以及反序列化

在Java中,序列化机制可以将对象转换为字节码文件,这些字节序列文件可以保存的硬盘上,也可以在网络中传输,并允许程序将字节序列恢复成原来的对象。
其中,对象的序列化,是指将一个Java对象写入IO流中,对象的反序列化是指从IO流中恢复该对象。
如果对象要支持序列化机制,需要实现serializable接口。

8.什么是缓冲流

缓冲流是Java I/O流的一种增强方式,通过使用缓冲流,可以显著提高读写操作的效率。
使用普通流(比如FileInputStream、FileOutputStream)直接读写文件时,每次调用read()或write()方法都会导致一次底层调用,而缓冲流则是在内存中维护一个缓冲区(buffer),数据首先会被读写到这个缓冲区中,只有当缓冲区满或者读取的数据不存在于当前缓冲区中才会触发实际的磁盘I/O操作。

9.IO流中的close与flush区别

调用flush()不会关闭流,close()方法会执行flush()方法并关闭流。
如果只是想确保数据立即写入而不关闭流,可以用flush(),如果是执行完毕所有操作不需要流时,使用close()。

10.序列化ID(serialVersionUID)的作用

serialVersionUID是一个被static final修饰的长整型变量,用于唯一标识一个可序列化的类,它的作用是在反序列化过程中确保发送方和接收方的版本兼容性。(序列化时,作为对象的一部分保存起来,反序列化时,校验是否一致)

11.字节流与字符流的区别

字节流 字符流
操作基本单元 字节 字符
是否使用缓冲区 是,如果频繁对一个资源进行IO操作,会先把需要操作的数据暂时放到内存中,以此来避免多次的IO操作,提高效率。
存在位置 可存在与文件、内存中。硬盘上所有文件都是以字节方式进行存储的 只存在于内存中
使用场景 适合操作文本文件之外的文件。比如:图片、音视频。 适合操作文本文件。(字符流内置字符编码的支持,可以自动处理字符编码和解码的工作;缓存机制(BufferedReader、BufferWriter))
Java相关类 InputStream、OutputStream Reader、Writer

4.多线程

1.创建线程的几种方式

创建线程有三种方式。

  • 继承Thread类并重写run方法来创建线程,实现简单但不可以继承其他类;
  • 实现Runnable接口并重写run方法,避免了单继承的局限性,更加灵活,实现解耦;
  • 实现Callable接口并重写Call方法,可以获取线程执行结果的返回值,并且可以抛出异常;
  • 底层的创建线程的方法就以上三种,至于其他线程池、匿名内部类等方式都是基于以上三种。

2.Runnable与Callable的区别

相同点

  • 都是可用来编写多线程程序的接口;
  • 都需要调用Thread.start()来启动线程;
  • Callable是类似Runnable的接口,实现Callable接口的类和实现Runnable接口的类都是可被其他线程执行的任务。

不同点

  • 核心区别就是Runnable接口重写的run方法无返回值,Callable的call方法有返回值且支持泛型;
  • Callable的call方法可以抛出异常,Runnable的run方法的异常只能内部消化,不能上抛。

3.如何启动一个线程?调用start和run方法的区别

1.线程调用run方法后,仅仅是对象调用方法,相当于在当前线程中执行了run方法中的代码块;
2.当线程调用start方法后,线程就进入了就绪状态,并且start方法启动线程将自动调用run方法;
3.总结:调用start方法开启线程,线程进入就绪状态,而run方法只是Thread类的一个普通方法。

4.为什么要使用多线程

1.重复利用多核CPU:多线程可以真正充分利用和发挥多核CPU的优势,同时独立完成多个任务而互不干扰;
2.提高运行效率:可以将一个任务拆分成多个子线程去执行,从而提高程序的运行效率;
3.防止阻塞:多线程同时处理一个任务,就算一个线程阻塞了,也不会影响整个任务。

5.volatile关键字的用法

volatile是Java中的一个关键字,同于修饰变量,以确保该变量的读写操作具有可见性和禁止指令重排序(内存屏障,防止JVM或者编译器优化而导致的指令重排序)。

6.什么是原子性?

原子性是指一个或多个操作,要么全部执行,要么全部不执行。

7.什么是可见性?

可见性是指一个线程修改了某个变量的值对其他线程可见,即任何线程在访问时都会看到最新的值。

8.什么是有序性?

有序性是指程序的执行按照代码的先后顺序来一行一行的执行,执行过程中不会出现代码指令重排序。

9.i++/i--的操作是原子性吗?

不是,因为i++/i--操作分为读取当前变量的值以及将当前值增加或减少1两个步骤。在多线程环境中,如果多个线程同时对同一变量进行自增或自减操作,可能会导致竞态条件。

10.long、double读写是原子性吗?

不是,但是可以通过volatile修饰来实现原子性。

11.如何保证可见性?

1.使用volatile关键字来修饰变量;
2.使用synchronized关键字或者Lock锁来修饰代码块;
3.使用final修饰变量(final可以保证变量在初始化后不可再修改,需要注意,final的可见性仅在初始化完成之后,如果是一个引用类型,在构造函数未完全执行结束之前访问该对象状态则不能保证)

12.什么是JUC?

JUC是java.util.concurrent并发线程包的简称,JUC这个包下的类包含了我们在处理并发编程中所用到的一些工具,包括锁、线程池、阻塞队列等。

13.什么是守护线程?

Java中的线程主要分为用户线程和守护线程两类。Java语言中无论是线程还是线程池,默认都是用户线程,因此用户线程又叫普通线程。守护线程又称为后台线程、服务线程或者精灵线程,守护线程是为用户线程服务的,当线程中的用户线程都执行结束后,守护线程会自动结束。守护线程具有用户线程不具备的自动结束生命周期的特征。

  • GC垃圾回收线程就是一个经典的守护线程,当我们程序不在有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器就会自动离开。
  • 在任何情况下,程序结束时,这个线程必须正常且关闭,就可以使用守护线程。

14.线程的状态有几种以及是如何流转的?

1.在操作系统中,线程分为初始、可运行、运行、休眠、终止五种状态;
2.在Java中,线程则分为六种状态。Java将可运行及运行都归于运行这一类,把休眠分为阻塞、等待、超时等待三种状态,所以Java中线程有新建、运行、阻塞、等待、超时等待、休眠六种状态;

状态之间的转换:

  • new -> runnable
    当我们new一个线程的时候,这个线程处于新建状态,而当我们调用线程的start方法后,线程由新建状态转换为运行状态;
  • runnable -> blocked
    当线程争抢锁失败后,线程就由运行状态转换为了阻塞状态;
  • runnable -> waiting
    当线程调用了Object类的wait方法或者Thread类的join方法再者是LockSuppory的park方法后,线程就由运行态转变为了无限等待;
  • runnable -> timedwaiting
    超时等待与等待类似,当线程调用带有时间参数的sleep、wait、park、join方法后,线程就由运行态转变了超时等待;
  • runnable -> terminated
    当线程执行完run方法后或者线程出现了没有捕捉到的异常后,线程就会转变为终止态。另外,被废弃的stop(杀死线程)方法也可以将线程转变为终止态。

15.如何让三个线程按照顺序执行

1.Thread.join()方法:可以让线程等待另一个线程完成后再继续,通过连续调用多个线程的join()方法,可以确保它们按照顺序执行;
2.通过使用Thread.wait()、Thread.notify()、Thread.notifyAll()方法;
3.其他更高层次的并发工具,比如CountDownLatch、CyclicBarrier、Phaser等。

16.如何让三个线程按照顺序执行

1.Thread.join()方法:可以让线程等待另一个线程完成后再继续,通过连续调用多个线程的join()方法,可以确保它们按照顺序执行;
2.通过使用Thread.wait()、Thread.notify()、Thread.notifyAll()方法;
3.其他更高层次的并发工具,比如CountDownLatch、CyclicBarrier、Phaser等。

17.线程相关的基本方法

1.线程等待wait
调用该方法会使线程转变为waiting态,只有在等待其他线程通知或被中断后才能够返回,调用wait方法会释放锁。

2.线程休眠sleep
sleep会使当前线程休眠,与wait方法不同,sleep不会释放锁,会使线程进入timedwaiting态。

3.线程让步yield
yield会让当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片。

4.等待其他线程终止join
在当前线程调用另一个线程的join方法,会使当前线程陷入阻塞状态,直到调用的线程结束。

5.线程唤醒notify
notify是Object类的方法,调用notify会将等待队列的一个线程唤醒。(如果等待队列有多个线程,会随机唤醒一个)

6.全部唤醒notifyAll

18.wait与sleep的区别

1.wait是Object类的方法,sleep是Thread类的方法;
2.sleep可以在任何地方使用,wait只能在synchronized方法或者被synchronized修饰的代码块中使用;
3.调用wait方法会进入waiting状态,调用sleep方法会进入timedwaiting状态;
4.wait不需要捕获异常,sleep需要;
5.最大的区别还是wait会释放锁,sleep不会。

19.如何实现线程的同步

1.使用syncornized关键字来修饰方法或者代码块,确保同一时刻只有一个线程在执行被保护的代码段;
2.使用ReentrantLock可重用锁,比synchronized更加灵活;(例如尝试获取锁、设置超时)
3.使用volatile定义一个状态变量;
4.使用atomic类(juc下),可以在不使用锁的情况下实现高效的线程安全操作;

19.Java之间线程的通信方式

1.使用wait、notify、notifyAll方法;
2.使用Java并发工具等。

20.线程与进程的区别

进程是操作系统资源分配的基本单位,线程(轻量级进程)是处理器任务调度和执行的基本单位。

20.Lock是悲观锁还是乐观锁?悲观锁和乐观锁的区别

Lock是悲观锁。
悲观锁,顾名思义,就是比较悲观,用悲观的态度解决一切数据冲突,每次别人的请求它都会先对数据加锁,然后对数据进行操作,最后再解锁。
乐观锁,就是对数据持有乐观态度,操作数据的时候不会对数据进行加锁,只有当数据提交的时候才会通过一种机制去验证数据是否冲突。(比较典型的就是版本号控制,定义一个version字段,每次修改就+1,当一个线程要更新数据的时候,会首先读取version值,提交数据时验证version值如果不一样就不更新,类似于CAS操作)。

  • CAS:一种无锁机制算法,是一种原子操作
    CAS包含三个参数,分别是内存位置(V)、预期旧值(A)、新值(B)。它会检查内存位置V的值是否等于预期旧值A,如果等于,将内存位置V的值设置为新值B,如果不是,不做任何修改。

21.什么是死锁以及死锁产生的条件

死锁是指在操作系统中,两个或多个并发线程在执行过程中,因争夺资源而导致的一种互相等待的现象,在无外力干扰条件下,这些线程都无法进一步执行下去。
死锁的产生必须满足四个必要条件:

  • 互斥:资源不能同时被多个线程占用。
  • 占有且等待:一个线程已经占有了一个或多个资源,此时再去请求其他线程占有的其他资源时就会造成等待,且等待过程中不会释放已经占有的资源。
  • 不剥夺条件:不会去强行占用其他线程占用的资源。
  • 循环等待:若干线程之间形成一种头尾相连的循环等待资源关系。

22.如何避免死锁

避免死锁,需要破坏死锁产生的四个必要条件之一。

  • 破坏互斥条件:避免使用互斥锁,可以使用CAS原子性操作。
  • 破坏占有且等待条件:一次性获取全部资源。
  • 破坏不剥夺条件:可以通过设置资源持有的超时时间从而自动释放资源。
  • 破坏循环等待条件:确保按照顺序来申请资源。

5.JVM

1.Java为什么能一次编写,多次运行

JVM时Java跨平台的关键。
在程序运行前,Java源代码(.java)需要经过编译器编译成字节码文件(.class)。在程序运行时,JVM负责将字节码文件翻译成特定平台下的机器码并运行,也就是说,只需要在不同平台上安装对应的JVM,就可以允许字节码文件。

6.MySQL

1.MySQL与Oracle的区别

Mysql是一个开源的关系型数据库管理系统,用于管理数据。
Oracle是一个收费的关系型数据库管理系统。
区别:

  • MySQL是轻量级数据库,小巧免费,Oracle是大型数据库软件,收费,支撑体系完善,安全性高;
  • MySQL可以使用双引号包起字符串,Oracle不允许;

2.主键外键有什么区别?

主键:用来标识一条记录的唯一标识,不可重复或者为空,默认为聚簇索引。
外键:表的某列是另一张表的主键,可重复或者为空,用来和其他表建立连接。

3.什么是范式化与反范式化?

范式化:是一系列设计数据库模型的规范,范式化的目的是减少数据冗余、避免数据异常并提高数据完整性的过程。通过将数据库划分为更小的、结构更合理的表,并定义他们的关系,范式化可以帮助数据库结构的逻辑性和高效性。
反范式化:是对范式化设计的数据库的性能优化策略,通过在表中增加冗余的数据或者字段来提高数据库的读取性能。没有冗余的数据库不一定是最好的数据库,有时为了提高查询效率,就必须降低范式标准,适当保留冗余数据。具体操作就是在一个表中增加另一个表的冗余字段,从而减少两个表查询时的关联,进而提高查询效率。

4.数据库三大范式?

范式是我们设计数据库时遵循的一种规范要求,主要为了消除重复数据减少冗余数据以及消除潜在的异常(插入异常、更新异常、删除异常)。

  • 第一范式(1NF):强调的是数据的原子性,即每一列都是不可再分的最小单元(比如省市区区分为三个字段)。
  • 第二范式(2NF):在满足第一范式的前提下,要求表中的数据都有唯一标识,并且要求表中的非主键必须依赖于整个主键,而不是仅依赖于部分主键。如果存在不完全依赖,就必须分解为更小的表以满足第二范式。
  • 第三范式(3NF):在满足第二范式的前提下,要求表中的非主键列之间不应该存在传递依赖关系,即数据库中的每一个非主键字段和主键字段直接相关,任何非主键列都不应该依赖其他非主键列。

5.什么是事务?

事务(Transaction)是指一个操作要么全部成功,要么全部失败。
数据库事务是指对数据库的一系列操作组成的逻辑工作单元。
事务具有四个关键属性(ACID)。

  • 原子性(Atomicity):事务是一个不可分割的单位,事务中的操作要么全部成功,要么全部失败。
  • 一致性(Consistency):事务前后数据的完整性必须保持一致。(转账)
  • 隔离性(Isolation):由并发事务所作的修改必须和其他事务所作的修改隔离。
  • 持久性(Durability):事务提交后对系统的影响是永久的。

6.如何进行SQL优化?

  • 优化查询语句:
    1.避免使用select *,避免返回不必要的字段。
    2.使用limit限制数据返回的条数。
    3.使用exists代替in操作。
    4.优化Join操作(小表驱动大表、减少Join表的数量)。

  • 优化索引:
    1.为常用字段添加索引,对where、join、order by、group by中的字段创建索引。
    2.避免过多索引,索引会增加写操作的开销,只在必要时创建索引。
    3.使用覆盖索引,从而避免回表操作。
    4.避免任何索引失效的场景。

  • 表结构优化:
    1.选择合适的数据类型,使用更小的数据类型来减少存储空间。
    2.范式化与反范式化,范式化能减少数据冗余,但会导致过多的表进而影响性能。

  • 分析工具:
    1.使用explain分析查询,判断是否使用了索引,是否有全表扫描等问题。
    2.使用show profile分析性能,查看详细的执行时间以及资源消耗。
    3.使用慢查询日志,找出执行时间长的SQL。

7.索引失效的场景。

1.模糊查询like以%开头。
2.or语句前后没有同时使用索引。
3.对where子句中进行函数或表达式操作。
4.索引列字段出现隐式类型转换(数据类型不匹配),比如字符串123。
5.使用!=或者not in,因为mysql无法利用索引来排除数据。
6.联合索引未遵循最左前缀原则(查询字段需要从最左边的字段开始)。

8.索引的理解

当数据库有大量的数据时,查询速度就会变慢,索引就是一个加快数据库查询的解决方案。
优点:减少服务器需要扫描的数据量、帮助服务器避免排序和生成临时表、将随机IO变为顺序IO。
缺点:降低了数据写入的效率(索引需要动态的维护)、索引占用了一定空间。

9.索引的分类

类型 说明
普通索引 最普通的索引
唯一索引 与普通索引类似,但要求索引列的值唯一且允许为null
主键索引 特殊的唯一索引,一个表只能有一个主键,不能为空值
联合索引 多个字段上创建的索引,使用时遵循最左前缀规则
全文索引 主要是为了快速检索大文本数据中的关键字信息

10.MyISAM和InnoDB的区别

1.MyISAM不支持事务,InnoDB支持事务;
2.MYISAM不支持外键,InnoDB支持;
3.MyISAM是表锁,InnoDB是行锁;
4.MyISAM支持全文索引,InnoDB不支持;

11.Mysql回表是什么?什么情况下需要回表

Mysql回表是指根据索引查询到的主键值再去访问主键索引,从而获取完整的数据记录。

  • 触发回表的条件:
    1.当查询语句需要返回的列不在索引列上时,即使索引定位到了相关行,仍然需要通过回表来获取其他列的值。
    2.使用了非聚簇索引,非聚簇索引只包含了索引列的副本以及指向对应主键的引用,查询需要通过回表才能获取完整的数据。
    3.使用了覆盖索引但超过了最大索引长度,在Mysql的InnoDB存储引擎中,每个索引项的最大长度时767字节,超过需要回表。(回表一般发生在读操作上)

  • 避免回表:
    1.使用覆盖索引,如果查询的字段都在某个索引上并且未超过最大索引长度限制,不会触发回表。
    2.使用聚簇索引,InnoDB引擎的主键索引是聚簇索引,包含整行的数据,不会触发回表。

  • 回表操作的问题和场景:
    1.性能问题:回表需要访问两次索引,增加了IO开销和CPU消耗,所以对性能有一定影响。
    2.数据一致性问题:回表基于物理地址来获取数据,如果回表过程中发生了数据修改,会导致数据一致性问题。

20.count(字段)与count(*)的区别

二者都是SQL中用来统计记录数的聚合函数,区别在于处理null时有不同的行为。
count(字段)查询结果不包含非空值,count(*)是包含的。

7.Redis

1.什么是Redis,它的作用是什么?

Redis是一个开源的、基于内存的键值对存储NoSQL数据库,通常被用作数据库、缓存和消息中间件。它支持多种数据结构(如字符串String、哈希Hash、列表List、集合Set、有序集合Zset),并提供了高性能的读写操作。

作用:主要用来减轻数据库的压力,比如说可以把数据以键值对的形式存到Redis中,后端访问数据库的时候,可以先查询Redis中有没有找个数据,如果有直接取出来就没必要访问数据库了。也可以解决分布式微服务当中的数据共享问题,将数据存到Redis中,这些服务能够共享Redis的数据。

8.ORM框架

1.Mybatis

1.什么是ORM框架?

ORM(Object-Relational-Mapping)指的是对象关系映射,它是一种编程技术,将面向对象的编程语言与关系型数据库之间建立起了一种映射关系,使得程序员可以通过编写面向对象的代码来完成数据的交互。
ORM的基本思想是将数据库表映射为类,将每一行数据映射为一个对象,将每一列映射为一个属性。ORM框架可以自动完成对象与数据库表之间的映射。

  • 全自动ORM
    Hibernate是自动化的,内部封装了JDBC与SQL,使得开发人员无需关心SQL是如何构造和执行的。
  • 半自动ORM
    Mybatis是半自动化的,内部封装了JDBC,但是将定义SQL的工作交给了用户实现。

2.Mybatis有哪些优点?

这里对比SpringDataJPA来描述。
1.灵活性
Mybatis:基于SQL语句编程,相对灵活,可以自定义一些较为复杂的SQL。
SpringDataJPA:基于全自动ORM概念,更侧重于抽象化SQL,从而使得代码更面向对象,但对于复杂的SQL不够灵活。
2.动态SQL支持
Mybatis:内置了强大的动态SQL功能。
SpringDataJPA:需要依靠QueryDSL来实现类似的功能,灵活性不如Mybatis。 3.数据库移植性
Mybatis:因为需要手动编写SQL,所以设计到不同的数据库移植需要修改代码。
SpringDataJPA:更加面向对象,理论上不需要进行代码的修改就可以完成数据库的移植。

3.Mybatis中,${}和#{}的区别是什么?

#{}为预编译参数,${}为文本替换。
#{}可以防止SQL注入(代码注入类型的网络攻击,攻击者通过网页表单、URL参数等插入恶意的SQL代码片段进而改变预期的SQL语句从而实现一些未经授权的操作)。

4.Mybatis和JDBC的区别?

JDBC是Java DataBase Connectivity的缩写,是Java语言提供的标准API。
Mybatis是持久层框架,简化了数据库访问逻辑,使得开发者不需要直接编写大量JDBC代码。

5.Mybatis有哪些核心组件?

1.SqlSessionFactoryBuilder(构造器)
这是Mybatis的一个构建器类,用于从XML配置文件、注解或自定义环境等创建SqlSessionFactory。
2.SqlSessionFactory(工厂接口)
一旦SqlSessionFactory被创建,他就应该被当作单例实例保存起来,重复使用,这个工厂对象用于创建SqlSession实例。
3.SqlSession(会话)
这是一个面向用户的接口,用于执行SQL命令,提交或回滚事务,以及获取映射器实例。
4.SQL Mapper(映射器)
由一个Java接口和XML文件(或注解)构成,需要给出对应的SQL和映射规则。它负责发送SQL去执行并返回响应结果。

6.Mybatis的缓存机制有什么用?

mybatis的缓存,包括一级和二级缓存,一级缓存的作用域是一个sqlSession内,二级缓存的作用域是针对一个Mapper进行缓存。

2.SpringDataJPA

1.JPA是什么框架?

JPA全称Java Persistence API,中文意为Java持久层API。JPA并不是一个框架,而是一类框架的统称,比如早期的Hibernate,以及SpringDataJPA,都归属于ORM框架,用在关系型数据库和业务实体之间做映射,并对这些实体实现快速的CRUD操作。

2.SpringDataJPA 和 JPA 的区别?

JPA是一个规范,本身不提供具体的实现。SpringDataJPA是建立在JPA之上的一个更高层次的抽象,旨在简化数据访问层的操作,它是SpringData的一部分。
总的来说,JPA是一种持久化标准,规定了如何将Java对象映射到数据库的表中,而SpringDataJPA是基于JPA的一个框架,它进一步简化了数据访问层的开发。

9.Spring

1.对IOC的理解

Spring IOC是Spring框架中的核心思想之一,它通过控制反转(IOC)和依赖注入(DI)的方式,将对象的创建和依赖关系的管理交给Spring容器处理。IOC的主要优点包括解耦、可维护性、可测试性以及灵活性。Spring提供了多种依赖注入的方式(如构造函数注入、setter方法注入和字段(注解)注入),并通过BeanFactory和ApplicationContext实现Bean的管理。IOC的设计使得程序更加模块化,便于扩展和维护。

2.对AOP的理解

Spring AOP是一种面向切面编程的技术,用于将横切关注点(如日志记录、事务管理、权限校验等)与核心业务逻辑分离。AOP的核心概念包括切面、通知、切点、连接点和织入。Spring AOP通过动态代理(JDK动态代理和CGLIB动态代理)实现织入,支持多种通知类型(如Before、After、Around等)。AOP的主要优点包括解耦、复用、可维护和灵活性。

3.BeanFactory和ApplicationContext的区别

BeanFactory和ApplicationContext是Spring框架自带的两个IOC容器。其中BeanFactory是IOC容器最基础的版本,而ApplicationContext是BeanFactort的子接口,扩展了其特性。
区别:BeanFactory采用延迟加载的形式注入Bean,只有在我们用到Bean的时候,才会对Bean进行加载实例化;ApplicationContext则是在容器启动的时候,一次性加载出来所有的Bean,这样的好处是,可以尽早的发现Spring中存在的错误配置,并且当我们需要某个Bean的时候,他会直接加载,系统的运行速度更快,但是相比之下,BeanFactory启动所占用的资源更少。

4.Spring Bean的生命周期

Spring Bean的生命周期指的是Bean对象从创建到销毁的一个过程。
Spring中Bean的生命周期大致分为实例化、属性赋值、初始化、使用、销毁五个阶段。
实例化:这个阶段主要是Spring加载配置化文件,并通过反射机制来创建Bean的过程;
属性赋值:实例化之后,Spring会调用Bean对应的set方法或者是构造方法来进行相应的属性赋值;
初始化:Bean完成实例化以及属性赋值后,就会调用Bean的初始化方法,进行一些额外的操作;
使用:这个阶段的Bean对象能够被真正的使用;
销毁:当某个Bean对象不再被使用的时候,就会被销毁。

5.Spring事务失效场景

  • 未正确使用@Transactional注解:未加注解或者加在了非public方法上;
  • 异常被捕获并未重新抛出:Spring默认行为是在遇到异常和错误时回滚事务;
  • 数据库引擎不支持事务:比如说Mysql的MyISAM;
  • 传播行为设置不当:事务的传播行为决定了在一个事务上下文调用另一个事务方法的行为;
  • 代理问题:如果绕过了Spring的AOP代理机制自己New对象的话事务会失效;
  • 超时或隔离级别设置不当:如果事务超时时间设置过短,可能会导致事务提前终止,不合理的隔离级别也会影响事务的行为;
  • 异常类型不匹配:默认情况下,Spring事务注解只对运行时异常(RuntimeException)和Error回滚,如果是检查异常(IOException),事务不会回滚。

6.Spring中事务的传播行为

Spring事务的传播行为定义了多个事务方法相互调用时,事务应该如何传播。

  • 如果当前存在事务就加入,否则就创建一个事务,这是事务的默认传播行为;
  • 如果当前存在事务就加入,没有就以非事务方式继续运行;
  • 如果当前存在事务就加入,没有就抛出异常;
  • 创建一个事务,如果当前存在事务,就暂停当前事务;
  • 以非事务方式执行,如果当前存在事务,就暂停当前事务;
  • 以非事务方式执行,如果当前存在事务,就抛出异常;
  • 如果当前存在事务,则在嵌套事务内执行,否则类似于默认。

7.事务的四种隔离级别

Spring事务的隔离级别定义了事务之间的可见性和并发性控制。

  • 读未提交:就是一个事务可以读到另一个事务未提交的数据,可能会导致脏读、不可重复读、幻读;
  • 读已提交:只允许一个事务读取另一个事务已经提交的数据;
  • 可重复读:事务开启后,不再允许修改操作,确保不会发生不可重复读;
  • 串行化:最高的隔离级别,串行化执行事务,但降低了并发性能。

10.SpringMVC

1.什么是MVC?

MVC是一种设计模式,这种模式下软件会分为Model(数据)、View(用户界面)、Controller(数据的处理逻辑)三层,MVC的好处是将软件进行分层,从而降低了代码的耦合读,便于代码维护。

2.SpringMVC的执行流程?

1.用户发起请求,请求先被Servlet拦截转发给SpringMVC框架;
2.SpringMVC里面的DispatcherServlet(前端控制器)接收到请求转发给HandlerMapping;
3.HandlerMapping负责解析请求找到匹配的Controller;
4.找到匹配的Controller后将请求参数传递给Controller中的方法;
5.Controller中的方法执行完后会返回一个ModelAndView;
6.视图解析器根据名称找到视图,然后把数据模型填充到视图里面再渲染成Html内容返回给客户端.

11.SpringBoot

1.SpringBoot自动装配原理

SpringBoot的自动装配实际上就是根据应用的依赖与配置(spring.factories)获取到对应的需要进行自动装配的类,并生成相应的Bean对象,然后把他们交给Spring容器帮我们管理。

  • 原理:
    SpringBoot的@SpringApplication注解是一个复合注解,主要作用就是标识这个类是SpringBoot的主配置类,SpringBoot允许这个类里面的main方法来启动程序。
    这个注解由三个子注解组成:
    @SpringBootConfiguration:标记当前类为SpringBoot的配置类;
    @EnableAutoCongiguration:启动SpringBoot的自动装配机制;
    @ComponentScan:扫描当前包及其子包下面的组件(@Service、@Compoent、@Repository等);

场景相关

1.如何保证接口幂等性?

幂等性:同一操作,无论是一次还是多次执行,产生的结果是一致的,不会因为多次执行而产生副作用。

  • 前端页面控制:当用户点击一次时,禁用按钮。
  • 服务端:通过特定的标识或者请求方式来校验请求的幂等性,比如客户端请求时携带一个唯一标识符(可以使用redis的Setnx(当指定的key不存在时,为key设置特定的值,如果存在,设置失败并返回0)来存储次标识符),服务端接收请求时对此标识符进行相关的逻辑处理。

设计模式

1.设计模式的原则

开闭、单一职责、接口隔离、依赖倒转、里氏替换、迪米特、合成复用。

2.设计模式的分类及功能

1.按目的分类
创建型模式:用于描述“怎么创建对象”,主要特点为“将对象的使用与创建分离”。包含的设计模式有(单例、原型、工厂、建造者)。
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构。包含的设计模式有(代理、适配器、桥接、装饰、外观、享元、组合)。
行为型模式:用于描述类或对象之间怎么相互协作共同完成单个对象都无法完成的任务。包含的设计模式有(策略、模板、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器)。

2.按作用范围分类
类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时便确定下来了。包含的设计模式有(工厂、适配器、模板方法、解释器)。
对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。除以上四种都为对象模式。

3.策略模式

含义:定义一系列可互相替换且独立封装的算法,在运行时可以动态替换。
核心:一个抽象(可以是接口,也可以是抽象类)+ 多个实现,程序运行时,根据类型去找对应的实现类。
优势:

  • 开闭原则:添加策略很方便,只需要多加一个策略类,不需要修改通用的逻辑。
  • 单一职责原则:逻辑解耦,每个策略类只需要关注自身的逻辑实现。
  • 提高代码的优雅度,减少if else。

4.单例模式

含义:确保某个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取该实例。
核心:

  • 将类的构造方法私有化,防止外部直接实例化该类。
  • 提供一个静态方法来获取该类的唯一实例,确保整个应用程序中只有一个实例存在。

优势:避免一个全局使用的类,频繁的创建与销毁,浪费系统资源。
写法:

  • 饿汉式(线程安全):预加载,类加载时直接创建对象。
  • 懒汉式(线程不安全):很懒,延迟加载,只有当第一次被调用时才去创建实例。
  • 懒汉加锁:在懒汉式的基础上使用synchronized关键字来修饰get方法实现线程同步,效率低。
  • 懒汉双检锁:改进版懒汉加锁,不直接通过synchroized关键字来修饰整个方法,而是配合volatile关键字来保证变量的可见性、有序性。
  • 静态内部类:通过静态内部类的特性(外部类被加载时,静态内部类并不会被加载)来实现单例。
  • 枚举:枚举默认为单例。

5.工厂模式

含义:将对象的实例化封装在工厂类中,将对象的创建与使用分离。 核心:传入参数来获取对应的对象。
优势:对象的创建与使用分离从而减少了系统的耦合度。
写法:

  • 简单工厂模式:将创建对象的方法单独写在抽象工厂类中(一个方法,单独写每个对象的创建逻辑)。
  • 工厂方法模式:为每一类对象都去创建一个工厂子类,每一个工厂子类实现自身的创建对象的逻辑。
  • 抽象工厂模式:工厂方法的升级版,适用于子类下面又可细分子类的场景。
  • Java
  • 面试题