Java基础3:集合容器+网络(搬砖整理,侵权删文)

Java基础3:集合容器+网络(搬砖整理,侵权删文)

六、网络编程

1、计算机网络的体系结构

​ 在计算机网络的基本概念中,分层次的体系结构是最基本的。计算机网络体系结构的抽象概念较多,在学习时要多思考。

1、网络协议是什么?

​ 在计算机网络中要做到有条不紊的交换数据,就必须遵循一些事先约定好的规则,比如交换数据的格式、是否发送一个应答信息等。这些规则被称为网络协议。

2、为什么要对网络协议分层?

1、简化问题的难度和复杂度。由于各层之间独立,我们可以分割大问题为小问题。

2、灵活性好。当其中一层的技术变化时,只要层间接口关系保持不变,其他层不受影响。

3、易于实现和维护。

4、促进标准化工作。分开后,每层功能可以相对简单的被描述。

​ 网络协议分层的缺点:功能可能出现在多个层里,产生了额外的开销。

​ 为了使不同体系结构的计算机网络都能互联,国际标准化组织ISO于1977年提出了一个试图使各种计算机在世界范围内互联成网的标准框架,即著名的开放系统互连基本参考模型OSI/RM,简称为OSI。

​ OSI的七层协议体系结构的概念清楚,理论也较完整,但他却复杂又不实用,TCP/IP体系结构则不同,但他现在却得到了非常广泛的应用。TCP/IP是一个四层体系结构,他包含应用层、运输层、网际层、网络接口层,不够从实质上讲TCP/IP只有最上边的三层,因为最下面的网络接口层并没有什么具体内容,因此在学习学习计算机网络时采取折中的办法,即综合OSI和TCP/IP的优点,采用一种只有五层协议的体系结构,这样及简洁又能将概念阐述清楚,有时为了方便,又把最底下两层称为网络接口层。

1、TCP/IP体系结构主要包括:应用层、运输层、网际层、网络接口层。

2、五层协议的体系结构包括:应用层、运输层、网络层、数据链路层、物理层。(没有实际应用)

3、OSI七层协议模型包括:应用层、表示层、会话层、运输层、网络层、数据链路层、物理层。

在这里插入图片描述

2、TCP/IP协议族

1、应用层

​ 应用层的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统DNS,支持万维网应用的HTTP协议,支持电子邮件的SMTP协议。

2、运输层

​ 运输层的主要任务是负责像两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。运输层主要使用以下两种协议:

​ 1、传输控制协议TCP:提供面向连接的,可靠的数据传输服务。

​ 2、用户数据协议UDP:提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LnPQNxFN-1626232491688)(C:\Users\四叔\AppData\Roaming\Typora\typora-user-images\image-20210606183417244.png)]

​ 每一个应用层协议一般都会使用到两个传输层协议:

​ 运行在TCP上的协议:

HTTP:超文本传输协议,用于普通浏览

HTTPS:HTTP协议的安全版本

FTP:文件传输协议,用于文件传输

POP3:邮局协议,收邮件用

SMTP:简单邮件传输协议,用来发电子邮件

TELNET:网络电传,通过一个终端登陆到网络

SSH:用于代替安全性差的TELNET,用于加密安全登录

​ 运行在UDP上的协议:

BOOTP:启动协议,应用于无盘设备

NTP:网络事件协议,用于网络同步

DHCP:动态主机配置协议,动态配置IP地址

​ 运行在TCP和UDP协议上:

DNS:域名服务,用于完成地址查找,邮件转发等工作

3、网络层

​ 网络层的任务就是选择合适得网间路由和交换结点,确保计算机通信的数据及时传送。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组的包进行传送。在TCP/IP体系结构中,由于网络层使用IP协议,因此分组也叫IP数据报,简称数据报。

​ 互联网是由最大得异构网络通过路由器相互连接起来的。互联网使用的网络层协议时无连接得网际协议和许多路由选择协议,因此互联网的网络层也叫做网际层或IP层。

4、数据链路层

​ 简称链路层,两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要专门的链路层协议。在两个相邻节点之间传送数据时,数据链路层将网络层交下来的IP数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(同步信息、地址信息、差错信息等)。在接收数据时,控制信息是接收端能够知道一个帧从哪个比特开始和到那个比特结束。

