一篇带你搞懂 java 集合

一、前言

集合是java的基础。
我们有了集合,在我们开发过程中,事半功倍。我们常用的集合有这几类:Array,Map,Set,Queue等,他们每一类在java迭代升级的过程中,也是有不同的升级优化。

二、集合全局观

这个是小编画的一个集合全家福,整体上是 Map和Collection;

collection 包含我们常用的Set + Queue + List
Map 包含常用的HashMap + Hashtable + treeMap
在这里插入图片描述

三、依次看一下

List类

我们依次梳理一下,数组中常用的几种:

ArrayList

ArrayList可以说是我们最常用的了,基本写代码都会用到。有几个特点

  • 线程不安全
  • 基于数组,需要连续内存
  • 随机访问快,(根据下标直接访问)
  • 尾部插入、尾部删除性能可以;其他部分插入、删除都会移动数据,因此性能低
  • 可以利用cpu缓存, 局部性原理

在这里插入图片描述

扩容机制是什么样的?

数组扩容均是建立一个新的数组,大小会计算好的, 然后把旧数组中的数据copy过去。

在这里插入图片描述

  • ArrayList() 会初始化 长度为0的数组

  • ArrayList(int initialCapacity) 会初始化指定容量的数组

  • public ArrayList(Collection<? extends E> c) 会使用c的大小做为容量

  • add方法,默认是尾插法,首次扩容为10,再次扩容为上次的1.5倍,底层是通过x + x>>1 ,(向右移1位,等于x/2)。

比如,当前数组大小是15,再次扩容的时候,会计算增量 15>>1=7,最终扩容到 15+7=22

   /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
 
  • addAll方法,比较 下一次扩容容量 和元素个数较大的。

初始空时,扩容为Math.max(10,实际元素个数);
有元素时,Math.max(原容量1.5倍,实际元素个数);

比如:

当前数组为空,addall了一个6个元素的list,那么此时元素是6个,6<10,数组容量是10个。
在这里插入图片描述
再addall 一个6个元素的list, 现在10个装不下,要扩容,下次扩容量为 10+10>>1 = 15,12<15,所以扩容到15.

在这里插入图片描述

如果我们插入6个后,我们再插入10个,那么我们就有16个元素,16>15,所以会扩容到16.

在这里插入图片描述

fail-fast 和 fail-safe

  • fail-fast
    ArrayList是典型代表,多线程操作,遍历的同时修改数组,立即抛出异常。

过程:在遍历之前,会把当前list的元素个数modCount记录下来,迭代的时候会记录迭代器修改的次数expectedModCount,两者会比较,如果不相等,证明被修改了,抛出ConcurrentModificationException异常。

优化方案:使用juc下的包代替java.util,如CopyOnWriteArrayList。
在这里插入图片描述

  • fail-safe
    CopyOnWriteArrayList是典型代表,遍历的同时可以修改,原理是读写分离。
    会copy一份数据,牺牲一致性,让遍历完成。
    在这里插入图片描述

LinkedList

特点:

  • 基于双向链表,无需连续内存
  • 随机访问慢,(要沿着链表遍历)
  • 头部插入删除性能高
  • 占内存多 (双向链表有更多的成员变量)

在这里插入图片描述

Vector

特点:

  • 数组实现,内存连续
  • 线程安全,Synchronized修饰
  • 扩容方式通过扩容因子判断
  • fail-fast

说一说面试题:

1.ArrayList 和 LinkedList 区别
2.ArrayList 和 Vector 区别
3.ArrayList如何扩容的?
。。。。

Map

map也是我们常用的数据结构,也是面试最经常问的,小编理解了Map的设计后,其实也是很佩服这个设计的。而且我们jdk1.7和jdk1.8结构是有所不同的,其中优化的理念,还是值得借鉴的。比如二次hash,链表转红黑树,以及为什么初始大小是16,扩容大小是2^n?

map这里呢,我们也挑选几个典型:

HashMap

HashMap,在1.7 和1.8 结构有所升级。

1.7 中的HashMap:
  • 结构:数组 + 链表
  • 线程不安全
  • 允许 null 作为 key和value

在这里插入图片描述

1.8 中的HashMap
  • 结构:数组 + 链表 or 红黑树
  • 线程不安全
  • 允许 null 作为 key和value

在这里插入图片描述

提问环节:

  • 1.8 和1.7 的Hashmap有什么区别?

数据结构不一样,1.7 是数组+链表,1.8 是数组+链表 or 红黑树;
链表插入方式:1.7是头插法,1.8是尾插法

  • 什么是红黑树??为什么用红黑树?

红黑树是特殊的平衡二叉树,她比平衡二叉树性能好一些,查询时间复杂度为 O(log2^n)。而链表的查询复杂度是O(1),当链表过长的时候,查询会很慢。
1.8 中转换红黑树的条件是 1.数组长度大于等于64,2.链表长度大于8;
使用红黑树就是为了优化长链表查询慢的问题。

  • 为什么不一上来就用红黑树?

链表短的时候,查询性能很快,没有必要直接用红黑树
存储方面:链表节点是Node,红黑树的节点是treeNode,treeNode成员变量比node多,所以内存会占用多,也没有必要。

  • 1.8转红黑树的阈值为什么是8 ?

阈值为8 ,也是综合考虑的。是为了尽量不要转成红黑树,除非链表真的很长了。
官方的一个demo:如果hash值够随机,在hash表内按泊松分布,在负载因子为0.75的情况下,统计了长度超过8的链表出现的概率是0.00000006 (亿分之6),选8,就是为了让树化 的概率足够小。链表转树,树转链表的开销也很大。

  • 红黑树如何退化为链表?

