java面试题学习总结

301 阅读18分钟

1、什么是面向对象,谈谈你对面向对象的理解

  • 面向过程更注重事情的每一个步骤和顺序,面向对象更注重事情有哪些参与者(对象),以及参与者需要做什么

  • 面向过程更加高效,而面向对象更易于复用,扩展和维护。-》降耦合,提扩展

  • 面向对象三大特性-》封装、继承、多态

  • 封装:明确标识出允许外部使用的所有成员函数和数据项,外部调用无需修改或者关注内部实现。private,getter,setter。再比如就是 操作数据库我们不需要关心如何建立连接,如何执行mysql 我们只需要引入mysql并且调用方法即可。

  • 继承:子类继承父类方法,并且作出自己的改变/扩展,父类已有的不需要在定义

  • 多态:一种事物的多种状态。基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。要有继承,方法重写,父类引用指向子类对象。无法调用子类特有的功能

  • 拓展,可以说一些jvm如何继承,多态的、

    • Animal a = new Dog();
    • java面向对象,面向对象关注左边,调用子类复写的方法,在编译期间无法知道运行的具体方法
    • 属于虚方法字节码invokeVitural方法,因此在类的方法区加载类的时候会建立虚方法表,可以引用到父类对象,避免每次执行的时候动态分派。

2、jdk,jre,jvm三者区别和联系

  • jdk:java development kit开发工具

  • jre:java runtime environment运行环境

  • jvm:java virtual machine虚拟机

jdk>jre>jvm

.java文件编译成.class文件,之后可以运行在各个系统的jvm中 一次编译,多次运行。

3、==和equals区别

  • ==对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址

  • equals:object中默认采用==比较,通常会重写

4、简述final作用,为什么局部内部类和匿名内部类只能访问局部final变量

  • 类:不能被继承
  • 方法:方法不能被子类覆盖,可以重载
  • 变量:不能改变其值 如果修饰类变量(static final),声明或者静态代码块赋值

如果修饰成员变量,声明的时候就要有初始值,或者在代码块,构造器赋值

  • 无static是成员变量,有static是类变量

如果修饰局部变量,声明可以不赋值,使用前一定赋值(如果是引用类型变量,赋值后不能再指向其他对象,但对象的值可以变化)

匿名内部类形如

public void test(final int a) {
	final int b = 10;
	new Thread(){
		public void run(){
			sout(a);
			sout(b);
		}
	}
}

局部内部类形如

public void test(final int x) {
	class InClass{
		public void InTest(){
			sout(x);
		}
	}
}
  • 内部类和外部类是一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕而销毁,所以当外部类方法结束时,局部变量就会销毁了,然而内部类对象可能还存在(只有没人在引用它时,才会死亡)
  • 因此可能出现内部类访问了一个不存在的变量。为解决,将局部变量复制一份作为内部类的成员变量
  • 将局部变量复制给内部类的成员变量时,必须保证这两个变量是一样的,也就是说如果在内部类中修改了成员变量,方法的局部变量也要随着改变
  • 因此将局部变量设置为final,初始化后,就不让再修改,保证内部类成员变量和方法的局部变量的一致性

6、String StringBuilder StringBuffer区别和使用场景

  • String是final修饰的,不可变,每次操作都会产生新的String对象

  • 另外两个都是在原对象上操作的,StringBuffer线程安全,StringBuilder线程不安全

  • StringBuffer是Sychronized修饰的

  • 性能:StringBuilder>StringBuffer>String

经常改变字符串时候后两个,优先StringBuilder,多线程共享变量使用StringBuffer

  • 含1.8前用的是char数组,1.9用的是byte数组,存储的大部分都是拉丁文,1byte就能实现,因此换为byte数组,但是如果是汉字又没办法表示了,因此补上一个字符编码集的标识。因此StringBuffer、StringBuilder和固有的一些字符也做了相应的修改。

  • String 含1.6前在永久代的字符串常量池,后在堆中,便于调优,仅仅调整堆空间大小,并且方法区收集垃圾过于苛刻,而使用的字符串常量池挺多的,便于GC,同时堆空间大

  • 字符串拼接,如果是new String()拼接底层调用了StringBuilder进行append操作,创建了很多对象,并且最终结果并不在字符串常量池中。调用intern方法含1.6以前会在字符串常量池中创建一个该字符串,String指向该字符串,但是之后只会创建指向你堆中该String的指针。

  • 扩容机制:StringBuilder和StringBuffer默认长度16,默认扩容2 * n + 2,如果加入内容后长度比这个还大,以新的长度作为扩容后长度

