面试题笔记

7 篇文章 0 订阅
2 篇文章 0 订阅

基础部分

Java概述

JDK、JRE、JVM之间的区别

整体来说: JDK包含了JREJRE中包含JVM

JVM: 操作系统实际与Java进行接触的第一层,是一个虚拟机,Java程序是运行在JVM上的,因此可以实现跨平台,JVM是在JRE中的

JRE: JRE大部分是由C/C++编写的,其中包含Java程序编译时所需的核心类库JVM,其中,核心类库中主要是java.lang包,包含运行Java程序必不可少的系统类,比如基本数据结构、基本数学函数、字符串处理、线程、异常处理类等,系统默认加载这个包

JDK: JDK除了包含JRE外,还包含其余的东西,比如编译Java代码的工具、监控JVM的工具、打包工具(jre.exe)

Java平台的跨平台原理是什么

跨平台是指Java程序在编译通过后,可以在多个系统平台运行

Java程序通过Java虚拟机在系统平台上运行,只要该系统可以安装Java虚拟机,就可以运行Java程序

字节码是什么?采用字节码的最大好处是什么?
  • 字节码: Java程序经过Java虚拟机编译后产生的文件,不面向任何其他的处理器,只面向虚拟机
  • 使用字节码的好处: 通过使用字节码,一定程度上解决了传统解释型语言执行效率低的问题,同时也保留了解释型语言可移植的特点,所以Java语言运行效率比较高;又因为字节码文件不针对任何特定的处理器,所以,Java语言可以在多种不同的机器上运行
  • Java源代码 -> 编译器 -> jvm可执行的Java字节码 -> jvm -> jvm中解释器 -> 机器可执行的二进制机器码 -> 程序执行

Java和C++的区别
  • 都是面向对象的语言,都支持封装、继承和多态
  • Java没有指针来访问内存,程序内存更安全
  • Java的类是单继承,接口可以多继承,C++可以多重继承
  • Java有自动内存管理机制,不需要程序员手动释放无用内存

Oracle JDK和OpenJDK的区别

  • 发布周期不同,Oracle JDK三年发布一次,OpenJDK三个月发布一次
  • OpenJDK完全开源、OracleJDK不完全开源
  • OracleJDKOpenJDK更稳定,OracleJDKOpenJDK代码几乎相同,OracleJDK有更多的类,整体来说,想要更稳定,总遇到莫名其妙的问题,用OracleJDK
  • OracleJDK在相应性和JVM性能方面比OpenJDK更好
  • 获取许可方式不同

基础语法

