2022年Java面试题基础篇

一丶基础知识

1.1 什么是面向对象,面向对象的特征,面向对象和面向过程的区别
面向对象更注重事情的每一个步骤以及顺序,更注重有哪些参与者(对象),及各自需要做什么;面向过程是分析出解决问题所需要的步骤,然后按这些步骤一步步的实现下去,使用的时候一个一个调用就可以了;区别从概念就很明显了,前者注重的是参与的对象,后者注重的是步骤;面向对象的特点就是易维护,耦合度相对于面向过程低,系统更加灵活,并且具有一些特性;面向过程特点就是性能比面向对象高,类调用时需要实例化,所以比较消耗资源。
面向对象的特性有继承,封装,多态,(抽象)。继承就是从已有类得到继承信息创建新类的过程,提供继承信息的类被称为父类(超类、基类),得到继承信息的类被称为子类(派生类),继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。封装通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口;面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。多态性是指允许不同子类型的对象对同一消息作出不同的响应;简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当 A 系统访问 B 系统提供的服务时, B 系统有多种提供服务的方式,但一切对 A 系统来说都是透明的。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事: 1. 方法重写(子类继承父类并重写父类中已有的或抽象的方法); 2. 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为。
1.2Java的基本数据类型有哪些 取值范围
1.3 JDK,JRE,JVM的区别和联系
JDK:Java Development Kit java开发工具包,是整个Java的核心,包括了Java运行环境JRE,Java工具和Java基础类库
JRE:Java Runtime Environment (Java运行时环境)是运行Java程序所必须的环境的集合,包含Java虚拟机和Java程序的一些核心类库
JVM:Java Virtual Machine(Java虚拟机)是整个Java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序
具体看下图
在这里插入图片描述
通过上图可以看出来 JDK包含JRE JRE包含JVM
1.3 重载和重写的区别
重载:在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
重载 总结:
1.重载Overload是一个类中多态性的一种表现
2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)
3.重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准
重写:从字面上看,重写就是 重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。(抛出的异常范围小于等于父类, 访问修饰符范围大于等于父类; 如果父类方法访问修饰符为 private则子类就不能重写该方法。 )
重写 总结:
1.发生在父类与子类之间
2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同
3.访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
4.重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常
区别:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分。
1.4 ==和equals的区别
==的作用:
基本类型: 比较的就是值是否相同
引用类型: 比较的就是地址值是否相同
equals 的作用:
引用类型: 默认情况下, 比较的是地址值。
特: equals 会在String、 Integer、 Date 这些类库中 被重写, 比较的是内容而不是地址!
也就是说 == 比较的是两个字符串内存地址( 堆内存) 的数值是否相等, 属于数值比较; equals(): 比较
的是两个字符串的内容, 属于内容比较。