7、重载和重写的区别

  • 重载:发生在同一个类中,方法名相同,参数、个数、顺序不同,方法返回值和访问修饰符可以不同(但是返回值不同在编译就会报错)

  • 重写:子类重写父类方法,方法名,参数列表必须向同,返回值范围小于等于父类,抛出异常范围小于等于父类,访问修饰符大于等于父类

父类方法private修饰则不能重写,认为是新方法。所有的private方法默认是final的

8、接口和抽象类的区别

  • 抽象类可以存在普通成员函数,接口中只能有public abstract方法

  • 抽象类中的成员变量是各种类型的,接口中成员变量只能public static final类型,因为接口是高度抽象的,是规范,确保变量只有一个,并且大家公用不能更改。

  • 抽象类单继承,接口可以实现多个

以上为基础三点

  • 接口的设计目的是对类的行为进行约束,提供一种机制,可以强制要求不同类有相同行为,只约束了行为的有无,对如何实现没有限制

  • 抽象类的设计目的是代码复用,当不同类具有某些相同的行为(行为集合A),且其中一部分行为的实现方法一致时(A的非真子集B),可以让这些类派生于一个抽象类

  • 在这个抽象类中实现了B,避免让所有子类实现B,达到了代码复用,而A-B的部分,留给各个子类实现,因为A-B没有实现,所以抽象类无法实例化

  • 先有子类再有父类,把子类共性抽出来成父类

  • 抽象类是对类本质的抽象,表达的是is a的关系如 bmw is a car,抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类实现。

  • 接口是对行为的抽象,表达的然是like a的关系,bird like a aricraft,其本质是bird,接口的核心是定义行为,即实现类可以做什么,至于实现主体是谁,如何实现无所谓

  • 使用场景,关注事物本质,用抽象类,关注操作,用接口

  • 抽象类功能远超接口,但定义抽象类代价高

9、list和set的区别

  • list:有序,按照对象进入顺序保存对象,可重复,允许多个null对象,可以使用iterator取出所有元素再遍历,也可使用get(int index)获取指定下标的元素

    • arrayList:基于动态数组,连续内存存储,适合下标访问,扩容机制:因为长度固定,超出需要新建数组,将旧的拷贝到新书组,如果不是尾插涉及到元素移动,使用尾插并指定初始容量可以极大提升性能甚至超过LinkedList

    • LinkedList:基于链表,可以存储在分散的内存总,适合做数据插入和删除操作,不适合查询,需要逐个遍历。

      • 变量里LinkedList必须使用iterator,不能用for,因为for每次get(i)取得某医院素都需要对List重新遍历,性能消耗极大。

      • 不要使用indexOf返回索引,利用其遍历,使用IndexOf会对list遍历,结果位空时会遍历整个列表。

  • set:无序,不可重复,最多有一个null,取元素时只能用iterator接口取得所有元素

10、hashCode和equals

  • hashcode作用是获取哈希码,返回int整数,这个hash码作用是确定对象在hash表中索引位置。java中任何类都有hashCode()函数,通过键值对可以通过键快速检索对应的值

为什么要有hashCode

以hashSet如何检查重复的例子来说明为什么要有hashCode

对象加入hashset时,hashset先计算对象的hashcode来判断对象加入的位置是否有值,如果没有,hashset会假设对象没有重复出现,如果有毁掉用equals来检查两个对象是否相同,如果相投,hashset不让其加入,不同则会在链表中链接,减少equals次数,提高执行速度。

两对象相等,hashcode相同

两对象相等,equals返回true

两对象hashcode相同,对象不一定相等

因此equals覆盖过,hashcode也必须覆盖

如果没重写hashcode,则class的两个对象无论如何也不会相等,即使两个对象数据相同