Java有哪些数据类型
  • 基本数据类型
    • 整数类型(byte、short、int、long
    • 浮点型(float、double
    • 字符型(char
    • 布尔型(boolean
  • 引用数据类型
    • 类(class
    • 接口
    • 数组
    • 字符串
switch的支持问题

jdk5之前,switch只能支持byte、short、char、int

jdk5之后,Java有了枚举类型,switch中也可以是枚举类型

jdk7之后,switch中也可以是字符串

在现在任何版本中,switch都不支持long

Java中四舍五入的原理

四舍五入的原理为在参数上加0.5然后向下取整

short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗

前者有错,因为s1 + 1,1是int类型,整个运算的结果就是int类型,需要强转才能赋值给short类型

s1 += 1 实际上是s1 = (short(s1 + 1)),其中包含一个隐式的强制类型转换

Java使用什么编码格式

Java使用Unicode编码标准,所以在各个平台都可以使用


访问修饰符public、private、protected以及默认(不写)时的区别

Java中可以使用访问修饰符来保护对类、变量、方法和构造方法的访问

  • public: 对所有类可见,使用对象: 类、接口、方法、变量
  • pivate: 在同一类内可见,使用对象: 变量、方法(不能修饰类(外部类))
  • protected: 对同一包内的类和所有子类可见,使用对象: 变量、方法(不能修饰类(外部类))
  • default: 在同一包内可见,不使用任何修饰符,使用对象: 类、接口、变量、方法
&和&&的区别
  • &运算符有两个用法: 按位与、逻辑与
  • &&运算符是短路与运算,逻辑与和短路与虽然都要求运算符左右两边的布尔值都是true整个表达式才是true,但是&&如果左边是false就不再关注右边的布尔值了
final finally finalize区别
  • final 可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值
  • finally: 一般用于try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法写在finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码
  • finalize: 是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc()的时候,由垃圾回收器调用finalize()回收垃圾,一个对象是否可回收的最后判断,是一个对象被杀死前的最后被调用的方法,但是人为调用该方法并不能杀死对象,该方法是一个被动调用的过程
this关键字的用法
  • this是自身的一个对象,代表对象本身,可以理解为指向对象本身的一个指针
  • this的用法大概有三种:
    • 普通的直接引用,this相当与是指向当前对象本身
    • 形参与成员名字重名,用this来区分
    • 引用本类的构造函数
super关键字的用法
  • super可以理解为是指向自己父类的一个指针,这个指针指的是离自己最近的一个父类
  • super的用法大概也是三种:
    • 普通的直接引用,调用父类的成员
    • 子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分
    • 引用父类的构造函数
this和super的区别
  • super: 它引用当前对象的直接父级中的成员,用来直接访问父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如: super.变量名 super.成员函数名
  • this: 它代表当前对象名,在函数中易产生二义性之外,应使用this来指明当前对象,如果函数的形参与类中的成员数据同名,这是需要用this来指明成员变量名
  • super()this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类中调用本类的其他构造方法
  • super()this()均需要放在构造器的第一行
  • 尽管可以使用this来调用一个构造器,但不能调用两个,thissuper不能同时出现在一个构造函数内,因为this必然会调用其他的构造函数,其他的构造函数中也会有super语句的存在,所以在同一个构造器中有相同的语句,就失去了语句的意义,同时编译也不会通过
  • this()super()都指的是对象,所以,都不可以在static环境中使用
  • 本质上讲,this是一个指向本对象的指针,然而super是一个Java关键字
static存在的意义
  • static存在的主要意义是在于创建独立于具体对象的域变量或者方法,此时即使不创建对象也能使用成员变量和方法
  • static关键字可以用来生成静态代码块来优化程序性能,static块可以置于类中的任意地方,类中可以有多个static块,在类被初次加载时,会按照static块的顺序来执行每个static块,而且只会执行一次
为什么说static块可以优化程序性能?

因为它的特性是只会在类加载的时候执行一次,因此讲一些只需要执行一次的初始化操作都放在static块中

static的特点
  • static所修饰的变量或者方法是独立于该类的任何对象的,这些变量和方法不属于任何一个实例对象,被类的所有实例对象共享
  • 该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并初始化,注意这是第一次用就要初始化,后面根据需要是可以再次被赋值的
  • static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配,赋值的话是可以被任意赋值的
  • static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即使没有创建对象,也可以去访问
  • 静态只能访问静态、非静态可以同时访问静态和非静态
static的应用场景
  • 修饰成员变量
  • 修饰成员方法
  • 静态代码块
  • 修饰类,只能修饰内部类
  • 静态导包

面向对象

面向对象和面向过程的区别

面向过程:

  • 优点: 性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源,比如: 单片机、嵌入式开发、Linux/Unix开发
  • 缺点: 没有面向对象易维护、易复用、易扩展

面向对象:

  • 优点: 易维护、易复用、易扩展,由于面向对象有封装、继承、多态的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
  • 缺点: 性能比面向过程低
面向对象的特性
  • 抽象: 抽象是将一类对象的共同特性总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,不关注这些行为的细节是什么
  • 封装: 把一个对象的属性私有化,同时提供一些可以被外界访问的属性和方法,如果属性不想被外界访问,我们可以不给外界提供方法的访问,但是如果一个类没有提供给外界访问方法,那么这个类也没有什么意义了
  • 继承: 使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或者新的功能,也可以用父类的功能,但是不能选择性地继承父类 ,简单说就是对已经存在的方法的复用
    • 子类拥有父类非private的属性和方法
    • 子类可以拥有自己的属性和方法
    • 子类可以用自己的方式实现父类的方法
  • 多态: 父类或者接口定义的引用变量可以指向子类或具体实现类的实例对象,提高了程序的扩展性,实现多态的两种形式:
    • 继承: 多个子类对同一个方法进行重写
    • 接口: 实现接口并覆盖接口中的同一个方法
什么是多态机制

多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态

多态分为编译时多态和运行时多态,其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会编程两个不同的函数,在运行时谈不上多态。运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性

多态的实现

多态实现的三个必要条件:

  • 继承: 在多态中必须存在有继承关系的子类和父类
  • 重写: 子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法
  • 向上转型: 在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法

对于Java来说,多态的实现机制遵循一个原则: 当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法


抽象类和接口的对比
  • 抽象类是用来捕捉子类的通用特性的,接口是抽象方法的集合
  • 抽象类是对类的抽象,是一种设计模板,接口是行为的抽象,是一种行为的规范

相同点:

  • 接口和抽象类都不能实例化
  • 都位于继承的顶端,用于被其他实现或继承
  • 都包含抽象方法,其子类都必须复写这些抽象方法

不同点:

  • 抽象类使用abstract关键字声明,接口使用interface关键字声明
  • 抽象类子类使用extends关键字来继承抽象类,如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现,接口子类使用implements关键字来实现,它需要提供接口中的所有声明的方法的实现
  • 抽象类可以有构造器,接口不能有构造方法
  • 抽象类中的方法可以是任意访问修饰符,接口方法默认修饰符是public,并且不允许定义为private或者protected
  • 一个类最多只能继承一个抽象类,一个类可以实现多个接口
  • 抽象类的字段声明可以是任意的,接口的字段默认是static和final

注:

jdk8之后,接口可以使用默认方法或者静态方法,接口可以提供默认的方法实现,不用强制子类实现

接口和抽象类的选择原则
  • 行为模式应该通过接口而不是抽象类来定义,通常优先选择接口,尽量少用抽象类
  • 选择抽象类的时候通常是如下情况,都需要定义子类的行为,又要为子类提供通用的功能
普通类和抽象类有什么区别
  • 普通类不能包含抽象方法,抽象类可以包含抽象方法
  • 抽象类不能直接实例化,普通类可以直接实例化
抽象类可以使用final修饰吗

不能,抽象类本身就是希望继承该类的子类实现其抽象方法,如果抽象方法是以final修饰的,就不能进行重写

创建一个对象用什么关键字?对象实例和对象引用有什么不同?

new关键字,new创建对象之后,对象引用存放在栈中,而对象引用又指向对象实例,对象实例存放在堆中,一个对象引用可以指向0或者1个对象实例,一个对象实例可以用n个对象引用


成员变量和局部变量的区别
  • 成员变量定义在方法外部,是类内部定义的变量;局部变量定义在方法中的变量
  • 成员变量对整个类有效;局部变量旨在方法体内有效
  • 成员变量随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中;局部变量在方法被调用,或者语句被执行时存在,存储在栈空间中,当方法调用完,或者语句结束后就自动释放
  • 成员变量有默认初始值局部变量没有默认初始值,使用前必须赋值
为什么调用子类构造方法前会先调用父类没有参数的构造方法?

需要帮助子类进行初始化工作,同时,一个类的构造方法的作用也是如此,用于完成对类对象的初始化工作

构造方法的特性
  • 名字和类名相同
  • 没有返回值,但是不能用void声明构造函数
  • 生成类的对象时自动执行,无需调用
静态变量和实例变量的区别
  • 静态变量: 静态变量由于不属于任何实例对象,属于类,所以在内存中只会存在一份,在类的加载过程中,JVM只为静态变量分配一次内存空间
  • 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就会有几份成员变量
静态变量和普通变量 的区别
  • 静态变量和非静态变量的区别是: 静态变量被所有对象所共享,在内存中也只有一个副本,它当且仅当在类初次加载时会被初始化,而非静态变量是对象独立拥有的,在创建对象时被初始化,存在多个副本,每个对象拥有的副本不影响
  • 静态成员变量的初始化顺序是按照定义的顺序进行初始化
静态方法和实例方法的区别
  • 在外部调用静态方法的时候,可以使用类名.方法名的方式,也可以使用对象名.方法名的方式,而实例方法只有后面这种方式
  • 静态方法在访问本类的成员时,只允许访问静态成员,不允许访问实例成员,实例方法无此限制

什么是内部类?内部类都有那些?

Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类,内部类的本身就是类的一个属性,与其他属性定义方式一致

内部类的分类:

  • 静态内部类:
    • 定义在类内部的静态类
    • 静态内部类可以访问外部类的所有静态变量,而不可访问外部类的非静态变量
    • 静态内部类的创建方法,new 外部类.静态内部类()
  • 成员内部类:
    • 定义在类内部,成员位置上的非静态类
    • 成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和非私有
    • 成员内部类依赖于外部类的实例,它的创建方法是外部类实例.new 内部类()
  • 局部内部类:
    • 定义在方法中的内部类
    • 定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法
    • 局部内部类的创建方式,在对应的方法内,new 内部类()
  • 匿名内部类:
    • 匿名内部类就是没有名字的内部类,使用比较多
    • 匿名内部类必须继承一个抽象类或者实现一个接口
    • 匿名内部类不能定义任何静态成员和静态方法
    • 当所在方法的形参需要被匿名内部类使用时,必须被final修饰
    • 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法
内部类的优点
  • 一个内部类对象可以访问和创建它的外部类对象的内容,包括私有成员
  • 内部类不为同一个包的其他类所见,具有很好的封装性
  • 内部类有效实现了多重继承,优化了Java单继承的缺陷
  • 匿名内部类可以很方便地定义回调
内部类应用场景
  • 多算法场合
  • 解决一些非面向对象的语句块
  • 示当使用内部类,使得代码更加灵活和富有扩展性
  • 当某个类除了它的外部类,不再被其他类使用时
为什么局部内部类和匿名内部类访问局部变量时,变量必须加final

因为生命周期问题,局部变量存在于栈中,当方法执行结束后,非final的局部变量就会被销毁,但是局部内部类中对该变量的引用还存在,所以加上final确保局部内部类使用的变量和外部的局部变量分开


构造器是否可以被重写?

构造器不能被继承,因此不能被重写,但是可以被重载

重写和重载的区别
  • 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,后者实现的是运行时的多态性
  • 重载发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
  • 重写发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类,如果父类方法访问修饰符为private则子类中就不是重写

==和equals的区别是什么
  • ==: 它的作用是判断两个对象的地址是不是相等,即判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型比较的是内存地址)
  • equals(): 它的作用是判断两个对象是否相等,但是又有两种情况:
    • 类重写了equals()方法,则比较两个对象的内容是否相同
    • 类没有重写equals()方法,此时等价于使用==比较两个对象

注:

String创建对象时,会先在常量池中查找是否已经存在该字符串,如果存在,就把常量池中的对象赋给对象引用

hashCode()是什么

hashCode()作用是获取哈希码,也被称为散列码,实际上是获取一个int整数,这个整数的作用是确定该对象在哈希表中的索引位置,Java中所有对象都可以调用hashCode()方法

散列表存储的是键值对,它能根据键快速地检索出对应地值(快速地找到所需要地对象)

为什么要获取hashCode

为了减少equals的次数,以HashSet举例,先获取要存储的对象的hashCode,然后根据该hashCode来判断要加入的位置,找到该位置以后,判断是否已经存有东西,没有的话直接存放,如果有东西的话,再调用equals进行比较,如果依然相同,说明是同一个对象,不存放,如果不同,根据相关规定进行存放

为什么重写过hashCode就一定要重写equals
  • 如果两个对象相等,hashCode也一定是相同的
  • 如果两个对象相等,两个对象互相调用equals的结果也一定是true
  • 因此,两个对象只有hashCode相同,并且互相调用时equals为true,这两个对象才相同
  • hashCode()的默认行为是对堆上的对象产生的独特值,如果没有重写hashCode(),则同一个类的两个对象无论如何都不会相同,即使这两个对象指向相同的数据
对象的相等和指向他们的引用相等,两者有什么不同

对象的相等比的是内存中存放的内容是否相等,引用相等比较的是他们指向的内存地址是否相等


当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递

是值传递,Java语言的方法调用只支持参数的值传递,当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用,对象的属性可以在被调用过程中被改变,但对对象引用的改变不会影响到调用者

为什么Java中只有值传递

Java总是采用按照值调用,也就是说,方法得到的是参数值的一个拷贝,然后就牵涉到两种情况:

  • 参数是基本数据类型,此时,参数在栈中存储,栈中保存的就已经是值本身,所以,方法获取的是该内存中内容的拷贝,也就是直接拿到值得拷贝,所以修改过后,两个值就已经不同
  • 参数是引用数据类型,此时,参数在栈中存储得其实是在堆中的数据的引用,得到参数在栈中数值的拷贝,此时拿到的是对对象的引用,如果进行了修改,对引用的修改会直接反应到在堆上的对象本身,所以两个都会发生改变(参照刘铁猛的C#某视频,讲的很清楚)

结论:

  • 一个方法不能修改一个基本数据类型的参数
  • 一个方法可以改变一个对象参数的状态
  • 一个方法不能让对象参数引用一个新的对象

JDK中常用的包
  • java.lang: 系统的基础类
  • java.io: 所有输入输出有关的类,比如文件操作
  • java.nio: 为了完善io包中的功能,提高io包中性能的一个新包
  • java.net: 与网络有关的类
  • java.util: 系统辅助类,工具类
  • java.sql: 数据库操作相关的类
import java和javax有什么区别

javax是扩展API的包,但是慢慢的,javax成为了JavaAPI的重要组成部分,但是,将javax合并到java包非常麻烦、甚至会破坏一些原有功能,所以就将javax包作为了标准API的一部分

IO流

Java中IO流分为几种?
  • 按照流的流向分,可以分为输入流和输出流
  • 按照操作单元划分,可以划分为字符流和字节流
  • 按照流的角色划分为节点流和处理流

Java中的流都是从四个抽象基类派生出来的:

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流

按操作方式分类结构图:

在这里插入图片描述

按操作对象分类结构图:

在这里插入图片描述

反射

什么是反射?

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法、对于任意一个对象,都能够调用它的任意一个方法和属性

这种动态获取的信息以及动态调用对象的方法的功能就是Java的反射机制

Java的反射机制分为两种:

  • 静态编译: 在编译时确定类型、绑定对象
  • 动态编译: 运行时确定类型、绑定对象
反射机制的优缺点
  • 优点: 运行期类型的判断,动态加载类,提高代码灵活度
  • 缺点: 性能瓶颈,反射相当与一系列解释操作,通知jvm要做的事情,性能比直接的Java代码要慢
反射机制的应用场景
  • 反射是框架设计的核心

  • 很多设计、开发比如模块化开发,动态代理都采用了反射的机制

    例子:

    • 使用JDBC时使用Class.forName注册Java的数据库连接器就是用了反射
    • Spring框架xml中配置模式装载Bean的过程
Java获取反射的三种方法
  • 通过new对象实现反射机制
  • 通过路径实现反射机制
  • 通过类名实现反射机制
public class Student {
    private Integer id;
    private String name;
}

// 通过建立对象获取反射
Student stu = new Student();
Class class1 = stu.getClass();
System.out.println(class1.getName());
// 所在通过路径-相对路径获取反射
Class class2 = Class.forName("work.xlrong.Student");
System.out.println(class2.getName());
// 通过类名获取反射
Class class3 = Student.class;
System.out.println(class3.getName());

常用API

什么是字符串常量池

字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,创建字符串时,jvm会先去检查字符串常量池,如果该字符串已经存在在常量池中,就返回它的引用,如果不存在,就实例化一个字符串放到内存池

String真的是不可变的吗?

String类使用字符数组保存字符串(private final char value[]),所以String是不可变的

String本身是不可变的,但是其引用是可变的,当我们对String类型进行重赋值时,该对象的引用地址转为指向新的内存地址,也就是新开辟了一块内存给新的字符串

注: String的不可变性是可以通过反射来改变的(反射可以访问私有变量)

// 创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); // Hello World
// 获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
// 改变value属性的访问权限
valueFieldOfString.setAccessible(true);
// 获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
// 改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); // Hello_World
String str="i"与 String str=new String(“i”)一样吗?

不一样,第一种方式的内存会分配在常量池中,第二种方式内存会分配在堆中(是一个对象,不是一个常量池中的常量)

第二种方式运行流程:

  1. 先去常量池找是否有“i”这个常量
    • 有这个常量,将常量的引用赋值给堆上创建的字符串对象,此时创建了1个对象
    • 没有这个常量,在常量池中新建该常量,并将常量的引用复制给对象创建的字符串对象,此时创建了2个对象
在使用HashMap时,使用String作为Key有什么好处?

HashMap内部是通过key的hashCode来确定value的存储位置的,由于String是存放在常量池中的,每一次创建字符串时,hashCode创建以后都会被缓存下来,不用再次计算,所以更快

String、StringBuilder、StringBuffer的区别
  • 可变性
    • String类使用字符数组保存字符串(private final char value[]),所以String是不可变的
    • StringBuilder和StringBuffer都继承自AbstractStringBuilder类,在该类中也是通过字符数组来保存数据,但是定义方式为: char[] value,所以是可变的
  • 线程安全性
    • String中的对象是不可变的,所以是线程安全的
    • AbstractStringBuilder是StringBuilder和StringBuffer的父类,但是StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以StringBuffer是线程安全的,StringBuilder不是线程安全的
  • 性能
    • 每次改变String类型时,都是生成一个新的String对象,并且String类型进行+运算时,本质上也是创建StringBuilder,进行append运算
    • StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用
    • 相同情况下使用StringBuilder相比使用StringBuffer能提升10%左右的性能提升,但是有线程不安全的风险
  • 总结
    • 操作少量数据使用Stirng
    • 单线程操作大量字符串使用StringBuilder
    • 多线程操作大量字符串使用StringBuffer

自动拆箱
  • 装箱: 将基本数据类型用它们对应的引用类型包装起来
  • 拆箱: 将包装类型转换为基本数据类型
Integer a= 127 与 Integer b = 127相等吗?

如果整型字面量的值在==-128到127==,那么自动装箱不会new新的Integer对象,而是直接引用常量池中的Integer对象

类加载器

类加载时机

执行到当前加载代码时才加载,只有第一次用到的时候加载

  • 创建类的实例
  • 调用类的方法
  • 访问类或者接口的类变量,或者为该类变量赋值
  • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
  • 初始化某个类的子类
  • 直接使用java.exe来运行某个主类
类加载过程
  1. 加载: 通过包名 + 类全限定名获取这个类准备用流进行传输,将这个类加载到内存中,加载完毕创建一个Class对象
  2. 连接:
    1. 验证: 判断文件中的信息是否符合Java虚拟机规范,看是否会造成隐患
    2. 准备: 初始化静态变量
    3. 解析: 将类的文件中的二进制引用转换为符号引用
  3. 初始化
类加载器分类
  • 启动类加载器: 虚拟机内置的类加载器(StringList)
  • 平台类加载器: 负责加载JDK中的一些特殊的模块,例如: java.sql.Date
  • 系统类加载器: 负责加载用户类路径上所指定的类库(我们自己写的)
使用场景
  • 获取系统类加载器: getSystemClassLoader()
  • 加载某一个类资源: getResourceAsStream() // 文件必须在src目录下
Properties prop = new Properties();

InputStream is = ClassLoader.getSystemClassLoader()
        .getResourceAsStream("prop.properties");

prop.load(is);

System.out.println(prop);

is.close();

集合

集合容器概述

什么是集合
  • 集合是一个存放数据的容器,准确的说是放数据对象引用的容器
  • 集合类存放的都是对象的引用,不是对象本身
  • 集合类型主要有三种: list、set、map
集合有什么特点
  • 集合用于存储对象的容器,对象是用来封装数据
  • 和数组相比对象的大小不确定,因为集合是可变长度的,数组需要提前定义大小
集合和数组的区别
  • 数组是固定长度的,集合是可变长度的
  • 数组可以存储基本数据类型,也可以存储引用数据类型,集合只能存放引用数据类型
  • 数组存储的元素必须是同一个数据类型,集合存储的对象可以是不同数据类型
使用集合的好处
  1. 容量自增长
  2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序的速度和质量
  3. 可以方便地扩展或改写集合,提高代码地复用性和可操作性
  4. 通过使用JDK自带的集合类,可以降低代码维护和学习新API技术
常见地集合类有哪些
  • 单例集合Collection:
    • Set:
      • HashSet
      • TreeSet
      • LinkedHash
    • List:
      • ArrayList
      • LinkedList
      • Stack
      • Vector
  • 双例集合(Map):
    • HashMap
    • TreeMap
    • HashTable
    • ConcurrentHashMap
    • Properties
List、Map、Set三者区别

在这里插入图片描述

Collection集合主要有List和Set两大接口

  • List: 一个有序容器,元素可以重复,可以插入多个null元素,元素都有索引,常见的实现类有ArrayList、LinkedList、Vector
  • Set: 一个无序容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性,Set接口常见的实现类有HashSet、LinkedHashSet、TreeSet

Map是一个键值对集合,存储键、值和之间的映射,key无序唯一,value不要求有序,允许重复,map没有继承collection接口,从map集合中检索元素时,只要给出键对象,就会返回对应的值对象。map的常见实现类有HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

集合底层数据结构
  • Collection:
    • List:
      • ArrayList: Object数组
      • Vector: Object数组
      • LinkedList: 双向循环链表
    • Set:
      • HashSet: 基于HashMap实现,底层使用HashMap来保存数据
      • LinkedHashSet: 继承与HashSet,内部是通过LinkedHashMap来实现的
      • TreeSet: 红黑树
  • Map:
    • HashMap: jdk8之前hashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,jdk8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认是8)时,将链表转化为红黑树,减少搜索时间
    • LinkedHashMap: 继承自HashMap,底层依然是基于拉链式散列结构(数组和链表或者红黑树)组成,同时,LinkedHashMap在HashMap的基础上增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序,同时通过对链表进行相应的操作,实现了访问顺序相关逻辑
    • HashTable: 数组+链表组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的
    • TreeMap: 红黑树
那些集合类是线程安全的
  • Vector: 比ArrayList多了个synchronized(线程安全),因为效率较低,所以已经不适用了
  • HashTable: 只比HashMap多了个synchronized(线程安全),不推荐使用
  • ConcurrentHashMap: jdk5之后支持高并发、高吞吐量的线程安全HashMap实现,它由Segment数组结构和HashEntry数组结构组成,Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获取对应的Segment锁
Java集合的快速失败机制 fail-fast

fail-fast是Java集合中的一个错误检测机制,当多个线程对集合进行结构上的改变操作时,有可能会触发fail-fast机制

例:

假设存在两个线程(线程1、线程2),线程1通过iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出ConcurrentModificationException异常,从而触发fail-fast机制

原因:

迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount变量,集合在被遍历期间如果内容发生变化,就会改变modCount的值,每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历,否则抛出异常,终止遍历

解决方法:

  • 在遍历过程中,所有涉及到改变modCount值的地方全部加上synchronized
  • 使用CopyOnWriteArrayList来替换ArrayList
怎么确保以及集合不会被修改

使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合

Collection集合

List接口
迭代器Iterator是什么
  • Iterator接口提供遍历任何Collection的接口,我们可以从一个Collection中使用迭代器方法来获取迭代器实例,迭代器取代了Java原先的Enumeration,迭代器允许调用者在迭代过程中移除元素
  • 所有Collection接口都继承了Iterator迭代器
Iterator怎么使用?有什么特点?

简单使用代码:

List<String> list = new ArrayList<>();
Iterator<String> i = list.iterator();
while(i. hasNext()){
    String s = i. next();
    System.out.println(s);
}

iterator的特点是只能单向遍历,但是更加安全,因为它可以在当前遍历的集合元素被修改的时候抛出ConcurrentModificationException异常

如何边遍历边移除Collection中的元素
List<String> list = new ArrayList<>();
Iterator<String> i = list.iterator();
while(i.hasNext()){
    i.remove();
}
为什么增强for中删除集合中的元素会出问题

因为在使用增强for遍历集合时,会自动生成一个迭代器来遍历该集合,如果在迭代器遍历集合的时候使用原集合的方法删除其中的元素,就会出问题

Iterator和ListIterator有什么区别
  • Iterator可以遍历Set和List集合,ListIterator只能遍历List
  • Iterator只能单向遍历,ListIterator可以双向遍历
  • ListIterator实现Iterator接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置
遍历一个List有那些不同的方式?每种方法的实现原理是什么?Java中遍历List的最佳实现是什么?

遍历方法大致有以下几种:

  • for循环遍历: 基于计数器,在集合外部维护一个计数器,以此读取每一个位置的元素,当读取到最后一个元素后停止
  • 迭代器遍历: 迭代器是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口,Java在Collections中支持了Iterator模式
  • foreach循环遍历: foreach内部也是采用了Iterator的方式实现,使用时不需要显式声明Iterator或计数器,有点是代码简洁,不易出错,缺点是只能做简单的遍历,不能在遍历的过程中操作数据集合

最佳实践: Java Collections提供了一个RandomAccess接口,用来标记List实现是否支持Random Access

  • 如果一个数据集合实现了该接口,就意味着它支持Random Access,按位置读取元素的平均时间复杂度为O(1),如ArrayList
  • 如果没有实现该接口,表示不支持RandomAccess,如LinkedList
  • 推荐的做法是: 支持RandomAccess的集合可以使用for循环遍历,否则推荐使用Iterator或foreach遍历
说一下ArrayList的优缺点

优点:

  • ArrayList底层以数组实现,是一种随机访问模式,ArrayList实现了RandomAccess接口,因此查找的时候非常快
  • ArrayList在顺序添加一个元素的时候非常方便

缺点:

  • 删除元素的时候,需要做一个元素复制操作,如果要复制的元素很多,那么就会比较耗费性能
  • 插入元素的时候,也需要做一次元素复制操作,缺点相同

ArrayList比较适合顺序添加、随机访问的场景

如何实现数组和List之间的转换?

数组转List: 使用Arrays.asList(array)进行转换

List转数组: 使用List自带的toArray()方法

ArrayList和LinkedList的区别是什么
  • 数据结构实现: ArrayList是动态数组的数据结构实现,而LinkedList是双向链表的数据结构实现
  • 随机访问效率: ArrayList比LinkedList在随机访问的时候效率要高,因为LinkedList是线性的数据结构,所以需要移动指针从前往后依次查找
  • 增加和删除效率: 在非首位的增加和删除操作,LinkedList要比ArrayList的效率要高得多,因为ArrayList增删操作要影响数组内的其他数据的下标
  • 内存空间的占用: LinkedList比ArrayList更占空间,因为LinkedList除了要存放数据以外,还需要存储两个引用,一个指向前一个元素,一个指向后一个元素
  • 线程安全: ArrayList和LinkedList都是不同步的,也就是不保证线程安全
  • 综合来说,如果需要频繁读取集合中的元素时,推荐使用ArrayList,如果需要频繁添加删除集合元素时,更推荐使用LinkedList
ArrayList和Vector的区别是什么
  • 这两个类都实现了List接口(List又实现了Collection接口),都是有序集合
    • 线程安全: Vector使用了synchronized来实现线程同步,是线程安全的,ArrayList非线程安全
    • 性能: ArrayList在性能方面优于Vector
    • 扩容: ArrayList和Vector都会根据实际需要的进行动态扩容,只不过在Vector扩容每次会增加1倍,ArrayList每次扩容会扩充50%
  • Vector类的所有方法都是同步的,线程安全,进行操作消耗资源多
  • ArrayList不是同步的
在进行数据插入时,ArrayList、LinkedList、Vector哪个速度更快
  • ArrayList和Vector底层的实现都是使用数组方式存储数据,数组元素数大于实际存储的数据以以便增加和插入元素,他们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢
  • Vector中的方法由于加了synchronized修饰,所以Vector是线程安全容器,性能上比ArrayList差一些
  • LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或者后向遍历,但插入数据时只需要记录当前项的前后项即可,所以,LinkedList插入速度较快
多线程场景下如何使用ArrayList?

ArrayList不是线程安全的,如果遇到多线程场景,可以通过Collections的synchronizedList方法将其转换成线程安全的容器后再使用,例:

List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++) {
    System.out.println(synchronizedList.get(i));
}
为什么ArrayList的elementData加transient修饰

ArrayList中数组定义如下:

private transient Object[] elementData;

ArrayList的定义为:

public class ArrayList<E> extends AbstractList<E> 
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList实现了Serializable接口,意味着ArrayList支持序列化,transient的作用是说不希望elementData数组被序列化,为此重写了writeObject实现:

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();
    // Write out array length
    s.writeInt(elementData.length);
    // Write out all elements in the proper order.
    for (int i=0; i<size; i++)
        s.writeObject(elementData[i]);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
}

每一次序列化时,先调用defaultWriteObject方法序列化ArrayList中的非transient元素,然后遍历elementData,只序列化已存入的元素,这样加快了序列化的速度,又减小了序列化之后的文件大小

List和Set的区别
  • List、Set都是继承自Collection接口
  • List特点: 有序容器,元素可以重复,可以插入多个null元素,元素都有索引,常用的实现类有ArrayList、LinkedList、Vector
  • Set特点: 无序容器,不可以重复存储元素,只允许存入一个null元素,必须保证元素唯一性,Set接口常用实现类是HashSet、LinkedHashSet和TreeSet
  • List支持for循环、也可以通过迭代器,Set只能通过迭代器(本质上是因为Set无序,不能通过下标获取元素)
  • Set和List的对比:
    • Set: 检索元素效率低,删除和插入效率高,插入和删除不会引起元素的位置改变
    • List: 和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变
Set接口
HashSet的实现原理

HashSet是基于HashMap实现的,HashSet的值存放在HashMap的key上,HashMap的value统一为present,因此HashSet的实现是比较简单的,对于HashSet的一些操作,其实基本上都是直接调用HashMap的相关方法

private static final Object PRESENT = new Object();
HashSet是如何检查重复的?HashSet是如何保证数据不可重复的?
  • 向HashSet中add元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equals方法进行比较
  • HashSet中的add方法会使用HashMap的put方法
  • HashMap的key是唯一的,由源码可以看出HashSet添加进去的值就是作为HashMap的Key,而且在HashMap中如果K-V相同时,会用新的V覆盖掉旧的V,所以不会重复
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
    map = new HashMap<>();
}
public boolean add(E e) {
    // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
    return map.put(e, PRESENT)==null;
}
hashCode和equals的相关规定
  • 如果两个对象相等,则hashCode也一定是相同的
  • 两个对象相等,两个对象互相调用equals返回true
  • 两个对象有相同的hashCode值,他们也不一定是相等的
  • 所以,equals方法被覆盖过,hashCode方法也必须被覆盖
  • hashCode的默认行为是对堆上的对象产生独特值,如果没有重写hashCode,则该class的两个对象无论如何都不可能相等,即使这两个对象指向相同的数据
==和equals的区别
  • == 是判断两个对象或者变量是不是指向同一个内存空间,equals是判断两个变量或实例所指向的内存空间的值是否相同
  • ==是指对内存空间地址进行比较,equals是对字符串的内容进行比较
HashSet和HashMap的区别
实现了什么存储结构添加元素唯一性速度
HashMap实现了Map接口存储键值对调用put向map中添加元素HashMap使用键(Key)计算hashCodeHashMap相对于HashSet较快,因为它是使用唯一的键获取对象
HashSet实现了Set接口存储对象调用add向set中添加元素HashSet使用成员对象来计算hashCode值,对于两个对象来说hashCode可能相同,所以equals方法用来判断对象的相等性,如果两个对象不同的话,返回falseHashSet较HashMap来说比较慢

对于这里的HashMap比HashSet快,我有两个可能的理解:

  • HashMap要获取一个元素的方法是使用get()方法,根据源码,可以看出,是一个比较复杂的算法,用到树什么的,姑且相信是一个很完善的算法

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    /**
         * Implements Map.get and related methods.
         *
         * @param hash hash for key
         * @param key the key
         * @return the node, or null if none
         */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

    但是HashSet是没有get方法的,HashSet想要知道里面有没有一个元素,可以用contains方法,而这个方法的底层逻辑是迭代器,性能上可能没有get方法好

  • 一个比较微妙的说法,个人觉得这个说法是挺合理的,是在StackOverFlow的一个回答,大概意思是,在HashSet和HashMap的key类型相同的情况下,两种结构的性能几乎完全相同,而存在HashMap的速度比HashSet快的说法的原因是,HashMap的key一般是一个字符串,而String和Integer这样的类型的哈希计算是很快的,而如果向HashSet存进得是一个复杂的自定义对象,这肯定就很慢了,所以会有HashSet比HashMap慢的感觉

    Q:
    I am not quite understanding the following statements:
    HashMap is faster than HashSet because the values are associated to a unique key.
    A:
    None of these answers really explain why HashMap is faster than HashSet. They both have to calculate the hashcode, but think about the nature of the key of a HashMap - it is typically a simple String or even a number. Calculating the hashcode of that is much faster than the default hashcode calculation of an entire object. If the key of the HashMap was the same object as that stored in a HashSet, there would be no real difference in performance. The difference comes in the what sort of object is the HashMap’s key.
    
Map接口
什么是Hash算法

哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值

什么是链表

链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查的功能

单链表、双链表

链表的优点:

  • 插入删除速度快
  • 内存利用率高,空间浪费少
  • 扩展灵活、大小不固定

链表的缺点:

  • 不能随机查找,查找效率低
HashMap的实现原理

HashMap是基于哈希表的Map接口的非同步实现,此实现提供所有可选的映射操作,并允许使用null值和null键,此类不保证映射的顺序,不保证顺序永远不变

HashMap的数据结构: Java语言中,基本的数据结构就两种: 数组、模拟指针(引用),所有的结构都可以用这两种结构来构造,HashMap其实就是链表散列的数据结构,数组和链表的结合体

HashMap是基于Hash算法实现的:

  1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况
    1. 如果key相同,覆盖原始值
    2. 如果key不相同,将当前的key-value放入链表中
  3. 获取时,直接找到hash值对应的下标,再进一步判断key是否相同,从而找到对应值

注: 在jdk1.8之后,HashMap的实现发生了优化,当链表中的节点数据超过8个之后,该链表会转为红黑树来提高查询效率,从原先的O(n)到O(logn)

HashMap在jdk7和jdk8中有哪些不同,HashMap的底层实现

Java中,保存数据有两种简单数据结构: 数组和链表

数组的特点是容易寻址,插入和删除困难

链表的特点是寻址困难,但是插入和删除容易,所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突

HashMap在jdk8之前:

jdk8之前采用的是拉链法,将链表和数组相结合,也就是说创建一个链表数组,数组中每一个格子就是一个链表,如果发生哈希冲突,将冲突的值加到链表中

HashMap在jdk8之后:

相比于之前的版本,jdk8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认是8),将链表转换为红黑树,减少搜索时间