1.5 String,StringBuffer,StringBuilder三者之间的区别
String是被final修饰的,不可变,每次操作都会产生新的String对象,String对象是不可变得,也就可以理解为常量,线程安全
StringBuffer和StringBuilder都是在原对象上操作
StringBuffer是线程安全的,之所以线程安全是因为它的方法都是synchronized修饰的,StringBuilder是线程不安全的;性能:StringBuilder>StringBuffer>String
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类, 定义了一些字符串的基
本操作, 如 expandCapacity、 append、 insert、 indexOf 等公共方法。
小结:
( 1) 如果要操作少量的数据用 String;
( 2) 多线程操作字符串缓冲区下操作大量数据用 StringBuffer;
( 3) 单线程操作字符串缓冲区下操作大量数据用 StringBuilder。
1.6 final finally finalize的区别
final:用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承。
finally:异常处理语句结构的一部分,表示总是执行。
finalize:Object 类的一个方法,在垃圾回收器执行的时候会调用被回收对象的此方法,可以覆盖此方法
提供垃圾收集时的其他资源回收,例如关闭文件等。该方法更像是一个对象生命周期的临终方法,当该方法
被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对
象“死亡”,这是一个被动的方法(其实就是回调方法),不需要我们调用
1.7 接口和抽象类的区别是什么
相同:(1)不能实例化
(2)可以将抽象类和接口类型作为引用类型
(3)一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类
不同:
抽象类:
(1)抽象类中可以定义构造器
(2)可以有抽象方法和具体方法
(3)接口中的成员全都是 public 的
(4)抽象类中可以定义成员变量
(5)有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法
(6)抽象类中可以包含静态方法
(7)一个类只能继承一个抽象类
接口:
(1)接口中不能定义构造器
(2)方法全部都是抽象方法
(3)抽象类中的成员可以是private,default,protected,public
(4)接口中定义的成员变量实际上都是常量
(5)接口中不能有静态方法
(6)一个类可以实现多个接口
接口的设计目的:是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为(共有的特征)。它只约束了行为的有无,但不对如何实现行为进行限制。
而抽象类的设计目的是代码复用,当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集记为B),可以让这些类都派生于一个抽象类。在这个 抽象类中实现了B,避免让所有的的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现,正是因为A-B在这里没有实现,所以抽象类不允许实例化出来,否则当调用A-B的时候,无法执行。
抽象类是对类本质的抽象,表达的是is a的关系,比如 Porsche is Car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
而接口是对行为的抽象,表达的是like a的关系,比如 Birds like a Aircraft(像飞行器一样可以飞),但其本质上 is a bird ,接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁,是如何实现的,接口并不关心。
使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
抽象类的功能远远超过接口,但是定义抽象类的代价高,因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上弱化了很多,但是它只是针对一个动作的描述,而且你可以在一个类中同事实现多个接口,在设计阶段会降低难度
1.8 throw &&throws
throw:throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。
throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行 throw 一定是抛出了某种异常
throws:throws 语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。
throws 主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。
throws 表示出现异常的一种可能性,并不一定会发生这种异常。
1.9 反射
在 Java 中的反射机制是指在运行状态中, 对于任意一个类都能够知道这个类所有的
属性和方法; 并且对于任意一个对象, 都能够调用它的任意一个方法; 这种动态获取信息以及动
态调用对象方法的功能成为 Java 语言的反射机制。
获取 Class 对象的 3 种方法:
调用某个对象的 getClass()方法
Personp=newPerson();
Classclazz=p.getClass();
调用某个类的 class 属性来获取该类对应的 Class 对象
Classclazz=Person.class;
使用 Class 类中的 forName()静态方法(最安全/性能最好)
Classclazz=Class.forName(“类的全路径”);(最常用)
1.10 JDK8 新特性
(1)lambda表达式
(2)方法引用
(3)函数式接口
(4)接口允许定义默认方法和静态方法
(5)StreamAPI
(6)日期/时间类改进
(7)Optional类
(8)Java8 Base64实现
1.11 Java的常见异常
Throwable 是所有 Java 程序中错误处理的父类, 有两种资类: Error 和 Exception。
Error: 表示由 JVM 所侦测到的无法预期的错误, 由于这是属于 JVM 层次的严重错误, 导致 JVM
无法继续执行, 因此, 这是不可捕捉到的, 无法采取任何恢复的操作, 顶多只能显示错误信息。
Exception: 表示可恢复的例外, 这是可捕捉到的。
1.运行时异常: 都是 RuntimeException 类及其子类异常, 如 ullPointerException(空指针异
常)、 IndexOutOfBoundsException(下标越界异常)等, 这些异常是不检查异常, 程序中可以选择捕获
处理, 也可以不处理。 这些异常一般是由程序逻辑错误引起的, 程序应该从逻辑角度尽可能避免这类
异常的发生。 运行时异常的特点是 Java 编译器不会检查它, 也就是说, 当程序中可能出现这类异常,
即使没有用 try-catch 语句捕获它, 也没有用 throws 子句声明抛出它, 也会编译通过。
2.非运行时异常 ( 编译异常) : 是 RuntimeException 以外的异常, 类型上都属于 Exception
类及其子类。 从程序语法角度讲是必须进行处理的异常, 如果不处理, 程序就不能编译通过。 如
IOException、 SQLException 等以及用户自定义的 Exception 异常, 一般情况下不自定义检查异常。
见的 RunTime 异常几种如下:
NullPointerException-空指针引用异常
ClassCastException-类型强制转换异常。
IllegalArgumentException-传递非法参数异常。
ArithmeticException-算术运算异常
ArrayStoreException-向数组中存放与声明类型不兼容对象异常
IndexOutOfBoundsException-下标越界异常
NegativeArraySizeException-创建一个大小为负数的数组错误异常
NumberFormatException-数字格式异常
SecurityException-安全异常
UnsupportedOperationException-不支持的操作异常
在这里插入图片描述

二丶集合

