Java基础知识

Java三大特性

封装:把数据和操控数据的方法封装起来,对数据的访问只能通过接口继承:继承是从已有类得到继承信息创建新类的过程

1.子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有,.可以用自己的方式实现父类的方法

多态多态:多态就是同一个行为具有多个不同的表现形式或形态的能力,就是同一个接口,使用不同的实例而执行不同操作

重写和重载的区别

  1. 重载:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理
  2. 重写:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变

    Java中是否可以重写一个private或者static方法

    Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的,static方法跟类的任何实例都不相关,Java中也不可以覆盖private的方法,因为private修饰的变量和方法只能在当前类中使用,如果是其他的类继承当前类是不能访问到private变量和方法的

    静态的方法可以被继承,但是不能被重写,如果父类和子类中存在同样名称和参数的静态方法,那么该子类的方法会把原来继承过来的父类的方法隐藏,而不是重写

Java中创建对象的几种方式

  1. 使用new
  2. 使用class类的newInstance方法,该方法调用无参的构造器创建对象(反射),Class.forName.newInstance()
  3. 使用clone方法
  4. 反序列化,比如调用ObjectInputStream类的readObject()方法

    抽象类和接口有什么不同

    不同点

  5. 抽象类可以定义构造函数,接口不能定义构造函数

  6. 抽象类可以有抽象方法和具体方法,接口中只能由抽象方法
  7. 抽象类的成员权限可以是public ,默认,protected(抽象类中抽象方法就是为了重写,所以不能被private修饰),而接口的成员只可以是public(方法默认:public abstrat,成员变量默认:public static final)
  8. 抽象类中可以包含静态方法,而接口中不可以包含静态方法(接口JDK1.8之后可以包含静态方法,之前不能包含是因为,接口不可以实现方法,只可以定义方法,所以不能使用静态方法(因为静态方法必须实现)现在可以包含了,只能直接用接口调用静态方法,1.8仍然不可以包含静态代码块)

    相同点

  9. 都不能被序列化

  10. 可以将抽象类和接口作为引用类型
  11. 一个类如果继承了某个抽象类或者实现了某个接口,就必须对其中所有的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类

    Constructor是否可被override

    继承的时候就知道父类的私有属性和构造方法不能被继承,所以Constructor也不能被override,但是可以overload

    Java基础类型

  12. byte 8位 -2’7-2’7-1

  13. short 16位 -2’15-2;15-1
  14. int 32位 -2’31-2’31-1
  15. long 64位 -2’63-2’63-1
  16. float 32位
  17. double 64位
  18. boolean 只有true和false两个取值
  19. char 16位,储存Unicode码,用单引号赋值

    int和Integer的区别

    Integer是为int类型提供的封装类,int型变量的默认值是0,Integer变量的默认值是null

    装箱和拆箱

    自动装箱就是Java编译器在基本数据类型和对应得包装类之间做的一个转化,比如把int转化为Integer,反之就是自动拆箱

    String,StringBuilder,StringBuffer得区别

  20. String:用于字符串操作,属于不可变类

  21. StringBuffer:用于字符串操作,StringBuffer属于可变类,对方法加了同步锁,现场安全(StringBuffer中并不是所有方法都是用了Synchronized修饰来实现同步)
  22. StringBuilder:与StringBuffer类似,都是字符串缓冲区,但线程不安全

执行效率:StringBuilder>StringBuffer>String

String与基本数据类型转换

parseXXX(String)或者valueOf(String)方法
例如praseInt方法直接转换为基本数据类型int,valueOf方法是转换为包装类型

String的常用方法

  1. indexOf():返回指定字符的索引。
  2. charAt():返回指定索引处的字符。
  3. replace():字符串替换。
  4. trim():去除字符串两端空白。
  5. split():分割字符串,返回一个分割后的字符串数组。
  6. getBytes():返回字符串的 byte 类型数组。
  7. length():返回字符串长度。
  8. toLowerCase():将字符串转成小写字母。
  9. toUpperCase():将字符串转成大写字符。
  10. substring():截取字符串。
  11. equals():字符串比较。

    final,finally,finalize的区别

    final:用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承
    finally:异常处理语句结构的一部分,表示总是执行
    finalize:Object的一个方法,在垃圾回收时会调用被回收对象的finalize

    static

    静态变量

    静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问他,静态变量在内存中只存在一份
    实例变量:每创建一个实例就会产生一个实例变量,他与该实例同生共死

    静态方法

    静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实 现,也就是说它不能是抽象方法。只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。

    静态语句块

    静态语句块在类初始化时运行一次。

    super

    访问父类的构造函数:可以使用super()函数访问父类的构造函数,从而委托父类完成一些初始化的工作
    访问父类的成员:如果子类重写了父类的某个方法,可以通过使用super关键字来应用父类的方法实现

    ==和equals的区别

    ==:如果比较的对象是基本数据类型,是比较数值是否相等,如果是比较引用数据类型,则比较的是对象的地址值是否相等
    equals方法:用来比较两个对象的内容是否相等,equals方法不能用于比较基本数据类型的变量,如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址,但是有很多类重写了equals方法,比如String,Integer等把它变成了值比较,所以一般情况下equals比较的是值是否相等

    两个对象的hashCode()相同,则equals()也一定为true吗

    两个对象的hashCode()相同,equals()不一定为true,因为在散列表中,hashCode()相等即两个键值对的哈希值相等,然后哈希值相等,并不一定能得出键值对相等(散列冲突)

    error和exception的区别

    error和exception的父类都是Throwable类
    Error类:一般是指虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,这类错误直接回导致应用程序中断,仅靠程序本身无法恢复和预防
    Exception:分为运行时异常和受检查的异常
  • 运行时异常:【如空指针异常、指定的类找不到、数组越界、方法传递参数错误、数 据类型转换错误】可以编译通过,但是一运行就停止了,程序不会自己处理;
  • 受检查异常:要么用 try … catch… 捕获,要么用 throws 声明抛出,交给父类处理。

    throw和throws的区别

    throw:
    throw 在方法体内部,表示抛出异常,由方法体内部的语句处理;
    throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例;
    throws:
    throws 在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理; 表示出现异常的可能性,并不一定会发生这种异常。

    常见的异常类

  • NullPointerException:当应用程序试图访问空对象时,则抛出该异常。

  • SQLException:提供关于数据库访问错误或其他错误信息的异常。
  • IndexOutOfBoundsException:指示某排序索引(例如对数组、字符串或向量的排序) 超出范围时抛出。
  • FileNotFoundException:当试图打开指定路径名表示的文件失败时,抛出此异常。
  • OException:当发生某种 I/O 异常时,抛出此异常。此类是失败或中断的 I/O 操作生成 的异常的通用类。
  • ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常。
  • IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参 数。

    对象克隆的实现

    两种方式:
  1. 实现 Cloneable 接口并重写 Object 类中的 clone() 方法;
  2. 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克 隆。

    注意:深克隆和浅克隆的区别:

  3. 浅克隆:拷贝对象和原始对象的引用类型引用同一个对象。浅克隆只是复制了对象的引 用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变 化,这就是浅克隆(例:assign())。

  4. 深克隆:拷贝对象和原始对象的引用类型引用不同对象。深拷贝是将对象及值复制过 来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝(例:JSON.parse() 和 JSON.stringify(),但是此方法无法复制函数类型)。

    深克隆的实现就是在引用类型所在的类实现 Cloneable 接口,并使用 public 访问修 饰符重写 clone 方法; Java 中定义的 clone 没有深浅之分,都是统一的调用 Object 的 clone 方法。为什 么会有深克隆的概念?是由于我们在实现的过程中刻意的嵌套了 clone 方法的调用。 也就是说深克隆就是在需要克隆的对象类型的类中重新实现克隆方法 clone()。