jdk7和jdk8比较:

jdk8主要是优化了以下问题:

  • resize扩容优化
  • 引入了红黑树,目的是避免单条链表过长而影响查询效率
  • 解决了多线程死循环的问题,但是仍然是非线程安全,多线程可能会造成数据丢失
不同jdk7jdk8
存储结构数组 + 链表数组 + 链表 + 红黑树
初始化方式单独函数: inflateTable()直接集成到了扩容函数 resize() 中
hash值计算方式扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突 & 链表长度 <8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式头插法(先讲原位置的数据移到后1位,再插入数据到该位置)尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)
什么是红黑树

二叉树: 每个节点上都有两个子节点

红黑树:

  • 红黑树是一种特殊的二叉查找树,红黑树的每个节点上都有存储位表示节点的颜色,可能是红或黑
  • 红黑树的每个节点是红色或者黑色,红黑树的根节点一定是黑色,叶子节点也一定是黑色(叶子节点是指为null的叶子节点)
  • 如果一个节点是红色的,那么它的子节点必须是黑的
  • 每个节点到叶子节点所经历的黑色节点数一样
  • 红黑树的基本操作是添加、删除,在对红黑树进行添加或者删除后,会用到旋转的方法(因为添加或者删除一个节点之后,红黑树可能就不满足红黑树的条件了,需要进行旋转变色,保持红黑树的特性)
