Java 核心面试知识点汇总
本文整理了 Java 开发核心面试知识点,涵盖 Java 基础(数据类型、面向对象、异常处理、多线程)、数据库(MySQL 索引、事务、SQL 优化)以及框架(Spring、Spring Boot)。内容包含常见面试题解答与原理分析,适合求职者复习参考。

本文整理了 Java 开发核心面试知识点,涵盖 Java 基础(数据类型、面向对象、异常处理、多线程)、数据库(MySQL 索引、事务、SQL 优化)以及框架(Spring、Spring Boot)。内容包含常见面试题解答与原理分析,适合求职者复习参考。

在程序运行前,Java 源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM 负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的 JVM,就可以运行字节码文件。
| 同一个类 | 同一个包 | 不同包的子类 | 不同包的非子类 | |
| private | √ | |||
| default | √ | √ | ||
| protected | √ | √ | √ | |
| public | √ | √ | √ | √ |
Java 数据类型包括基本数据类型和引用数据类型两大类。
基本数据类型有 8 个,可以分为 4 个小类,分别是整数类型(byte/short/int/long)、浮点类型(float/double)、字符类型(char)、布尔类型(boolean)。其中,4 个整数类型中,int 类型最为常用。2 个浮点类型中,double 最为常用。另外,在这 8 个基本类型当中,除了布尔类型之外的其他 7 个类型,都可以看做是数字类型,它们相互之间可以进行类型转换。
引用类型就是对一个对象的引用,根据引用对象类型的不同,可以将引用类型分为 3 类,即数组、类、接口类型。引用类型本质上就是通过指针,指向堆中对象所持有的内存空间,只是 Java 语言不再沿用指针这个说法而已。
扩展阅读
对于基本数据类型,你需要了解每种类型所占据的内存空间,面试官可能会追问这类问题:
int 类型占 4 字节(32 位),数据范围是 -2^31 ~ 2^31-1。
Java 中的变量分为成员变量和局部变量,它们的区别如下:
成员变量:
局部变量:
实例变量若为引用数据类型,其默认值一律为 null。若为基本数据类型,其默认值如下:
Java 语言是面向对象的语言,其设计理念是'一切皆对象'。但 8 种基本数据类型却出现了例外,它们不具备对象的特性。正是为了解决这个问题,Java 为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。
自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;
Integer、Double 不能直接进行比较。整数、浮点类型的包装类,都继承于 Number 类型,而 Number 类型分别定义了将数字转换为 byte、short、int、long、float、double 的方法。所以,可以将 Integer、Double 先转为转换为相同的基本数据类型(如 double),然后使用==进行比较。
示例代码
Integer i = 100;
Double d = 100.00;
System.out.println(i.doubleValue() == d.doubleValue());
int 是基本数据类型,Integer 是 int 的包装类。二者在做==运算时,Integer 会自动拆箱为 int 类型,然后再进行比较。届时,如果两个 int 值相等则返回 true,否则就返回 false。
什么是对象: 对象就是事物存在的实体,万物皆对象。举个简单的例子,比如人类就是一个对象,然而对象是有属性和方法的,那么姓名,年龄,身高,体重,性别这些是每个人都有的特征可以概括为属性,当然了我们还会思考,学习,这些行为相当于对象的方法。不过,不同的对象就会有不同的行为。
面向对象: 面向对象是一种优秀的程序设计方法,就是把数据及其操作方法放在一起,将其看成一个整体。对同类对象抽象出其共性,形成类。再答三大特征。
面向对象的程序设计方法具有三个基本特征:封装、继承、多态。其中,封装指的是将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能;继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;多态指的是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。
扩展阅读
抽象也是面向对象的重要部分。
封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以实现以下目的:
现实中的事物通常会体现出多种形态,例如学生小明既是学生也是人,即出现了两种形态。Java 作为面向对象的语言,同样可以描述一个事物的多种形态,如 Student 类继承了 Person 类,即一个 Student 的对象既是 Student,又是 Person。
多态的实现离不开继承,在设计程序时,我们可以将参数的类型定义为父类型。在调用程序时,则可以根据实际情况,传入该父类型的某个子类型的实例,这样就实现了多态。对于父类型,可以有三种形式,即普通的类、抽象类、接口。对于子类型,则要根据它自身的特征,重写父类的某些方法,或实现抽象类/接口的某些抽象方法。
首先,Java 是单继承的,指的是 Java 中一个类只能有一个直接的父类。Java 不能多继承,则是说 Java 中一个类不能直接继承多个父类。
Java 语言之所以摒弃了多继承的这项特征,是因为多继承容易产生混淆。比如,两个父类中包含相同的方法时,子类在调用该方法或重写该方法时就会迷惑。
准确来说,Java 是可以实现"多继承"的。因为尽管一个类只能有一个直接父类,但是却可以有任意多个间接的父类。这样的设计方式,避免了多继承时所产生的混淆。
重载发生在同一个类中,若多个方法之间方法名相同、参数列表不同,则它们构成重载的关系。重载与方法的返回值以及访问修饰符无关,即重载的方法不能根据返回类型进行区分。
重写发生在父类子类中,若子类方法想要和父类方法构成重写关系,则它的方法名、参数列表必须与父类方法相同。另外,返回值要小于等于父类方法,抛出的异常要小于等于父类方法,访问修饰符则要大于等于父类方法。还有,若父类方法的访问修饰符为 private,则子类不能对其重写。
JDK 中有一个名为 jre 的目录,里面包含两个文件夹 bin 和 lib,bin 就是 JVM,lib 就是 JVM 工作所需要的类库。
Object 类提供了如下几个常用方法:
hashCode() 用于获取哈希码(散列码),equals() 用于比较两个对象是否相等,它们应遵守如下规定:
Object 类提供的 equals() 方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object 类中 equals() 方法的默认实现是没有实用价值的,所以通常都要重写。
equals() 方法重写时,通常也要将 hashCode() 进行重写。
String 类是 Java 最常用的 API,它包含了大量处理字符串的方法,比较常用的有:
当对字符串进行修改的时候,特别是字符串对象经常改变的情况下,需要使用 StringBuffer 和 StringBuilder 类。
由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
先看看 "hello" 和 new String("hello") 的区别:
显然,采用 new 的方式会多创建一个对象出来,会占用更多的内存,所以一般建议使用直接量的方式创建字符串。
如果拼接的都是字符串直接量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的。
如果拼接的字符串中包含变量,则在编译时编译器采用 StringBuilder 对其进行优化,即自动创建 StringBuilder 实例并调用其 append() 方法,将这些字符串拼接在一起。
JVM 会使用常量池来管理字符串直接量。在执行这句话时,JVM 会先检查常量池中是否已经存有"abc",若没有则将"abc"存入常量池,否则就复用常量池中已有的"abc",将其引用赋值给变量 a。
null 是没有地址,""是有地址但是里面的内容是空的,好比做饭 null 说明连锅都没有 而""则是有锅没米。
接口使用 interface 修饰; 类可以实现多个接口;
抽象类使用 abstract 修饰; 抽象类只能单继承; 如果一个类继承了抽象类,①如果实现了所有的抽象方法,子类可以不是抽象类;②如果没有实现所有的抽象方法,子类仍然是抽象类。
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。
面向接口编程就是先把客户的业务逻辑线提取出来,作为接口,业务具体实现通过该接口的实现类来完成。当客户需求变化时,只需编写该业务逻辑的新的实现类,通过更改配置文件 (例如 Spring 框架) 中该接口的实现类就可以完成需求,不需要改写现有代码,减少对系统的影响。
在 Java 中,可以按照如下二个步骤处理异常:
关于异常处理:
在 Java 中,处理异常的语句由 try、catch、finally 三部分组成。其中,try 块用于包裹业务代码,catch 块用于捕获并处理某个类型的异常,finally 块则用于回收资源。当业务代码发生异常时,系统会创建一个异常对象,然后由 JVM 寻找可以处理这个异常的 catch 块,并将异常对象交给这个 catch 块处理。若业务代码打开了某项资源,则可以在 finally 块中关闭这项资源,因为无论是否发生异常,finally 块一定会执行。
关于抛出异常:
当程序出现错误时,系统会自动抛出异常。除此以外,Java 也允许程序主动抛出异常。当业务代码中,判断某项错误的条件成立时,可以使用 throw 关键字向外抛出异常。在这种情况下,如果当前方法不知道该如何处理这个异常,可以在方法签名上通过 throws 关键字声明抛出异常,则该异常将交给 JVM 处理。
不管 try 块中的代码是否出现异常,也不管哪一个 catch 块被执行,甚至在 try 块或 catch 块中执行了 return 语句,finally 块总会被执行。
static:就是多个对象共享同一份数据
一个类的不同对象有些共享的数据,这样我们就可以使用 static 来修饰,一旦使用了 static,那么这样的内容不再属于对象,而是属于类的,所以凡是本类的对象,都共享同一份。它可以用来修饰成员变量,修饰成员方法,以及静态代码块。
static 修饰的类可以被继承。
static: 此变量会被这个类的所有对象所共享,这些对象都可以调用、改变它的值; 无需创建对象也可以调用此方法; 静态方法只可以访问 静态的 属性/变量/方法。
final: final 属性不可被外部更改,而且必须初始化。
动态获取类的信息以及动态调用对象的方法就叫反射。
Java 程序中的对象在运行时可以表现为两种类型,即编译时类型和运行时类型。例如 Person p = new Student();,这行代码将会生成一个 p 变量,该变量的编译时类型为 Person,运行时类型为 Student。
有时,程序在运行时接收到外部传入的一个对象,该对象的编译时类型是 Object,但程序又需要调用该对象的运行时类型的方法。这就要求程序需要在运行时发现对象和类的真实信息,而解决这个问题就是反射。
序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。其中,对象的序列化(Serialize),是指将一个 Java 对象写入 IO 流中,对象的反序列化(Deserialize),则是指从 IO 流中恢复该 Java 对象。
若对象要支持序列化机制,则它的类需要实现 Serializable 接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的,Java 的很多类已经实现了 Serializable 接口,如包装类、String、Date 等。
进程:进程是程序运行时的一个实例。
线程:线程是 CPU 调度和分派的基本单位,它被包含在进程之中,是进程中的实际运作单位。
多线程:多个线程同时运行。比如说地图导航,当油站过多时,一次查询会很耗时,所以我们可以将一段很长的路线按照路径长度分成若干个条件同时查询,或者说某个程序多个功能同时运转。
并发:在同一时刻,有多个指令在单个 CPU 上交替执行
并行:在同一时刻,有多个指令在多个 CPU 上同时执行
创建线程的方式有继承 Thread 类、实现 Runnable 接口。
通过继承 Thread 类(Thread 类实现了 Runnable 接口)来创建并启动线程的步骤如下:
通过实现 Runnable 接口来创建并启动线程的步骤如下:
扩展阅读
采用实现 Runnable 接口的方式创建多线程的优缺点:
采用继承 Thread 类的方式创建多线程的优缺点:
鉴于上面分析,因此一般推荐采用实现 Runnable 接口的方式来创建多线程。
1、start 方法用来启动相应的线程;
2、run 方法只是 thread 的一个普通方法,在主线程里执行。
只能对处于新建状态的线程调用 start() 方法,否则将引发 IllegalThreadStateException 异常。
在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直'霸占'着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的 Java 对象一样,仅仅由 Java 虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
当线程对象调用了 start() 方法之后,该线程处于就绪状态,Java 虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于 JVM 里线程调度器的调度。
如果处于就绪状态的线程获得了 CPU,开始执行 run() 方法的线程执行体,则该线程处于运行状态,如果计算机只有一个 CPU,那么在任何时刻只有一个线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个 CPU 上轮换的现象。
当一个线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务。当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。当发生如下情况时,线程将会进入阻塞状态:
针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:
线程会以如下三种方式结束,结束后就处于死亡状态:
握手:
①四川 8633 请求建立连接 (SYN),并且发送出序号。
②服务端接受到信号,即有确认号(ACK),此时并同样返回请求序号 Seq
③客户端接受到信号,即有确认号(ACK),连接已经建立
挥手:
①客户端申请断开连接即 FIN(我这边准备断开连接了)
②服务端接收信息返回,表示我已经接收到(收到,请稍等,我这边准备一下)
③服务端发送信息表示可以断开连接 (我准备好了,你可以断开连接了)
④客户端接受信息,同时返回信息通知服务端自己收到信息,开始断开 连接 (好的,拜拜!)
HTTPS 简单讲是 HTTP 的安全版,完全不同的连接方式,HTTPS 一般免费证书较少,因而需要一定费用。
将对象封装到 stringBuilder 中,调用 reverse 方法反转。
抽象类不能被实例化; 抽象类可以有抽象方法,只需申明,无须实现; 有抽象方法的类一定是抽象类; 抽象类的子类必须实现抽象类中的所有抽象方法,否则子类仍然是抽象类; 抽象方法不能被 static、final 修饰。
(1)用来修饰一个引用 如果引用时类的成员变量,则必须当场赋值,其值不可变。
(2)用来修饰一个方法 当使用 final 修饰方法时,这个方法可以被继承,但无法被重写。
(3)用来修饰类 当用 final 修改类时,无法被继承。比如常用的 String 类就是最终类。
get 请求因为浏览器对 url 长度有限制,所以参数个数有限制,而 post 请求参数个数没有限制; 因为 get 请求参数暴露在 url 上,所以安全方面 post 比 get 更加安全; get 请求只能进行 url 编码,而 post 请求可以支持多种编码方式; 在浏览器进行回退操作时,get 请求是无害的,而 post 请求则会重新请求一次。
①冒泡排序:每次冒泡过程都是从数列的第一个元素开始,然后依次和剩余的元素进行比较,从左到右两两相邻的元素比大小,高的就和低的换一下位置。最后最高 (值最大) 的肯定就排到后面了。
②选择排序:首先在未排序数列中找到最小元素,然后将其与数列的首部元素进行交换,然后,在剩余未排序元素中继续找出最小元素,将其与已排序数列的末尾位置元素交换。以此类推,直至所有元素均排序完毕。
③插入排序:将待插元素,依次与已排序好的子数列元素从后到前进行比较,如果当前元素值比待插元素值大,则将移位到与其相邻的后一个位置,否则直接将待插元素插入当前元素相邻的后一位置,因为说明已经找到插入点的最终位置。
④快速排序:简单的说,就是设置一个标准值,将大于这个值的放到右边 (不管排序),将小于这个值的放到左边 (不管排序),那么这样只是区分了左小右大,没有排序,没关系,左右两边再重复这个步骤。直到不能分了为止。
(1)队列先进先出,栈先进后出。
(2)遍历数据速度不同。
栈只能从头部取数据 也就最先放入的需要遍历整个栈最后才能取出来,而且在遍历数据的时候还得为数据开辟临时空间,保持数据在遍历前的一致性;
队列则不同,他基于地址指针进行遍历,而且可以从头或尾部开始遍历,但不能同时遍历,无需开辟临时空间,因为在遍历的过程中不影像数据结构,速度要快的多。
MySQL 的分页语法:
在 MySQL 中,SELECT 语句默认返回所有匹配的行,它们可能是指定表中的每个行。为了返回第一行或前几行,可使用 LIMIT 子句,以实现分页查询。LIMIT 子句的语法如下:
-- 在所有的查询结果中,返回前 5 行记录。 SELECT prod_name FROM products LIMIT 5; -- 在所有的查询结果中,从第 5 行开始,返回 5 行记录。 SELECT prod_name FROM products LIMIT 5,5;
总之,带一个值的 LIMIT 总是从第一行开始,给出的数为返回的行数。带两个值的 LIMIT 可以指定从行号为第一个值的位置开始。
优化 LIMIT 分页:
在偏移量非常大的时候,例如 LIMIT 10000,20 这样的查询,这时 MySQL 需要查询 10020 条记录然后只返回最后 20 条,前面的 10000 条记录都将被抛弃,这样的代价是非常高的。如果所有的页面被访问的频率都相同,那么这样的查询平均需要访问半个表的数据。要优化这种查询,要么是在页面中限制分页的数量,要么是优化大偏移量的性能。
优化此类分页查询的一个最简单的办法就是尽可能地使用索引覆盖扫描,而不是查询所有的列,然后根据需要做一次关联操作再返回所需的列。对于偏移量很大的时候,这样做的效率会提升非常大。考虑下面的查询:
SELECT film_id,description FROM sakila.film ORDER BY title LIMIT 50,5;
如果这个表非常大,那么这个查询最好改写成下面的样子:
SELECT film.film_id,film.description FROM sakila.film INNER JOIN ( SELECT film_id FROM sakila.film ORDER BY title LIMIT 50,5 ) AS lim USING(film_id);
常用的聚合函数有 COUNT()、AVG()、SUM()、MAX()、MIN(),下面以 MySQL 为例,说明这些函数的作用。
COUNT() 函数:
COUNT() 函数统计数据表中包含的记录行的总数,或者根据查询结果返回列中包含的数据行数,它有两种用法:
COUNT() 函数可以与 GROUP BY 一起使用来计算每个分组的总和。
AVG() 函数 ():
AVG() 函数通过计算返回的行数和每一行数据的和,求得指定列数据的平均值。
AVG() 函数可以与 GROUP BY 一起使用,来计算每个分组的平均值。
SUM() 函数:
SUM() 是一个求总和的函数,返回指定列值的总和。
SUM() 可以与 GROUP BY 一起使用,来计算每个分组的总和。
MAX() 函数:
MAX() 返回指定列中的最大值。
MAX() 也可以和 GROUP BY 关键字一起使用,求每个分组中的最大值。
MAX() 函数不仅适用于查找数值类型,也可应用于字符类型。
MIN() 函数:
MIN() 返回查询列中的最小值。
MIN() 也可以和 GROUP BY 关键字一起使用,求出每个分组中的最小值。
MIN() 函数与 MAX() 函数类似,不仅适用于查找数值类型,也可应用于字符类型。
表与表之间常用的关联方式有两种:内连接、外连接,下面以 MySQL 为例来说明这两种连接方式。
内连接:
内连接通过 INNER JOIN 来实现,它将返回两张表中满足连接条件的数据,不满足条件的数据不会查询出来。
外连接:
外连接通过 OUTER JOIN 来实现,它会返回两张表中满足连接条件的数据,同时返回不满足连接条件的数据。外连接有两种形式:左外连接(LEFT OUTER JOIN)、右外连接(RIGHT OUTER JOIN)。
除此之外,还有一种常见的连接方式:等值连接。这种连接是通过 WHERE 子句中的条件,将两张表连接在一起,它的实际效果等同于内连接。出于语义清晰的考虑,一般更建议使用内连接,而不是等值连接。
以上是从语法上来说明表与表之间关联的实现方式,而从表的关系上来说,比较常见的关联关系有:一对多关联、多对多关联、自关联。
SQL 注入的原理是将 SQL 代码伪装到输入参数中,传递到服务器解析并执行的一种攻击手法。也就是说,在一些对 server 端发起的请求参数中植入一些 SQL 代码,server 端在执行 SQL 操作时,会拼接对应参数,同时也将一些 SQL 注入攻击的"SQL"拼接起来,导致会执行一些预期之外的操作。
举个例子:
比如我们的登录功能,其登录界面包括用户名和密码输入框以及提交按钮,登录时需要输入用户名和密码,然后提交。此时调用接口/user/login/ 加上参数 username、password,首先连接数据库,然后后台对请求参数中携带的用户名、密码进行参数校验,即 SQL 的查询过程。假设正确的用户名和密码为 ls 和 123456,输入正确的用户名和密码、提交,相当于调用了以下的 SQL 语句。
SELECT * FROM user WHERE username = 'ls' AND password = '123456'
SQL 中会将#及--以后的字符串当做注释处理,如果我们使用 ' or 1=1 # 作为用户名参数,那么服务端构建的 SQL 语句就如下:
select * from user where or 1=1 #' and password='123456'
而#会忽略后面的语句,而 1=1 属于常等型条件,因此这个 SQL 将查询出所有的登录用户。其实上面的 SQL 注入只是在参数层面做了些手脚,如果是引入了一些功能性的 SQL 那就更危险了,比如上面的登录功能,如果用户名使用这个 ' or 1=1;delete * from users; #,那么在";"之后相当于是另外一条新的 SQL,这个 SQL 是删除全表,是非常危险的操作,因此 SQL 注入这种还是需要特别注意的。
如何解决 SQL 注入
WHERE 是一个约束声明,是在结果返回之前起作用的。
HAVING 是一个过滤声明,是在查询返回结果后对查询结果进行的过滤操作。
它是帮助 MySQL 高效获取数据的数据结构,主要是用来提高数据检索的效率,降低数据库的 IO 成本,同时通过索引列对数据进行排序,降低数据排序的成本,也能降低了 CPU 的消耗。
增加索引也有许多不利的方面,主要表现在如下几个方面:
MySQL 的索引可以分为以下几类:
查询比较频繁的字段和像作为查询条件,排序字段或分组的字段。
可以使用 EXPLAIN 语句查看索引是否正在使用。
举例,假设已经创建了 book 表,并已经在其 year_publication 字段上建立了普通索引。执行如下语句:
EXPLAIN SELECT * FROM book WHERE year_publication=1990;
EXPLAIN 语句将为我们输出详细的 SQL 执行信息,其中:
如果 possible_keys 行和 key 行都包含 year_publication 字段,则说明在查询时使用了该索引。
建议按照如下的原则来设计索引:
索引并非越多越好,一个表中如有大量的索引,不仅占用磁盘空间,还会影响 INSERT、DELETE、UPDATE 等语句的性能,因为在表中的数据更改时,索引也会进行调整和更新。
联合索引是指对表上的多个列进行索引,联合索引的创建方法与单个索引创建的方法一样,不同之处仅在于有多个索引列。从本质上来说,联合索引还是一棵 B+ 树,不同的是联合索引的键值数量不是 1,而是大于等于 2,参考下图。
另外,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,所以使用联合索引时遵循最左前缀集合。
我们当时做压测的时候有的接口非常的慢,接口的响应时间超过了 2 秒以上,因为我们当时的系统部署了运维的监控系统 Skywalking,在展示的报表中可以看到是哪一个接口比较慢,并且可以分析这个接口哪部分比较慢,这里可以看到 SQL 的具体执行时间,所以可以定位是哪个 sql 出了问题。
如果,项目中没有这种运维的监控系统,其实在 MySQL 中也提供了慢日志查询的功能,可以在 MySQL 的系统配置文件中开启这个慢日志的功能,并且也可以设置 SQL 执行超过多少时间来记录到一个日志文件中,我记得上一个项目配置的是 2 秒,只要 SQL 执行的时间超过了 2 秒就会记录到一个日志文件中,我们就可以在日志文件找到执行比较慢的 SQL 了。
如果一条 sql 执行很慢的话,我们通常会使用 mysql 自动的执行计划 explain 来去查看这条 sql 的执行情况,比如在这里面可以通过 key 和 key_len 检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况,第二个,可以通过 type 字段查看 sql 是否有进一步的优化空间,是否存在全索引扫描或全盘扫描,第三个可以通过 extra 建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复。
MySQL 的默认的存储引擎 InnoDB 采用的 B+ 树的数据结构来存储索引,选择 B+ 树的主要的原因是:第一阶数更多,路径更短,第二个磁盘读写代价 B+ 树更低,非叶子节点只存储指针,叶子阶段存储数据,第三是 B+ 树便于扫库和区间查询,叶子节点是一个双向链表。
第一:在 B 树中,非叶子节点和叶子节点都会存放数据,而 B+ 树的所有的数据都会出现在叶子节点,在查询的时候,B+ 树查找效率更加稳定;
第二:在进行范围查询的时候,B+ 树效率更高,因为 B+ 树都在叶子节点存储,并且叶子节点是一个双向链表。
聚簇索引主要是指数据与索引放到一块,B+ 树的叶子节点保存了整行数据,有且只有一个,一般情况下主键在作为聚簇索引的;
非聚簇索引值的是数据与索引分开存储,B+ 树的叶子节点保存对应的主键,可以有多个,一般我们自己定义的索引都是非聚簇索引。
其实跟刚才介绍的聚簇索引和非聚簇索引是有关系的,回表的意思就是通过二级索引找到对应的主键值,然后再通过主键值找到聚集索引中所对应的整行数据,这个过程就是回表。
【备注:如果面试官直接问回表,则需要先介绍聚簇索引和非聚簇索引】
select 后面查询的字段都可以从这个索引的树中获取,这种情况一般可以说是用到了覆盖索引。
索引在使用的时候没有遵循最左匹配法则和模糊查询,如果%号在前面也会导致索引失效。
通常情况下,想要判断出这条 sql 是否有索引失效的情况,可以使用 explain 执行计划来分析。
这个在项目还是挺常见的,当然如果直说 sql 优化的话,我们会从这几方面考虑,比如:建表的时候、使用索引、sql 语句的编写、主从复制,读写分离,还有一个是如果量比较大的话,可以考虑分库分表。
ACID,分别指的是:原子性、一致性、隔离性、持久性;我举个例子:
A 向 B 转账 500,转账成功,A 扣除 500 元,B 增加 500 元,原子操作体现在要么都成功,要么都失败
在转账的过程中,数据要一致,A 扣除了 500,B 必须增加 500
在转账的过程中,隔离性体现在 A 像 B 转账,不能受其他事务干扰
在转账的过程中,持久性体现在事务提交后,要把数据持久化(可以说是落盘操作)
我们在项目开发中,多个事务并发进行是经常发生的,并发也是必然的,有可能导致一些问题
第一是脏读,当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是'脏数据',依据'脏数据'所做的操作可能是不正确的。
第二是不可重复读:比如在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
第三是幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
解决方案是对事务进行隔离。
MySQL 支持四种隔离级别,分别有:
第一个是,未提交读(read uncommitted)它解决不了刚才提出的所有问题,一般项目中也不用这个。第二个是读已提交(read committed)它能解决脏读的问题的,但是解决不了不可重复读和幻读。第三个是可重复读(repeatable read)它能解决脏读和不可重复读,但是解决不了幻读,这个也是 mysql 默认的隔离级别。第四个是串行化(serializable)它可以解决刚才提出来的所有问题,但是由于让是事务串行执行的,性能比较低。所以,我们一般使用的都是 mysql 默认的隔离级别:可重复读。
其中 redo log 日志记录的是数据页的物理变化,服务宕机可用来同步数据,而 undo log 不同,它主要记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据,比如我们删除一条数据的时候,就会在 undo log 日志文件中新增一条 delete 语句,如果发生回滚就执行逆操作;
redo log 保证了事务的持久性,undo log 保证了事务的原子性和一致性。
事务的隔离性是由锁和 mvcc 实现的。
其中 mvcc 的意思是多版本并发控制。
MySQL 主从复制的核心就是二进制日志 (DDL(数据定义语言)语句和 DML(数据操纵语言)语句),它的步骤是这样的:
第一:主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。
第二:从库读取主库的二进制日志文件 Binlog,写入到从库的中继日志 Relay Log。
第三:从库重做中继日志中的事件,将改变反映它自己的数据。
垂直分库:以表为依据,根据业务将不同表拆分到不同库中。
垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中。
水平分库:将一个库的数据拆分到多个库中。
水平分表:将一个表的数据拆分到多个表中 (可以在同一个库内)。
主键约束:主键列上没有任何两行具有相同值(即重复值),不允许空(NULL);
唯一性约束:保证一个字段或者一组字段里的数据都与表中其它行的对应数据不同。和主键约束不同,唯一性约束允许为 null,但是只能有一行;
唯一性索引:不允许具有索引值相同的行,从而禁止重复的索引和键值。
不是线程安全的。
当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
aop 是面向切面编程,在 spring 中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑封装起来。一般比如可以做为公共日志保存,事务处理等。
spring 实现的事务本质就是 aop 完成,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
第一个,如果方法上异常捕获处理(try catch),自己处理了异常,没有抛出,就会导致事务失效,所以一般处理了异常以后,别忘了抛出去就行了;
第二个,如果方法抛出检查异常,如果报错也会导致事务失效,最后在 spring 事务的注解上,就是@Transactional 上配置 rollbackFor 属性为 Exception,这样别管是什么异常,都会回滚事务。
首先会通过一个非常重要的类,叫做 BeanDefinition 获取 bean 的定义信息,这里面就封装了 bean 的所有信息,比如,类的全路径,是否是延迟加载,是否是单例等等这些信息。
在创建 bean 的时候,第一步是调用构造函数实例化 bean
第二步是 bean 的依赖注入,比如一些 set 方法注入,像平时开发用的@Autowire 都是这一步完成
第三步是处理 Aware 接口,如果某一个 bean 实现了 Aware 接口就会重写方法执行
第四步是 bean 的后置处理器 BeanPostProcessor,这个是前置处理器
第五步是初始化方法,比如实现了接口 InitializingBean 或者自定义了方法 init-method 标签或@PostContruct
第六步是执行了 bean 的后置处理器 BeanPostProcessor,主要是对 bean 进行增强,有可能在这里产生代理对象
最后一步是销毁 bean
循环依赖:循环依赖其实就是循环引用,也就是两个或两个以上的 bean 互相持有对方,最终形成闭环。比如 A 依赖于 B,B 依赖于 A。
循环依赖在 spring 中是允许存在,spring 框架依据三级缓存已经解决了大部分的循环依赖。
①一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的 bean 对象
②二级缓存:缓存早期的 bean 对象(生命周期还没走完)
③三级缓存:缓存的是 ObjectFactory,表示对象工厂,用来创建某个对象的
由于 bean 的生命周期中构造函数是第一个执行的,spring 框架并不能解决构造函数的的依赖注入,可以使用@Lazy 懒加载,什么时候需要对象再进行 bean 对象的创建。
1、用户发送出请求到前端控制器 DispatcherServlet,这是一个调度中心
2、DispatcherServlet 收到请求调用 HandlerMapping(处理器映射器)。
3、HandlerMapping 找到具体的处理器 (可查找 xml 配置或注解配置),生成处理器对象及处理器拦截器 (如果有),再一起返回给 DispatcherServlet。
4、DispatcherServlet 调用 HandlerAdapter(处理器适配器)。
5、HandlerAdapter 经过适配调用具体的处理器(Handler/Controller)。
6、DispatcherServlet 响应用户。
在 Spring Boot 项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:
其中@EnableAutoConfiguration是实现自动化配置的核心注解。
该注解通过@Import注解导入对应的配置选择器。关键的是内部就是读取了该项目和该项目引用的 Jar 包的的 classpath 路径下 META-INF/spring.factories 文件中的所配置的类的全类名。
在这些配置类中所定义的 Bean 会根据条件注解所指定的条件来决定是否需要将其导入到 Spring 容器中。
一般条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的 class 文件,如果有则加载该类,把这个配置类的所有的 Bean 放入 spring 容器中使用。
第一类是:声明 bean,有@Component、@Service、@Repository、@Controller、@RestController(@RestController=@Controller+@ResponseBody)
第二类是:依赖注入相关的,有@Autowired、@Qualifier、@Resourse
第三类是:设置作用域 @Scope
第四类是:spring 配置相关的,比如@Configuration,@ComponentScan 和 @Bean
第五类是:跟 aop 相关做增强的注解 @Aspect,@Before,@After,@Around,@Pointcut
有@RequestMapping:用于映射请求路径;
@RequestBody:注解实现接收 http 请求的 json 数据,将 json 转换为 java 对象;
@RequestParam:url 地址传参 参数名称=参数值;
@PathViriable:路径参数 http://localhost:8080/dish/zhangsan;
@ResponseBody:注解实现将 controller 方法返回对象转化为 json 对象响应给客户端。
@RequestHeader:获取指定的请求头数据,还有像@PostMapping、@GetMapping 这些。
Spring Boot 的核心注解是@SpringBootApplication , 他由几个注解组成 :

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online