Java序列化

对象序列化是一个用于将对象状态转换为字节流的过程,可以将其保存到磁盘文件中 或通过网络发送到任何其他程序。从字节流创建对象的相反的过程称为反序列化。而创建 的字节流是与平台无关的,在一个平台上序列化的对象可以在不同的平台上反序列化。序 列化是为了解决在对象流进行读写操作时所引发的问题。
序列化的实现:将需要被序列化的类实现 Serializable 接口,该接口没有需要实现的 方法,只是用于标注该对象是可被序列化的,然后使用一个输出流(如: FileOutputStream)来构造一个 ObjectOutputStream 对象,接着使用 ObjectOutputStream 对象的 writeObject(Object obj) 方法可以将参数为 obj 的对象写 出,要恢复的话则使用输入流。
什么情况下需要序列化
a)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过 RMI 传输对象的时候。

1、动态代理是什么?有哪些应用?

动态代理:当想要给实现了某个接口的类中的方法,加一些额外的处理。比如说加日志,加事务 等。可以给这个类创建一个代理,故名思议就是创建一个新的类,这个类不仅包含原来类 方法的功能,而且还在原来的基础上添加了额外处理的新功能。这个代理类并不是定义好 的,是动态生成的。具有解耦意义,灵活,扩展性强。
动态代理的应用: Spring 的 AOP 、加事务、加权限、加日志。

2、怎么实现动态代理?

首先必须定义一个接口,还要有一个 InvocationHandler(将实现接口的类的对象传 递给它)处理类。再有一个工具类 Proxy(习惯性将其称为代理类,因为调用它的 newInstance() 可以产生代理对象,其实它只是一个产生代理对象的工具类)。利用到 InvocationHandler,拼接代理类源码,将其编译生成代理类的二进制码,利用加载器加 载,并将其实例化产生代理对象,最后返回。 每一个动态代理类都必须要实现 InvocationHandler 这个接口,并且每个代理类的实 例都关联到了一个 handler,当我们通过代理对象调用一个方法的时候,这个方法的调用 就会被转发为由 InvocationHandler 这个接口的 invoke 方法来进行调用。我们来看看 InvocationHandler 这个接口的唯一一个方法 invoke 方法:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable

proxy: 指代我们所代理的那个真实对象 method: 指代的是我们所要调用真实对象的某个方法的 Method 对象 args: 指代的是调用真实对象某个方法时接受的参数

Proxy 类的作用是动态创建一个代理对象的类。它提供了许多的方法,但是我们用的 最多的就是 newProxyInstance 这个方法:

public static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler handler) throws IllegalArgumentException

loader: 一个 ClassLoader 对象,定义了由哪个 ClassLoader 对象来对生成的代理 对象进行加载; interfaces:一个 Interface 对象的数组,表示的是我将要给我需要代理的对象提供 一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口 (多态),这样我就能调用这组接口中的方法了 handler:一个 InvocationHandler 对象,表示的是当我这个动态代理对象在调用方 法的时候,会关联到哪一个 InvocationHandler 对象上。

通过 Proxy.newProxyInstance 创建的代理对象是在 Jvm 运行时动态生成的一个对 象,它并不是我们的 InvocationHandler 类型,也不是我们定义的那组接口的类型,而是 在运行是动态生成的一个对象

Java集合

Collection

Set

  1. SetTreeSet: 基于红黑树实现,支持有序性操作,例如:根据一个范围查找元素的操 作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
  2. HashSet: 基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素 的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
  3. LinkedHashSet: 具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入 顺序。

    List

  4. ArrayList: 基于动态数组实现,支持随机访问。

  5. Vector: 和 ArrayList 类似,但它是线程安全的。
  6. LinkedList: 基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和 删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。

    Map

  7. TreeMap: 基于红黑树实现。

  8. HashMap: 基于哈希表实现。
  9. HashTable: 和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可 以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可 以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更 高,因为 ConcurrentHashMap 引入了分段锁。
  10. LinkedHashMap: 使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少 使用(LRU)顺序。

    ArrayList和LinkedList的区别

    ArrayList: 底层是基于数组实现的,查找快,增删较慢;
    LinkedList: 底层是基于链表实现的。确切的说是循环双向链表(JDK1.6 之前是双向循 环链表、1.7 之后取消了循环),查找慢、增删快。LinkedList 链表由一系列表项连接而 成,一个表项包含 3 个部分:元素内容、前驱表和后驱表。链表内部有一个 header 表 项,既是链表的开始也是链表的结尾。header 的后继表项是链表中的第一个元素, header 的前驱表项是链表中的最后一个元素。

    是不是Array List的增删就是比LinkedList要慢

    ArrayList的增删未必就是比LinkedList慢

  11. 如果增删都是在末尾来操作【每次调用的都是 remove() 和 add()】,此时 ArrayList 就不需要移动和复制数组来进行操作了。如果数据量有百万级的时,速度是会比 LinkedList 要快的。(我测试过)

  12. 如果删除操作的位置是在中间。由于 LinkedList 的消耗主要是在遍历上,ArrayList 的 消耗主要是在移动和复制上(底层调用的是 arrayCopy() 方法,是 native 方法)。 LinkedList 的遍历速度是要慢于 ArrayList 的复制移动速度的如果数据量有百万级的时, 还是 ArrayList 要快。(我测试过)

    补充:https://blog.csdn.net/weixin_39148512/article/details/79234817 ArrayList 集合实现 RandomAccess 接口有何作用?为何 LinkedList 集合却没实 现这接口? 1、RandomAccess 接口只是一个标志接口,只要 List 集合实现这个接口,就能支持 快速随机访问。通过查看 Collections 类中的 binarySearch() 方法,可以看出,判断 List 是否实现 RandomAccess 接口来实行indexedBinarySerach(list,key) 或 iteratorBinarySerach(list,key)方法。再通过查看这两个方法的源码发现:实现 RandomAccess 接口的 List 集合采用一般的 for 循环遍历,而未实现这接口则采用 迭代器,即 ArrayList 一般采用 for 循环遍历,而 LinkedList 一般采用迭代器遍 历。 2、ArrayList 用 for 循环遍历比 iterator 迭代器遍历快,LinkedList 用 iterator 迭代器遍历比 for 循环遍历快。所以说,当我们在做项目时,应该考虑到 List 集合的 不同子类采用不同的遍历方式,能够提高性能!

