数据结构与集合

写在前边:本文是个人学习笔记。(我是根据自己知识体系来做的,未必适合别人阅读)

本文整理自《码出高效:java开发手册》

目录

本文整理自《码出高效:java开发手册》

List集合

集合初始化

数组集合

集合与泛型

元素的比较:

JDK中使用的排序方法TimSort

hashCode 和 equals

fail-fast机制

Map类集合:

 

  关于树:

平衡二叉树:

二叉查找树:

红黑树:

TreeMap

HashMap

ConcurrentHashMap 


程序 = 数据结构 + 算法

 

集合框架图:

  在集合框架图中分两类,第一类是按照单个元素存储的collection,set 和 list 都是继承的collection接口,第二类是 按照key-value存储的Map。

List集合

list集合是线性数据结构的主要表现,集合元素通常明确的上一个和下一个元素,也存在明确的第一个元素和最后一个元素。

  在List体系中,最常用的也即是ArrayList 和 LinkedList 根据名字就可以看出来,两个的底层数据结构分别是数组和链表(双向链表) ,根据数据结构我们可以知道,数组是支持随机访问的,测试表明,十万数据,数组随机访问是链表的100倍。但是我们知道数组的需要的是连续的内存地址,链表可以是非连续的内存地址,但是因为链表要记录前驱后继,多以同样存一个数据,要浪费更多的空间。同样因为底层数据结构的问题,数组的插入比较麻烦,因为插在最后边还好,但是插在中间,就要将后边的数据后移,才能插入,而链表的好处是只需要修改前驱和后继就好了。所以插入删除操作比较容易快一点。但是请注意,他们都是非线程安全的。

  LinkedList  

  请注意LinkedList 具有的方法:addFirst()添加到头结点上,add()添加到末尾节点上,sort(null)从小到大排序。很奇怪,我拿到的api文档,没看到sort()。但是真的能用。

  Map集合

  非线程安全,以kay-value 键值对存储,key都是唯一的,但是value可以相同。 当put相同的key的时候,会覆盖掉原来的key的value。HashTable是线程安全的,但是由于无法突破性能瓶颈,而被逐渐遗弃。想要使用线程安全的就可以使用ConcurrentHashMap。

keySet() 可以得到所有的主键; values() 可以得到所有的值;entrySet() 可以的到所有的键值视图。 

 set集合

  set是不允许重复元素的集合类型,set体系最常用的是HashSet ,TreeSet, LinkedHashSet。相同的元素是放不进去set的。

HasHSet是使用HashMap实现的,只是将value固定为一个静态对象,使用key保证集合的唯一性,但是不保证顺序性。

LinkedHashMap是继承自HashSet,具有HashSet的优点,内部使用链表维护了插入顺序。

集合初始化

  集合初始化通常进行分配容量,设置特定参数等相关工作。分析集合的初始化及相关源码,洞悉容量分配,参数设定等相关逻辑,可以帮助我们更好的了解集合特性,提升代码质量 。

  就拿ArrayList来说,如果能在自己的预测范围之内进行初始化的话,就避免不断扩容带来的不必要的性能损失,动态数组的扩容是需要重新创建一个更大容量的数组的,并且需要将原来的内容复制到新的数组中来。如果知道JVM垃圾回收的话,频繁的new,以及回收不用的,都要造成性能的浪费。还需要知道一点,如果不指定初始话大小的话,默认是10,在第一次add的时候分配容量。后续每次扩容都会调用 Array.copyOf()

  再看一下HashMap,对于HashMap的扩容是非常影响性能的,因为要重建hash表。默认初始化是16,负载因子是0.75,这两个参数的乘积,决定可以存放的元素的个数。同样,HashMap的容量不会在new的时候分配,而是在第一次put的时候分配。我们需要知道的是,通过查看底层源码,我们会知道,即使你初始化容量了,底层会帮你找到离你最近的2的幂次方。这样做的好处是:这样的方式计算落槽位置更快。比方说,若果没有初始化,按照默认值16来,想要存1000个元素,必须扩容7次,这与指定1000相比,浪费了很多性能。