2.1 常见的数据结构
在这里插入图片描述
简单来说
数组是最常用的数据结构,数组的特点是长度固定,数组的大小固定后就无法扩容了,数组只能存储一种类型的数据,添加删除的操作慢,因为要移动其他的元素。
栈是一种基于先进后出(FILO) 的数据结构, 是一种只能在一端进行插入和删除操作的特殊线性表。 它按照先进后出的原则存储数据, 先进入的数据被压入栈底, 最后的数据在栈顶, 需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来) 。
队列是一种基于先进先出(FIFO) 的数据结构, 是一种只能在一端进行插入, 在另一端进行删除操作的特殊线性表, 它按照先进先出的原则存储数据, 先进入的数据, 在读取数据时先被读取出来。
链表是一种物理存储单元上非连续、 非顺序的存储结构, 其物理结构不能只表示数据元素的逻辑顺序, 数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 链表由一系列的结节(链表中的每一个元素称为结点) 组成, 结点可以在运行时动态生成。 根据指针的指向, 链表能形成不同的结构,例如单链表, 双向链表, 循环链表等。
树是我们计算机中非常重要的一种数据结构, 同时使用树这种数据结构, 可以描述现实生活中的很多事物, 例如家谱、 单位的组织架构等等。 有二叉树、 平衡树、 红黑树、 B 树、 B+树。散列表, 也叫哈希表, 是根据关键码和值(key 和 value)直接进行访问的数据结构, 通过 key 和value 来映射到集合中的一个位置, 这样就可以很快找到集合中的对应元素。
堆是计算机学科中一类特殊的数据结构的统称, 堆通常可以被看作是一棵完全二叉树的数组对象。
图的定义: 图是由一组顶点和一组能够将两个顶点相连的边组成的
以下为详细介绍
2.1.1 数组
数组时刻恶意在内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始,例如下面这段代码就是将数组的第一个元素赋值为1
int[] data =new int[100];data[0]=1;
优点:
(1)按照索引查询元素速度快
(2)按照索引遍历数组方便
缺点:
(1)数组的大小固定后就无法扩容了
(2)数组只能存储一种数据类型
(3)添加删除的操作慢,因为要移动其他的元素
适用场景:
频繁查询,对存储空间要求不大,很少增加和删除的情况
2.1.2 栈
在这里插入图片描述栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。 栈的特点是:先进后出,或者说是后进先出,从栈顶放入元素的操作叫入栈,取出元素叫出栈。
栈的结构就像一个集装箱,越先放进去的东西越晚才能拿出来,所以,栈常应用于实现递归功能方面的场景,例如斐波那契数列。
2.1.3 队列
在这里插入图片描述
队列与栈一样,也是一种线性表,不同的是,队列可以在一端添加元素,在另一端取出元素,也就是:先进先出。从一端放入元素的操作称为入队,取出元素为出队,示例图如下:

使用场景:因为队列先进先出的特点,在多线程阻塞队列管理中非常适用。
2.1.4 链表
在这里插入图片描述
链表是物理存储单元上非连续的、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,每个元素包含两个结点,一个是存储元素的数据域 (内存空间),另一个是指向下一个结点地址的指针域。根据指针的指向,链表能形成不同的结构,例如单链表,双向链表,循环链表等。
优点:
链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;
添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快;
缺点:
因为含有大量的指针域,占用空间较大;
查找元素需要遍历链表来查找,非常耗时。
适用场景:
数据量较小,需要频繁增加,删除操作的场景
2.1.5 树
在这里插入图片描述
树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做 “树” 是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
(1)每个节点有零个或多个子节点;
(2)没有父节点的节点称为根节点;
(3)每一个非根节点有且只有一个父节点;
(4)除了根节点外,每个子节点可以分为多个不相交的子树;
在日常的应用中,我们讨论和用的更多的是树的其中一种结构,就是二叉树。
二叉树是树的特殊一种,具有如下特点:
(1)每个结点最多有两颗子树,结点的度最大为2。
(2)左子树和右子树是有顺序的,次序不能颠倒。
(3)即使某结点只有一个子树,也要区分左右子树。
二叉树是一种比较有用的折中方案,它添加,删除元素都很快,并且在查找方面也有很多的算法优化,所以,二叉树既有链表的好处,也有数组的好处,是两者的优化方案,在处理大批量的动态数据方面非常有用。
扩展:
二叉树有很多扩展的数据结构,包括平衡二叉树、红黑树、B+树等,这些数据结构二叉树的基础上衍生了很多的功能,在实际应用中广泛用到,例如mysql的数据库索引结构用的就是B+树,还有HashMap的底层源码中用到了红黑树。这些二叉树的功能强大,但算法上比较复杂,想学习的话还是需要花时间去深入的。

2.1.6 散列表
散列表,也叫哈希表,是根据关键码和值 (key和value) 直接进行访问的数据结构,通过key和value来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素。
记录的存储位置=f(key)
这里的对应关系 f 成为散列函数,又称为哈希 (hash函数),而散列表就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里,这种存储空间可以充分利用数组的查找优势来查找元素,所以查找的速度很快。
哈希表在应用中也是比较常见的,就如Java中有些集合类就是借鉴了哈希原理构造的,例如HashMap,HashTable等,利用hash表的优势,对于集合的查找元素时非常方便的,然而,因为哈希表是基于数组衍生的数据结构,在添加删除元素方面是比较慢的,所以很多时候需要用到一种数组链表来做,也就是拉链法。拉链法是数组结合链表的一种结构,较早前的hashMap底层的存储就是采用这种结构,直到jdk1.8之后才换成了数组加红黑树的结构,其示例图如下:
在这里插入图片描述
从图中可以看出,左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
哈希表的应用场景很多,当然也有很多问题要考虑,比如哈希冲突的问题,如果处理的不好会浪费大量的时间,导致应用崩溃。
2.1.7 堆
堆是一种比较特殊的数据结构,可以被看做一棵树的数组对象,具有以下的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。
堆的定义如下:n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。
(ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2),满足前者的表达式的成为小顶堆,满足后者表达式的为大顶堆,这两者的结构图可以用完全二叉树排列出来,示例图如下:
在这里插入图片描述因为堆有序的特点,一般用来做数组中的排序,称为堆排序。
2.1.8 图
图是由结点的有穷集合V和边的集合E组成。其中,为了与树形结构加以区别,在图结构中常常将结点称为顶点,边是顶点的有序偶对,若两个顶点之间存在一条边,就表示这两个顶点具有相邻关系。
按照顶点指向的方向可分为无向图和有向图:

在这里插入图片描述
图是一种比较复杂的数据结构,在存储数据上有着比较复杂和高效的算法,分别有邻接矩阵 、邻接表、十字链表、邻接多重表、边集数组等存储结构,这里不做展开,读者有兴趣可以自己学习深入。
2.2 集合和数组的区别
区别: 数组长度固定集合长度可变
数组中存储的是同一种数据类型的元素, 可以存储基本数据类型, 也可以存储引用数据类型;
集合存储的都是对象, 而且对象的数据类型可以不一致。 在开发当中一般当对象较多的时候, 使用集合来存储对象 。
2.3 List 和 Map、 Set 的区别
List 和 Set 是存储单列数据的集合, Map 是存储键值对这样的双列数据的集合;
List 中存储的数据是有顺序的, 并且值允许重复;
Map 中存储的数据是无序的, 它的键是不允许重复的, 但是值是允许重复的;
Set 中存储的数据是无顺序的, 并且不允许重复, 但元素在集合中的位置是由元素的 hashcode
决定, 即位置是固定的(Set 集合是根据 hashcode 来进行数据存储的, 所以位置是固定的, 但是这
个位置不是用户可以控制的, 所以对于用户来说 set 中的元素还是无序的) 。
2.4 Collection和map的区别
容器内每个位置存储的元素个数不同
collection类型者,每个位置只有一个元素
map类型者,持有k-v pair(一对的) 像个小型数据库pair
2.5 List和map,set的实现类
在这里插入图片描述
Connection 接口:
List 有序、 可重复
ArrayList
优点:底层数据结构是数组, 查询快, 增删慢。
缺点:线程不安全, 效率高
Vector
优点:底层数据结构是数组, 查询快, 增删慢。
缺点:线程安全, 效率低,已给舍弃了
LinkedList:双向链表,由一个个node对象组成,每一个node持有前后节点的引用,当前元素对象,前一个节点信息,后一个节点信息
优点:底层数据结构是链表, 查询慢, 增删快。
缺点:线程不安全, 效率高
当LinkedList添加一个元素时,会默认的往LinkedList最后一个节点后添加,具体步骤为
(1)获得最后一个节点last作为当前节点l用当前节点1
(2)添加参数e、null创建一个新的Node对象
(3)将新创建的Node对象链接到最后节点,也就是last
(4)如果当前的LinkedList为空,那么添加的node就是first,也是last
(5)当前LinkedList的size+1,表示结构改变次数的对象modCount+1
整个添加过程中,系统只做了两件事情,添加一个新的节点,然后保存该节点和前节点的引用关系。
LinkedList的删除中,也是改变节点之间的引用关系去实现的,具体逻辑整理如下:
(1)如果前一个节点prev为null,即第一个节点元素,则链表first = x下一个节点;
(2)如果前一个节点prev不为null,即不是第一个节点元素,则将当前节点的next赋值给prev.next,x.prev置为null,也就是当前节点x的prev和next和当前链表中的其他元素不存在任何联系;
(3)如果下一个节点next为null,即为最后一个元素,则链表last = x前一个节点;
(4)如果下一个节点next不为null,即不为最后一个元素,则将当前节点的prev赋值给next.prev,同样当前节点x的prev和next和当前链表中的其他元素不存在任何联系;

Set 无序,唯一
HashSet
底层数据结构是哈希表。 (无序,唯一)
如何来保证元素唯一性?
依赖两个方法: hashCode()和 equals()
LinkedHashSet
底层数据结构是链表和哈希表。 (FIFO 插入有序,唯一)
(1)由链表保证元素有序
(2)由哈希表保证元素唯一
TreeSet
底层数据结构是红黑树。 (唯一, 有序)
(1)如何保证元素排序的呢?
自然排序
比较器排序
(2)如何保证元素唯一性的呢?
根据比较的返回值是否是 0 来决定
Map 接口有四个实现类:
HashMap:基于哈希表的Map接口实现,非线程安全,高效,支持null值和null键,线程不安全
HashTable:线程安全,低效,不支持null值和null键
LinkedHashMap:线程不安全,是HashMap的一个子类,保存了记录的插入顺序
TreeMap:能够把它保存的记录根据键排序,默认是键值的升序排序,线程不安全
2.6 ArrayList 扩容机制