HashMap的put方法的具体流程
  • 当我们put的时候,首先要计算key的hash值,调用hash方法,hash方法其实是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以hash函数大概的作用就是: 高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞,按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做hash处理,相当与散列生效的只有几个低bit,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16big和低16bit异或来简答处理减少碰撞,而且jdk8使用了复杂度O(logn)的树结构来提升碰撞性能

在这里插入图片描述

  1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向
    ⑥,如果table[i]不为空,转向③;
  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的
    是hashCode以及equals;
  4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值
    对,否则转向5;
  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操
    作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩
    容。
HashMap的扩容操作是怎么实现的
  1. 在jdk8种,resize方法是在hashMap中的键值对大于阈值或者初始化时,调用resize方法进行扩容
  2. 每次扩容的时候,都是扩充2倍
  3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置,在putVal中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值(默认初始值时12),这个时候在扩容的同时也会伴随在桶上面的元素进行重新分发,这也是jdk8版本的一个优化的地方,在jdk7中,扩容之后需要重新去计算其Hash值,根据其Hash值对其进行分发,但在jdk8中,则是根据在同一个桶的位置中进行判断(e.hash& oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
HashMap是怎么解决哈希冲突的

哈希冲突: 两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做哈希冲突(哈希碰撞)

使用数组加链表的方式可以初步解决哈希冲突

hash函数:

如果只使用hashCode()来进行取余运算,那么就只有参与运算的数据的低位参与计算,就很容易出现哈希冲突哈希冲突,hash()的作用就是使高位也参与到运算

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}
能否使用任何类作为Map的Key

