Java开发中用的比较多的数据结构

一、ArrayList
上面也说了底层结构就是数组,直接看源码,如下直接就创建了个new Object[],所谓有序是直接添加到末尾,添加顺序与下标顺序保持一致,值可以重复
从这里就可以看出为什么是扩容1.5倍,以及每次扩容都copy

二、LinkedList
源码可以直接参考:https://blog.csdn.net/wen_and_zi/article/details/100702014   通过其Node<E>类里面的三个属性  // 存储的数据 E item; // 下一项 Node<E> next; // 前一项 Node<E> prev; 就可以看到其为双向链表,添加时一样添加到最后与下标保持一致,值可以重复。接下来简单看下add与remove方法可以看到linkedList是一个双向链表,每个节点都是一个Node(prve,e,next),再看一下这个remove方法,,首先是校验下标是否异常,然后就是移除操作这个node是通过下标拿到Node元素,只不过用了一个判断头遍历还是尾遍历,这个方法就是将Node中的三个元素都置为null然后将对应的头尾重新连接,这里我们可以发现无论是add还是remove都有modCount++操作,这个其实是为了防止在多个迭代器同时操作这个集合的时候发生不可预测的异常,比如一个正在遍历另外一个删除,这样最好的方法就是抛出异常。包括ArrayList也是这样的。https://blog.csdn.net/weixin_34161064/article/details/88759557

三、ArrayList与LinkedList比较:
1、时间复杂度:ArrayList,get() 直接读取第几个下标,复杂度 O(1) ,add(E) 添加元素,直接在后面添加,复杂度O(1),add(index, E) 添加元素,在第几个元素后面插入,后面的元素需要向后移动,复杂度O(n),remove()删除元素,遍历后寻找到下标位置,后面的元素需要逐个移动,复杂度O(n)。
LinkedList,get() 获取第几个元素,依次遍历,复杂度O(n),add(E) 添加到末尾,复杂度O(1),add(index, E) 添加第几个元素之后,需要先查找到第几个元素,再进行前后绑定,复杂度O(n),remove()删除元素,不考虑寻找位置的过程,复杂度O(1)。
2、扩容:  ArrayList底层是一个数组,在没有指定大小的情况下,数组默认的初始容量是10,当超过容量会触发1.5倍的扩容,旧的数组会被拷贝到新的数组中,新数组大小为原来的1.5倍。这样一定程度会造成空间的浪费。LinkedList底层是双向链表,大小是动态变化的,没有增容问题,插入一个开辟一个空间。可以看做是无限大。
3、应用:ArrayList:当操作是在一列数据的后面添加或者删除数据,而不是在前面或中间,并且需要随机地访问其中的元素时。LinkedList:当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时
4、ArrayList对应的高并发类是CopyOnWriteArrayList,HashSet对应的高并发类是 CopyOnWriteArraySet,HashMap对应的高并发类是ConcurrentHashMap

四、HashSet

如上图直接看源码,可以看到,直接就交给HashMap实现了,HashMap的数据存储是通过数组+链表/红黑树实现的,存储大概流程是通过hash函数计算在数组中存储的位置,如果该位置已经有值了,判断key是否相同,相同则覆盖,不相同则放到元素对应的链表中,如果链表长度大于8,就转化为红黑树,如果容量不够,则需扩容(注:这只是大致流程)。
但是map是k,v结构,我们看下其add方法源码:

如上我们可以看到hashSet就是直接只取用了HashMap的key来使用,同时利用map中key覆盖特性保证了其去重能力。HashSet的remove方法通过HashMap的remove方法来实现。HashSet是通过HashMap来实现的,HashMap通过hash(key)生成的HashCode来确定存储的位置,虽然顺序固定但是无法保证是按照插入顺序排序的所以是不具备存储顺序性的,因此HashSet遍历出的元素也并非按照插入的顺序。那么我们如果想让Set有序怎么办呢?使用LinkedHashSet即可,LinkedHashSet也是用LinkedHashMap实现,但是其有序性保障,可以参考如下链接。参考:https://www.cnblogs.com/LiaHon/p/11257805.html同时HashMap虽然key是无序的但是遍历内容的时候确又是有序的,这个接着往下看吧。

五、TreeSet
TreeSet底层是二叉树,可以对对象元素进行排序,是一个有序的集合,它的作用是提供有序的Set集合。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。但是自定义类需要实现comparable接口,重写comparaTo() 方法。TreeSet实际上是TreeMap实现的。当我们构造TreeSet时;若使用不带参数的构造函数,则TreeSet的使用自然比较器;若用户需要使用自定义的比较器,则需要使用带比较器的参数。

六、HashSet与TreeSet区别
1、有序性:HashSet是无序的,因为其存储位置是由HashCode计算得出,而TreeSet是基于红黑树是有序的,默认自然排序,对于对象需要自定义实现CompareTo,自然排序,是根据集合元素的大小,以升序排列。定制排序,应该使用Comparator接口,实现 int compare(T o1,T o2)方法。
2、元素是否为null:HashSet元素可以为null但是只允许一个,而TreeSet元素不能为null,这个是因为二叉树中每个节点就不允许为null
3、使用场景:HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。