ArrayList的扩容机制

  1. 当使用 add 方法的时候首先调用 ensureCapacityInternal 方法,传入 size+1 进去, 检查是否需要扩充 elementData 数组的大小;
  2. newCapacity = 扩充数组为原来的 1.5 倍(不能自定义),如果还不够,就使用它指定要 扩充的大小 minCapacity ,然后判断 minCapacity 是否大于 MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8) ,如果大于,就取 Integer.MAX_VALUE
  3. 扩容的主要方法:grow
  4. ArrayList 中 copy 数组的核心就是 System.arraycopy 方法,将 original 数组的所有 数据复制到 copy 数组中,这是一个本地方法。

    HashMap相关

    1.数据结构

    JDK1.7:Entry数组+链表
    JDK1.8:Node数组+链表/红黑树,当链表上的元素个数超过8个并且数组长度>=64时自动转换成红黑树,节点变成树节点,以提高搜索效率和插入效率到O(logN),entry和Node都包含key,value,hash,next属性

    2.HashMap的13个成员变量

    Java面试 - 图1
    1.默认初始容量:16,必须是 2 的整数次方
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    2.默认加载因子的大小:0.75,可不是随便的,结合时间和空间效率考虑得到的
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    3.最大容量: 2^ 30 次方
    static final int MAXIMUM_CAPACITY = 1 << 30;
    4.当前 HashMap 修改的次数,这个变量用来保证 fail-fast 机制
    transient int modCount;
    5.阈值,下次需要扩容时的值,等于 容量*加载因子
    int threshold;
    6.树形阈值:JDK 1.8 新增的,当使用 树 而不是列表来作为桶时使用。必须必 2 大
    static final int TREEIFY_THRESHOLD = 8;
    7.非树形阈值:也是 1.8 新增的,扩容时分裂一个树形桶的阈值(?不是很懂 - -),要比 TREEIFY_THRESHOLD 小
    static final int UNTREEIFY_THRESHOLD = 6;
    8.树形最小容量:桶可能是树的哈希表的最小容量。至少是 TREEIFY_THRESHOLD 的 4 倍,这样能避免扩容时的冲突
    static final int MIN_TREEIFY_CAPACITY = 64;
    9.缓存的 键值对集合(另外两个视图:keySet 和 values 是在 AbstractMap 中声明的)
    transient Set> entrySet;
    10.哈希表中的链表数组
    transient Node[] table;
    11.键值对的数量
    transient int size;
    12.哈希表的加载因子
    final float loadFactor;

    3.HashMap的初始容量和加载因子

    由于 HashMap 扩容开销很大(需要创建新数组、重新哈希、分配等等),因此与扩容相关的两个因素:
  • 容量:数组的数量
  • 加载因子:决定了 HashMap 中的元素占有多少比例时扩容

成为了 HashMap 最重要的部分之一,它们决定了 HashMap 什么时候扩容。
HashMap 的默认加载因子为 0.75,这是在时间、空间两方面均衡考虑下的结果:

  • 加载因子太大的话发生冲突的可能就会大,查找的效率反而变低
  • 太小的话频繁 rehash,导致性能降低

当设置初始容量时,需要提前考虑 Map 中可能有多少对键值对,设计合理的加载因子,尽可能避免进行扩容。
如果存储的键值对很多,干脆设置个大点的容量,这样可以少扩容几次。

4.HashMap的添加操作

//添加指定的键值对到 Map 中,如果已经存在,就替换
public V put(K key, V value) {
//先调用 hash() 方法计算位置
return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
//如果当前 哈希表内容为空,新建,n 指向最后一个桶的位置,tab 为哈希表另一个引用
//resize() 后续介绍
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果要插入的位置没有元素,新建个节点并放进去
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果要插入的桶已经有元素,替换
// e 指向被替换的元素
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//p 指向要插入的桶第一个 元素的位置,如果 p 的哈希值、键、值和要添加的一样,就停止找,e 指向 p
e = p;
else if (p instanceof TreeNode)
//如果不一样,而且当前采用的还是 JDK 8 以后的树形节点,调用 putTreeVal 插入
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
//否则还是从传统的链表数组查找、替换

  1. //遍历这个桶所有的元素<br /> for (int binCount = 0; ; ++binCount) {<br /> //没有更多了,就把要添加的元素插到后面得了<br /> if ((e = p.next) == null) {<br /> p.next = newNode(hash, key, value, null);<br /> //当这个桶内链表个数大于等于 8,就要树形化<br /> if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st<br /> treeifyBin(tab, hash);<br /> break;<br /> }<br /> //如果找到要替换的节点,就停止,此时 e 已经指向要被替换的节点<br /> if (e.hash == hash &&<br /> ((k = e.key) == key || (key != null && key.equals(k))))<br /> break;<br /> p = e;<br /> }<br /> }<br /> //存在要替换的节点<br /> if (e != null) {<br /> V oldValue = e.value;<br /> //替换,返回<br /> if (!onlyIfAbsent || oldValue == null)<br /> e.value = value;<br /> afterNodeAccess(e);<br /> return oldValue;<br /> }<br />}<br />++modCount;<br />//如果超出阈值,就得扩容<br />if (++size > threshold)<br /> resize();<br />afterNodeInsertion(evict);<br />return null;

}
根据代码可以总结插入逻辑如下:

  1. 先调用 hash() 方法计算哈希值
  2. 然后调用 putVal() 方法中根据哈希值进行相关操作
  3. 如果当前 哈希表内容为空,新建一个哈希表
  4. 如果要插入的桶中没有元素,新建个节点并放进去
  5. 否则从桶中第一个元素开始查找哈希值对应位置
    1. 如果桶中第一个元素的哈希值和要添加的一样,替换,结束查找
    2. 如果第一个元素不一样,而且当前采用的还是 JDK 8 以后的树形节点,调用 putTreeVal() 进行插入
    3. 否则还是从传统的链表数组中查找、替换,结束查找
    4. 当这个桶内链表个数大于等于 8,就要调用 treeifyBin() 方法进行树形化
  6. 最后检查是否需要扩容

