Java集合:
Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有以下主要的子接口:List、Set 、 Queue、Map。
注:图中只列举了主要的继承派生关系,并没有列举所有关系。比方省略了AbstractList, NavigableSet等抽象类以及其他的一些辅助类,如想深入了解,可自行查看源码
区别:
List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
Set(注重独一无二的性质): 存储的元素不可重复的。
Queue(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
Map(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
1.ArrayList的底层实现原理是什么?
1.ArrayList底层是用动态数组实现的。
2.ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10。
3.ArrayList在进行扩容的时候扩容的大小是原来容量的1.5倍,每次扩容都需要拷贝数组
4.ArrayList在添加数据的时候
确保数组已使用长度(size)+1之后够存下下一个数据
计算数组的容量,如果当前数组已使用长度+1后的大于当前数组长度,则调用grow方法进行扩容(原来的1.5倍)
确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上
添加成功返回true
2.ArrayList list = new ArrayList(10)中的list扩容了几次?
该语句只是声明和实例了一个ArrayList,指定了容量为10,并没有扩容
3.如何实现数组和list之间的转换
数组转List ,使用JDK中java.util.Arrays工具类的asList方法
List转数组,使用List的toArray方法。无参toArray方法返回 Object数组,传入初始化长度的数组对象,返回该对象数组
再问:
用Arrays.asList转List后,如果修改了数组内容,list受影响吗?
Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
List用toArray转数组后,如果修改了List内容,数组受影响吗?
list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响
4.ArrayList和Array(数组)的区别?
1.ArrayList 内部基于动态数组实现,比 Array(静态数组) 使用起来更加灵活。
2.ArrayList会根据实际存储的元素动态地扩容或缩容,而 Array 被创建之后就不能改变它的长度了。
3.ArrayList 允许你使用泛型来确保类型安全,Array 则不可以。
4.ArrayList 中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。Array 可以直接存储基本类型数据,也可以存储对象。
5.ArrayList 支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如 add()、remove()等。Array 只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。
6.ArrayList创建时不需要指定大小,而Array创建时必须指定大小。
5.ArrayList与LinkedList的区别?
- 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
- ArrayList和LinkedList都不是线程安全的如果需要保证线程安全,有两种方案:1.在方法内使用,局部变量则是线程安全的2.使用线程安全的ArrayList和LinkedList(如下)
- List<Object> syncLinkedList = Collections.synchronizedList(new LinkedList<>()),使用synchronized包装一下(用synchronized锁),但是这个对于性能会有一些下降
- 底层数据结构: ArrayList 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构
- 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
- ArrayList底层是数组,内存连续,节省内存LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
- 操作数据效率:
- ArrayList按照下标查询的时间复杂度O(1)【内存是连续的,根据寻址公式】, LinkedList不支持下标查询查找(未知索引): ArrayList需要遍历,链表也需要链表,时间复杂度都是O(n)新增和删除ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)
6.ArrayList可以添加null值吗?
ArrayList 中可以存储任何类型的对象,包括 null 值。不过,不建议向ArrayList 中添加 null 值, null 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常
7.LinkedList为什么不能实现RandomAccess接口?
RandomAccess 是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于 LinkedList 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 RandomAccess 接口。
8.HashMap和Hashtable的区别?
1.线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
2.效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它;
3.对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
4.初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
5.底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable 没有这样的机制。
9.HashMap和HashSet的区别?
HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
- (1)HashSet实现了Set接口, 仅存储对象; HashMap实现了 Map接口, 存储的是键值对.
- (2)HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法. 依靠HashMap来存储元素值,(利用hashMap的key键进行存储), 而value值默认为Object对象. 所以HashSet也不允许出现重复值, 判断标准和HashMap判断标准相同, 两个元素的hashCode相等并且通过equals()方法返回true.
10.HashSet如何检查重复?
当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。
11.说一下HashMap的实现原理?
(
- 1,底层使用hash表数据结构,即数组+(链表 | 红黑树)
- 2,添加数据时,计算key的值确定元素在数组中的下标 key相同则替换 不同则存入链表或红黑树中
- 3,获取数据通过key的hash计算数组下标获取元
)
- HashMap的数据结构: 底层使用hash表数据结构,即数组和链表或红黑树
- 1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
- 2. 存储时,如果出现hash值相同的key,此时有两种情况。
- a. 如果key相同,则覆盖原始值;
- b. 如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中
- 3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
-
- 再问:HashMap的jdk1.7和jdk1.8有什么区别
- JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
- jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时并且数组长度达到64时,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表
12.HashMap的长度为什么是2的幂次方?
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
13.HashMap多线程操作导致死循环问题
- JDK1.7 及之前版本的 HashMap 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。
- 为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用 ConcurrentHashMap 。
(
- jdk7的的数据结构是:数组+链表
- 在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环
- 比如说,现在有两个线程
- 线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入
- 线程二也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。
- 当线程一再继续执行的时候就会出现死循环的问题
- 线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B,形成循环。
- 当然,JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题
)
14.HashMap为什么线程不安全?
- 数据覆盖:在JDK1.8中,如果多个线程同时对HashMap进行操作,可能会出现一个线程的数据覆盖另一个线程的数据的情况。
- 死循环和数据丢失:在JDK1.7中,当HashMap进行扩容操作时,如果有线程在扩容过程中被挂起,而其他线程已经完成了数据迁移,那么当被挂起的线程重新获得CPU资源并继续执行时,可能会因为数据已经被改变而导致死循环或数据丢失。
- 底层数组结构:HashMap底层维护了一个数组,数组中的每个元素是一个链表或红黑树(JDK1.8中引入)。在多线程环境下,如果多个线程同时对这个数组进行修改,比如增加或删除元素,可能会导致链表或树的结构损坏,从而引发错误。
15.ConcurrentHashMap和Hashtable的区别
1.ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
2.底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
3.实现线程安全的方式(重要):在 JDK1.7 的时候,ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
16.HashMap的put方法的具体流程
- HashMap是懒惰加载,在创建对象时并没有初始化数组在无参的构造函数中,设置了默认的加载因子是0.75
- 1. 判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)
- 2. 根据键值key计算hash值得到数组索引
- 3. 判断table[i]==null,条件成立,直接新建节点添加
- 4. 如果table[i]==null ,不成立
- 4.1 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
- 4.2 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
- 4.3 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操 作,遍历过程中若发现key已经存在直接覆盖value
- 5. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。
17.讲一讲HashMap的扩容机制?
- 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)
- 每次扩容的时候,都是扩容之前容量的2倍;
- 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
- 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
- 如果是红黑树,走红黑树的添加
- 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
18.了解hashMap的寻址算法吗?
- 这个哈希方法首先计算出key的hashCode值,然后通过这个hash值右移16位后的二进制进行按位异或运算得到最后的hash值。
- 在putValue的方法中,计算数组下标的时候使用hash值与数组长度取模得到存储数据下标的位置,hashmap为了性能更好,并没有直接采用取模的方式,而是使用了数组长度-1 得到一个值,用这个值按位与运算hash值,最终得到数组的位置
19.HashMap如何解决Hash冲突?
首先,HashMap底层是采用了数组的这样一个结构来存储数据,数组的默认长度是16,当我们通过put方法去添加数据的时候,HashMap会根据key的hash值进行取模运算,最终把这样一个值保存到数组的指定位置,但是这样的设计模式可能存在hash冲突的问题,也就是两个不同的hash值的key可能相同,因此hashMap引入了一个链式寻址法来解决hash冲突的问题,也就是存在冲突的key呢,HashMap会把这些key组成一个单项链表,然后采用尾插法把key存储到链表的尾部,另外为了避免链表过长导致查询效率下降,所以当链表长度大于8且数组长度大于等于64的时候,HashMap会把当前的链表存储转换成红黑树存储,从而减少链表数据查询的时间复杂度,从而提高查询效率
此外还有一些解决hash冲突的方法
1.再hash法:就是如果某个hash函数产生了冲突,再用另外一个hash进行计算,比如布隆过滤器就采用了这种方法
2.开放寻址法:就是直接从冲突的数组位置从下寻找一个空的数组下标进行数据存储,这个ThreadLocal里里面就有用到
3.建立公共溢出区:也就是将存在的key统一存放再公共溢出区中