理论上来讲任何类都可以作为Map的Key,在使用之前,需要考虑几点:

  • 如果类重写了equals()方法,也应该重写hashCode()方法
  • 类的所有实例都需要遵循与equals()和hashCode()
  • 一个类如果使用equals(),不应该在hashCode()使用它
  • 用户自定义Key类最佳实践是视之为不可变的,这样hashCode值可以被缓存起来,拥有更好的性能,不可变的类也可以确保hashCode和equals在未来不会改变,这样就会解决与可变相关的问题
为什么HashMap中String、Integer这样的包装类适合做K

String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

  • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  • 内部已经重写了equals()、hashCode()等方法,遵循了HashMap内部的规范(不情况可以去上面看看putValue的过程),不容易出现Hash值计算错误的错误
如果使用Object作为HashMap的Key,应该怎么办
  • 重写hashCode()和equals()
  • 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能回导致更多的哈希碰撞
  • 重写equals()方法,需要遵循自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性
HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标

hashCode返回的是一个int整数类型,范围是-(2 ^ 31)~(2 ^ 31 - 1),而HashMap的容量范围是16~2 ^ 30,所以说,很有可能hashCode得到的数是在hashMap的容量范围外

解决方法:

  • HashMap自己实现的hash()方法,通过两次扰动使得自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均
  • 在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)来获取数组下标的方式进行存储,这样

设计模式

什么是设计模式

设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结

为什么要使用/学习设计模式

  • 使用设计模式是为了可重用代码(相同的业务逻辑可以直接套用),代码更容易被理解,代码可靠性更高
  • 比如Spring、SpringMVC等框架都或多或少涉及到了设计模式的运用,学习设计模式可以帮助我们理解框架的运行逻辑

都有哪些设计模式

  • 创建型模式: 工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模型
  • 结构型模式: 适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
  • 行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式