11、ArrayList和LinkedList区别

  • arrayList:基于动态数组,连续内存存储,适合下标访问,扩容机制:因为长度固定,超出需要新建数组,将旧的拷贝到新书组,如果不是尾插涉及到元素移动,使用尾插并指定初始容量可以极大提升性能甚至超过LinkedList

  • LinkedList:基于链表,可以存储在分散的内存总,适合做数据插入和删除操作,不适合查询,需要逐个遍历。

  • 变量里LinkedList必须使用iterator,不能用for,因为for每次get(i)取得某一个元素都需要对List重新遍历,性能消耗极大。

  • 不要使用indexOf返回索引,利用其遍历,使用IndexOf会对list遍历,结果位空时会遍历整个列表。

12、HashMap和HashTable有什么区别,底层是什么

HashMap没有使用sychronized线程不安全,hashTable反之

HashMap允许key value位null hashTable不允许

  • HashMap底层实现原理
    • 1.7数组+链表。头插法,多线程出现循环链,死循环。Entry[16]
    • 1.8数组+链表+红黑树。尾插法,多线程丢数据。Node[],首次put才会创建长度16的数组
    • todo:负载因子
    • Node包含了hash值,这个值时key.hashcode再次hash得到的,高16位和低16位置异或,降低hash碰撞,更好的散列化
  • hashMap扩容策略
    • 1.7直接扩容两倍
    • 1.8数组长度小于64,直接两倍扩容,容量 * 加载因子 = 扩容阈值 16 * 0.75 = 12 存入12个元素即扩容到32,数组长度>64当前位置链表长度>8转为红黑树。当红黑树元素小于6还原链表。

13、ConcurrentHashMap原理

线程安全版本hashMap

  • jdk7:reentrantLock+segment+HashEntry,一个segment中包含了一个table数组,每个位置包含一个HashEntry链表。

    • 元素查询:二次hash,第一次定位到segment,第二次定位到table链表头部
    • 锁:segment分段锁,segment继承了reentrantlock,锁定操作的segment,其他的不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment,get方法无需加锁,volatile保证。
  • jdk8:(数组+链表)sychronized+cas+node+红黑树,node的val和next都用volatile修饰,保证可见性,查找,替换,赋值操作都使用cas。

    • 锁:锁链表的head节点,不影响其他元素的读写,锁的粒度更细,效率更高,扩容时,阻塞所有的读写操作,并发扩容。头部多一个treebin节点,syc锁住链表头部。
    • 读操作无锁:node的val和next使用volatile修饰,时刻更新,数组用volatile修饰,保证扩容时被读线程感知

14、ioc容器

15、什么是字节嘛,采用字节码的好处是什么

jvm理解的代码是字节码.class文件,跨平台,通过字节码解决了执行效率低的问题(编译和运行拆开了),保留了可移植特点,一次编译,多次运行

16、java类加载器 classloader

bootstrap:ext父类加载器,负责加载java home/lib下的jar和class,引导类加载器

ext:app父类加载器,负责加载java home/lib/ext下的jar和class,扩展加载器

app:自定义加载器的父类,负责加载classpath 系统加载器,系统类加载器

17、双亲委派模型

加载一个类的时候先向上委派,查找缓存,是否加载了该类,有则返回(加载过的不用再加载),没有继续向上,直到bootstrap都没加载过后,向下查找,查找加载路径,有则加载返回,没有则继续向下查找。

主要是为了安全,避免自己写的类替换java核心类,如String

避免类重复加载,jvm中区分不同类,不仅仅根据类名,相同的class文件被不同的classloader加载就是2个类

18、java中的异常体系

所有的异常来自顶级父类Throwable

旗下有两个子类Exception和Error

Error是程序无法处理的错误,一旦出现这个错误,程序被迫停止运行

Exception不会让程序停止,又分为了RuntimeException运行时异常和CheckedException检查异常

RunTime常常发生在程序运行过程中,会导致当前线程执行失败,checked常常发生在程序编译过程中,导致程序编译不通过

19、gc如何判断对象可以被回收

引用计数法:每个对象有一个引用计数属性,新增一个引用计数增加1,引用释放时计数-1,计数位0可以回收,java没有使用,比如a引用b,b引用a,他们都不再使用,但是相互引用,计数器为1,永远无法被回收,但是效率非常高

