通过这一篇清楚集合(List、Set、Map)大部分问题总结

一:集合概述

集合框架:用于存储数据的容器。

1.1 集合的特点

1)对象封装数据,对象多了也需要存储。集合用于存储对象

对象可以用于封装数据,当对象非常多时九需要使用集合去存储对象

2)对象的个数确定可以使用数组,对象的个数不确定的可以用集合。因为集合是可变长度的。

当数据的个数明确,使用数组可以节省空间;当数据的个数不明确时,就需要使用集合存储,集合长度是可变的。

1.2 集合和数组的区别

1)长度

  1. 数组是固定长度的;
  2. 集合可变长度的。

2)存储类型

  1. 数组可以存储基本数据类型,也可以存储引用数据类型;
  2. 集合只能存储引用数据类型

3)存储元素的类型

  1. 数组存储的元素必须是同一个数据类型;
  2. 集合存储的对象可以是不同 数据类型。

1.3 常用的集合类

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

1)Collection接口的子接口包括:Set接口和List接口

  1. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  2. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

2)Map接口的实现类主要有:HashMap、TreeMap、Hashtable、 ConcurrentHashMap以及Properties等

1.4 List,Set,Map三者的区别

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

  1. List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引

常用的实现类有 ArrayList、LinkedList 和 Vector。

  1. Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素只允许存入一个null元素,必须保证元素唯一性

Set 接口常用实现类是 HashSet、 LinkedHashSet 以及TreeSet。

Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不 要求有序,允许重复。

Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、 ConcurrentHashMap

1.5 集合框架(List、set、map)底层数据结构

List

  1. Arraylist: Object数组
  2. Vector: Object数组
  3. LinkedList: 双向循环链表

Set

  1. HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
  2. LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于Hashmap 实现一样,不过还是有一点点区别的。
  3. TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

Map

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

1.6 线程安全的集合类

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

2)statck:堆栈类,先进后出。

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

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

1.7 Iterator 和 ListIterator 的区别

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

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

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

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

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

二:List接口

2.1 遍历一个 List 的不同的方式

  1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读 取每一个位置的元素,当读取到后一个元素后停止。
  2. 迭代器遍历Iterator。Iterator 是面向对象的一个设计模式,目的是屏 蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
  3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明Iterator 或计数器。

优点是代码简洁,不易出错;
缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替 换。

2.2 ArrayList 的优缺点

ArrayList的优点如下:

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

ArrayList 的缺点如下:

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

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

2.3 数组和 List 之间的转换

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

String[] array = new String[]{"123","456"};
Arrays.asList(array);

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

List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();

2.4 ArrayList 和 LinkedList 的区别

1)数据结构实现ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

2)随机访问效率ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。

3)增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为ArrayList 增删操作要影响数组内的其他数据的下标。

4)内存空间占用LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

5)线程安全:ArrayList 和 LinkedList 都是不同步的,也就是都不保证线程安全

2.5 ArrayList 和 Vector 的区别

相同点:

  1. 两个类都实现了 List 接口(List 接口继承了 Collection 接口)
  2. 他们都是有序集合

不同点:

  1. 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而ArrayList 是非线程安全的。

  2. 性能:ArrayList 在性能方面要优于 Vector。

    vector需要同步

  3. 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%

2.6 插入数据时,ArrayList、LinkedList、Vector谁速度较快

ArrayLis和Vector实现是使用数组方式存储数据

  1. ArrayLis:数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。
  2. Vector 中的方法由于加了 synchronized 修饰,因此 Vector是线程安全容器,但性能上较ArrayList差。

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

2.7 Vector,ArrayList, LinkedList的区别是什么

1)Vector、ArrayList都是以类似数组的形式存储在内存中,LinkedList则以链表的形式进行存储。

2.)List中的元素有序、允许有重复的元素,Set中的元素无序、不允许有重复元素。

3.)Vector线程同步,ArrayList、LinkedList线程不同步。

4) LinkedList适合指定位置插入、删除操作,不适合查找;ArrayList、Vector适合查找,不适合指定位置的插入、删除操作。

5)ArrayList在元素填满容器时会自动扩充容器大小的50%,而Vector则是100%,因此ArrayList更节省空间

三:Set接口

3.1 List 和 Set 的区别

相同点:

  1. List , Set 都是继承自Collection 接口