设计模式的六大原则

  • 开放封闭原则
    • 原则思想: 尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化
    • 描述: 一个软件产品在生命周期内都会发生变化,既然变化是一个已经确定的事实,我们就应该在设计的时候尽量适应这些变化,提高项目的稳定性和灵活性
    • 优点: 单一职责原则告诉我们,每个类都有自己负责的职责,里氏替换原则不能破坏继承关系的体系
  • 里氏替换原则
    • 原则思想: 使用的基类可以在任何地方使用继承的子类,完美的继承基类
    • 描述: 子类可以扩展父类的功能,但不呢个改变父类原有的功能,子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法,子类中可以增加自己特有的方法
    • 优点: 增加程序的健壮性,即使增加了子类,原有的子类还可以继续运行,互不影响

Git

本地操作

  • 将仓库内所有变更都加入到暂存区: git add -A
  • 比较工作区和暂存区的所有差异: git diff
  • 将工作区指定文件恢复成和暂存区一致: git checkout 文件1 文件2 ...
  • 将暂存区指定文件恢复成和Head一致: git reset 文件1 文件2 ...
  • 将暂存区和工作区所有文件恢复成和Head一致: git reset -hard
  • 用difftool比较任意两个commit的差异: git difftool 提交1 提交2
  • 查看那些文件没被git托管: git ls-files --others

分支操作

  • 查看当前工作分支及本地分支: git branch -v
  • 查看本地和远端分支: git branch -av
  • 查看远端分支: git branch -rv
  • 切换到指定分支: git checkout 指定分支
  • 基于当前分支创建新分支: git branch 新分支
  • 基于指定分支创建新分支: git branch 新分支 指定分支
  • 基于某个commit创建分支: git branch 新分支 某个commit的id
  • 创建并切换到某分支: git checkout -b 新分支
  • 安全删除本地某分支: git branch -d 要删除的分支
  • 删除已经合并到master分支的所有本地分支: git branch --merged master | grep -v '^\*\| master' | xargs -n 1 git branch -d
  • 删除远端origin已经不存在的所有本地分支: git remote prune orign
  • 将A分支合并到当前分支中且为merge创建commit: git merge A分支
  • 将A分支合并到B分支中且为merge创建commit: git merge A分支 B分支

变更历史

  • 当前分支各个commit用一行显示: git log --oneline
  • 显示就近的n个commit: git log -n
  • 用图示显示所有分支的历史: git log --oneline --graph --all
  • 查看涉及到某文件变更的所有commit: git log 文件
  • 某文件各行最后修改对应的commit以及作者: git blame 文件

标签操作

  • 查看已有标签: git tag
  • 新建标签: git tag v1.0
  • 新建带备注标签: git tag -a v1.0 -m '前端食堂'
  • 给指定的commit加标签: git tag v1.0 commitid
  • 推送一个本地标签: git push origin v1.0
  • 推送全部未推送过的本地标签: git push origin --tags
  • 删除一个本地标签: git tag -d v1.0
  • 删除一个远程标签: git push origin :refs/tags/v1.0

远程交互

  • 查看所有远程仓库: git remote -v
  • 添加远程仓库: git remote add url
  • 删除远程仓库: git remote remove remote的名称
  • 重命名远程仓库: git remote rename 旧名称 新名称
  • 将远程所有分支和标签的变更都拉到本地: git fetch remote
  • 把远程分支的变更拉到本地,且merge到本地分支: git pull origin 分支名
  • 将本地分支push到远端: git push origin 分支名
  • 删除远端分支: git push remote --delete 远端分支名 git push remote :远端分支名

Maven

生命周期

常见的生命周期序列
  • pre-clean -> clean -> post-clean
  • compile -> test -> package -> install
  • pre-site -> site -> post-site

同一生命周期内,执行后面的命令,前面的所有命令会自动执行

依赖传递

依赖具有传递性
  • 直接依赖: 在当前项目中通过依赖配置建立的依赖关系
  • 简介传递: 被引用的资源如果依赖其他资源,当前的项目就间接依赖其他资源
Maven有什么依赖传递问题
  • 路径优先: 当依赖中出现相同的资源时,层级越深,优先级越低,层级越浅,优先级越高
  • 声明优先: 当资源中相同层级被依赖时,配置顺序靠前的覆盖配置顺序靠后的
  • 特殊优先: 当同级配置了相同资源的不同版本,后配置的覆盖先配置的
可选依赖

可选依赖是隐藏当前工程所依赖的资源,隐藏后对应资源将不具有依赖:

<optional>false</optional>
排除依赖

排除依赖是隐藏当前资源对应的依赖关系

<exclusions>
    <exclusion>

    </exclusion>
</exclusions>

Docker

什么是Docker

Docker是一个快速交付应用,运用应用的技术

Docker是一个容器化技术,解决软件或程序跨环境迁移问题,如果软件或程序在迁移时,连同所依赖的Libs和Deps一同打包迁移,使用线程隔离方式保证容器隔离性

  • 可以将应用及其依赖、运行环境一起打包为一个镜像
  • 运行时利用沙箱机制形成隔离容器,各个应用互不干扰
  • 启动、移除都可以一行命令完成,方便快捷

Docker都解决了什么问题

  • 解决大型项目依赖关系复杂、不同组件依赖的兼容性问题
    • Docker允许开发中将应用、依赖、函数库、配置一起打包,形成可移植镜像
    • Docker应用运行在容器中,形成沙箱机制,相互隔离
  • 解决开发、测试、生产环境差异问题
    • Docekr镜像中包含完整运行环境,包含系统的函数库,仅依赖系统的Linux内核,因此可以在任意Linux系统上运行

Docker和虚拟机的区别

  • 虚拟机: 在操作系统中模拟硬件设备,然后运行另一个操作系统,比如在Windows系统里运行Ubuntu,然后可以运行任意的Ubuntu应用
  • Docker: 容器中只封装了应用运行所必须的函数库和依赖,体积小

镜像和容器的区别

  • 镜像: Docker将应用程序及其所需依赖、函数库、环境、配置等文件打包在一起,称为镜像
  • 容器: 镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器进程做隔离,对外不可见
  • Docker中容器是独立运行的一个或一组应用,是镜像运行时的实体

Nginx

什么是Nginx

Nginx是一个轻量级的Web服务器、反向代理服务器、电子邮件IMAP/POP3代理服务器,特点为占用内存少、并发能力强

Nginx有什么应用场景

  • 反向代理: 将一个ip的请求转发到另一个ip下
  • 负载均衡: 将对一个服务器的请求转发到多个ip下

Rest风格

  • Rest是可扩展和可互操作的,不针对所使用的语言和技术

  • Rest常用的HTTP请求: 用于获取资源的Get、用于创建资源的Post、用于更新资源的Put、用于删除资源的Delete

  • @RequestMapping有什么作用: 可以讲HTTP请求映射到Controller中,之后就可以轻松的使用Resuful风格来进行Controller编写


集合类存放在Java.util包中,主要有3种: list、set、map

主要接口:

  • Collection: Collection是集合List、Set、Queue的最基本的接口
  • Iterator: 迭代器,可以通过迭代器遍历集合中的数据
  • Map: 映射表的基础接口

在这里插入图片描述

在这里插入图片描述

List

Java的List是非常常用的数据类型,List是有序的Collection,Java List一共有三个实现类: ArrayList、Vector、LinkedList

ArrayList

ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问,数组的缺点是每个数组元素之间不能有间隔,当数组大小超过当前容量时,需要对当前数组进行扩容,将已经存在的数据复制到新的存储空间中,当在ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高,所以,ArrayList适合随机查找和遍历,不适合插入和删除

Vector

Vector和ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写引起不一致性,但是实现同步需要消耗很多资源,所以Vector使用速度比ArrayList慢

LinkedList

LinkedList是使用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢,但是由于LinkedList是基于链表实现的,所以对数据的增删操作很快,同时,由于LinkedList提供了List接口中没有的操作表头和标为的方法,所以还可以作为堆栈、队列、双向队列使用

Set

Set中数据不重复,Set集合用于存储无序的元素,值不能重复,对象的相等性本质是对象的hashCode值进行判断,如果想要两个不同的对象视为相等,就一定要重写hashCode和equals方法

Redis

  • 缓存穿透: 缓存中一定不存在的数据,去访问数据库,高并发时,造成数据库崩溃

    • 布隆过滤器

    • 给这个数据创建一个过期时间非常短的缓存

  • 缓存击穿: 某一条缓存非常频繁的被访问,缓存过期时,大量访问数据库

    • 使用互斥锁

    • 将该缓存设置为永远不过期

  • 缓存雪崩: 相同过期时间的缓存同时过期,同时大量访问数据库

    • 加锁或者队列

    • 创建时添加一个随机的时间缓存增量