1.当链表长度小于等于6
2.红黑树的节点,如果 删除节点前,红黑树的 根节点,根的左节点,根的右节点,根的左孙子,有一个为null,就会转为链表。

在这里插入图片描述

如何防止hash碰撞?hash如何计算的?

hashMap 做了二次hash操作
第一次,根据key获取到对应的hashCode值;
第二次,根据hashCode 进行二次hash;
最后,用二次hash的值与数组容量进行取余运算。

根据key计算了hashcode,为什么还要进行二次hash?

保证hash结果更加的均匀,防止长链表产生。
二次hash是通过 hashcode ^ (hashcode>>>16) 计算的,目的是为了分配更加的均匀,防止链表过长

从这个图可以看到,二次hash结果分布更加均匀。
在这里插入图片描述

数组容量为什么是2^n?

为了提高整体性能,两个地方用到了这个数据
1.取余计算桶的时候,如果数组长度是2^n,那么可以直接用位运算取代取模
eg:
97 % 16 =1
97 & (16-1) = 1
2.方便扩容移动数据
扩容移动数据的时候,根据扩容后桶的长度,计算每个key对应的新桶的位置 A,
如果 A & oldCap == 0, 那就留在原位,否则,新位置 = 旧位置 + oldCap;可以直接移动。

不用2^n 可以吗?

可以,但是综合考虑性能
hashtable 就不是用的2^n

get的流程是什么样的?

首先根据 key 获取hashcode
再 根据hashcode进行二次hash
最后 按位与得到桶的位置
如果桶的位置没有数据,就直接返回null。
如果桶有数据,就依次遍历链表,通过equals()判断key是否相等,相等的话返回对应的value,没有的话返回null。

put流程是什么样的?1.7和1.8 区别?

首先 根据key 获取hashcode
在根据hashcode 进行二次hash
最后 按位与得到桶的位置
如果桶没有数据,就做成node节点,插入
如果桶有数据,判断数据是否存在?通过equals判断存在
存在,更新数据
不存在,插入数据
====如果是treeNode,走红黑树添加逻辑
====如果是普通node,走链表添加逻辑,1.7 头部插入node节点,1.8尾部插入Node节点
添加完后判断是否转红黑树
返回前检查容量是否超过阈值,一旦超过,进行扩容。(先插入,再判断树化,再判断扩容)

1.7并发扩容死链问题?死循环问题?

首先,hashmap是线程不安全的,所以在高并发的时候,会有问题。
其次,1.7是通过头插法完成的,当扩容的时候,a–b,会变为 b–a。node节点还是node节点,只是改变了前后的链接。

在这里插入图片描述

比如当前有两个线程来操作
在这里插入图片描述
其中一个线程2,已经完成了上面的扩容。现在链表的顺序是b–a。
在这里插入图片描述
线程1开始迁移数据,第一轮循环,把a先迁移,然后e的指针指到b,next的指针指到null。
在这里插入图片描述
第二轮循环,next指针,指向b的下一个,是a。e把b迁移走,e指向next,指到a。
在这里插入图片描述
在这里插入图片描述
第三次循环,next指向null,a要用头插法插到头部,就形成了 第一个node是a,a的next是b,b的next又是a,这样就出现了死链。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

负载因子为什么是0.75?

综合条件考虑的,从空间和时间考虑
大于这个值,空间节省了,但是链表会变长,影响效率。
小于这个值,扩容次数多了,hash冲突少了,空间多了。

hashMap出现HashDos问题,如何解决的?

通过红黑树解决,防止链表太长,性能急剧下降。

ConcurrentHashMap

ConcurrentHashMap ,在1.7 和1.8 结构有所升级

1.7的ConcurrentHashMap
  • 结构 : segment + 数组 + 链表
  • 线程安全,使用ReentrantLock ,用自旋锁来保证线程安全

在这里插入图片描述

1.8的ConcurrentHashMap
  • 结构 : 数组 + 链表 or 红黑树
  • 线程安全,使用 CAS + Synchronized保证线程安全

在这里插入图片描述

提问时间:
1.7和1.8 ConcurrentHashMap 有什么区别?

1.数据结构不一样
====1.7 segment + 数组 + 链表
====1.8 数组 + 链表or红黑树
2.初始化时机不一样
====1.7,饿汉式初始化,初始化的时候,就初始化好segment,以及segment0的数组,数组大小根据容量和并发度来计算。
在这里插入图片描述
====1.8,懒汉式初始化,真正put数据的时候创建
在这里插入图片描述
3. 插入方式不同,1.7头插法,1.8尾插法。
4.扩容时机不同
====1.7 当超过 容量负载因子大小,才扩容
====1.8 当 >= 容量
负载因子,就扩容,eg 12 个就扩容了
5.锁的对象不一样
====1.7锁的是segment
在这里插入图片描述
1.8 锁的是链表的第一个Node
在这里插入图片描述

ConcurrentHashMap如何保证线程安全的?
ConcurrentHashMap 和 hashMap的区别?
ConcurrentHashMap 和 hashtable的区别?

HashTable

  • 结构 : 数组 + 链表 or 红黑树
  • 线程安全,所有方法通过 Synchronized修饰
  • 不允许 null 作为 key和value,否则报空指针错误

四、小结

集合这个还是很值得研究的。包括里的设计思想。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

你个佬六

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

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

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

打赏作者

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

抵扣说明:

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

余额充值