可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连时,证明对象不可用,判断为回收对象

GCRoot的对象有:

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象(比如new usr() 其他对象被usr引用,无法被回收,称usr就是gcRoots,使用完被弹出)

  • 方法区中静态属性引用的对象static

  • 方法区中常量引用的对象static final

  • 本地方法栈JNI(即一般说的Native方法)引用的对象

可达性算法的不可达对象并不是立刻死亡的,对象拥有一次自我拯救的机会:

第一次可达性分析发现没有与GCRoots相连接的引用链

第二次是在由虚拟机自动建立的finalizer队列中判断是否要执行finalize()方法

  • 当对象变成不可达时,GC会判断该对象是否覆盖了finalize方法,没覆盖则直接回收,否则,若对象未执行finalize方法,将其放入F-Queue队列

  • 执行该队列对象的finalize方法,执行完毕后,判断是否可达,不可达回收,可达复活

  • 每个对象只能触发一次finalize方法,运行代价高,不确定性大,不推荐使用

20、线程的生命周期和状态

五种状态:创建,就绪,运行,阻塞,死亡

阻塞的三种:

  • 等待阻塞:运行线程执行wait方法,释放线程资源,jvm把线程放入等待池,释放锁,不能自动唤醒,必须依靠notify,wait是object的方法
  • 同步阻塞:运行的线程获取对象的同步锁,若同步锁被别的线程占用,jvm会把它放入锁池
  • 其他阻塞:运行的线程执行sleep或者join方法,或者io请求,会成阻塞状态,当sleep,join等超时,io处理完毕,线程重回就绪状态,是thread的方法

新建状态:新创建了一个对象

就绪状态:线程对象创建后,其他线程调用了该对象的start方法,该状态的线程位于可运行线程池中,变得可运行,等待cpu使用权

运行状态:就绪状态线程获取了cpu使用权,执行代码

阻塞状态:因为某种原因放弃cpu使用权,停止运行,直到线程重新进入就绪状态

死亡状态:程序执行完了或者异常推出了run方法,结束生命周期

21、sleep wait join yield区别

锁池:竞争同步锁的线程都会放在锁池中,比如当前对象的锁已经被某一个线程得到,其他线程需要在这个锁池等待,前面线程释放同步锁后锁池线程竞争同步锁,当某个线程得到后进入就绪队列等待cpu资源分配

等待池:调用wait方法后,线程放入等待池中,等待池不会竞争同步锁,只有调用了notify后等待池线程才会竞争锁,notify是随机从等待池中选一个放到锁池,notifyall是将等待池所有线程放到锁池中

  • sleep是thread的静态本地方法,wait是object的本地方法

  • sleep不会释放锁,wait释放锁,加入等待队列中

  • sleep方法不依赖同步器sychronized但是wait依赖

  • sleep不需要被唤醒,wait需要

  • sleep一般使用域当前线程休眠,或者论循进行操作,wait多用于多线程之间的通信

  • sleep让出cpu执行时间,强制上下文切换,wait不一定,wait后还是有可能重新竞争到锁执行的,提供了带时间的wait方法(wait notify)

注:sleep实则是把cpu运行资格和执行权释放,不再运行线程,当定时时间结束在取回cpu资源,即带着锁进入了冻结状态,其他线程无法获得这个锁,如果睡眠期间其他线程调用了这个线程interrupt方法,那么这个线程会抛出interruptexception异常返回,这点和wait一样。

yield后线程进入就绪状态,释放cpu执行权,保留cpu执行资格,有可能cpu下次线程调度还会让这个线程获取到执行权继续执行

join后线程进入阻塞状态,b线程调用a的join,线程b进入阻塞队列,直到线程a结束或者中断

22、对线程安全的理解

应该叫内存安全,堆是共享内存,可被所有线程访问,多个线程访问一个对象时,如果不用进行额外的同步控制或者协调操作,调用这个对象的行为都可以获得正确的结果,这个线程就是安全的

堆是进程和线程的共有空间,分全局堆和局部堆。全局堆是所有没分配的空间,局部对就是用户分配的空间,堆在操作系统对进程初始化时分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,不然造成内存泄漏,对象和数组放在此处