旧的

  • 基本数据类型传值,对形参的修改不会影响实参;引用类型传引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象;注: String, Integer, Double等immutable的类型特殊处理,可以理解为传值,最后的操作不会修改实参对象
  • 重写的返回值、抛出的异常范围小于等于父类,访问修饰符的范围大于等于父类
  • ==作用于基本数据类型,直接比较其存储的是否相同;作用于引用类型的变量,比较的是所指向对象的地址
  • euqals不能作用与基本数据类型,如果没有对equals方法重写,比较的是引用类型的变量所指向的对象的地址,重写了equals之后,重写的是所指向对象的内容
  • String不可变的对象,创建的可以算是个字符串常量String每次改变都会产生新的String对象;StringBuffer是线程安全的、可变的字符串序列;StringBuilder线程不安全、速度快、单线程使用。
  • 使用final地原因:
    • 把方法锁定,放置任何继承类修改其含义
    • 提升效率,早年Java实现版本中会将final方法转为内嵌调用,现在已经不需要通过final来提升性能
  • 类中所有的private方法都隐式地指定为final
  • Objectequals方法用于比较2个对象的内存地址是否相同,String类对该方法进行了重写,用于比较字符串的值是否相同
  • Objectclone方法用于创建并返回当前对象的一份拷贝,一般情况下,对于任何对象x,表达式x.clone != xtruex.clone().getClass() == x.getClass()trueObject本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常
  • ObjecttoString如果不进行重写的话,返回的是类的名字@实例的哈希码的16进制的字符串
  • 抽象类和接口的区别
    • 抽象类的子类用extends来继承,接口用implements来继承
    • 抽象类可以有构造方法,接口没有
    • 抽象类可以有main方法,接口不能有
    • 类可以实现多个接口,但只能继承一个抽象类
    • 接口中的方法默认是用public修饰的,抽象类中的方法可以是任意修饰符
    • jdk1.8之后,可以在接口中定义使用defaultstatic关键字修饰的方法
  • 单例模式: 某个类的实例在多线程环境下只会被创建出来一次
    • 饿汉式单例模式: 线程安全,一开始就初始化
    • 懒汉式单例模式: 非线程安全,延迟初始化
    • 双检锁单例模式: 线程安全,延迟初始化
  • finalize方法: Object类的一个方法,在垃圾回收器执行的时候会调用被回收对象的该方法,可以覆写此方法,当该方法被系统调用则代表该对象即将"死亡",但是需要注意的是,我们主动行为上调用该方法并不会导致对象"死亡",这是一个被动的方法(回调方法),不需要我们调用
  • sleep方法没有释放锁,wait方法释放锁
  • 所有异常的根类为java.lang.ThrowableThrowable又包含ErrorException两个子类
  • Exception包含系统异常(软件本身缺陷所导致的问题,发生此类异常时还可以让软件系统继续运行或者让软件死掉,因此,编译器不强制try...catch,此类异常又被称为checked异常)和普通异常(发生此类异常时软件还可以正常运行,所以编译器不强制使用try...catch,此类异常又被称为unchecked异常)
  • Exception分为运行时异常(Runtime Exception)和受检查时异常(Checked Exception)
  • Map类型的键是不可重复的值是可以重复的
  • Set元素在集合中的位置由元素的hashCode决定,位置是固定的
  • List的三个实现类:
    • LinkedList: 基于链表,增删快、查找慢
    • ArrayList: 基于数组,线程不安全、效率高、查找快、增删慢
    • Vector: 基于数组是,线程安全、效率低
  • Map的三个实现类:
    • HashMap: 基于hash表,线程不安全、高效、支持空值和空键
    • HashTable: 线程安全,低效,不支持空值和空键,现在不推荐使用这个类,因为HashTable是遗留类,内部很多没有优化和冗余的实现,在多线程环境下,推荐使用ConcurrentHashMap
    • LinkedHashMap: HashMap的一个子类,保存了记录的插入顺序
  • SortMap的一个实现类:
    • TreeMap: 能够把它保存的记录根据键排序,默认是键值的升序排序
  • Set的两个实现类:
    • HashSet: 底层由HashMap实现,不允许集合中有重复的值,使用时需要重写equals()hashCode()方法
    • LinkedHashSet: 继承于HashSet,同时又基于LinkedHashMap来进行实现,底层使用的是LinkedHashMap
  • HashMap底层原理: jdk1.8之前是通过数组+单向链表实现的、jdk1.8之后通过数组+单向链表数组+红黑树实现,当节点数小于等于6时,使用数组+单向链表、当节点数大于等于8时使用数组+红黑树

异常相关

  • 容易产生NPE的情况:
    • 返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱时可能产生NPE
    • 数据库的查询结果可能是null
    • 集合里的元素可能是null,即使该集合isNotEmpty
    • 远程调用返回对象时,一律要求进行空指针判断,防止NPE
    • 对于Session中获取的数据,建议进行NPE检查,避免空指针
    • 级联调用obj.getA().getB().getC()时,容易产生NPE

集合相关

  • ArrayListLinkedList都实现了List接口,但是LinkedList还实现了Qeque接口,所以LinkedList还可以当作队列来使用
  • fail-fastfail-safe
    • ArrayList是典型的fail-fast,遍历的同时不能修改,尽快失败,线程不安全
    • CopyOnWriteArrayList是典型的fail-safe,遍历的同时可以修改,原理是读写分离,线程安全
  • 树化: 单个槽中链表长度 > 8时,
    • 数组容量如果小于64,优先进行扩容
    • 数组容量大于64,转红黑树
  • 树退化:
    • 扩容时,原先红黑树上一部分数据可能会转移到别的槽中,当红黑树中的元素小于等于6时,退化成链表
    • 调用remove方法删除数据时,删除之前校验红黑树根节点 左儿子 左孙子 右儿子 是否存在,如果有一个不存在,退化成链表
  • HashTableConcurrentHashMap区别: HashTable使用的是Synchronized关键字修饰,ConcurrentHashMap是JDK1.7 使用了锁分段技术来保证线程安全的。JDK1.8ConcurrentHashMap 取消了Segment 分段锁,采用CASsynchronized来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树synchronized只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。

线程相关

  • 创建线程的方法: 继承Thread实现Runnable接口使用Callable接口(可以使用CompletableFuture)

  • waitsleep锁特性的不同:

    • wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
    • wait方法执行后会释放对象锁,允许其它线程获取该对象锁
    • sleep如果在synchronized代码块中执行,并不会释放对象锁
  • Java并发编程三个问题:

    • 原子性: 一个线程在CPU中操作不可暂定,也不可中断,要么执行完成,要么不执行
    • 内存可见性: 默认情况下,当一个线程修改内存中某个变量时,主内存值发生了变化,并不会主动通知其他线程,即其他线程并不可见
    • 有序性: 程序执行的顺序按照代码的先后顺序执行
  • volatile修饰的变量对所有线程是可见的,是共享的,当一个线程修改了一个被volatile修饰的共享变量的值,会主动通知其他线程新值(volatile解决多线程内存不可见问题,对于一写多读,可以解决变量同步的问题,但是如果是多写,无法解决线程安全问题)

JavaWeb

  • Cookie存放在客户端,是一个磁盘文件,由浏览器在文件夹中寻找,最大容量4KB,一个站点最多保存20个CookieCookie只能保存ASCII字符串,可以通过设置Cookie的过期时间使Cookie长期存储;Session是服务器中的对象,由浏览器进行安全处理,只能由浏览器找到,Session没有上限,但最好不要在Session中存放过多东西,Session能够保存任何数据类型Session默认关闭浏览器之后删除,不能达到长期存储的效果

  • 重定向和请求转发的区别:

    • 转发:用requestgetRequestDispatcher()方法得到ReuqestDispatcher对象,调用forward()方法

      request.getRequestDispatcher("other.jsp").forward(request, response);

      重定向:调用responsesendRedirect()方法response.sendRedirect("other.jsp");

    • 重定向2次请求、请求转发1次请求()

    • 重定向地址栏会变,请求转发地址栏不变

    • 重定向是浏览器跳转,请求转发是服务器跳转

    • 重定向可以跳转到任意网址,请求转发只能跳转到当前项目

    • 请求转发不会丢失请求数据,重定向会丢失

  • GetPost的区别:

    • 由于URL长度的限制,Get传输的数据量比较小,一般不超过2k~4k
    • Post一般认为长度不受限制
    • Get限制From表单的数据集的值必须为ASCII字符;而POST支持整个ISO10646字符集
    • Get自行效率比Post好,Getfrom的默认提交方法