2.7 HashMap的底层原理
2.7.1 1.7与1.8有何不同
1.7的时候实现方式是数组+链表 1.8开始 改为数组+链表或者数组+红黑树
2.7.2 为什么要用红黑树,为何不一上来就树化,树化阈值为什么是8,何时会树化,何时会退化为链表
(1)红黑树用来避免DOS攻击,防止链表超长时性能下降,树化应当是偶然情况
①hash表的查找,更新的时间复杂度是O(1),而红黑树的查找,更新的时间复杂度是O(log2n),TreeNode占用空间也比普通Node的大,如非必要,尽量使用链表
②hash值如果足够随机,则在hash表内按泊松,在负载因子0.75,长度超过8的链表出现概率是亿分之6,选择8就是为了让树化几率足够小
(2)树化两个条件:链表长度超过树化阈值;数组容量>=64
(3)退化情况1:在扩容时如果拆分树时,树元素个数<=6 则会退化链表
(4)退化情况2:remove树节点是,若root,root.left,root.right,root.left.left有一个null,也会退化链表
2.7.3 hashmap的负载因子为什么默认是0.75
HashMap的底层是哈希表,是存储键值对的结构类型,它需要通过一定的计算才可以确定数据在哈希表中的存储位置;一般的数据结构,不是查询快就是插入快,HashMap就是一个插入慢、查询快的数据结构。但这种数据结构容易产生两种问题:① 如果空间利用率高,那么经过的哈希算法计算存储位置的时候,会发现很多存储位置已经有数据了(哈希冲突);② 如果为了避免发生哈希冲突,增大数组容量,就会导致空间利用率不高。
而加载因子就是表示Hash表中元素的填满程度。加载因子=填入表中的元素个数/散列表的长度
加载因子越大,填满的元素越多,空间利用率越高,但发生冲突的机会变大了;
加载因子越小,填满的元素越少,冲突发生的机会减小,但空间浪费了更多了,而且还会提高扩容rehash操作的次数。
负载因子太小了浪费空间并且会发生更多次数的resize,太大了哈希冲突增加会导致性能不好,所以0.75只是一个折中的选择。
2.7.4 map.put(k,v)实现原理
( 1) 首先将 k,v 封装到 Node 对象当中( 节点) 。
( 2) 先调用 k 的 hashCode()方法得出哈希值, 并通过哈希算法转换成数组的下标。
( 3) 下标位置上如果没有任何元素, 就把 Node 添加到这个位置上。 如果说下标对应的位置上有链表。 此时, 就会拿着 k 和链表上每个节点的 k 进行 equal。 如果所有的 equals 方法返回都是 false, 那么这个新的节点将被添加到链表的末尾。 如其中有一个 equals 返回了 true, 那么这个节点的 value 将会被覆盖。

2.7.5 map.get(k,v)实现原理
(1)、 先调用 k 的 hashCode()方法得出哈希值, 并通过哈希算法转换成数组的下标。
(2)、 在通过数组下标快速定位到某个位置上。 重点理解如果这个位置上什么都没有, 则返回 null。 如果这个位置上有单向链表, 那么它就会拿着参数 K 和单向链表上的每一个节点的 K 进行 equals, 如果所有 equals 方法都返回false, 则 get 方法返回 null。 如果其中一个节点的 K 和参数 K 进行 equals 返回 true, 那么此时该节点的 value就是我们要找的 value 了, get 方法最终返回这个要找的 value。
2.7.6 如何解决hash冲突
2.7.7 HashMap和CurrentHashMap的区别
区别对比一(HashMap 和 HashTable 区别):
1、 HashMap 是非线程安全的, HashTable 是线程安全的。
2、 HashMap 的键和值都允许有 null 值存在, 而 HashTable 则不行。
3、 因为线程安全的问题, HashMap 效率比 HashTable 的要高。
4、 Hashtable 是同步的, 而 HashMap 不是。 因此, HashMap 更适合于单线程环境, 而 Hashtable
适合于多线程环境。 一般现在不建议用 HashTable:
①是 HashTable 是遗留类, 内部实现很多没优化和冗余。
②即使在多线程环境下, 现在也有同步的 ConcurrentHashMap 替代, 没有必要因为是多线程
而用 HashTable。
区别对比二(HashTable 和 ConcurrentHashMap 区别):
HashTable 使用的是 Synchronized 关键字修饰, ConcurrentHashMap 是 JDK1.7 使用了锁分段技
术来保证线程安全的。 JDK1.8ConcurrentHashMap 取消了 Segment 分段锁, 采用 CAS 和 synchronized
来保证并发安全。 数据结构跟 HashMap1.8 的结构类似, 数组+链表/红黑二叉树。
synchronized 只锁定当前链表或红黑二叉树的首节点, 这样只要 hash 不冲突, 就不会产生并发,
效率又提升 N 倍。
2.8 CurrentHashMap

三丶线程