栈是每个线程独有的,保存运行状态和局部变量,栈在线程开始的时候初始化,每个线程的栈独立,栈是安全的,切换线程会自动切换栈

23、Thread和runnable区别

Thread和runnable是继承关系,没有可比性,两者都会newThread,然后执行run方法,没有复杂操作选择继承Thread,简单执行一个任务,实现runnable

继承Thread只能单继承,runnable是接口可以多实现,更加灵活,同时runnable可以数据共享,如Mythread mt = new Mythread() 之后new Thread(mt)多个,其中mt中的属性共用的,便于多个线程操作一个资源。

callable:有返回值,可以抛异常。FutureTask实现runnable接口,同时与callable接口有关联(new FutureTask<>(new MyThread()))。new Thread(runnable),runnable中传入实现类futureTask即可。

24、说说你对守护线程的理解

守护线程:为所有非守护线程提供服务的线程,任何一个守护线程都是整个jvm中所有非守护线程(用户线程)的保姆 用户线程执行完毕,守护线程就中断了,因此无法控制终止,不要将io file等重要操作逻辑给他分配。

例:GC垃圾回收,当程序中不再有任何运行的Thread,程序不再产生垃圾,即gc是jvm仅剩的线程时,GC自动离开。

应用场景:为其他线程提供服务,任何情况下,程序结束时,这个线程必须立即正常关闭,就可以作为守护线程 守护线程中创建线程也是守护线程。java自带的多线程的框架如executorservice会将守护线程自动转为用户线程 所以如果时候后台线程就不能使用java自带的线程池

25、ThreadLocal的原理和适合用场景

每个Thread对象都有一个ThreadLocalMap类型的成员变量threadLocals,它储存本线程中所有ThreadLocal对象及其对应的值

创建一个ThreadLocal,在两个不同线程里分别设置值,可以不一样,并且同时存在,因为里面有个ThreadLocalMap,key位Thread,value是值

即ThreadLocalMap中有个Entry,key是ThreadLocal,value是存的值

static final ThreadLocal<Object> tl = new ThreadLocal<>(); 线程中调用tl.get/set/remove相当于从当前Thread对象中的ThreadLocalMap通过key tl获取值。

场景:

  • 对象跨层传递,使用ThreadLocal避免多次传递 如service,dao等不用传参,只需要用ThreadLocal获取就行
  • 线程之间的数据隔离
  • 事务操作,用于存储线程事务信息
  • 数据库链接,session会话管理

26、ThreadLocal内存泄露原因,如何避免

内存泄漏为不再被使用的对象的内存不能被回收,就是内存泄漏,长期导致oom内存溢出

强引用:例如new,强引用的对象不会被垃圾回收器回收,内存不足,java宁愿抛出oom错误。

如果想取消强引用和某个对象的关联,可以赋值null,这样jvm会在合适的时间回收

弱引用:jvm回收时,无论内存是否充足,都会回收被弱引用关联的对象,在java中,用java.lang.ref.WeakReference来标识。在缓存中使用弱引用较多

ThreadLocal的实现是Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value是线程变量的副本

ThreadLocal的key是弱引用,会被回收,但是value是强引用不会被回收,所以会产生内存泄漏

为什么要使用弱引用(key):如果使用强引用,回收ThreadLocal时,ThreadLocalMap还有个ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,Entry导致内存泄漏。反之使用弱引用,当key是null时,下一次使用ThreadLocalMap调用set,get,remove方法会清除value值。

  • 内存泄漏本质:ThreadLocalMap和Thread生命周期一样长,没有手动删除对应key就会导致内存泄漏。

使用ThreadLocal正确方法:

  • 每次用好ThreadLocal调用他的remove方法清除数据
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除。不用每次都实例化。

27、并发,并行,串行的区别

串行在时间上不能重叠,前一个任务没完成,下一个任务要等着

并行在时间上是重叠的,两个任务同一时刻互不干扰同时运行

并发允许两个任务彼此干扰,同一时间点只有一个任务运行,交替执行

28、并发的三大特性

原子性、可见性、有序性。