​ 一般的web应用的通信传输流是这样的:

img

​ 发送端在层与层之间传输数据时,每经过一层时会被打上一个该层所属的首部信息。反之,接收端在层与层之间传输数据时,每经过一层时会把对应的首部信息去除。

5、物理层

​ 在物理层上所传送的数据单位是比特。物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。

3、TCP/IP协议族

​ 在互联网使用的各种协议中最重要的和最著名的就是TCP/IP两个协议。现在人们经常提到的TCP/IP并不是单指TCP和IP这两个具体的协议,而往往时表示互联网所使用的整个TCP/IP协议族。

img

​ 网络协议套件IPS是一个网络通讯模型,以及一整个网络传输协议家族,为网际服务的基础通讯架构。它常被称为TCP/IP协议族,因为该协定家族的两大核心协议:TCP(传输控制协议)和IP(网际协议),是最早通过的标准。

1、TCP的传输过程

​ TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,在发送数据之前,通信双方必须在彼此间建立起一条连接。所谓的链接,其实就是客户端和服务端保存的一份关于对方的信息,如IP地址、端口号等。

​ TCP可以看成是一种字节流,他会处理IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双发需要交换一些连接的参数。这些参数可以放在TCP头部。

​ 一个TCP连接有一个四元组构成,分别是两个IP地址和两个端口号。一个TCP里按揭通常分为三个阶段:连接、数据传输、关闭。通过三次握手建立起一个链接,通过四次挥手来关闭一个链接。当一个链接被建立或被终止时,交换的报文段只包含TCP头部,而没有数据。

2、TCP报文的头部结构

TCPHeader.png

​ 几个重点字段:

1、序号:seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。

2、确认序号:ack序号,占32位,只有ACK标志位1时,确认序号字段才有效,ack=seq+1.

3、标志位:共6个,即ACK(确认序号有效)、FIN(释放一个链接)、PSH(接收方应该尽快将这个报文交给应用层)、RST(重置链接)、SYN(发起一个新连接)、URG(紧急指针有效)。

​ 需要注意的是:

1、不要将确认序号ack与标志位ACK搞混。

2、确认方ack=发起方seq+1,两端配对。

3、三次握手

​ 三次握手的本质时确认通信双方收发数据的能力。

​ 简单理解三次握手:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AxSt60LS-1626232491691)(C:\Users\四叔\AppData\Roaming\Typora\typora-user-images\image-20210607111913749.png)]

三次握手.png

​ 第一次握手:客户端要向服务端发起连接请求,首先客户端随机生成一个起始序列号ISN(比如是100),那么客户端向服务端发送的报文段包含SYN标志位(也就是SYN=1), 序列号seq=100。

​ 第二次握手:服务端收到客户端发来的报文后,发现SYN=1,知道这是一个连接请求,于是将客户端的起始序列号100存起来,并且随机生成一个服务端的起始序列号(比如是300)。然后给客户端回复一段报文,回复报文包含SYN和ACK标志(也就是SYN=1,ACK=1)、序列号seq=300、确认号ack=101(客户端发过来的序列号+1)。

​ 第三次握手:客户端收到服务端发过来的报文后发现ACK=1并且ack=101,于是知道服务器已经接收到了序列号为100的那段报文;同时发现SYN=1,知道服务端同意了这次链接,于是就将服务端的序列号300给存下来。然后客户端再回复一段报文给服务端,报文包含ACK标志位(ACK=1)、ack=301(服务端序列号+1)、seq=101(第一次握手时发送报文是占据一个序列号的,所以这次的seq就从101开始,需要注意的是不携带数据的ACK报文是不占据一个序列号的,所以后面第一次正式发送数据时seq还是101)。当服务器收到报文后发现ACK=1并且ack=301,就知道客户端收到序列号为300的报文了,就这样客户端和服务端通过TCP建立了连接。

4、四次挥手

​ 四次挥手的目的是关闭一个链接。

四次挥手.jpeg

​ 比如客户端初始化序列号ISA=100,服务端初始化序列号ISA=300。TCP连接成功后发送了1000个字节的数据,服务端在客户端发FIN报文前总共回复了2000个字节的数据。’