3.1 BIO、 NIO、 AIO 有什么区别?
BIO: BlockIO 同步阻塞式 IO, 就是我们平常使用的传统 IO, 它的特点是模式简单使用方便, 并发处理能低。
NIO: NewIO 同步非阻塞 IO, 是传统 IO 的升级, 客户端和服务器端通过 Channel( 通道) 通讯,实现了多路复用。
AIO: AsynchronousIO 是 NIO 的升级, 也叫 NIO2, 实现了异步非堵塞 IO, 异步 IO 的操作基于事件和回调机制。
3.2 Threadloal 的原理 ThreadLocal: 为共享变量在每个线程中创建一个副本, 每个线程都可以访问自己内部的副本变量。 通过 threadlocal 保证线程的安全性。
其实在 ThreadLocal 类中有一个静态内部类 ThreadLocalMap(其类似于 Map), 用键值对的形式存储每一个线程的变量副本, ThreadLocalMap 中元素的 key 为当前 ThreadLocal 对象, 而 value 对应线程的变量副本。ThreadLocal 本身并不存储值, 它只是作为一个 key 保存到 ThreadLocalMap 中, 但是这里要注意的是它作为一个 key 用的是弱引用, 因为没有强引用链, 弱引用在 GC 的时候可能会被回收。 这样就会在 ThreadLocalMap 中存在一些 key 为 null 的键值对( Entry) 。 因为 key 变成 null 了, 我们是没法访问这些 Entry 的, 但是这些 Entry 本身是不会被清除的。 如果没有手动删除对应 key 就会导致这块内存即不会回收也无法访问, 也就是内存泄漏。
使用完 ThreadLocal 之后, 记得调用 remove 方法。 在不使用线程池的前提下, 即使不调用 remove方法, 线程的"变量副本"也会被 gc 回收, 即不会造成内存泄漏的情况。
3.3 同步锁,死锁,乐观锁,悲观锁
同步锁:
当多个线程同时访问同一个数据时, 很容易出现问题。 为了避免这种情况出现, 我们要保证线程同步互斥, 就是指并发执行的多个线程, 在同一时间内只允许一个线程访问共享数据。 Java 中可以 使用 synchronized 关键字来取得一个对象的同步锁。
死锁:
何为死锁, 就是多个线程同时被阻塞, 它们中的一个或者全部都在等待某个资源被释放。
乐观锁:
总是假设最好的情况, 每次去拿数据的时候都认为别人不会修改, 所以不会上锁, 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据, 可以使用版本号机制和 CAS 算法实现。 乐观锁适用于多读的应用类型, 这样可以提高吞吐量, 像数据库提供的类似于 write_conditio 机制, 其实都是提供的乐观锁。 在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
悲观锁:
总是假设最坏的情况, 每次去拿数据的时候都认为别人会修改, 所以每次在拿数据的时候都会上锁, 这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用, 其它线程阻塞,用完后再把资源转让给其它线程) 。 传统的关系型数据库里边就用到了很多这种锁机制, 比如行锁,表锁等, 读锁, 写锁等, 都是在做操作之前先上锁。 Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
3.4 synchronized底层实现原理
synchronized 可以保证方法或者代码块在运行时, 同一时刻只有一个方法可以进入到临界区, 同时它还可以保
证共享变量的内存可见性。
Java 中每一个对象都可以作为锁, 这是 synchronized 实现同步的基础:
(1)普通同步方法, 锁是当前实例对象
(2)静态同步方法, 锁是当前类的 class 对象
(3)同步方法块, 锁是括号里面的对象
3.5 synchronized 和 volatile 的区别是什么?
volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存) 中的值是不确定的, 需要从主存中读取; synchronized 则是锁定当前变量, 只有当前线程可以访问该变量, 其他线程被阻塞住。
volatile 仅能使用在变量级别; synchronized 则可以使用在变量、 方法、 和类级别的。
volatile 仅能实现变量的修改可见性, 不能保证原子性; 而 synchronized 则可以保证变量的修改可见性和原子性。
volatile 不会造成线程的阻塞; synchronized 可能会造成线程的阻塞。
volatile 标记的变量不会被编译器优化; synchronized 标记的变量可以被编译器优化。
3.6 synchronized 和 Lock 有什么区别?
首先 synchronized 是 java 内置关键字, 在 jvm 层面, Lock 是个 java 类;
synchronized 无法判断是否获取锁的状态, Lock 可以判断是否获取到锁;
synchronized 会自动释放锁(a 线程执行完同步代码会释放锁; b 线程执行过程中发生异常会释放锁), Lock 需在 finally 中手工释放锁(unlock()方法释放锁) , 否则容易造成线程死锁;用 synchronized 关键字的两个线程 1 和线程 2, 如果当前线程 1 获得锁, 线程 2 线程等待。 如果线程 1 阻塞, 线程 2 则会一直等待下去, 而 Lock 锁就不一定会等待下去, 如果尝试获取不到锁, 线 程可以不用一直等待就结束了;
synchronized 的锁可重入、 不可中断、 非公平, 而 Lock 锁可重入、 可判断、 可公平(两者皆可);
Lock 锁适合大量同步的代码的同步问题, synchronized 锁适合代码少量的同步问题。
3.7 什么是线程?线程和进程的区别?
线程: 是进程的一个实体, 是 cpu 调度和分派的基本单位, 是比进程更小的可以独立运行的基本单位。
进程: 具有一定独立功能的程序关于某个数据集合上的一次运行活动, 是操作系统进行资源分配和调度的一个独立单位。
特点: 线程的划分尺度小于进程, 这使多线程程序拥有高并发性, 进程在运行时各自内存单元相互独立, 线程之间内存共享, 这使多线程编程可以拥有更好的性能和用户体验。
3.8 创建线程有几种方式
(1)继承 Thread 类并重写 run 方法创建线程, 实现简单但不可以继承其他类
(2)实现 Runnable 接口并重写 run 方法。 避免了单继承局限性, 编程更加灵活, 实现解耦。
(3)实现 Callable 接口并重写 call 方法, 创建线程。 可以获取线程执行结果的返回值, 并且可以抛出异常。
(4)使用线程池创建( 使用 java.util.concurrent.Executor 接口)
示例代码如下:

在这里插入图片描述
3.9 Runnable和Callable的区别
主要区别
Runnable 接口 run 方法无返回值; Callable 接口 call 方法有返回值, 支持泛型
Runnable 接口 run 方法只能抛出运行时异常, 且无法捕获处理; Callable 接口 call 方法允许抛出
异常, 可以获取异常信息
3.10如何启动一个新线程,调用start和run方法的区别

在这里插入图片描述
线程对象调用 run 方法不开启线程。 仅是对象调用方法。
线程对象调用 start 开启线程, 并让 jvm 调用 run 方法在开启的线程中执行
调用 start 方法可以启动线程, 并且使得线程进入就绪状态, 而 run 方法只是 thread 的一个普通方
法, 还是在主线程中执行。
3.11 线程有哪几种状态以及各种状态之间的转换
(1)第一种是new ->新建状态。在生成线程对象,并没有调用该对象的start方法,这时线程是处于创建状态
(2)第二种是Runnable->就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。
(3)第三种是Running->运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
(4)第四种是阻塞状态,阻塞状态是线程因为某种原因放弃cpu使用权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
①等待-通过调用线程的wait()方法让线程等待某工作的完成
②超时等待-通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时,join()等待线程终止或者超时,或者I/O处理完毕时,线程重新转入就绪状态。
③同步阻塞-线程在获取synchronized同步锁失败(因为锁被其他线程所占用),它会进入同步阻塞状态。
(5)第五是dead->死亡状态:线程执行完了或者因异常退出了run方法,该线程结束生命周期。
在这里插入图片描述
3.12 线程相关的基本方法
线程相关的基本方法有wait,notify,notifyAll,sleep,join,yield等
(1)线程等待(wait)
调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意的事调用wait()方法后,会释放对象的锁,因此,wait方法一般用在同步方法或同步代码块中。
(2)线程睡眠(sleep)
sleep导致当前线程休眠,与wait方法不同的是sleep不会释放当前占有的锁,sleep(long)会导致线程进入TIMED-WAITING状态,而wait()方法会导致当前线程进入WAITING状态。
(3)线程让步(yield)
yield会使当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到CPU时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
(4)线程中断(interrupt)
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)
(5)Join等待其他线程终止
join()方法,等待其他线程终止,在当前线程中调用一个线程的join()方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待cpu的宠幸。
(6)线程唤醒(notify)
Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程, 如果所有线程都在此对象上等待, 则会选择唤醒其中一个线程, 选择是任意的, 并在对实现做出决定时发生, 线程通过调用其中一个 wait()方法, 在对象的监视器上等待, 直到当前的线程放弃此对象上的锁定, 才能继续执行被唤醒的线程, 被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll(), 唤醒再次监视器上等待的所有线程。
3.13 wait()和sleep()的区别
(1)来自不同的类
wait():来自 Object 类;
sleep():来自 Thread 类;
(2)关于锁的释放:
wait():在等待的过程中会释放锁;
sleep():在等待的过程中不会释放锁
(3)使用的范围:
wait():必须在同步代码块中使用;
sleep():可以在任何地方使用;
(4)是否需要捕获异常
wait():不需要捕获异常;
sleep():需要捕获异常;
3.14 为什么要使用线程池
在实际使用中, 线程是很占用系统资源的, 如果对线程管理不完善的话很容易导致系统问题。 因此, 在大多数
并发框架中都会使用线程池来管理线程, 使用线程池管理线程主要有如下好处:
(1)使用线程池可以重复利用已有的线程继续执行任务, 避免线程在创建销毁时造成的消耗;
(2) 由于没有线程创建和销毁时的消耗, 可以提高系统响应速度;
(3) 通过线程可以对线程进行合理的管理, 根据系统的承受能力调整可运行线程数量的大小等;
3.15 线程池的分类
在这里插入图片描述
newCachedThreadPool: 创建一个可进行缓存重复利用的线程池
newFixedThreadPool: 创建一个可重用固定线程数的线程池, 以共享的无界队列方式来运行这些
线程, 线程池中的线程处于一定的量, 可以很好的控制线程的并发量
newSingleThreadExecutor: 创建一个使用单个 worker 线程的 Executor, 以无界队列方式来运
行该线程。 线程池中最多执行一个线程, 之后提交的线程将会排在队列中以此执行
newSingleThreadScheduledExecutor: 创建一个单线程执行程序, 它可安排在给定延迟后运行命
令或者定期执行
newScheduledThreadPool: 创建一个线程池, 它可安排在给定延迟后运行命令或者定期的执行
newWorkStealingPool: 创建一个带并行级别的线程池, 并行级别决定了同一时刻最多有多少个
线程在执行, 如不传并行级别参数, 将默认为当前系统的 CPU 个数
3.16 核心参数
corePoolSize: 核心线程池的大小
maximumPoolSize: 线程池能创建线程的最大个数
keepAliveTime: 空闲线程存活时间
unit: 时间单位, 为 keepAliveTime 指定时间单位
workQueue: 阻塞队列, 用于保存任务的阻塞队列
threadFactory: 创建线程的工程类
handler: 饱和策略( 拒绝策略)
3.17 线程池的原理
在这里插入图片描述
线程池的工作过程如下:
当一个任务提交至线程池之后,
(1)线程池首先判断核心线程池里的线程是否已经满了。 如果不是, 则创建一个新的工作线程来执行任
务。 否则进入
(2)判断工作队列是否已经满了, 倘若还没有满, 将线程放入工作队列。 否则进入 3.
(3)判断线程池里的线程是否都在执行任务。 如果不是, 则创建一个新的工作线程来执行。 如果线程池
满了, 则交给饱和策略来处理任务。
3.18 拒绝策略
ThreadPoolExecutor.AbortPolicy( 系统默认) : 丢弃任务并抛出 RejectedExecutionException 异常, 让你感知到任务被拒绝了, 我们可以根据业务逻辑选择重试或者放弃提交等策略。
ThreadPoolExecutor.DiscardPolicy: 也是丢弃任务, 但是不抛出异常, 相对而言存在一定的风险, 因为我们提交的时候根本不知道这个任务会被丢弃, 可能造成数据丢失。 ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务, 然后重新尝试执行任务( 重复此过程) , 通常是存活时间最长的任务, 它也存在一定的数据丢失风险 ThreadPoolExecutor.CallerRunsPolicy: 既不抛弃任务也不抛出异常, 而是将某些任务回退到调用者, 让调用者去执行它。
3.19 线程池的关闭
关闭线程池, 可以通过 shutdown 和 shutdownNow 两个方法
原理: 遍历线程池中的所有线程, 然后依次中断
(1)shutdownNow 首先将线程池的状态设置为 STOP,然后尝试停止所有的正在执行和未执行任务的线
程, 并返回等待执行任务的列表;
(2) shutdown 只是将线程池的状态设置为 SHUTDOWN 状态, 然后中断所有没有正在执行任务的线程
3.20 同步和异步的区别
1.同步与异步
同步:同步是指一个进程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程会一直等待下去,直到收到返回信息才继续执行下去。
异步:异步是指进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态,当有信息返回的时候会通知进程进行处理,这样就可以提高执行的效率了,即异步是我们发出的一个请求,该请求会在后台自动发出并获取数据,然后对数据进行处理,在此过程中,我们可以继续做其他操作,不管它怎么发出请求,不关心它怎么处理数据。
以上总结起来,通俗地讲,也就是说,同步需要按部就班地走完一整个流程,完成一整个动作,打个比方:同步的时候,你在写程序,然后你妈妈叫你马上拖地,你就必须停止写程序然后拖地,没法同时进行。而异步则不需要按部就班,可以在等待那个动作的时候同时做别的动作,打个比方:你在写程序,然后你妈妈让你马上拖地,而这时你就贿赂你弟弟帮你拖地,于是结果同样是拖好地,你可以继续敲你的代码而不用管地是怎么拖的哈哈。
2.同步与异步适用的场景
就算是ajax去局部请求数据,也不一定都是适合使用异步的,比如应用程序往下执行时以来从服务器请求的数据,那么必须等这个数据返回才行,这时必须使用同步。而发送邮件的时候,采用异步发送就可以了,因为不论花了多长时间,对方能收到就好。总结得来说,就是看需要的请求的数据是否是程序继续执行必须依赖的数据
3.21多线程使用场景
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
主方法:
在这里插入图片描述

四丶并发

1.为什么要使用并发编程
(1)充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升
(2)方便进行业务拆分,提升系统并发能力和性能:在特殊的业务场景下,先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值