插入过程中涉及到几个关键的方法

  • hash():计算对应的位置
  • resize():扩容
  • putTreeVal():树形节点的插入
  • treeifyBin():树形化容器

    5.HashMap中的哈希函数Hash()

    HashMap 中通过将传入键的hashcode进行无符号右移16位,然后进行按位异或,得到这个键的哈希值
    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

由于哈希表的容量都是 2 的 N 次方,在当前,元素的 hashCode() 在很多时候下低位是相同的,这将导致冲突(碰撞),因此 1.8 以后做了个移位操作:将元素的 hashCode() 和自己右移 16 位后的结果求异或。
由于 int 只有 32 位,无符号右移 16 位相当于把高位的一半移到低位:Java面试 - 图2
Java面试 - 图3
这样可以避免只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,可以避免哈希值分布不均匀。
而且,采用位运算效率更高。

6.get和put方法

put:当我们想要往一个HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确定在table中存储的位置,如果该位置没有元素,就直接插入,否者迭代该处元素链表并依次比较其key的hash值,如果两个hash值相等且key值相等,则用新的Entry的value覆盖原来节点的value,如果两个hasash值相等但是key值不相等,则将该节点插入该链表的链头。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
Node e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// hash值不相等,即key不相等;为红黑树结点
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
// 为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值,转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。
对putVal方法添加元素的分析如下:

  • ①如果定位到的数组位置没有元素 就直接插入。
  • ②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。