七、HashMap
https://blog.csdn.net/woshimaxiao1/article/details/83661464   不太建议参考

https://blog.csdn.net/qazwyc/article/details/76686915
源码分析:
基本创建时候只分配了默认的加载因子0.75并未分配具体主体数组空间,看一下put方法在这里面我们同样可以看到hashmap中也维护这自己的Node类它的Node包含了hash哈希值、key、value以及Node<K,V> next,这个next如果没有hash碰撞就是null当有碰撞的时候就是链表中指向的下一个元素。
HashMap由数组+链表组成的+红黑树。内部维护了一个存储数据的Entry数组,HashMap采用链表解决冲突,每一个Entry本质上是一个单向链表。当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后通过indexFor(hash,length)求该key-value对的存储位置,计算方法是先用hash&0x7FFFFFFF后,再对length取模,这就保证每一个key-value对都能存入HashMap中,当计算出的位置相同时,由于存入位置是一个链表,则把这个key-value对插入链表头。      HashMap中key和value都允许为null。扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。这里正好顺表说下为什么要重写equals还要重写hashcode方法,直接看Hashmap的源码就知道,无论是put方法,还是get方法进行判断的时候都是先比较的hashCode。我们如果只重写equals方法,hashMap里面取值我们存一个然后再get一个会发现hashCode不一致,从而取不到值。默认是对内存地址的hash加密。而不是对其各个属性值。如下图所示:其他的都一样都是先计算hash如果不这样,无法利用hash算法快速定位了,也没有任何意义了。另外说下负载因子0.75这个值,其实看源码上的注释也提到了,这个是一个综合考虑的值,假如负载因子是1.0的时候,也就意味着,只有当数组的所有值全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的,肯可能等全部都填充的时候已经有较多的hash冲突产生了,这个时候就会导致链表比较长,这样反而得不偿失,复杂度从O(1)变成了O(n)。当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。但是相对也如果是0.5提高了效率但是占用了更多的空间。这个就跟垃圾回收的eden:s0:s1的调优比例一样,可以根据具体情况而定。所以也有人说自定义初始容量以及加载因子可以适当调优。同时HashMap还有个多线程并发致命问题:高并发下的形链表问题https://blog.csdn.net/weixin_30344795/article/details/95930150?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromBaidu-1.control&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromBaidu-1.control
分析hashMap的put方法https://blog.csdn.net/tc_1337/article/details/81700672   1.6hash碰撞放在头部,1.8放在尾部。关于链表转红黑树,其实不单单是链表长度达到8同时还得满足主数组长度超过64,也就是桶的数量达到64才能链表转红黑树

八、TreeMap
是一个有序的key-value集合,它是通过红黑树实现的,支持序列化,该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。HashMap:适用于在Map中插入、删除和定位元素。Treemap:适用于按自然顺序或自定义顺序遍历键(key)。

九、HashTable
Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。      Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中。      Hashtable同样实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆。 
与HashMap比较:Hashtable 线程安全很好理解,因为它每个方法上都加入了Synchronize。这里我们分析一下HashMap为什么是线程不安全的:HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。举个例子put方法,调用的addEntry(),现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。      Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。      Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。

十、ConcurrentHashMap
https://blog.csdn.net/weixin_44460333/article/details/86770169
ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。它与HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。但是其并不能保证操作的原子性(如果不明白可以看https://blog.csdn.net/yrsg666/article/details/111870765,多线程并发安全三要素:可见性、原子性、有序性),volatile只能保证可见性以及有序性行,立即刷回内存以及让其他线程内存副本失效能力。所以在其put操作的时候仍旧使用了Lock。ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put,首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁,如果达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。再看下put方法:将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。最后会解除在 1 中所获取当前 Segment 的锁。至于get方法逻辑比较简单:get 逻辑比较简单:只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁上面是1.7,尴尬!  1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。那就是查询遍历链表效率太低。其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。再来看下1.8中的put方法:根据 key 计算出 hashcode 。判断是否需要进行初始化。f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。如果都不满足,则利用 synchronized 锁写入数据。如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
get 方法:根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。如果是红黑树那就按照树的方式获取值。就不满足那就按照链表的方式遍历获取值。

十一、linkedHashmap
https://www.cnblogs.com/LiaHon/p/11180869.html
linked提供排序能力,hash提供快速检索能力。构造函数new LinkedHashMap(16,0.75,true)前两个与HashMap一样,第三个参数默认false表示遍历时按照插入顺序展示,如果true表示每次操作都会放到最后这个操作无论是put还是get。具体可以看上面的例子。为false用作保证顺序,为true时候用作Lru算法较多。

 

 

 

 

 

 

 

 

参考:
https://www.cnblogs.com/michaelwang2018/p/9606209.html
https://blog.csdn.net/qq_41207757/article/details/107916148
https://www.cnblogs.com/LiaHon/p/11257805.html
https://blog.csdn.net/weixin_44460333/article/details/86770169      concurrentHashMap

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值