数组集合

  数组是一种顺序表,我们可以通过索引下标快速定位获取指定位置的元素。提一下为什么下标从0开始,而不是从1开始。因为需要拿下标直接进行偏移量进行运算,如果从1开始,每次都要减1,减法对于cpu来说是一种双数运算,在数组下标频繁使用的情境下,是很耗能的。

  数组分为静态初始化,和动态初始化,不管哪一种,大小一旦初始化完成都不能再改变。非线程安全。Vector线程安全,同样因为性能基本被废弃。

  args3是静态初始化,args4是动态初始化。 

  对于数组的遍历,优先推荐的是 foreach方式,其次用for循环。

  Array.asList()  可以帮我们将数组转化为集合,但是仍然是固定大小的。但是请注意:数组转化为集合时,并不能使用修改集合的相关方法,add() remove() clear(),都会报 不允许操作异常 。  Array.asList() 体现的是适配器模式,后台的数据仍然是原有数组。 set()间接的对数组修改值。

  请注意 Array.asList() 装换来的 ArrayList并不是我们认识的ArrayList,想要达到我们认识的那个ArrayList还要这样做:

 

 接下来是集合转数组,

 所以不要用toArray()无参方法把集合转为数组,这样会导致泛型丢失, 如果长度不够也不行。

集合与泛型

总结一下 <? extends T> 明确了泛型的上限,只能到T类型了

               <? super T> 明确了泛型的下限,至少是T以上的类型。

 

元素的比较:

Comparable 和 Comparator 

 

JDK中使用的排序方法TimSort

hashCode 和 equals

fail-fast机制

 

 

Map类集合:

Map是与 Collection 平级的接口,

  Map接口除了传统的增删改查方式外,主要还有了返回所有key    keySet(),返回所有value   values(),返回所有KV键值对entrySet()的方法。对于这些返回的视图,支持删除,但是不支持修改和增加。

  关于是否能够将KV设置成空:

需要注意的是ConcurrentHashMap   KV 都不可以为空。

 

  关于树:

平衡二叉树:


  树在某种特殊的情况下,会畸形,就是成为链表,这是我们不想看到的。

二叉查找树:

  大致可以这样理解,二叉搜索树是一颗排序树,父节点大于左孩子,小于右兄弟。但是如果根节点选的不好,很容易造成树的野蛮生长。所以

平衡二叉树:AVL

红黑树

红黑树和平衡二叉树的比较:

总结:显然红黑树没有达到绝对的平衡,这样做的好处是,在频繁的插入和删除操作的时候,相比平衡二叉树就旋转的次数更少,但是红黑是并不会让树失衡。那么如果是进行大量的查找操作,就不如平衡二叉树好一点了。

TreeMap

  首先我来谈自己的理解,因为TreeMap对key进行了自然排序,我自己试过就是,如果key不同类型,是没办法自然排序的。也就是说想要有序就得是key同一个类型。

也就是说,TreeMap的排序是基于比较器的,并不是像HashMap那样基于HashCode和equals的。如果没有传入比较器,俺么就按自然顺序排序。

最后注意一点,就是TreeMap 是非线程安全的,在多线程下使用,需要自行加锁控制。

HashMap

谈一下为什么要有负载因子的问题,为什么要有负载因子,就是通过增加空位数来减少冲突的发生。比方说初始化为为16,那么默认负载因子是0.75,就是16*0.75=12,实际上只能放12个数据。这样做的好处是可以减少冲突。这个系数就是负载因子。

因为HashMap的在并发下有安全性问题,所以想要解决,我们就要看向

ConcurrentHashMap 

最主要一点就是线程安全,在jdk1.8之前,使用的是分段锁。jdk11做了优化如下

ConcurrentHashMap的存储结构:

在jdk1.8之前 使用分段锁,从1.8开始:

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值