前言
随着互联网行业的不断发展,Java作为一种广泛应用的编程语言,依然在企业级应用开发中占据着重要地位。Java开发人员的需求量始终保持稳定,无论是前端、后端开发,还是全栈开发,Java程序员在各个领域中都扮演着重要角色。因此,Java程序员的面试成为了求职过程中至关重要的一环。
在面试过程中,考察技术能力的同时,也会考察编程思想、架构设计、解决问题的能力等多个方面。Java程序员面试问题范围广泛,涉及到Java基础、框架、并发、多线程、数据库、分布式等各个技术领域。本文将详细解析一些常见的面试问题,并给出解答与思路,帮助你更好地准备Java程序员面试。
一、基础概念
1.1 什么是 Java ?
Java 是一种广泛使用的计算机编程语言,由 Sun Microsystems 公司于 1995 年推出。它是一种面向对象的编程语言,可以编写各种类型的应用程序,包括 Web 应用程序、桌面应用程序、移动应用程序和企业应用程序等。
Java 之父:詹姆斯·高斯林(James Gosling)
1.2 Java 语言有哪些特点?
Java 语言有以下几个主要特点:
- 简单易学:Java 语言的语法比较简单,易于学习和理解,有利于快速开发。
- 面向对象:Java 语言是一种面向对象的编程语言,它支持封装、继承和多态等面向对象的特性。
- 跨平台性:Java 语言具有跨平台性,可以在多种不同的计算机系统上运行,而不需要针对不同平台编写不同的代码,这是因为 Java 程序是通过编译器将源代码编译成字节码,然后在 JVM 上运行。
- 安全性:Java 语言具有较高的安全性,它通过类加载器、字节码校验器、安全管理器等多种机制来保证程序的安全性。
- 高性能:Java 语言具有较高的性能,虽然它是一种解释型语言,但通过 JIT 编译器和即时编译技术等优化手段,可以实现接近于本地代码的执行效率。
- 大量的类库和工具:Java 语言拥有丰富的类库和工具,包括标准类库、第三方类库和开发工具等,开发人员可以使用这些类库和工具来快速开发应用程序。
- 开放性:Java 语言具有开放性,它是一种开源的编程语言,拥有庞大的开发者社区和丰富的资源。
1.3 JDK 与 JRE 有什么区别?
- JDK:Java 开发工具包(Java Development Kit),提供了 Java 的开发环境和运行环境。
- JRE:Java 运行环境(Java Runtime Environment),提供了 Java 运行所需的环境。
- JDK 包含了 JRE。如果只运行 Java 程序,安装 JRE 即可。要编写 Java 程序需安装 JDK.
二、Java基础语法
2.1 Java的基本数据类型有哪些?
Java的基本数据类型分为两类:原始数据类型和引用数据类型。原始数据类型包括:
- 整数类型:byte(1字节),short(2字节),int(4字节),long(8字节)
- 浮点类型:float(4字节),double(8字节)
- 字符类型:char(2字节)
- 布尔类型:boolean(1字节,值为true或false)
2.2 Java中final, finally, finalize的区别是什么?
- final:用于修饰类、方法和变量。被修饰的类不可被继承,方法不可被重写,变量的值不可更改。
- finally:用于异常处理,指的是无论异常是否发生,finally块中的代码都会执行。通常用于资源的清理工作,如关闭文件、数据库连接等。
- finalize:是Object类中的一个方法,垃圾回收器在对象被回收之前调用此方法。通常在此方法中进行一些清理操作,如释放资源等。
2.3 Java中的对象和类的区别是什么?
- 类(Class) :类是对象的模板或蓝图,它定义了对象的属性和方法。
- 对象(Object) :对象是类的实例,是在程序运行时由类创建的实体。每个对象都拥有类定义的属性和方法。
2.4 什么是包装类?为什么需要包装类?
Java 中有 8 个基本类型,分别对应的 8 个包装类
- byte -- Byte
- boolean -- Boolean
- short -- Short
- char -- Character
- int -- Integer
- long -- Long
- float -- Float
- double -- Double
为什么需要包装类:
- 基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持
- 不符合面向对象思维
- 包装类提供很多方法,方便使用,如 Integer 类 toHexString(int i)、parseInt(String s) 方法等等
2.5 equals 和 == 的区别
在 Java 中,==
运算符用于比较两个对象的引用是否指向同一内存地址,而 equals()
方法用于比较对象的实际内容。例如,对于字符串比较,若直接使用 ==
,可能因字符串常量池的存在导致意外结果。正确的做法是使用 equals()
方法,以确保比较的是字符串的值。需要注意的是,自定义类如果没有重写 equals()
方法,则默认继承自 Object
类的实现,即仍然基于引用地址比较。因此,在需要比较对象逻辑相等性时,应正确重写 equals()
和 hashCode()
方法,以保证一致性。
2.6 简述 Java 访问修饰符
default: 默认访问修饰符,在同一包内可见 private: 在同一类内可见,不能修饰类 protected : 对同一包内的类和所有子类可见,不能修饰类 public: 对所有类可见
2.7 简述自动装箱拆箱
对于 Java 基本数据类型,均对应一个包装类。
装箱就是自动将基本数据类型转换为包装器类型,如 int->Integer
拆箱就是自动将包装器类型转换为基本数据类型,如 Integer->int
三、Java面向对象
3.1 Java的继承和多态机制是什么?
- 继承 :Java允许子类继承父类的属性和方法,从而实现代码的复用。使用关键字
extends
声明继承关系。 - 多态:多态是指同一方法调用可以根据不同对象的不同实现产生不同的行为。Java实现多态有两种方式:方法重载(编译时多态)和方法重写(运行时多态)。
3.2 抽象类与接口的区别?
- 抽象类:可以包含抽象方法和已实现的方法。抽象类可以有构造方法,但不能被实例化。子类继承抽象类时,必须实现其中的抽象方法(除非子类也是抽象类)。
- 接口:接口只包含抽象方法(Java 8以后也可以有默认方法和静态方法)。类实现接口时,必须实现接口中的所有方法。接口支持多继承。
3.3 创建对象有哪些方式
有五种创建对象的方式
- 1、new关键字
ini
Person p1 = new Person();
- 2.Class.newInstance
ini
Person p1 = Person.class.newInstance();
- 3.Constructor.newInstance
ini
Constructor<Person> constructor = Person.class.getConstructor();
Person p1 = constructor.newInstance();
- 4.clone
ini
Person p1 = new Person();
Person p2 = p1.clone();
- 5.反序列化
ini
Person p1 = new Person();
byte[] bytes = SerializationUtils.serialize(p1);
Person p2 = (Person)SerializationUtils.deserialize(bytes);
3.4 简述 Object 类常用方法
- hashCode:通过对象计算出的散列码。用于 map 型或 equals 方法。需要保证同一个对象多次调用该方法,总返回相同的整型值。
- equals:判断两个对象是否一致。需保证 equals 方法相同对应的对象 hashCode 也相同。
- toString: 用字符串表示该对象
- clone:深拷贝一个对象
3.5 一个对象的内存布局是怎么样的?
-
1.对象头 : 对象头又分为 MarkWord 和 Class Pointer 两部分。
- MarkWord:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位,gc记录信息等等。
- ClassPointer:用来指向对象对应的 Class 对象(其对应的元数据对象)的内存地址。在 32 位系统占 4 字节,在 64 位系统中占 8 字节。
-
2.Length:只在数组对象中存在,用来记录数组的长度,占用 4 字节
-
3.Instance data: 对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的)
-
4.Padding :Java 对象占用空间是 8 字节对齐的,即所有 Java 对象占用 bytes 数必须是 8 的倍数,是因为当我们从磁盘中取一个数据时,不会说我想取一个字节就是一个字节,都是按照一块儿一块儿来取的,这一块大小是 8 个字节,所以为了完整,padding 的作用就是补充字节,保证对象是 8 字节的整数倍。
四、Java集合框架
4.1 Java集合框架的主要接口有哪些?
Java集合框架主要分为两大类:Collection 和Map。
- Collection:List(如ArrayList,LinkedList),Set(如HashSet,TreeSet),Queue(如LinkedList,PriorityQueue)
- Map:HashMap,TreeMap,LinkedHashMap,Hashtable
4.2 ArrayList和LinkedList的区别?
- ArrayList:基于动态数组实现,适合随机访问,查询速度快,但在数组中间插入和删除元素时性能较差(O(n))。
- LinkedList:基于双向链表实现,插入和删除操作效率高,适合做队列和栈,但查询效率较低(O(n))。
4.3 hashMap 1.7 和 hashMap 1.8 的区别?
重点区别
不同点 | hashMap 1.7 | hashMap 1.8 |
---|---|---|
数据结构 | 数组+链表 | 数组+链表+红黑树 |
插入数据的方式 | 头插法 | 尾插法 |
hash 值计算方式 | 9次扰动处理(4次位运算+5次异或) | 2次扰动处理(1次位运算+1次异或) |
扩容策略 | 插入前扩容 | 插入后扩容 |
4.4 HashMap的底层数据结构是什么?
JDK 7 中,HashMap 由"数组+链表"组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
在 JDK 8 中,HashMap 由"数组+链表+红黑树"组成。链表过长,会严重影响 HashMap 的性能,而红黑树搜索的时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK 8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:
- 当链表超过 8 且数据总量超过 64 时会转红黑树。
- 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。
链表长度超过 8 体现在 putVal 方法中的这段代码:
scss
//链表长度大于8转换为红黑树进行处理
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
table 长度为 64 体现在 treeifyBin 方法中的这段代码::
java
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
}
MIN_TREEIFY_CAPACITY 的值正好为 64。
arduino
static final int MIN_TREEIFY_CAPACITY = 64;
JDK 8 中 HashMap 的结构示意图:
4.4 Collection 和 Collections 有什么区别?
(1)Collection是最基本的集合接口,Collection派生了两个子接口list和set,分别定义了两种不同的存储方式。
(2)Collections是一个包装类,它包含各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等)。
此类不能实例化,就像一个工具类,服务于Collection框架。
五、异常
5.1 Java异常类层次结构?
-
Throwable 是 Java 语言中所有错误与异常的超类。
- Error 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。
- Exception 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
- 运行时异常
都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
- 非运行时异常 (编译异常)
是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
5.2 常见的异常类有哪些?
NullPointerException:空指针异常; SQLException:数据库相关的异常; IndexOutOfBoundsException:数组下角标越界异常; FileNotFoundException:打开文件失败时抛出; IOException:当发生某种IO异常时抛出; ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出此异常; NoSuchMethodException:无法找到某一方法时,抛出; ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常; NumberFormatException:当试图将字符串转换成数字时,失败了,抛出; IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数。 ArithmeticException当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。
5.3 可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)区别?
- 可查异常(编译器要求必须处置的异常):
正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。
除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
- 不可查异常(编译器不要求强制处置的异常)
包括运行时异常(RuntimeException与其子类)和错误(Error)。
5.4 throw和throws的区别?
- 异常的申明(throws)
在Java中,当前执行的语句必属于某个方法,Java解释器调用main方法执行开始执行程序。若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。 在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。如下所示:
csharp
public static void method() throws IOException, FileNotFoundException{
//something statements
}
- 异常的抛出(throw)
如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它,这就是抛出异常。如下所示:
arduino
public static double method(int value) {
if(value == 0) {
throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常
}
return 5.0 / value;
}
六、并发与多线程
6.1 Java中线程的创建方式有哪些?
Java中创建线程的方式有两种:
- 继承Thread类:通过继承Thread类,并重写其run方法来创建线程。
- 实现Runnable接口:实现Runnable接口,并将其传递给Thread对象,通过调用Thread的start方法启动线程。
6.2 什么是线程池?为什么使用线程池?
线程池是一个保存线程的集合,它可以有效地管理并重用线程。使用线程池的好处包括:
- 减少资源消耗:线程池中的线程可以重复使用,避免了频繁创建和销毁线程的开销。
- 提高响应速度:由于线程池中的线程是预先创建好的,因此可以快速响应任务的请求。
- 方便管理:线程池提供了任务队列和线程池大小的控制,可以有效避免资源的浪费。
6.3 什么是死锁,如何避免死锁?
死锁是指两个或多个线程在执行过程中因争夺资源而造成的相互等待的状态,导致程序无法继续执行。死锁的四个必要条件是:互斥条件、请求与保持条件、不剥夺条件、循环等待条件。 避免死锁的方法:
- 避免嵌套锁:尽量避免多个锁的嵌套。
- 使用定时锁:设置线程获取锁的超时时间,避免死锁的发生。
- 锁的顺序:保证所有线程按照相同的顺序获取锁,避免循环等待的情况。
6.4 线程池的执行流程?
-
判断线程池中的线程数是否大于设置的核心线程数
-
如果小于 ,就创建一个核心线程来执行任务
-
如果大于 ,就会判断缓冲队列是否满了
-
如果没有满 ,则放入队列,等待线程空闲时执行任务
-
如果队列已经满了 ,则判断是否达到了线程池设置的最大线程数
- 如果没有达到 ,就创建新线程来执行任务
- 如果已经达到了 最大线程数,则执行指定的拒绝策略
-
-
6.5 聊聊 ThreadLocal 吧
- ThreadLocal其实就是线程本地变量 ,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离。
- ThreadLocal 有一个静态内部类 ThreadLocalMap ,ThreadLocalMap 又包含了一个 Entry 数组,Entry 本身是一个弱引用 ,他的 key 是指向 ThreadLocal 的弱引用,弱引用的目的是为了防止内存泄露,如果是强引用那么除非线程结束,否则无法终止,可能会有内存泄漏的风险。
- 但是这样还是会存在内存泄露的问题,假如 key 和 ThreadLocal 对象被回收之后,entry 中就存在 key 为 null ,但是 value 有值的 entry 对象,但是永远没办法被访问到,同样除非线程结束运行。解决方法就是调用 remove 方法删除 entry 对象。
七、Spring框架
7.1 Spring框架的核心特性是什么?
- 依赖注入(DI) :通过Spring容器将对象的依赖关系通过配置注入到类中,解耦了类之间的关系,提高了系统的可维护性。
- 面向切面编程(AOP) :Spring通过AOP对业务逻辑进行增强,支持事务管理、安全控制等方面的功能。
- Spring MVC:提供了基于模型-视图-控制器(MVC)设计模式的Web框架,用于开发Web应用。
- 事务管理:Spring提供了统一的事务管理接口,支持声明式事务管理和编程式事务管理。
7.2 Spring Boot是什么?它和Spring有什么区别?
- Spring Boot:是Spring的一个子项目,旨在简化Spring应用的开发过程。它通过自动配置和约定优于配置的方式,让开发者可以快速启动并运行Spring应用。
- 区别:Spring框架需要大量的配置,而Spring Boot通过自动配置减少了大量的手动配置,使得开发者能够专注于业务逻辑。
7.3 Spring中都应用了哪些设计模式
1、简单工厂模式
简单工厂模式的本质就是一个工厂类根据传入的参数,动态的决定实例化哪个类。
Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象。
2、工厂方法模式
应用程序将对象的创建及初始化职责交给工厂对象,工厂Bean。
定义工厂方法,然后通过config.xml配置文件,将其纳入Spring容器来管理,需要通过factory-method指定静态方法名称。
3、单例模式
Spring用的是双重判断加锁的单例模式,通过getSingleton方法从singletonObjects中获取bean。
kotlin
/**
* Return the (raw) singleton object registered under the given name.
* <p>Checks already instantiated singletons and also allows for an early
* reference to a currently created singleton (resolving a circular reference).
* @param beanName the name of the bean to look for
* @param allowEarlyReference whether early references should be created or not
* @return the registered singleton object, or {@code null} if none found
*/
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
4、代理模式
Spring的AOP中,使用的Advice(通知)来增强被代理类的功能。Spring实现AOP功能的原理就是代理模式(① JDK动态代理,② CGLIB字节码生成技术代理。)对类进行方法级别的切面增强。
5、装饰器模式
装饰器模式:动态的给一个对象添加一些额外的功能。
Spring的ApplicationContext中配置所有的DataSource。这些DataSource可能是不同的数据库,然后SessionFactory根据用户的每次请求,将DataSource设置成不同的数据源,以达到切换数据源的目的。
在Spring中有两种表现:
一种是类名中含有Wrapper,另一种是类名中含有Decorator。
6、观察者模式
定义对象间的一对多的关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
Spring中观察者模式一般用在listener的实现。
7、策略模式
策略模式是行为性模式,调用不同的方法,适应行为的变化 ,强调父类的调用子类的特性 。
getHandler是HandlerMapping接口中的唯一方法,用于根据请求找到匹配的处理器。
8、模板方法模式
Spring JdbcTemplate的query方法总体结构是一个模板方法+回调函数,query方法中调用的execute()是一个模板方法,而预期的回调doInStatement(Statement state)方法也是一个模板方法。
八、Mysql
8.1 详细说一下一条 MySQL 语句执行的步骤
Server 层按顺序执行 SQL 的步骤为:
- 客户端请求 -> 连接器(验证用户身份,给予权限)
- 查询缓存(存在缓存则直接返回,不存在则执行后续操作)
- 分析器(对 SQL 进行词法分析和语法分析操作)
- 优化器(主要对执行的 SQL 优化选择最优的执行方案方法)
- 执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口)-> 去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果)
8.2 什么是事务?MySQL中的事务特性是什么?
事务是指一组作为单个单位执行的操作,要么全部执行,要么全部不执行。MySQL中的事务具有四个特性(ACID):
- 原子性(Atomicity) :事务中的操作要么全部成功,要么全部失败。
- 一致性(Consistency) :事务执行前后,数据库的状态必须是一致的。
- 隔离性(Isolation) :多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
- 持久性(Durability) :一旦事务提交,其对数据库的修改应该永久保存在数据库中。
8.3 MySQL中的JOIN操作有哪些类型?
MySQL中的JOIN操作有四种:
- INNER JOIN:返回两个表中匹配的记录。
- LEFT JOIN:返回左表中的所有记录及右表中匹配的记录,右表没有匹配记录时显示NULL。
- RIGHT JOIN:返回右表中的所有记录及左表中匹配的记录,左表没有匹配记录时显示NULL。
- FULL OUTER JOIN:返回左右表中所有的记录,当没有匹配记录时显示NULL。
8.4 MySQL中的索引是什么?它的作用是什么?
索引是MySQL中用于加速查询操作的数据结构。它通过为表中的一列或多列创建一个快速查找的结构,使得数据库在查询时可以更加高效地定位数据。常见的索引类型有:
- 普通索引(Index) :简单的索引,不限制数据的唯一性。
- 唯一索引(Unique Index) :要求索引列的值唯一。
- 主键索引(Primary Key Index) :主键列的值必须唯一且不能为空。
- 全文索引(Fulltext Index) :用于对文本进行全文搜索。
8.5 MySQL索引有哪些类型
数据结构维度
- B+树索引:所有数据存储在叶子节点,复杂度为
O(logn)
,适合范围查询。 - 哈希索引: 适合等值查询,检索效率高,一次到位。
- 全文索引:
MyISAM
和InnoDB
中都支持使用全文索引,一般在文本类型char,text,varchar
类型上创建。 R-Tree
索引: 用来对GIS
数据类型创建SPATIAL
索引
物理存储维度
- 聚集索引:聚集索引就是以主键创建的索引,在叶子节点存储的是表中的数据。(
Innodb
存储引擎) - 非聚集索引:非聚集索引就是以非主键创建的索引,在叶子节点存储的是主键和索引列。(
Innodb
存储引擎)
逻辑维度
- 主键索引:一种特殊的唯一索引,不允许有空值。
- 普通索引:
MySQL中
基本索引类型,允许空值和重复值。 - 联合索引:多个字段创建的索引,使用时遵循最左前缀原则。
- 唯一索引:索引列中的值必须是唯一的,但是允许为空值。
- 空间索引:
MySQL5.7
之后支持空间索引,在空间索引这方面遵循OpenGIS
几何数据模型规则。
8.6 索引有哪些优缺点?
优点:
- 索引可以加快数据查询速度,减少查询时间
- 唯一索引可以保证数据库表中每一行的数据的唯一性
缺点:
- 创建索引和维护索引要耗费时间
- 索引需要占物理空间,除了数据表占用数据空间之外,每一个索引还要占用一定的物理空间
- 以表中的数据进行增、删、改的时候,索引也要动态的维护。
8.7 索引什么时候会失效?
- 查询条件包含
or
,可能导致索引失效 - 如果字段类型是字符串,
where
时一定用引号括起来,否则索引失效 like
通配符可能导致索引失效。- 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。
- 在索引列上使用 mysql 的内置函数,索引失效。
- 对索引列运算(如,
+、-、*、/
),索引失效。 - 索引字段上使用
(!= 或者 < >,not in)
时,可能会导致索引失效。 - 索引字段上使用
is null, is not null
,可能导致索引失效。 - 左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。
- mysql 估计使用全表扫描要比使用索引快,则不使用索引。
8.8 哪些场景不适合建立索引?
- 数据量少的表,不适合加索引
- 更新比较频繁的也不适合加索引
- 区分度低的字段不适合加索引(如性别)
where、group by、order by
等后面没有使用到的字段,不需要建立索引- 已经有冗余的索引的情况(比如已经有
a,b
的联合索引,不需要再单独建立a
索引)
8.9 一次B+树索引树查找过程
假设有以下表结构,并且初始化了这几条数据
sql
CREATE TABLE `employee` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`date` datetime DEFAULT NULL,
`sex` int(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_age` (`age`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into employee values(100,'小伦',43,'2021-01-20','0');
insert into employee values(200,'俊杰',48,'2021-01-21','0');
insert into employee values(300,'紫琪',36,'2020-01-21','1');
insert into employee values(400,'立红',32,'2020-01-21','0');
insert into employee values(500,'易迅',37,'2020-01-21','1');
insert into employee values(600,'小军',49,'2021-01-21','0');
insert into employee values(700,'小燕',28,'2021-01-21','1');
执行这条查询SQL,需要执行几次的树搜索操作?可以画下对应的索引树结构图~
csharp
select * from Temployee where age=32;
其实这个,这个大家可以先画出idx_age
普通索引的索引结构图,大概如下:
再画出id
主键索引,我们先画出聚族索引结构图,如下:
这条 SQL 查询语句执行大概流程是这样的:
- 搜索
idx_age
索引树,将磁盘块1
加载到内存,由于32<43
,搜索左路分支,到磁盘寻址磁盘块2
。 - 将
磁盘块2
加载到内存中,由于32<36
,搜索左路分支,到磁盘寻址磁盘块4
。 - 将
磁盘块4
加载到内存中,在内存继续遍历,找到age=32
的记录,取得id = 400
. - 拿到
id=400
后,回到id主键索引树
。 - 搜索
id主键索引树
,将磁盘块1
加载到内存,因为300<400<500
,所以在选择中间分支,到磁盘寻址磁盘块3
。 - 虽然在
磁盘块3
,找到了id=400,但是它不是叶子节点,所以会继续往下找。到磁盘寻址磁盘块8
。 - 将
磁盘块8
加载内存,在内存遍历,找到id=400
的记录,拿到R4
这一行的数据,好的,大功告成。
8.10 什么是回表?如何减少回表?
当查询的数据在索引树中,找不到的时候,需要回到主键索引树 中去获取,这个过程叫做回表。
比如在第6小节中,使用的查询SQL
csharp
select * from Temployee where age=32;
需要查询所有列的数据,idx_age
普通索引不能满足,需要拿到主键id的值后,再回到id
主键索引查找获取,这个过程就是回表。
8.11 什么是覆盖索引?
如果我们查询SQL的select *
修改为 select id, age
的话,其实是不需要回表 的。因为id
和age
的值,都在idx_age
索引树的叶子节点上,这就涉及到覆盖索引的知识点了。
覆盖索引是
select
的数据列只用从索引中就能够取得,不必回表,换句话说,查询列要被所建的索引覆盖。
8.12 聊聊索引的最左前缀原则
索引的最左前缀原则,可以是联合索引的最左N个字段 。比如你建立一个组合索引(a,b,c)
,其实可以相当于建了(a),(a,b),(a,b,c)
三个索引,大大提高了索引复用能力。
当然,最左前缀也可以是字符串索引的最左M个字符。 。比如,你的普通索引树是酱紫:
这个SQL: select * from employee where name like '小%' order by age desc;
也是命中索引的。
结束语:技术征途,永不止步
本文仅揭开了Java面试宝库的一角。技术深似海,每一次面试都是对知识体系的检验与升级。真正的强者,懂得将面试挑战转化为持续精进的动力。想要解锁更多独家面试真题解析、高频考点深度剖析与实战避坑指南?
欢迎持续关注「技海拾贝」公众号! 我们将在后续系列文章中,深入探讨分布式、JVM调优、系统设计等进阶领域,助你构建坚不可摧的技术壁垒,从容征服每一场技术面试!