​ 第一次握手:当客户端的数据都传输完成后,客户端向服务端发出连接释放报文(当然数据没发完时也可以发送连接释放报文并停止发送数据),释放连接报文包含FIN标志位(FIN=1)、序列号seq=1101(100+1+1000,其中1是连接时占的一个序列号)。需要注意的是客户端发出FIN报文段后只是不能发数据了,但还是可以正常收数据;另外FIN报文段即使不携带数据也要占据一个序列号。

​ 第二次挥手:服务端收到客户端发的FIN报文后给客户端回复确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=1102(客户端FIN报文序列号+1)、序列号seq=2300(300+2000)。此时服务端处于关闭等待状态,而不是立马给客户端发FIN报文,这个状态还要持续一段时间,因此服务端可能还有数据没有发完。

​ 第三次握手:服务端将最后数据(比如50个字节)发送完毕后就向客户端发出连接释放报文,报文包含FIN和ACK标志位(FIN=1 ACK=1)、确认号和第二次挥手一样ack=1102、序列号seq=2350(2300+50).

​ 第四次挥手:客户端收到服务端发的1FIN报文后,向服务端发出确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=1102、序列号seq=2351。注意客户端发出报文后不是立马释放TCP连接,而是要经过2MSL(最长报文段寿命的2倍时长)后才释放TCP连接。而服务端一旦接收到客户端发出的确认报文就会立马释放TCP连接,所以服务端结束TCP连接的时间要比客户端早一些。

4、常见面试题

1、为什么TCP链接的时候是三次?两次可以吗?

​ 因为要考虑连接时丢包的问题,如果是只握手两次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数据(可以理解服务端已经连接成功),而客户端一直没有收到服务端的确认报文,所以客户端就不知道是否已经准备好了(可以理解为客户端未连接成功),这种情况下客户端不会提供给服务端数据,也会忽略服务端发过来的数据。

​ 如果是三次握手,即便发生丢包也不会有问题,比如如果第三次握手客户端发的确认ack报文丢失,服务端在一段时间内没有收到确认ack报文的华就会重新进行第二次握手,也就是服务端会重发SYN报文段,客户端收到重发的报文后再次给服务端发送确认ack报文。

2、为什么TCP连接的时候是3次,关闭的时候却是4次?

​ 因为只有客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了,服务端还有没有数据发客户端是不知道的。而服务端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了,但我服务端还有一些数据没发完,等这些数据发完了服务端才能给客户端发FIN报文(所以不能一次性将确定报文和FIN报文发给客户端,就是这里多出来一次)。

3、为什么客户端发出第四次挥手的确认报文后要等2MSL的时间才能释放TCP连接?

​ 这里同样是考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文了,这样报文一去一回最长的时间就是2MSL,所以需要等这么长时间来确认服务器确实收到了。

4、如果已经建立连接,但客户端突然出现故障怎么办?

​ TCP设有一个保活计时器,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若2小时还没收到客户端的任何数据,服务器就会发赠送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没有反应,服务器就认为客户端出了故障,接着就关闭连接。

5、什么是HTTP,HTTP与HTTPS的区别

​ HTTP是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范。

​ 区别:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IcZscbsl-1626232491693)(C:\Users\四叔\AppData\Roaming\Typora\typora-user-images\image-20210607150516457.png)]

6、常见的HTTP状态码

​ HTTP状态码表示客户端HTTP请求的返回结果、标识服务器处理是否异常、表明请求出现的错误等。

​ 状态码的类别:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xpbAgPfI-1626232491694)(C:\Users\四叔\AppData\Roaming\Typora\typora-user-images\image-20210608090214236.png)]

​ 常用的HTTP状态码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CSFoj4BR-1626232491695)(C:\Users\四叔\AppData\Roaming\Typora\typora-user-images\image-20210608090246831.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-auSZJOlR-1626232491696)(C:\Users\四叔\AppData\Roaming\Typora\typora-user-images\image-20210608090258724.png)]

7、GET和POST的区别

​ 浏览器和服务器的交互是通过HTTP协议执行的,而GET和POST正是HTTP协议中的两种方法。