get:通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。(HashMap在存储过程中并没有将key,value分开来存储,而是当作一个整体key-value来处理的,这个整体就是Entry对象)
public V get(Object key) {
Node e;
//还是先计算 哈希值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
//tab 指向哈希表,n 为哈希表的长度,first 为 (n - 1) & hash 位置处的桶中的头一个节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果桶里第一个元素就相等,直接返回
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//否则就得慢慢遍历找
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//如果是树形节点,就调用树形节点的 get 方法
return ((TreeNode)first).getTreeNode(hash, key);
do {
//do-while 遍历链表的所有节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
查找方法总结:

  • 先计算哈希值;
  • 然后再用 (n - 1) & hash 计算出桶的位置;
  • 在桶里的链表进行遍历查找。

时间复杂度一般跟链表长度有关,因此哈希算法越好,元素分布越均匀,get() 方法就越快,不然遍历一条长链表,太慢了。

7.resize方法

每次添加时会比较当前元素个数和阈值:
//如果超出阈值,就得扩容
if (++size > threshold)
resize();
扩容:
final Node[] resize() {
//复制一份当前的数据
Node[] oldTab = table;
//保存旧的元素个数、阈值
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新的容量为旧的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//如果旧容量小于等于 16,新的阈值就是旧阈值的两倍
newThr = oldThr << 1; // double threshold
}
//如果旧容量为 0 ,并且旧阈值>0,说明之前创建了哈希表但没有添加元素,初始化容量等于阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//旧容量、旧阈值都是0,说明还没创建哈希表,容量为默认容量,阈值为 容量加载因子
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR
DEFAULT_INITIAL_CAPACITY);
}
//如果新的阈值为 0 ,就得用 新容量加载因子 重计算一次
if (newThr == 0) {
float ft = (float)newCap
loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//更新阈值
threshold = newThr;
//创建新链表数组,容量是原来的两倍
@SuppressWarnings({“rawtypes”,”unchecked”})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
//接下来就得遍历复制了
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
//旧的桶置为空
oldTab[j] = null;
//当前 桶只有一个元素,直接赋值给对应位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果旧哈希表中这个位置的桶是树形结构,就要把新哈希表里当前桶也变成树形结构
((TreeNode)e).split(this, newTab, j, oldCap);
else { //保留旧哈希表桶中链表的顺序
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
//do-while 循环赋值给新哈希表
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
扩容过程中几个关键的点:

  • 新初始化哈希表时,容量为默认容量,阈值为 容量*加载因子
  • 已有哈希表扩容时,容量、阈值均翻倍
  • 如果之前这个桶的节点类型是树,需要把新哈希表里当前桶也变成树形结构
  • 复制给新哈希表中需要重新索引(rehash),这里采用的计算方法是e.hash & (newCap - 1),等价于 e.hash % newCap

结合扩容源码可以发现扩容的确开销很大,需要迭代所有的元素,rehash、赋值,还得保留原来的数据结构。
所以在使用的时候,最好在初始化的时候就指定好 HashMap 的长度,尽量避免频繁 resize()。

HashMap的缺点

  1. HashMap不是同步的,当多线程并发访问一个哈希表时,需要在外部进行同步操作,否者会引发数据不同步问题,但是你可以选择加锁,可以考虑用Collections.synchronizedMap包一层,变成个线程安全的Map

Map m = Collections.synchronizedMap(new HashMap(…));

  1. 为什么哈希表的容量一定要是2的整数次幂

    首先,capacity 为 2的整数次幂的话,计算桶的位置 h&(length-1) 就相当于对 length 取模,提升了计算效率; 其次,capacity 为 2 的整数次幂的话,为偶数,这样 capacity-1 为奇数,奇数的最后一位是 1,这样便保证了 h&(capacity-1) 的最后一位可能为 0,也可能为 1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性; 而如果 capacity 为奇数的话,很明显 capacity-1 为偶数,它的最后一位是 0,这样 h&(capacity-1) 的最后一位肯定为 0,即只能为偶数,这样任何 hash 值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间。

    因此,哈希表容量取 2 的整数次幂,有以下 2 点好处:

  • 使用减法替代取模,提升计算效率;
  • 为了使不同 hash 值发生碰撞的概率更小,尽可能促使元素在哈希表中均匀地散列。
  1. HashMap中equals()和HashCode()有什么作用
    HashMap 的添加、获取时需要通过 key 的 hashCode() 进行 hash(),然后计算下标 ( n-1 & hash),从而获得要找的同的位置。
    当发生冲突(碰撞)时,利用 key.equals() 方法去链表或树中去查找对应的节点。
  2. hash的实现,为什么要这么实现?

    在 JDK 1.8 的实现中,是通过 hashCode() 的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16)。 主要是从速度、功效、质量 来考虑的,这么做可以在桶的 n 比较小的时候,保证高低 bit 都参与到 hash 的计算中,同时位运算不会有太大的开销。

HashTable

HashTable和HashMap的实现原理几乎一样,差别无非是

  1. HashTable不允许key和value为null
  2. HashTable 是线程安全的。但是 HashTable 线程安全的策略实现代价却太大了,简单 粗暴,get/put 所有相关操作都是 synchronized 的,这相当于给整个哈希表加了一把大 锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于 将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差

    HashMap与HashTable的区别是多少

  3. HashTable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是 任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的实现,它 以最大限度地减少实现此接口所需的工作。

  4. HashMap 的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理;Hashtable 遇到 null,直接返回 NullPointerException。
  5. Hashtable 是线程安全的,而 HashMap 不是线程安全的,但是我们也可以通过 Collections.synchronizedMap(hashMap),使其实现同步。

    ConcurrentHashMap

    Spring

    @RestController和@Controller

    @Controller返回一个页面

    单独使用@Controller不加@ResponseBody的话一般使用在要返回一个试图的情况,这种情况属于比较传统的SpringMVC的应用,对于前后端不分离的情况

    @RestController返回JSON或XML形式数据

    但是@RestController只返回对象,对象数据直接以JSON或XML形式写入HTTP响应中,这种情况属于RESTful Web服务,适用于前后端分离
    @Controller+@ResponseBody=@RestController

    IOC

    IOC是一种控制思想,就是将原来在程序中手动创建对象的控制权,交由Spring框架管理,IOC容器时Spring用来实现IOC的载体,IOC容器其实就是个Map(key:value),Map中存放的是各种对象,
    将对象之间的相互依赖关系交给ioc容器来管理,并由ioc容器完成对象的注入,ioc容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的

    AOP

    AOP:面向切面编程,能够将那些与业务无关,却为业务模块所共同的逻辑和责任(事务处理,日记管理,权限控制等)控制起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性

    Spring中的bean的作用域有哪些

  • singleton:唯一bean实例,Spring中的bean默认都是单例的
  • prototype:每次请求都会创建一个新的bean实例
  • request:每一次http请求都会产生一个新的bean,该bean仅在当前http request内有效
  • session:每一次http请求都会产生一个新的bean,该bean仅在当前http session内有效
  • global-session:全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了,

    Spring中单例bean的线程安全了解吗

    的确存在安全问题,当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题,常用的解决方法:
  1. 在Bean对象中尽量避免定义可变的成员变量(不现实)
  2. 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal中(推荐)

    Bean的生命周期

  3. Spring启动,查找并加载需要被Spring管理的Bean,进行Bean的实例化

  4. Bean实例化后,对Bean的引入和值注入到Bean的属性中
  5. 如果Bean实现了BeanNameAware接口的话,Spring将bean的id传递给setBeanName()方法:
  6. 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
  7. 如果 Bean 实现了 ApplicationContextAware 接口的话,Spring 将调用 Bean 的 setApplicationContext() 方法,将 Bean 所在应用上下文引用 传入进来;
  8. 如果 Bean 实现了 BeanPostProcessor 接口,Spring 就将调用它们的 postProcessBeforeInitialization() 方法;
  9. 如果 Bean 实现了 InitializingBean 接口,Spring 将调用它们的 afterPropertiesSet() 方法。类似地,如果 Bean 使用 init-method 声明了初始化 方法,该方法也会被调用;
  10. 如果 Bean 实现了 BeanPostProcessor 接口,Spring 就将调用它们的 postProcessAfterInitialization() 方法;
  11. 此时,Bean 已经准备就绪,可以被应用程序使用了。它们将一直驻留在应用上下文中,直到应用上下文被销毁;
  12. 如果 Bean 实现了 DisposableBean 接口,Spring 将调用它的 destory() 接口方法,同样,如果 Bean 使用了 destory-method 声明销毁方法, 该方法也会被调用。

    Spring中的事务

    事务时逻辑上的一组操作,要么都执行,要么都不执行

    事务特性

  • 原子性:事务是最小的执行单位,不允许分割,事务的原子性确保动作要么全部完成,要么完全不起作用
  • 一致性:执行事务前后,数据保持一致
  • 隔离性:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的
  • 持久性:一个事务被提交后,他对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

    Spring事务接口介绍

    1.PlatformTransactionManagere:事务管理器
    Spring 并不直接管理事务,而是提供了多种事务管理器 ,它们将事务管理的职责委托给 Hibernate 或者 JTA 等持久化机制所提供的相关平台框架 的事务来实现。 Spring 事务管理器的接口是org.springframework.transaction.PlatformTransactionManager ,通过这个接口,Spring 为 各个平台如 JDBC、Hibernate 等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。
    PlatformTransactionManager 接口中定义了三个方法,代码如下:
    Public interface PlatformTransactionManager()…{
    // 根据指定的传播行为,返回当前活动的事务或创建一个新事务。
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    // 使用事务目前的状态提交事务
    Void commit(TransactionStatus status) throws TransactionException;
    // 对执行的事务进行回滚
    Void rollback(TransactionStatus status) throws TransactionException; }
    我们刚刚也说了 Spring 中 PlatformTransactionManager 根据不同持久层框架所对应的接口

    SpringMVC

    SpringMVC工作原理

    客户端发送请求》前端控制器DispatcherServlet接受客户端请求》找到处理器映射HandlerMapping解析请求对应的Handler》HandlerAdapter会根据Handler来调用真正的处理器来处理请求,并处理相应的业务逻辑》处理器返回一个模型视图ModelAndView》视图解析器进行解析》返回一个视图对象》前端控制器DispatchereServlet渲染数据(Moder)》将得到视图对象返回给用户

    SpringMVC运行流程描述

  1. 用户向服务器发送请求,请求被 Spring 前端控制Servelt DispatcherServlet 捕获;
  2. DispatcherServlet 对请求 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 Handler 对 象以及 Handler 对象对应的拦截器),最后以 HandlerExecutionChain 对象的形式返 回;
  3. DispatcherServlet 根据获得的 Handler,选择一个合适的HandlerAdapter;(附 注:如果成功获得 HandlerAdapter 后,此时将开始执行拦截器的 preHandler(…)方法)
  4. 提取 Request 中的模型数据,填充 Handler 入参,开始执行Handler(Controller)。 在填充 Handler 的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作:
    • HttpMessageConveter: 将请求消息(如 Json、xm l等数据)转换成一个对象,将 对象转换为指定的响应信息;
    • 数据转换:对请求消息进行数据转换。如 String 转换成Integer、Double等;
    • 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日 期等;
    • 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到 BindingResult 或 Error 中;
  5. Handler 执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象;
  6. 根据返回的 ModelAndView,选择一个适合的 ViewResolver(必须是已经注册到 Spring 容器中的 ViewResolver)返回给DispatcherServlet;
  7. ViewResolver 结合 Model 和 View,来渲染视图;
  8. 将渲染结果返回给客户端。

    SpringMVC重要组件

    1、前端控制器 DispatcherServlet(不需要工程师开发),由框架提供(重要)

    作用:Spring MVC 的入口函数。接收请求,响应结果,相当于转发器,中央处理 器。有了 DispatcherServlet 减少了其它组件之间的耦合度。用户请求到达前端控制器, 它就相当于 mvc 模式中的 c,DispatcherServlet 是整个流程控制的中心,由它调用其它 组件处理用户的请求,DispatcherServlet 的存在降低了组件之间的耦合性。

    2、处理器映射器 HandlerMapping(不需要工程师开发),由框架提供

    作用:根据请求的 url 查找 Handler。HandlerMapping 负责根据用户请求找到 Handler 即处理器(Controller),SpringMVC 提供了不同的映射器实现不同的映射方 式,例如:配置文件方式,实现接口方式,注解方式等。

    3、处理器适配器 HandlerAdapter

    作用:按照特定规则(HandlerAdapter 要求的规则)去执行 Handler。 通过 HandlerAdapter 对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更 多类型的处理器进行执行。

    4、处理器 Handler(需要工程师开发)

    注意:编写 Handler 时按照 HandlerAdapter 的要求去做,这样适配器才可以去正确 执行 Handler。 Handler 是继 DispatcherServlet 前端控制器的后端控制器,在 DispatcherServlet 的控制下 Handler 对具体的用户请求进行处理。 由于 Handler 涉及到 具体的用户业务请求,所以一般情况需要工程师根据业务需求开发 Handler。

    5、视图解析器 View resolver(不需要工程师开发),由框架提供

    作用:进行视图解析,根据逻辑视图名解析成真正的视图(view)。View Resolver 负责将处理结果生成 View 视图,View Resolver 首先根据逻辑视图名解析成物理视图名 即具体的页面地址,再生成 View 视图对象,最后对 View 进行渲染将处理结果通过页面展 示给用户。 SpringMVC 框架提供了很多的 View 视图类型,包括:jstlView、 freemarkerView、pdfView 等。 一般情况下需要通过页面标签或页面模版技术将模型数 据通过页面展示给用户,需要由工程师根据业务需求开发具体的页面。

    6、视图 View(需要工程师开发)

    View 是一个接口,实现类支持不同的 View 类型(jsp、freemarker、pdf…) 注意:处理器 Handler(也就是我们平常说的 Controller 控制器)以及视图层 View 都是需要我们自己手动开发的。其他的一些组件比如:前端控制器 DispatcherServlet、处 理器映射器 HandlerMapping、处理器适配器 HandlerAdapter 等等都是框架提供给我们 的,不需要自己手动开发。

    SpringMVC注解

  9. @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类 上,则表示类中的所有响应请求的方法都是以该地址作为父路径;

  10. @RequestBody:注解实现接收 http 请求的 json 数据,将 json 转换为 java 对象;
  11. @ResponseBody:注解实现将 conreoller 方法返回对象转化为 json 对象响应给客 户。

    SpringMVC 的控制器是不是单例模式,如果是,有什么问 题,怎么解决?

    是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能, 解决方案是在控制器里面不能写字段。

    SpringMVC 怎么样设定重定向和转发的?

    1、转发:在返回值前面加 “forward:”,譬如:”forward:user.do?name=method4” 2、重定向:在返回值前面加 “redirect:”,譬如:”redirect:http://www.baidu.com

    SpringMVC 和 Struts2 的区别有哪些?

  12. SpringMVC 的入口是一个 servlet 即前端控制器(DispatchServlet),而 Struts2 入 口是一个 filter过虑器(StrutsPrepareAndExecuteFilter);

  13. SpringMVC 是基于方法开发(一个 url 对应一个方法),请求参数传递到方法的形 参,可以设计为单例或多例(建议单例),Struts2 是基于类开发,传递参数是通过类的属 性,只能设计为多例。
  14. Struts2 采用值栈存储请求和响应的数据,通过 OGNL 存取数据;SpringMVC 通过参 数解析器是将 request 请求内容解析,并给方法形参赋值,将数据和视图封装成 ModelAndView 对象,最后又将 ModelAndView 中的模型数据通过 request 域传输到页 面。Jsp 视图解析器默认使用 jstl。

    如何解决 POST 请求中文乱码问题,GET的又如何处理 呢?

    1、解决 post 请求乱码问题:在 web.xml 中配置一个 CharacterEncodingFilter 过滤 器,设置成 utf-8;
    2、get 请求中文参数出现乱码解决方法有两个:
    (1)修改 tomcat 配置文件添加编码与工程编码一致,如下:
    <ConnectorURIEncoding=”utf8” connectionTimeout=”20000” port=”8080” protocol=”HTTP/1.1” redirectPort=”84>
    (2)对参数进行重新编码:
    String userName = new String(request.getParamter(“userName”).getBytes(“ISO8859-1”)”utf-8”)
    ISO8859-1 是 tomcat 默认编码,需要将 tomcat 编码后的内容按 utf-8 编码。

    SpringMVC 里面拦截器是怎么写的?

    有两种写法,一种是实现 HandlerInterceptor 接口;另外一种是继承适配器类,接着 在接口方法当中,实现处理逻辑,然后在 SpringMVC 的配置文件中配置拦截器即可。

    SpringBoot

    YAML是什么

    YAML 是一种人类可读的数据序列化语言。它通常用于配置文件。 与属性文件相比,如果我们想要在配置文件中添加复杂的属性,YAML 文件就更加结构 化,而且更少混淆。可以看出 YAML 具有分层配置数据。

    SpringBoot自动配置原理

    @SpringBootApplication等同于三个注解
  • @SpringBootConfiguration:我们点进去以后可以发现底层是Configuration注解,说白了就是支持JavaConfig的方式来进行配置(使用Configuration配置类等同于XML文件)。
  • @EnableAutoConfiguration:开启自动配置功能(后文详解)
  • @ComponentScan:这个注解,学过Spring的同学应该对它不会陌生,就是扫描注解,默认是扫描当前类下的package。将@Controller/@Service/@Component/@Repository等注解加载到IOC容器中。

