JAVA基础面试题(第九篇)中! 集合与数据结构

JAVA集合和数据结构也是面试常考的点,内容也是比较多。

在看之前希望各位如果方便可以点赞收藏,给我点个关注,创作不易!

JAVA集合

11. HashMap 中 key 的存储索引是怎么计算的?

首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)计算得到存储的位置。看看源码的实现:

// jdk1.7
方法一:
static int hash(int h) {int h = hashSeed;if (0 != h && k instanceof String) {return sun.misc.Hashing.stringHash32((String) k);}h ^= k.hashCode(); // 为第一步:取hashCode值h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4);
}
方法二:
static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但实现原理一样return h & (length-1);  //第三步:取模运算
}// jdk1.8
static final int hash(Object key) {   int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);/* h = key.hashCode() 为第一步:取hashCode值h ^ (h >>> 16)  为第二步:高位参与运算*/
}

这里的 Hash 算法本质上就是三步:**取key的 hashCode 值、根据 hashcode 计算出hash值、通过取模计算下标。**其中,JDK1.7和1.8的不同之处,就在于第二步。我们来看下详细过程,以JDK1.8为例,n为table的长度。

在这里插入图片描述

12. HashMap 的put方法流程?

简要流程如下:

  • 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;

  • 如果数组是空的,则调用 resize 进行初始化;

  • 如果没有哈希冲突直接放在对应的数组下标里;

  • 如果冲突了,且 key 已经存在,就覆盖掉 value;

  • 如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;

  • 如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。

在这里插入图片描述

13. HashMap 的扩容方式?

HashMap 在容量超过负载因子所定义的容量之后,就会扩容。Java 里的数组是无法自动扩容的,方法是将 HashMap 的大小扩大为原来数组的两倍,并将原来的对象放入新的数组中。

那扩容的具体步骤是什么?让我们看看源码。

先来看下JDK1.7 的代码:

void resize(int newCapacity) {   //传入新的容量Entry[] oldTable = table;    //引用扩容前的Entry数组int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了return;}Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组transfer(newTable);                         //!!将数据转移到新的Entry数组里table = newTable;                           //HashMap的table属性引用新的Entry数组threshold = (int)(newCapacity * loadFactor);//修改阈值}
这里就是使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。void transfer(Entry[] newTable) {Entry[] src = table;                   //src引用了旧的Entry数组int newCapacity = newTable.length;for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组Entry<K,V> e = src[j];             //取得旧Entry数组的每个元素if (e != null) {src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)do {Entry<K,V> next = e.next;int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置e.next = newTable[i]; //标记[1]newTable[i] = e;      //将元素放在数组上e = next;             //访问下一个Entry链上的元素} while (e != null);}}}

newTable[i] 的引用赋给了 e.next ,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到 Entry 链的尾部(如果发生了 hash 冲突的话)。

14. 一般用什么作为HashMap的key?

一般用Integer、String 这种不可变类当 HashMapkey,而且 String 最为常用。

  • 因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。这就是 HashMap 中的键往往都使用字符串的原因。
  • 因为获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非常重要的,这些类已经很规范的重写了 hashCode() 以及 equals() 方法。

15. HashMap为什么线程不安全?

在这里插入图片描述

  • 多线程下扩容死循环。JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
  • 多线程的put可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。
  • put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。此问题在JDK 1.7和 JDK 1.8 中都存在。

16. ConcurrentHashMap 的实现原理是什么?

ConcurrentHashMap 在 JDK1.7 和 JDK1.8 的实现方式是不同的。

先来看下JDK1.7

JDK1.7中的ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即ConcurrentHashMap 把哈希桶切分成小数组(Segment ),每个小数组有 nHashEntry 组成。

其中,Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色;HashEntry 用于存储键值对数据。

在这里插入图片描述
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问,能够实现真正的并发访问。

再来看下JDK1.8

在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加低粒度的锁。

将锁的级别控制在了更细粒度的哈希桶元素级别,也就是说只需要锁住这个链表头结点(红黑树的根节点),就不会影响其他的哈希桶元素的读写,大大提高了并发度。

在这里插入图片描述

17. ConcurrentHashMap 的 put 方法执行逻辑是什么?

先来看JDK1.7

首先,会尝试获取锁,如果获取失败,利用自旋获取锁;如果自旋重试的次数超过 64 次,则改为阻塞获取锁。

获取到锁后:

  • 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
    遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
  • 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
  • 释放 Segment 的锁。

再来看JDK1.8

大致可以分为以下步骤:

  • 根据 key 计算出 hash值。
  • 判断是否需要进行初始化。
  • 定位到 Node,拿到首节点 f,判断首节点 f:
  • 如果为 null ,则通过cas的方式尝试添加。
  • 如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容。
  • 如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入。
  • 当在链表长度达到8的时候,数组扩容或者将链表转换为红黑树。

18. ConcurrentHashMap 的 get 方法是否要加锁,为什么?

get 方法不需要加锁。因为 Node 的元素 val 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。

这也是它比其他并发集合比如 Hashtable、用 Collections.synchronizedMap()包装的 HashMap 安全效率高的原因之一。

static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;//可以看到这些都用了volatile修饰volatile V val;volatile Node<K,V> next;
}

19. get方法不需要加锁与volatile修饰的哈希桶有关吗?

没有关系。哈希桶table用volatile修饰主要是保证在数组扩容的时候保证可见性。

static final class Segment<K,V> extends ReentrantLock implements Serializable {// 存放数据的桶transient volatile HashEntry<K,V>[] table;

20. ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?

我们先来说value 为什么不能为 null ,因为ConcurrentHashMap 是用于多线程的 ,如果map.get(key)得到了 null ,无法判断,是映射的value是 null ,还是没有找到对应的key而为 null ,这就有了二义性。

而用于单线程状态的HashMap却可以用containsKey(key) 去判断到底是否包含了这个 null 。

我们用反证法来推理:

假设ConcurrentHashMap 允许存放值为 null 的value,这时有A、B两个线程,线程A调用ConcurrentHashMap .get(key)方法,返回为 null ,我们不知道这个 null 是没有映射的 null ,还是存的值就是 null 。

假设此时,返回为 null 的真实情况是没有找到对应的key。那么,我们可以用ConcurrentHashMap .containsKey(key)来验证我们的假设是否成立,我们期望的结果是返回false。

但是在我们调用ConcurrentHashMap .get(key)方法之后,containsKey方法之前,线程B执行了ConcurrentHashMap .put(key, null )的操作。那么我们调用containsKey方法返回的就是true了,这就与我们的假设的真实情况不符合了,这就有了二义性。

总结:数据结构和集合这快的面试考点还是很多的,希望大家仔细看看,这只是一部分。我们分三部分来更新。

多谢大家支持!!!

如果没看过前几篇的文章的,可以点击链接去看看,如果方便可以点赞收藏,给我点个关注,创作不易!

JAVA基础面试题(第八篇)上! 集合与数据结构

JAVA基础面试题(第七篇)!异常

JAVA基础面试题(第六篇)!序列化与IO流

JAVA基础面试题(第五篇)!反射与泛型

JAVA基础面试题(第四篇)!equal、hashcode及String解析

JAVA基础面试题(第三篇)!面向对象

JAVA基础面试题(第二篇)!基础语法与关键字

JAVA基础面试题(第一篇)!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/826147.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

隧道代理的优势与劣势分析

“随着互联网的快速发展&#xff0c;网络安全已经成为一个重要的议题。为了保护个人和组织的数据&#xff0c;隧道代理技术逐渐成为网络安全的重要工具。隧道代理通过在客户端和服务器之间建立安全通道&#xff0c;加密和保护数据的传输&#xff0c;有效地防止黑客入侵和信息泄…

15-partition table (分区表)

ESP32-S3的分区表 什么是分区表&#xff1f;&#x1f914; ESP32-S3的分区表是用来确定在ESP32-S3的闪存中数据和应用程序的布局。每个ESP32-S3的闪存可以包含多个应用程序&#xff0c;以及多种不同类型的数据&#xff08;例如校准数据、文件系统数据、参数存储数据等&#x…

Scala 第一篇 基础篇

Scala 第一篇 基础篇 一、变量与常量 1、变量2、常量 二、数据类型 1、数据基本类型概览2、元组的声明与使用3、Range介绍和使用4、Option 类型的使用和设计5、类型别名 三、运算符四、程序逻辑 1、一切都是表达式2、分支语句3、循环语句 五、集合 1、List2、Set3、Map4、Arra…

MySQL高级(索引-性能分析-explain执行计划)

explain 或者 desc 命令获取 MySQL 如何执行 select 语句的信息&#xff0c;包括在 select 语句执行过程中表如何连接和连接的顺序。 -- 直接在 select 语句之前加上关键字 explain / desc explain select 字段列表 from 表名 where 条件 &#xff1b; explain select * …

电机控制专题(一)——最大转矩电流比MTPA控制

文章目录 电机控制专题(一)——最大转矩电流比MTPA控制前言理论推导仿真验证轻载1Nm重载30Nm 总结 电机控制专题(一)——最大转矩电流比MTPA控制 前言 MTPA全称为Max Torque Per Ampere&#xff0c;从字面意思就可以知道MTPA算法的目的是一个寻优最值问题&#xff0c;可以从以…

SQL Server 2022 安装及使用

SQL Server 2022 前言一、安装SQL Server 2022下载SQL Server 2022安装SQL Server 2022配置SQL Server 2022 二、安装SQL Server Management Studio下载SQL Server Management Studio安装SSMS-Setup-CHS 三、使用SQL Server 2022四、解决连接到服务器报错问题 前言 SQL Serve…

git 快问快答

我在实习的时候&#xff0c;是用本地开发&#xff0c;然后 push 到 GitHub 上&#xff0c;之后再从 Linux 服务器上拉 GitHub 代码&#xff0c;然后就可以了。一般程序是在 Linux 服务器上执行的&#xff0c;我当时使用过用 Linux 提供的命令来进行简单的性能排查。 在面试的时…

应用编程之进程(三-通信篇)

所谓进程间通信指的是系统中两个进程之间的通信&#xff0c;不同的进程都在各自的地址空间中、相互独立、隔离&#xff0c;所以它们是处在于不同的地址空间中&#xff0c;因此相互通信比较难&#xff0c;Linux 内核提供了多种进程间通信的机制。 大部分的程序是不要考虑进程间…

Microchip逆市扩张,接连收购2家公司

尽管年初传来降薪停工的消息&#xff0c;全球领先的半导体解决方案供应商Microchip并未因此停下扩张的脚步。相反&#xff0c;该公司在短短的一个月内&#xff0c;接连宣布收购两家公司&#xff0c;展现了其坚定的市场布局和前瞻的战略眼光。 4月11日&#xff0c;Microchip成功…

二进制OpenStack

二进制搭建OpenStack 1.环境准备 1.1机器的准备 主机名服务器配置操作系统IP地址controller-node4C8Gcentos7.9172.17.1.117computer-node4C8Gcentos7.9172.17.1.118 1.2网络架构 [rootcotroller-node ~]# ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noque…

Java JNI调用本地方法1(调用C++方法)

一、基础概念 1、JNI&#xff08;Java Native interface&#xff09;:sun公司提供的JNI是Java平台的一个功能强大的接口&#xff0c;实现java和操作系统本地代码的相互调用功能&#xff0c;系统本地代码通常是由其他语言编写的&#xff0c;如C。 二、JNI使用步骤 1、定义一个J…

选定进行压缩的卷可能已损坏。请使用chkdsk来修复损坏问题,然后尝试再次压缩该卷

Windows Server 2008R2环境下&#xff0c;进行磁盘重新分区时&#xff0c;想要对系统盘进行“压缩卷”&#xff0c;结果报错提示“选定进行压缩的卷可能已损坏。请使用Chkdsk来修复损坏问题&#xff0c;然后尝试再次压缩该卷。”这是硬盘出现了坏道导致的&#xff0c;硬盘出错无…

中仕公考:教师编制和事业单位d类一样吗?

教师编制和事业单位D类在考试内容、专业要求、晋升途径等方面有很大的不同中仕为大家介绍一下&#xff1a; 考试内容&#xff1a;教师编的考试包括教育综合知识和学科专业知识&#xff0c;有的地区会额外考公共基础知识。事业单位D类的考试更侧重于职业能力倾向测验和综合应用…

Linux的学习之路:14、文件(1)

摘要 有一说一文件一天学不完&#xff0c;细节太多了&#xff0c;所以这里也没更新完&#xff0c;这里部分文件知识&#xff0c;然后C语言和os两种的文件操作 目录 摘要 一、文件预备 二、c文件操作 三、OS文件操作 1、系统文件I/O 2、接口介绍 四、思维导图 一、文件…

uniapp全局监听分享朋友圈或朋友

把大象装进冰箱需要几步&#xff1a; 1、创建shart.js文件 export default{data(){return {//设置默认的分享参数//如果页面不设置share&#xff0c;就触发这个默认的分享share:{title:标题,path:/pages/index/index,imageUrl:图片,desc:描述,content:内容}}},onLoad(){let ro…

若依前后端部署到一起

引用&#xff1a;https://blog.csdn.net/qq_42341853/article/details/129127553 前端改造&#xff1a; 配置打包前缀 修改router.js 编程hash模式&#xff1a; 前端打包&#xff1a;npm run build:prod 后端修改&#xff1a; 添加thymeleaf包&#xff0c;和配置文件 spri…

JAVA 项目<果园之窗>_1

这几天有空看能不能把水果店管理系统整出来&#xff0c;目标是整个网页版本的&#xff0c;以我的电脑做服务器&#xff0c;数据存在mysql中 以我目前的理解整个项目大致可分为前端部分、后端部分、数据库部分&#xff0c;也就这三个部分 目前打开并运行了一个别人的项目&#…

leetcode(474.最大连续1的个数)(python)

看数据范围知&#xff0c;复杂度不超过&#xff08;nlogn&#xff09;&#xff0c;不过感觉LeetCode很少卡算法时间。 题目要求数组的*****的个数&#xff0c;想到dp动态规划 dp[i][0]表示前i个数字&#xff0c;以第i个元素结尾&#xff0c;全为1的个数 dp[i][1]表示前i个数…

React间接实现一个动态组件逻辑

在开发一个浏览器插件的时候&#xff0c;用的plasmo框架和react支持的&#xff0c;里面使用react开发一个菜单功能&#xff0c;但是又不想使用react-router&#xff0c;所以就想着能不能使用一个很简单的方式做一个替代方案&#xff1f;那肯定是可以。 我在引入一个组件后&…

vue2响应式 VS vue3响应式

Vue2响应式 存在问题&#xff1a; 新增属性&#xff0c;删除属性&#xff0c;界面不会更新。 直接通过下标修改数组界面不会自动更新。 Vue2使用object.defineProperty来劫持数据是否发生改变&#xff0c;如下&#xff1a; 能监测到获取和修改属性&#xff1a; 新增的属性…