​ HTTP全称为Hyper Test Transfer Protocol,超文本传输协议,目的是保证浏览器与服务器之间的通信。HTTP的工作方式是客户端与服务端之间的请求-应答协议。HTTP协议中定义了浏览器与服务器进行交互的不同方法,基本方法有四种,分别是GET,POST,PUT,DELETE。这四种方法对应服务器资源的查、改、增、删。

​ GET:从服务器上获取数据,也就是所谓的查,仅仅是获取服务器资源,不进行修改。

​ POST:向服务器提交数据,这就涉及到了数据的更新,也就是更改服务器的数据。

​ PUT:向服务器新添加数据。

​ DELETE:删除服务器数据 。

GET与POST的区别:

1、GET是不安全的,因为在传输过程中,数据被放在请求URL中;POST的所有操作对用户来说都是不可见的。但是这种做法也不是绝对的,也可以是在GET请求加上request body,给POST请求带上URL参数。

2、GET请求提交的URL中的数据最多只能是2048字节,这个限度是浏览器或者服务器给添加的,http协议并没有对URL的长度进行限制,目的是为了保证服务器和浏览器能够正常运行,防止有人恶意发送请求。POST请求没有大小限制。

3、GET限制Form表单的数据集的值必须为ASCⅡ字符,POST支持整个ISO10646字符集。

4、GET执行效率比POST好,GET是form提交的默认方法。

5、GET产生一个TCP数据包,POST产生两个TCP数据包。对于GET方式请求,浏览器会把http leader和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200(返回数据)。

8、对称加密和非对称加密

​ 对称加密是指加密和解密使用同一个密钥的方式,这种方式存在最大的问题就是密钥发送问题,即如何安全的将密钥打给对方。非对称加密是指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但是私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收加密信息后,使用自己的私钥进行解密。由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来非常慢。

9、HTTP2

​ HTTP2可以提高网页的性能。

​ 在HTTP1中浏览器限制了同一个域名下的请求数量(Chrome下一般是六个),当在请求很多资源的时候,由于对头阻塞当浏览器达到最大请求数量时,剩余的资源需等待当前的六个请求完成之后才能发起请求。HTTP2中引入了多路复用技术,这个技术可以只通过一个TCP连接就可以传输所有的请求数据。多路复用技术可以绕过浏览器限制同一域名下的请求数量问题,进而提高了网页的性能。

10、Session、Cookie和Token

​ HTTP协议本身是无状态的,即服务器无法判断用户身份。

1、什么是Cookie

​ cookie是由web服务器保存用户浏览器上的小文件(key-value格式),包含用户相关的信息,客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个cookie。客户端浏览器会把cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该cookie一同提交给服务器。服务器检查该cookie,以此来辨认用户身份。

2、什么是session?

​ session是依赖cookie实现的,session是服务器端对象。

​ session是浏览器和服务器会话过程中,服务器分配的一块存储空间。服务器默认为浏览器在cookie中设置sessionid,浏览器在向服务器请求过程中传输cookie包含sessionid,服务器根据sessionid获取出会话中存储的信息,然后确定会话的身份信息。

3、cookie与session的区别

​ 存储位置与安全性:cookie数据存放在客户端上,安全性较差,session数据放在服务器上,安全性相对更高;

​ 存储空间:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,session无此限制。

​ 占用服务器资源:session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。

4、什么是Token?

​ Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码进行对比,判断用户名和密码正确与否,并作出相应的提示,在这样的背景下,Token便应运而生。

​ Token的定义:是服务端生成的一串字符串,以作客户端请求的一个令牌,当第一次登陆后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

​ 使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。

​ Token是在服务端产生的,如果前端使用用户名和密码向服务端请求认证,服务端认证成功,那么在服务端会返回Token给前端。前端可以每次请求的时候带上Token证明自己的合法地位。

5、session与token的区别

1、session机制存在服务器压力增大,CSRF跨站伪造请求攻击,扩展性不强等问题

2、session存储在服务端,token存储在客户端

3、token提供认证和手枪功能,作为身份认证,token安全性比session好