其中@EnableAutoConfiguration是关键,是启动自动配置,内部实际上就是去加载META-INF/spring.factories文件的信息,然后筛选出以EnableAutoConfiguration为key的数据,加载到IOC容器中,实现自动配置功能!

@EnableAutoConfiguration

这个注解可以帮助我们自动载入应用程序所需要的所有默认配置
点进去看
Java面试 - 图4
有两个比较重要的注解

  • @AutoConfigurationPackage:自动配置包
  • @Import:给IOC容器导入组件

    @AutoConfigurationPackage

    Java面试 - 图5
    可以发现,依靠的还是@Import注解,再点进去看,发现重要的是以下的代码:
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
    BeanDefinitionRegistry registry) {
    register(registry, new PackageImport(metadata).getPackageName());
    }
    在默认的情况下就是将主配置类(@SpringBootApplication)的所在包及其子包里边的组件扫描到Spring容器中。
    跟@ComponentScan的区别就是:比如说,你用了Spring Data JPA,可能会在实体类上写@Entity注解。这个@Entity注解由@AutoConfigurationPackage扫描并加载,而我们平时开发用的@Controller/@Service/@Component/@Repository这些注解是由ComponentScan来扫描并加载的。

  • 简单理解:这二者扫描的对象是不一样的。

    @Import

    简单梳理就是:

  • Spring启动的时候会扫描所有jar路径下的META-INF/spring.factories将其文件包装成Properties对象

  • 从Properties对象获取到key值为EnableAutoConfiguration的数据,然后添加到容器里面去

    什么是Spring Boot Stater

    启动器是一套方便的依赖没描述符,它可以放在自己的程序中。你可以一站式的获取你所需要的 Spring 和相关技术,而不需要依赖描述符的通过示例代码搜索和复制黏贴的负载。
    例如,如果你想使用 Sping 和 JPA 访问数据库,只需要你的项目包含spring-boot-starter-data-jpa 依赖项,你就可以完美进行。

    Mysql

    数据库的三范式

    第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项
    第二范式:要求实体的属性完全依赖于主关键字,所谓完全依赖是指不能存在仅依赖主关键字一部分的属性
    第三范式:任何非主属性不依赖于其他非主属性

    数据类型

    主要分为数值型,浮点型,日期/时间吗,字符串(字符)型和二进制型。
    unsigned:无符号的,只能是正数。signed:有符号数,可能是正数也可能是负数,浮点数默认是有符号数。

    1.数值型

    Java面试 - 图6

    2.浮点型

    Java面试 - 图7

    3.日期/时间

    timestamp:时间戳类型,就是指一个时间的数据值,本质是一个数字,一个重要作用就是能够自动获得时间戳的数据值。
    Java面试 - 图8

    4.字符型

    varchar :变长字符串 使用时必须设定长度,最大值理论值65535个
    char :定长字符串,使用时需要设定长度,不设定默认为1,最大理论值255个,适用于存储的数据都是可预见的明确的固定长度的字符,如手机号,身份证号码
    Java面试 - 图9

    5.二进制类型

    blob类型:小图片用blob类型,最大长度64K,
    mediumblob类型:大图片使用mediumblob,最大是16M
    longblob类型:最大长度是4G
    Java面试 - 图10

    6.特殊类型

    enum (枚举类型)单选项字符串数据类型:只能插入指定的数据格式,否则会报错。
    mysql-> create table user3(id int primary key auto_increment,
    -> name varchar(10),
    -> sex enum(‘boy’,’girl’)
    -> );