29、为什么使用线程池?解释下线程池参数?

  • 降低资源消耗,提高利用率
  • 提高响应速度(不用先创建线程)
  • 提升可管理性,统一分配调度
参数意义
corePoolSize线程池常驻核心线程数
maximumPoolSize能够容纳的最大线程数
keepAliveTime空闲线程存活时间
unit存活时间单位
workQueue存放提交但未执行任务的队列
threadFactory创建线程的工厂类
handler等待队列满后的拒绝策略

30、简述线程池处理流程

graph TD
线程池执行任务 --> conditionA{核心线程数是否已满}
conditionA -- 已满 -->conditionB{任务队列是否已满}
conditionA -- 未满 -->创建核心线程执行
conditionB -- 已满 --> conditionC{最大线程数是否达到}
conditionB -- 未满 -->将任务放入到队列中
conditionC -- 已达到 --> 根据拒绝策略处理
conditionC -- 未达到 --> 创建临时线程执行

31、线程池阻塞队列的作用,为什么先添加队列,而不是先创建最大线程。

  • 一般队列只能保证有限长度,超出缓冲长度,就报错,阻塞队列只是阻塞,可以保留继续想要入队的任务。不用手动控制阻塞与否。
  • 阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使线程进入wait状态,释放cpu资源
  • 阻塞队列自带阻塞和唤醒功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,维持核心现成的存活,不一直占用cpu资源
  • 创建新线程,要获取全局锁,其他线程就要阻塞,影响整体效率。

32、线程池中复用原理

线程池将线程和任务解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时一个线程必须一个任务的限制。

线程池中,同一个线程可以从阻塞队列中不断获取新任务执行,核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start来创建新县城 ,而是让线程执行一个循环任务,不停检查是否有任务需要执行,有就执行,调用任务的run方法,将run方法当成普通方法执行。这种方式是达成了使用固定线程将所有任务的run泛该法串联起来。线程调用任务的run方法,而不是调用线程的start方法。

33、Spring是什么

轻量级的J2EE框架,是容器框架,装javabean,中间层框架可以起一个连接作用,让开发更快更简洁。

Spring是一个轻量级的控制反转ioc和面向切面aop的容器框架

  • 大小和开销而言,spring都是轻量级

  • ioc达到降耦合

  • 提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑和系统级服务进行内聚性的开发

  • 包含并管理应用对象bean的配置和生命周期

  • 将简单的组件配置组合成复杂的应用。

34、谈谈你对AOP的理解

系统的不同组件专注于自身核心业务,其他功能如打印日志,安全,事务等交叉逻辑业务,封装成一个切面,注入到目标对象(具体业务逻辑)中使用。AOP可以对某个对象或某些功能增强,可以在执行某方法前后做一些额外的事。

相比于OOP(允许定义从上到下的关系,但是不支持从左到右的关系(如日志等)),AOP可以避免大量代码的重复,利于各个模块的复用。

35、谈谈你对IOC的理解

从容器概念,控制反转,依赖注入回答。

ioc容器:ioc实际是map(k,v),存放各种对象(如,xml里配置bean节点,@repository,@service,@controller,@component),项目启动会读取bean节点,跟去全限定类名使用反射创建对象放到map中。我们在代码里使用对象时,通过DI注入(autowired, resource,etc),会读取xml节点ref属性根据id注入。

控制反转: 没有IOC前,A依赖B,对象A在初始化时,需要主动创建对象B或者使用已经创建的B,控制权在自己手中

引入IOC后,A与B失去了直接联系,相当于A依赖IOC容器,当A需要B时,IOC主动创建B,之后注入到A所需的位置。(IOC也叫粘合剂,万能胶)

对象A获得依赖对象B的过程,从主动变成被动,控制权颠倒,因此叫控制反转。

依赖注入: 获得依赖对象的过程反转了,以前是A创建B,现在是创建B注入到A中。

36、BeanFactory和ApplicationContext有什么区别

ApplicationContext是BeanFactory的子接口

ApplicationContext提供了更完整的功能:

1、继承MessageSource,因此支持国际化

2、统一的资源文件访问方式

3、提供在监听器中注册Bean的事件

4、同时加载多个配置文件