4、session这种会话存储方式只适用于客户端和服务端代码运行在同一台服务器上,token适用于项目级的前后端分离。

6、如果客户端禁止cookie能实现session吗?

​ cookie与session一般认为是两个独立的东西,session采用的是在服务器端保持状态的方案,而cookie采用的是在客户端保持状态的方案。

​ 但为什么禁用cookie就不能得到session呢?因为session使用sessionid来确定当前对话所对应的服务器session,而sessionid是通过cookie来传递的,禁用cookie相当于失去了sessionid,也就得不到session了。

​ 假定用户关闭cookie的情况下使用session,其实现方式有以下几种:

1、手动通过URL传值、隐藏表单传送sessionid。

2、用文件、数据库等形式保存sessionid,在跨页过程中手动调用。

11、Servlet

1、servlet是线程安全的吗?

​ servlet不是线程安全的,多线程并发读写会导致数据不同步问题。

​ 解决的办法是尽量不要定义name属性,而是要把name变量分别定义在doGet()和doPost()方法内。虽然使用synchronized(name){}语句块可以解决问题,但是会造成线程等待,不是很科学的方法。

​ 注意:多线程的并发得读写servlet类属性会导致数据不同步,但是如果只是并发的读取属性而不写入,则不存在不同步问题。因此servlet里的只读属性最好定义为final类型的。

2、servlet接口中的方法

​ 在Java web程序中,servlet主要负责接收用户请求HttpServletRequest,在doGet(),doPost()中做相应的处理并将回应HttpServletResponse反馈给用户。Servlet可以设置初始化参数,供Servlet内部使用。

​ servlet接口定义了五个方法,其中前三个跟servlet生命周期相关:

	void init(ServletConfig config) throws ServletException
    void service(ServletRequest req, ServletResponse resp) throws ServletException, 			java.io.IOException
    void destory()
    java.lang.String getServletInfo()
    ServletConfig getServletConfig()

3、生命周期

​ web容器加载servlet并将其实例化后,servlet生命周期开始,容器运行其init()方法进行servlet初始化;请求到达时调用servlet的service()方法,此方法会根据需要调用与请求与对应的doGet或doPost等方法;当服务器关闭或项目被卸载时服务器会将servlet实例销毁,此时会调用servlet的destroy()方法。

​ init方法和destroy方法只会执行一次,service方法在客户端每次请求时都会执行。servlet中有时会用到一些需要初始化与销毁的资源,因此可以把初始化资源的代码放入init方法中,销毁资源的代码放入与destroy方法中,这样就不需要每次处理客户端的请求都需要初始化与销毁资源。

十、集合容器

1、集合容器概述

1、什么是集合

集合框架:用于存储数据的容器。集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。

接口:表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现,从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。

实现:集合接口的具体实现,是重用性很高的数据结构。

算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。这些算法通常是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现。事实上,算法是可复用的函数。

​ 集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部分上,而不是为了让程序能正常运转而将注意力于低层设计上。通过这些在无关API之间的简易的互用性,使你免除了为改编对象或转换代码以便联合这些API而去写大量的代码。 它提高了程序速度和质量。

2、集合的特点

集合的特点主要有如下两点:

  • 对象封装数据,对象多了也需要存储。集合用于存储对象。
  • 对象的个数确定可以使用数组,对象的个数不确定的可以用集合。因为集合是可变长度的。

3、集合和数组的区别

  • 数组是固定长度的;集合可变长度的。
  • 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
  • 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

4、使用集合框架的好处

  1. 容量自增长;
  2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
  3. 允许不同 API 之间的互操作,API之间可以来回传递集合;
  4. 可以方便地扩展或改写集合,提高代码复用性和可操作性。
  5. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。

5、常见的集合有哪些

Map接口和Collection接口是所有集合框架的父接口:

1、Collection接口的子接口包括:Set接口和List接口
2、Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
3、Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
4、List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

6、List,Set,Map三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各有什么特点?

img

​ Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。

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

7、集合框架底层数据结构

Collection

List
Arraylist: Object数组
Vector: Object数组
LinkedList: 双向循环链表
Set
HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
LinkedHashSet: LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

Map

​ HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
​ LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
​ HashTable: 数组+链表组成的,数组是 HashTable的主体,链表则是主要为了解决哈希冲突而存在的
​ TreeMap: 红黑树(自平衡的排序二叉树)

8、哪些集合类是线程安全的?

vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。

statck:堆栈类,先进后出。

hashtable:就比hashmap多了个线程安全。

enumeration:枚举,相当于迭代器。

9、Java集合的快速失败机制 “fail-fast”?

​ 是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

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

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

​ 解决办法:

  1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
  2. 使用CopyOnWriteArrayList来替换ArrayList

10、怎么确保一个集合不能被修改

​ 可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。

2、Collection接口

1、List接口

1、迭代器 Iterator 是什么?

​ Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。

2、Iterator 怎么使用?有什么特点?
List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
  String obj = it. next();
  System. out. println(obj);
}

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

3、如何边遍历边移除 Collection 中的元素?

​ 边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:

Iterator<Integer> it = list.iterator();
while(it.hasNext()){
   *// do something*
   it.remove();
}

​ 错误的做法是:

for(Integer i : list){
   list.remove(i)
}

​ 运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

4、Iterator 和 ListIterator 有什么区别?

1、Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。

2、Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。

3、ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置

5、遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

遍历方式有以下几种:

1、for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。

2、迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。

3、foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

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

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

6、说一下 ArrayList 的优缺点

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

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

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

7、如何实现数组和 List 之间的转换?
  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法。
// list to array
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();

// array to list
String[] array = new String[]{"123","456"};
Arrays.asList(array);
8、ArrayList 和 LinkedList 的区别是什么?

​ 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
​ 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
​ 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
​ 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
​ 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
​ 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

​ 补充:数据结构基础之双向链表。双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

9、ArrayList 和 Vector 的区别是什么?

​ 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合

​ 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
​ 性能:ArrayList 在性能方面要优于 Vector。
​ 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
​ Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。

​ Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。

10、插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性?

​ ArrayList、LinkedList、Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。

​ Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差。

​ LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快。

11、多线程场景下如何使用 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));
}
12、为什么 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,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。

13、List 和 Set 的区别

​ List , Set 都是继承自Collection 接口。

​ List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

​ Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

​ 另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。

​ Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。

​ List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。

2、Set接口

1、说一下 HashSet 的实现原理?

​ HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。

2、HashSet如何检查重复?HashSet是如何保证数据不可重复的?

​ 向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。HashSet 中的add ()方法会使用HashMap 的put()方法。HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。

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()的相关规定:

1、如果两个对象相等,则hashcode一定也是相同的
2、两个对象相等,对两个equals方法返回true
3、两个对象有相同的hashcode值,它们也不一定是相等的
4、综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
5、hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

==与equals的区别

1、==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同

2、==是指对内存地址进行比较 equals()是对字符串的内容进行比较

3、==指引用是否相同 equals()指的是值是否相同

3、HashSet与HashMap的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-azbvTvL3-1626232453560)(C:\Users\四叔\AppData\Roaming\Typora\typora-user-images\image-20210618184739728.png)]

4、BlockingQueue是什么?

​ Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

5、在 Queue 中 poll()和 remove()有什么区别?

​ 相同点:都是返回第一个元素,并在队列中删除返回对象。

​ 不同点:如果没有元素poll会返回null,而remove会直接抛出 NoSuchElementException 异常。

3、Map接口

1、说一下 HashMap 的实现原理?

​ HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

​ HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

​ HashMap 基于 Hash 算法实现的:

​ 1、当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
​ 2、存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
​ 3、获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
​ 4、理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

​ 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

2、HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现

​ 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。

​ JDK1.8之前:JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

img

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

jdk1.8中HashMap数据结构

​ JDK1.7 VS JDK1.8

​ JDK1.8主要解决或优化了一下问题:

  1. resize 扩容优化
  2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
  3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LS0isLR4-1626232453562)(C:\Users\四叔\AppData\Roaming\Typora\typora-user-images\image-20210618191551698.png)]

3、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 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。

putVal方法执行流程图

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

4、HashMap的扩容操作是怎么实现的?

①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;

②.每次扩展的时候,都是扩展2倍;