mysql> insert into user3(name,sex)
-> values
-> (“zhang”,”boy”),
-> (“wang”,”girl”);

mysql> select from user3;
+——+———-+———+
| id | name | sex |
+——+———-+———+
| 1 | zhang | boy |
| 2 | wang | girl |
+——+———-+———+
*set
(集合类型)多选字符串的数据类型:适用于存储表单界面的多选项值
mysql> create table user4(
-> id int primary key auto_increment,
-> name varchar(10),
-> hobby set(‘zhang’,’wang’,’li’,’yang’)
-> );

mysql> insert into user4(hobby)
-> values
-> (“zhang,wang”);

mysql> select hobby as 爱好 from user4;

+——————+
| 爱好 |
+——————+
| zhang,wang |
+——————+

数据库存储引擎

查看引擎命令

show engines; //查看系统所支持的引擎类型:

show variables like ‘%storage_engine’; //查看mysql引擎

show create table tablename; //可以查看某个表所使用的引擎

InnoDB引擎

InnoDB 是事务型数据库的首选引擎,支持事务安全表 (ACID ) ,支持行锁定和外键。
InnoDB 作为默认存储引擎,特性有:

  • InnoDB 给 MySQL 提供了具有提交、回滚和崩溃恢复能力的事务安全 (ACID 兼容)存储引擎。InnoDB 锁定在行级并且也在 SELECT 语句中提供一个类似 Oracle 的非锁定读。这些功能增加了多用户部署和性能。在 SQL 查询中,可以自由地将 InnoDB 类型的表与其他MySQL 的表的类型混合起来,甚至在同一个查询中也可以混合。
  • InnoDB 是为处理巨大数据量的最大性能设计。它的 CPU 效率可能是任何其他基于磁盘的关系数据库引擎所不能匹敌的。
  • InnoDB 存储引擎完全与 MySQL 服务器整合,InnoDB 存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。InnoDB 将它的表和索引存在一个逻辑表空间中,表空间可以包含数个文件〈或原始磁盘分区) 。这与 MyISAM 表不同,比如在 MyISAM 表中每个表被存在分离的文件中。InnoDB 表可以是任何尺寸,,即使在文件尺寸被限制为 2GB 的操作系统上。
  • InnoDB 支持外键完整性约束 (FOREIGN KEY) 。存储表中的数据时, 每张表的存储都按主键顺序存放, 如果没有显示在表定义时指定主键,InnoDB 会为每一行生成一个 6B 的ROWID,并以此作为主键。
  • InnoDB 被用在众多需要高性能的大型数据库站点上。
  • InnoDB 不创建目录,使用 InnoDB 时,MySQL 将在 MySQL 数据目录下创建一个名为ibdata1 的 10MB 大小的自动扩展数据文件,以及两个名为ib_logfile0ib_logfilel5MB大小的日志文件。

    MyISAM引擎

    MyISAM 基于 ISAM 的存储引擎,并对其进行扩展。它是在 Web、数据存储和其他应用环境下最常使用的存储引擎之一。MyISAM 拥有较高的插入、查询速度,但不支持事务。在MyISAM 主要特性有:

  • 大文件 (达 63 位文件长度) 在支持大文件的文件系统和操作系统上被支持。

  • 当把删除、更新及插入操作混合使用的时候,动态尺寸的行产生更少碎片。这要通过合并相邻被删除的块,以及若下一个块被删除,就扩展到下一块来自动完成。
  • 每个 MyISAM 表最大索引数是 64,这可以通过重新编译来改变。每个索引最大的列数是 16 个。
  • 最大的键长度是 1000B,这也可以通过编译来改变。对于键长度超过 250B 的情况,一个超过 1024B 的键将被用上。
  • BLOB 和TEXT 列可以被索引
  • NULL 值被允许在索引的列中。这个值占每个键的 0~1 个字节
  • 所有数字键值以高字节优先被存储以允许一个更高的索引压缩。
  • 每表一个AUTO_INCREMENT 列的内部处理。MyISAM 为 INSERTUPDATE 操作自动更新这一列。这使得 AUTO_INCREMENT列更快〈至少 10%) 。在序列顶的值被删除之后就不能再利用。
  • 可以把数据文件和索引文件放在不同目录。
  • 每个字符列可以有不同的字符集。
  • 有VARCHAR 的表可以固定或动态记录长度。
  • VARCHAR 和CHAR 列可以多达 64KB。

    使用 MyISAM 引擎创建数据库,将生产 3 个文件。文件的名字以表的名字开始,扩展名指出文件类型, frm文件存储表定义,数据文件的扩展名为.MYD (MYData),索引文件的扩展名是.MYI MYIndex)