5、载入多个(有继承关系)上下文,使得每一个上下文都专注于一个特定的层次,比如应用层的web层。 。。。。。

37、简述Spring bean的生命周期

38、Spring支持的bean作用域

39、Spring框架中的单例Bean是线程安全的吗

40、Spring框架中使用了哪些设计模式

41、Spring事务的实现方式原理以及隔离

42、Spring事务的传播机制

43、事务什么时候失效

44、什么是bean的自动装配,有哪些方式

45、spring springmvc,springboot的

46、springmvc工作流程

47、springmvc的九大组件

48、springboot自动配置原理

49、如何理解springboot的starter

50、什么是嵌入式服务器,为什么使用嵌入式服务器

51、mybatis的优缺点

优点:

  • 基于sql语句编成,灵活,不会对应用程序或者数据库现有设计造成任何影响,sql写xml中,解除sql与程序代码的耦合,便于统一管理,提供xml标签,支持编写动态sql语句,可重用。
  • 和jdbc相比,减少了50%以上的代码两,消除了其冗余代码,不需要手动开关链接
  • 很好的与各种数据库兼容(因为mybatis使用jdbc来链接数据库,所以只需要jdbc支持的数据库mybatis都支持)
  • 能够与spring很好的集成
  • 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护

缺点:

  • sql语句编写工作量打,尤其是字段多,关联表多
  • sql语句依赖数据库,数据库移植性差,不能随意更换数据库

52、 mybatis和hibernate对比

53、#{}和${}区别

54、mybatis插件运行原理及开发流程

55、索引的基本原理

索引用来快速搜寻具有特定值的记录,没索引则需要遍历整张表

索引的原理:把无序的数据变为有序的查询

  • 把创建了索引的列的内容排序
  • 对排序结果生成倒排表
  • 在倒排表内容尚拼上数据的地址链
  • 在查询的时候先拿到倒排表的内容,再取出数据地址链,从而拿到具体数据。

56、 聚簇索引和非聚簇索引区别

聚簇索引:数据存储和索引放到了一块,并且按照一定顺序组织的,找到索引就找到了数据,数据的物理存放顺序和索引顺序一致,即索引相邻,对应数据也在相邻物理地址存放

非聚簇索引:叶子节点不存数据,存储数据行地址,也就是说根据所因查询到数据行的位置,再取磁盘找到数据。相当于书的目录。

聚簇索引优势

  • 可以直接获得数据,不需要二次查询(回表),效率高
  • 范围查询效率高,因为数据按照大小排列
  • 适合用在排序的场合

聚簇索引劣势

  • 维护索引昂贵,插入或者主键更新导致分页(页分裂,降低数据存储率)。建议在大量插入新行后,选在负载较低的时间段通过OPTIMIZE TABLE优化表,因为被一懂得行数聚可能造成碎片,使用独享表空间可以弱化碎片
  • 使用UUID作为主键,使数据存储稀疏,这样聚簇索引会比扫描全表更慢,所以建议使用自增的int作为主键
  • 如果主键比较大,辅助索引会变得更大,因为辅助索引的叶子存储的主键值,过长的主键值,会导致非叶子节点占用更多的物理空间

innodb因定有主键,主键是聚簇索引,不受动设置则会使用unique索引,如果没有则会使用数据库内一个行的隐藏id当做主键索引。

57、mysql索引的数据结构,各自优劣

哈希表、有序数组、搜索树、b+tree

58、索引的设计原则

  • 适合索引的列出现在where子句中,或者连接子句指定的列。
  • 基数较小的类没必要创建索引,效果差
  • 使用短索引,如果对长字符串索引,应该指定前缀长度,可以节省大量空间,如果搜索词超过前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配。
  • 不要过度使用索引,会占用额外磁盘空间,降低读写性能,修改表内容时,索引可能重构,索引列越多时间越久,因此保持索引有利于查询即可。
  • 定义有外键的数据列一定要建立索引
  • 如果不查询整行数据,建立联合索引,二级索引产生回表。
  • 主键设定应当为自增主键,否则B+树造成数据分页,降低每页的使用率,造成不必要的硬盘读写,降低效率