数据库(MySQL)

  • B树和B+树对比:

    • B+树层级更低(矮胖),IO次数更少(非叶子节点中的数据全是索引)
    • B+树每次都需要查询叶子节点,查询性能更稳定,对象地,也说明平均查询速度会B树会比B+树快(B+树所有数据都在叶子节点,也就意味着每一次查询都需要完整地从根节点走到叶子节点,而B树中所需查询数据很可能在非叶子节点,并且查询到所需数据之后会结束查询,所以平均速度会快)
    • B+树查询范围更加方便(B+树同层级之间由链表连接,可以很方便地在同层级之间进行左右移动)
  • dropdeletetruncate的区别:

    • drop可以用来删除表或数据库,并且将表所占用的空间全部释放;delete只删除记录,truncate会清除表数据并重置id从1开始
    • truncatedelete只删除数据不删除表的结构,drop语句将删除表的结构被依赖的约束constrain、触发器trigger,依赖于该表的存储过程/函数将保留,但是变为invalid状态
    • 速度上来说: drop>truncate>delete
    • 使用上,想删除部分数据行用delete,想删除表用drop,想保留表而将所有数据删除,如果和事务无关,用truncate即可,如果和事务有关,或者想触发tigger,还是用delete
    • deleteDML语句,不会自动提交,drop/truncateDDL语句,执行后会自动提交
  • MySQL使用什么记录货币: NMERICDECIMAL(这两个类型是作为字符串存储,而不是作为二进制浮点数,所以可以保持精度)

  • 事务的四个特征:

    • 原子性: 整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样
    • 一致性: 在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏
    • 隔离性: 隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同时间内,执行相同的功能,事务的隔离性将确保每一个事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化成序列化请求,使得在同一时间仅有一个请求用于同一数据
    • 持久性: 在事务完成之后,该事务所对数据库做的更改便持久的保存在数据库之中,并不会被回滚
  • 视图是虚拟的表,视图只包含使用时动态检索数据的查询,不包含任何列或数据;使用视图可以简化复杂的操作,隐藏具体的细节,保护数据;视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有order by而对视图再次进行order by,则原先排序会被覆盖

  • 触发器主要是通过事件执行触发而被执行的,而存储过程可以通过存储过程名称被直接调用

  • 索引是一种特殊的查询表,数据库的搜索引擎可以利用它加速对数据的检索,它类似于显示生活中书的目录,不需要查询整本书内容就可以找到想要的数据;索引可以是唯一的,创建索引允许指定单个列或者多个列,缺点是它减慢了数据录入的速度,同时也增加了数据库的尺寸大小

  • union在进行表连接后会筛选掉重复的记录,所以后在表连接后对所产生的结果集及逆行排序运算,删除重估的记录再返回结果;union all会显示重复结果,仅仅是两个结果合并后返回,所以效率高

  • 三大范式:

    • 第一范式: 每个列都不可以再拆分
    • 第二范式: 在第一范式的基础上,非主键列完全依赖于主键,而不能依赖于主键的一部分
    • 第三范式: 在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键
  • SQL优化

    优化成本: 硬件 > 系统配置 > 数据库表结构 > SQL及索引

    优化效果: 硬件 < 系统配置 < 数据库表结构 < SQL及索引

  • MySQL层优化五原则:

    • 减少数据访问: 设置合理的字段类型,启动压缩,通过索引访问等减少磁盘IO
    • 返回更少的数据: 只返回需要的字段和数据分页处理,减少磁盘IO及网络IO
    • 减少交互次数: 批量DML操作,函数存储等减少数据连接次数
    • 减少服务器CPU开销: 尽量减少数据库排序操作以及全表查询,减少CPU内存占用
    • 利用更多资源: 使用表分区,可以增加并行操作,更大限度利用CPU资源
  • 总结来说,SQL优化关键:

    • 最大化利用索引
    • 尽可能避免全表扫描
    • 减少无效数据的查询
  • SELECT语句的语法顺序:

    SELECT 
    DISTINCT <select_list>
    FROM <left_table>
    <join_type> JOIN <right_table>
    ON <join_condition>
    WHERE <where_condition>
    GROUP BY <group_by_list>
    HAVING <having_condition>
    ORDER BY <order_by_condition>
    LIMIT <limit_number>
    
  • SELECT语句的执行顺序:

    FROM
    <表名> # 选取表,将多个表数据通过笛卡尔积变成一个表。
    ON
    <筛选条件> # 对笛卡尔积的虚表进行筛选
    JOIN <join, left join, right join...> 
    <join> # 指定join,用于添加数据到on之后的虚表中,例如left join会将左表的剩余数据添加到虚表中
    WHERE
    <where条件> # 对上述虚表进行筛选
    GROUP BY
    <分组条件> # 分组
    <SUM()等聚合函数> # 用于having子句进行判断,在书写上这类聚合函数是写在having判断里面的
    HAVING
    <分组筛选> # 对分组后的结果进行聚合筛选
    SELECT
    <返回数据列表> # 返回的单列必须在group by子句中,聚合函数除外
    DISTINCT
    # 数据除重
    ORDER BY
    <排序条件> # 排序
    LIMIT
    <行数限制>
    
  • 避免不走索引的场景(仅推荐在数据量较为庞大的时候进行SQL优化):

    • 尽量避免在字段的开头进行模糊查询,比如name like '%张%',可能会导致数据库引擎放弃索引进行全表查询,如果确实需要进行字段开头的模糊查询,推荐使用:
      • 使用MySQL内置函数INSTR来匹配,作用类似于Java中的indexOf
      • 使用FullText全文索引,用match against检索
      • 数据量较大的情况,建议使用ES等来搜索
      • 当表数据量较少时,直接用like ‘%xx%’就行,几乎感觉不到性能损耗
    • 尽量避免使用innot in,可能会导致走全表索引,可以使用exists
    • 尽量避免使用or,会导致数据库索引放弃索引进行全表扫描,可以用union代替or
    • 尽量避免进行null值的判断,会导致数据库引擎放弃索引进行全表扫描,可以对字段赋初值0
    • 尽量避免在where条件中等号的左侧进行表达式、函数操作age - 2 = 20,会导致数据库索引放弃索引进行全表扫描
    • 数据量较大时,避免使用where 1 = 1进行条件拼接,会导致数据库索引放弃索引进行全表扫描
    • 查询条件不能用<>或者!=
    • where条件仅包含复合索引非前置列
    • 隐式类型转换造成不使用索引(where name = 123)
    • order by条件要与where条件一致,否则order by不会利用索引进行排序
  • 其他优化

    • 避免出现select *
    • 避免使用不确定结果的函数(now()current_user())
    • 多表关联查询时,小表在前,大表在后
    • 使用表的别名
    • where子句替换having子句
    • 调整where子句中的连接顺序(MySQL采用从左往右,自上而下的顺序解析where子句),应将过滤数多的条件放在前面
  • 增删改DML语句优化

    • 大批量插入数据时推荐使用多个值的insert语句,不建议使用分开的insert,原因:

      • 减少SQL语句解析的操作
      • 在特定场景可以减少对数据库的连接次数
      • SQL语句较短,可以减少网络传输的IO
    • 适当使用commit可以释放事务占用的资源而减少消耗,commit可以释放的资源有:

      • 事务占用的undo数据块
      • 事务在redo log中记录的数据块
      • 释放事务增加的,减少锁争用影响性能,特别是在需要使用delete删除大量数据的时候,必须分解删除量并定期commit
    • 避免重复查询更新的数据

      业务中经常出现的更新行的同时又希望获取该行信息的需求,如更新一行记录的时间戳,并同时查询到该时间戳是什么:

      简单方法,但是需要访问数据库两次:

      update t1 set time = now() where id = 1
      select time from t1 where id = 1
      

      使用变量:

      update t1 set time = now() where id = 1 and @not: = now();
      select @now();
      
  • 查询条件优化

    • 对于复杂的查询,可以使用中间临时表暂存数据

    • 优化group by语句,如果显式包括一个相同的列的order by子句,MySQL可以自行对其进行优化

      -- order by null 禁止排序
      select col1, col2, count(*) from t1 group by col1, col2 order by null;
      
    • 优化join语句

    • 优化union查询

    • 拆分复杂SQL为多个小SQL,避免大事务

    • 使用truncate代替delete

    • 使用合理的分页方式进行分页

  • 建表优化

    • 在表中简历索引,优先考虑whereorder by使用到的字段
    • 尽量使用数字型字段。若只含数据信息的字段尽量不要设计为字符型
    • 查询数据量大的表会造成查询缓慢,主要的原因是扫描行数过多(可以使用比如分段分页查询的方式)
    • varchar/nvarchar代替char/nchar
  • 使用比较多的连接方式

    • left join(左外连接或左连接): 给定两张表A和表B,连接条件为主键和从表中的外键相等,也就是通过相同的字段连接,可以查询的范围是左半部分和重叠的部分
    • right join(右外连接或右连接): 给定两张表A和表B,连接条件为主键和从表中的外键相等,也就是通过相同的字段连接,可以查询的范围是右半部分和重叠的部分
    • inner join(内连接): 连接条件为两表中相同的字段,查询结果为量表重叠的记录
    • self join(自连接): 针对相同的表进行的连接被称为自连接
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值