MEMORY引擎

MEMORY 存储引擎将表中的数据存储到内存中,为查询和引用其他表数据提供快速访问。MEMORY 主要特性有:

  • MEMORY 表的每个表可以有多达 32 个索引,每个索引 16 列,以及 500B 的最大键长度。
  • MEMORY 存储引擎执行 HASH 和 BTREE 索引。
  • 可以在一个MEMORY 表中有非唯一键
  • MEMORY 表使用一个固定的记录长度格式。
  • MEMORY 不支持BLOB 或TEXT 列。
  • MEMORY 支持 AUTO_INCREMENT 列和对可包含NULL 值的列的索引
  • MEMORY 表在所有客户端之间共享 (就像其他任何非 TEMPORARY 表) 。
  • MEMORY 表内容被存在内存中,内存是 MEMORY 表和服务器在查询处理时的空闲中创建的内部表共享
  • 当不再需要 MEMORY 表的内容时,要释放被 MEMORY 表使用的内存,应该执行DELETE FROM 或TRUNCATE TABLE,或者删除整个表 〈使用DROP TABLE) 。

    存储引擎的选择

    不同存储引擎都有各自的特点,以适应不同的需求。下面是各种引擎的不同的功能:

  • 如果要提供提交、回滚和崩溃恢复能力的事务安全 (ACID 兼容) 能力,并要求实现并发控制,InnoDB 是个很好的选择;

  • 如果数据表主要用来插入和查询记录,则 MyISAM 引擎能提供较高的处理效率
  • 如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存中的 Memory 引擎,MySQL 中使用该引擎作为临时表,存放查询的中间结果;
  • 如果只有 INSERT 和 SELECT 操作,可以选择 Archive 引擎,Archive 存储引擎支持高并发的插入操作,但是本身并不是事务安全的。Archive 存储引擎非常适合存储归档数据,如记录日志信息可以使用 Archive 引擎。

使用哪一种引擎要根据需要灵活选择, 一个数据库中多个表可以使用不同引擎以满足各种性能和实际需求。使用合适的存储引擎,将会提高整个数据库的性能。

Mysql的事务的基本特性和事务隔离级别

事务基本特性

A:原子性指的是一个事务中的操作要么全部成功,要么全部失败
C:一致性指的是数据库总是从一个一致性的状态转换为另外一个一致性的状态
I:隔离性是指一个事务的修改在最终提交前,对其他事务是不可见的
D:持久性指一旦事务提交,所做的修改就会永久保存在数据库中

ACID靠什么保证的

A原子性有undo log日志保证,他记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
C一致性一般有代码层面来保证
I隔离性由MVCC来保证
D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复

事务隔离级别

read uncommit:读未提交,可能会读到其他事务未提交的数据,也叫做脏读。
read commit:读已提交,两次读取结果不一致,叫做不可重复读,不可重复读解决了脏读的问题,他只会读取已经提交的事务
repeatable read:可重复读,这是mysql的默认级别,就是每次读取结果 都一样,但是有可能产生幻读
serializable:串行,一般不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。

Mysql的锁机制

Mysql中的锁分为分享锁/读锁(Shared Locks),排他锁/写锁(Exclusive Locks),间隙锁,行锁(Record Locks),表锁。
在四个隔离级别中加锁是会影响性能的,而读未提交时没有加任何锁的,然后对它来说没有隔离的效果,所以读未提交的性能是最好的
对于串行化加的是一把大锁,读的时候加共享锁,不能写,写的时候加的是排他锁,阻塞其他事务的写入和读取,若是其他事务长时间不能写入就直接报超时,所以串行化是性能最差的,对于它没有什么并发性可言
对于读提交和可重复读,他们俩的实现是兼顾解决数据问题,然后又要有一定的并发性,所以在实现上锁机制会比串行化优化很多,提高并发性,所以性能比较好

Sql语句的执行顺序

  • 查询语句中的执行顺序

查询中用到的关键词主要包含六个,并且他们的顺序依次为:select—from—where—group by—having—order by
其中select和from是必须的,其他关键词是可选的

from—where—group by—having—select—order by from:需要从哪个数据表检索数据 where:过滤表中数据的条件 group by:如何将上面过滤出的数据分组 having:对上面已经分组的数据进行过滤的条件 select:查看结果集中的哪个列,或列的计算结果 order by:按照什么样的顺序来查看返回的数据

Redis

Redis的介绍

Redis是一个使用C语言开发的数据库,Redis跟传统数据库不一样的地方是Redis的数据是存在内存中的,所以读写非常快,所以Redis被广泛用于缓存方向,
Redis除了做缓存外,还经常用于做分布式锁,甚至消息队列
Redis提供了多种数据类型来支持不同的业务场景,Redis还支持事务,持久化,Lua脚本,多种集群方案。

缓存数据的处理流程

总结来说:

  1. 如果用户请求的数据在缓存中就直接返回
  2. 如果缓存中不存在就去看数据库是否存在
  3. 如果数据库中存在的话就更新缓存中的数据
  4. 如果数据库中不存在的话就返回空数据

    Mybatis

    #{}和${}的区别

    {}是预编译处理,${}是字符串替代
    Mybatis在处理#{}时,会把sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值
    Mybatis在处理${}时,就是把{}替换为变量的值
    使用#{}可以有效的防止SQL注入,提高系统安全性

    Mybatis有几种分页方式

  5. 数组分页

  6. Sql分页
  7. 拦截器分页
  8. RowBounds分页

    Mybatis是如何进行分页的,分页插件的原理是什么

    Mybatis使用RowBounds对象进行分页的,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页
    分页插件的原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数

    一级,二级缓存

  9. 一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Session flush或close之后,该Session中的所有Cache就将清空。默认打开一级缓存

  10. 二级缓存与一级缓存机制相同,默认也是采用PerpetualCache,HashMap存储,不同在于其存储作用域为Mapper(Namespace)并且可以自定义存储源,如Ehcache,要开启二级缓存,你需要在你的Sql映射文件中添加一行
  11. 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了CUD操作后,默认该作用域下所有select中的缓存将被clear

    JavaConfig

    java config是指基于java配置的spring。传统的Spring一般都是基本xml配置的,后来spring3.0新增了许多java config的注解,特别是spring boot,基本都是清一色的java config。
    例如:
  • @Configuration
  • @ComponentScan
  • @Bean