三次握手

  • 第一次:客户端给服务器发送syn报文
  • 第二次:服务器收到syn后应答syn+ack报文
  • 第三次:客户端收到syn+ack后回应一个ack
  • 服务端收到ack后,建立完成

为什么需要三次?

  • 第一次确认客户端发送能力和服务端接收能力
  • 第二次确认服务端发送能力和客户端接受能力
  • 第三次服务端收到了客户端的包,确认能力。

四次挥手

初始双方处于established状态,假如是客户端先发起请求

  • 第一次:客户端发送fin报文,报文指定序列号,客户端处于FIN_WAIT1状态
  • 第二次:服务端接到fin报文,发送ack报文,序列号+1,表示接到客户端报文,处于CLOSE_WATI状态
  • 第三次:服务端也想断开链接了,给客户端发送FIN报文指定序列号,处于LAST_ACK状态。
  • 第四次:客户端收到fin,发送ack应答,序列号+1,处于TIME_WAIT2状态,过一阵确保服务端收到自己的ACK报文后进入CLOSED状态
  • 服务端收到ACK后关闭连接,处于CLOSED状态。

为什么客户端发送 ACK 之后不直接关闭,而是要等一阵子才关闭?

  • 确保服务器是否已经收到了我们的 ACK 报文,如果没有收到的话,服务器会重新发 FIN 报文给客户端,客户端再次收到 ACK 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。至于 TIME_WAIT 持续的时间至少是一个报文的来回时间。一般会设置一个计时,如果过了这个计时没有再次收到 FIN 报文,则代表对方成功就是 ACK 报文,此时处于 CLOSED 状态。

comparable和Comparator

  • comparable写在类中,实现compareTo方法。
  • comparator单独写一个比较类,继承comparator,实现compare方法,传入两个对象比较。

throw和throws区别

  • throw:运行时异常,抛出对象实例,一个具体的异常类型,手动抛出给上级。--处理异常的过程
  • throws:抛出可能产生的所有异常,在方法声明后添加,多个用逗号隔开,不需要处理,返回给调用者异常名,但是返回的异常不一定发生了--生成异常的过程

tcp滑动窗口

tcp一次传输数据数量。大小动态调整

  • ack丢失无妨,后续ack也会通知对方自己收到了什么
  • 数据包丢失,发送端会一直收到我要收到xxx(序号)的ack,连续收到三次后发送端会重新发送数据包
  • 如果发送过快,接收端缓冲区填满,接收端动态调整大小,通过ack通知发送端,开始变小,如果缓冲区满了会调整为0,发送发不在发送数据,但是定期发送窗口探测数据段,接收新的窗口大小。

tcp拥塞控制

如果网络状况一般,上来使用滑动窗口发送大量数据,雪上加霜。因此tcp引入慢启动,先发少量数据探路,摸清网络情况在根据情况调整传输速度。

  • 拥塞窗口:最开始的时候定义拥塞窗口大小为1,没接收到一个ack应答,拥塞窗口+1,发送数据包的时候使用拥塞窗口大小和接收端主机ack的窗口大小比较,取较小值。
  • 实际上这种增长速度很快,指数级别,初始慢,增长快,为了不增长那么快,引入慢启动阈值,超过这个大小不再按照指数增长,按照线性增长
  • tcp开始启动,慢启动阈值等于窗口最大值,网络阻塞时,慢启动阈值变为一半,拥塞窗口重置为1。
  • 少量丢包触发超时重传,大量丢包认定为网络阻塞。

延迟应答

  • 接收端立刻返回ack,说明窗口小
  • 如果接收缓冲区为1m,一次收到了0.5m数据,立刻应答则返回滑动窗口值为500k,但是可能处理的很快,这500k在10ms内就已经被消费掉了,接收端没有达到自己极限,窗口再大点也能处理,可以稍后在发送ack,同时设置窗口大一点。

try catch finall执行顺序

  • try正常执行:try->finally->finally外的
  • try出现异常:try异常前的->catch中没有throw和return则执行完catch,有则执行throw和return前面的->finally,如果有return或者throw直接执行完跳出该线程->执行catch中没执行完的throw和return后返回

String[] args

主函数中的传参,run->edit configuration中可以设置,默认为空,长度为0