不同点:

  1. List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复可以插入多个null元素元素都有索引。
  2. Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素只允许存入一个null元素,必须保证元素唯一性
  3. 循环方式: List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
  4. Set:检索元素效率低下删除和插入效率高,插入和删除不会引起元素位置改变
  5. List:和数组类似,List 可以动态增长查找元素效率高插入删除元素效率低,因为会引起其他元素位置改变

3.2 hashSet 的实现原理

1)HashSet 是基于 HashMap 实现的

  1. HashSet的值存放于HashMap的key上
  2. HashMap的value统一为PRESENT

因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层HashMap 的相关方法来完成,

HashSet 不允许重复的值

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

向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
HashSet 中的add ()方法会使用HashMap 的put()方法。

HashMap比较key是否相等是先比较 hashcode 再比较equals

HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为 HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复

3.4 hashCode()与equals()

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

3.5 ==与equals的区别

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

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

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

3.6 HashSet与HashMap的区别

在这里插入图片描述

四:Map接口

4.1 HashMap 的实现原理

HashMap 基于 Hash 算法实现

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

理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

4.2 HashMap在JDK1.7和JDK1.8中有哪些不同

在Java中,保存数据有两种比较简单的数据结构:数组和链表。

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

在这里插入图片描述

4.3 HashMap的put方法的具体流程

在这里插入图片描述
图例说明:
①.判断键值对数组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.4 HashMap的扩容操作是怎么实现的

在jdk1.8中:

  1. resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行
    扩容;
  2. 每次扩展的时候,都是扩展2倍;
  3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

4.5 HashMap 与 HashTable 的区别

1)线程安全:

  1. HashMap 是非线程安全的,HashTable 是线程安全的;
  2. HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap );

2)效率: 因为线程安全的问题,

  1. HashMap 要比 HashTable 效率高一点。
  2. 另外,HashTable 基本被淘汰,不要在代码中使用它;

3) 对Null key 和Null value的支持:

  1. HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。
  2. 但是在HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。

4) 初始容量大小和每次扩充容量大小的不同

  1. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11之后每次扩充,容量变为原来的2n+1HashMap 默认的初始化大小为16之后每次扩充,容量变为原来的2倍

5) 底层数据结构

  1. JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
  2. Hashtable 没有这样的机制

6)推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代

4.6 如何决定使用 HashMap 还是TreeMap

在Map中插入、删除和定位元素这类操作,HashMap是 好的选择。

需要对一个有序的key集合进行遍历,TreeMap是更好的选择。

4.7 HashMap 和 ConcurrentHashMap 的区别

ConcurrentHashMap对整个桶数组进行了分割分段(Segment)然后在每一个分段上都用lock锁进行保护相对于HashTablesynchronized 锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。

HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

4.8 ConcurrentHashMap 和 Hashtable 的区别

区别主要体现在实现线程安全的方式上不同
1)底层数据结构:

  1. JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
  2. Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的

2)实现线程安全的方式(重要)

  1. 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍)。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用Node 数组+链表+红黑树的数据结构来实现,并发控制使用synchronized 和 CAS 来操作。虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性**,只是为了兼容旧版本**;
  2. Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

3) 图例说明
JDK1.7的ConcurrentHashMap:
在这里插入图片描述
JDK1.8的ConcurrentHashMap(TreeBi(img)n: 红黑二叉树节点 Node: 链表节点)
在这里插入图片描述

HashTable:

在这里插入图片描述

4.9 HashTable, HashMap,TreeMap区别?

1) HashTable线程同步,HashMap非线程同步。

2)HashTable不允许<键,值>有空值,HashMap允许<键,值>有空值。

3)HashTable使用Enumeration,HashMap使用Iterator。

4) HashTable中hash数组的默认大小是11,增加方式的old*2+1HashMap中hash数组的默认大小是16,增长方式一定是2的指数倍

5) TreeMap能够把它保存的记录根据键排序,默认是按升序排序。

4.10 HashMap的扩容因子

默认0.75,也就是会浪费1/4的空间,达到扩容因子时,会将list扩容一倍,0.75 是时间与空间一个平衡值;

五:集合框架的技术细节

5.1 HashMap的死循环

在并发的情况,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%问题,所以一定要避免在并发环境下使用HashMap。

在HashMap本来就不支持多线程使用,要并发就用ConcurrentHashmap。

5.2 ConcurrentHashMap在jdk1.8中的改进

改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然
ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

?abc!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值