③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;//oldTab指向hash桶数组
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
        if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值
            threshold = Integer.MAX_VALUE;
            return oldTab;//返回
        }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold
    }
    // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂
    // 直接将该值赋给新的容量
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 新的threshold = 新的cap * 0.75
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    // 计算出新的数组长度后赋给当前成员变量table
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶数组
    table = newTab;//将新数组的值复制给旧的hash桶数组
    // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散
    if (oldTab != null) {
        // 遍历新数组的所有桶下标
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
                oldTab[j] = null;
                // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树
                if (e.next == null)
                    // 用同样的hash映射算法把该元素加入新的数组
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // e是链表的头并且e.next!=null,那么处理链表中元素重排
                else { // preserve order
                    // loHead,loTail 代表扩容后不用变换下标,见注1
                    Node<K,V> loHead = null, loTail = null;
                    // hiHead,hiTail 代表扩容后变换下标,见注1
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍历链表
                    do {             
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
                                // 代表下标保持不变的链表的头元素
                                loHead = e;
                            else                                
                                // loTail.next指向当前e
                                loTail.next = e;
                            // loTail指向当前的元素e
                            // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,
                            // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
                            // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。
                            loTail = e;                           
                        }
                        else {
                            if (hiTail == null)
                                // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

5、HashMap是怎么解决哈希冲突的?

1、什么是哈希?

​ Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

​ 所有散列函数都有如下一个基本特性**:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同**。

2、什么是哈希冲突?

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

3、HashMap的数据结构

​ 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突:

img
这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化

4、hash()函数

​ 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}

​ 这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)

5、JDK1.8新增红黑树

img

​ 通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);

6、总结

简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:

  1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
  2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
  3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快

6、能否使用任何类作为 Map 的 key?

​ 可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:

1、如果类重写了 equals() 方法,也应该重写 hashCode() 方法。

2、类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。

3、如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。

4、用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。

7、为什么HashMap中String、Integer这样的包装类适合作为K?

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

​ 1、都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
​ 2、内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况

8、如果使用Object作为HashMap的Key,应该怎么办呢?

​ 重写hashCode()和equals()方法。

1、重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
2、重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;

9、HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

​ hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

​ 解决方法:

1、HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
2、在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

10、HashMap 的长度为什么是2的幂次方

​ 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

​ 算法设计:我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

​ 为什么是两次扰动:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的。

11、HashMap 与 HashTable 有什么区别?

​ 1、线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
​ 2、效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
​ 3、对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
​ 4、**初始容量大小和每次扩充容量大小的不同 **: ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
​ 5、底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
​ 6、推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

12、如何决定使用 HashMap 还是 TreeMap?

​ 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

13、ConcurrentHashMap 和 Hashtable 的区别?

​ ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

​ 1、底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
​ 2、实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

img

img

img

​ 总结:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。

14、ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?

1、JDK1.7

​ 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:

​ 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

img

​ 1、该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
​ 2、Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

2、JDK1.8

​ 在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

img

插入元素过程

​ 如果相应位置的Node还没有初始化,则调用CAS插入相应的数据;

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
}

​ 如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;

if (fh >= 0) {
    binCount = 1;
    for (Node<K,V> e = f;; ++binCount) {
        K ek;
        if (e.hash == hash &&
            ((ek = e.key) == key ||
             (ek != null && key.equals(ek)))) {
            oldVal = e.val;
            if (!onlyIfAbsent)
                e.val = value;
            break;
        }
        Node<K,V> pred = e;
        if ((e = e.next) == null) {
            pred.next = new Node<K,V>(hash, key, value, null);
            break;
        }
    }
}

​ 1、如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值。
​ 2、如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount。

4、辅助工具类

1、Array 和 ArrayList 有何区别?

​ 1、Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。

​ 2、Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。

​ 3、Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。
​ 对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

2、如何实现 Array 和 List 之间的转换?

  • Array 转 List: Arrays. asList(array) ;
  • List 转 Array:List 的 toArray() 方法。

3、comparable 和 comparator的区别?

  • comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
  • comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序

一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort().

4、Collection 和 Collections 有什么区别?

​ java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
​ Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

5、TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?

​ TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。

​ Collections 工具类的 sort 方法有两种重载的形式:

​ 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;

​ 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值