一、List和Set以及Map

1121cyy 2021-08-04 原文


java中list和map详解


1、List , Set, Map都是接口,前两个继承至Collection接口(Collection接口下还有个Queue接口,有PriorityQueue类),Map为独立接口,

(1)List下有ArrayList,Vector,LinkedList

(2)Set下有HashSet,LinkedHashSet,TreeSet

(2)Map下有Hashtable,LinkedHashMap,HashMap,TreeMap

 注意:Queue接口与List、Set同一级别,都是继承了Collection接口。LinkedList既可以实现Queue接口,也可以实现List接口.Queue接口窄化了对LinkedList的方法的访问权限(即在方法中的参数类型如果是Queue时,就完全只能访问Queue接口所定义的方法 了,而不能直接访问 LinkedList的非Queue的方法)。

2、list详解:List 存储有序,可重复

(1)ArrayList解析

  1)ArrayList的实现原理:ArrayList继承AbstractList类,实现了List和RandomAccess,Cloneable, Serializable接口,底层是基于动态的数组。底层使用数组实现,默认初始容量为10.当超出后,会自动扩容为原来的1.5倍,即自动扩容机制。List list = Collections.synchronizedList(new ArrayList(…))即可线程安全。源码解析如下。

  2)ArrayList的优缺点

   a、优点: 底层数据结构是数组,查询快,增删慢。

   b、缺点: 线程不安全,效率高

(2)LinkedList解析

  1)LinkedList的实现原理:LinkedList继承AbstractList类,实现了List,Serializable,Queue接口,LinkedList是通过双向链表去实现的,既然是链表实现那么它的随机访问效率比ArrayList要低,顺序访问的效率要比较的高。每个节点都有一个前驱(之前前面节点的指针)和一个后继(指向后面节点的指针)。源码解析如下。

  2)ArrayList的优缺点

   a、优点: 底层数据结构是链表,查询慢,增删快。

   b、缺点: 线程不安全,效率高

(3)Vector解析

  1)Vector实现原理:在ArrayList中每个方法中添加了synchronized关键字来保证同步。

  2)Vector的优缺点

   a、优点: 底层数据结构是数组,查询快,增删慢。

   b、缺点: 线程安全,效率低

(4)三种list的选择(元素可重复):要安全?

  是:Vector

  否:ArrayList或者LinkedList

  查询多?:ArrayList

  增删多?:LinkedList

  知道要用List,但是不知道是哪个List,就用ArrayList。

  ArrayList是基于动态的数组的数据结构 LinkedList是基于链表的数据结构

3、Set存储无序,唯一

  set保证里面元素的唯一性其实是靠两个方法,一是equals()和hashCode()方法先是判断set集合中是否有与新添加数据的hashcode值一致的数据,如果有,那么将再进行第二步调用equals方法再进行一次判断,假如集合中没有与新添加数据hashcode值一致的数据,那么将不调用eqauls方法。

(1)HashSet:底层数据结构是哈希表。(无序,唯一)

  使用Set集合都是需要去掉重复元素的, 如果在存储的时候逐个equals()比较, 效率较低,哈希算法提高了去重复的效率, 降低了使用equals()方法的次数,HashSet调用add()方法存储对象的时候, 先调用对象的hashCode()方法得到一个哈希值, 然后在集合中查找是否有哈希值相同的对象,如果没有哈希值相同的对象就直接存入集合,如果有哈希值相同的对象, 就和哈希值相同的对象逐个进行equals()比较,比较结果为false就存入, true则不存

(2)LinkedHashSet:底层数据结构是链表和哈希表。(FIFO插入有序,唯一),由链表保证元素有序,由哈希表保证元素唯一

  1)TreeSet:底层数据结构是红黑树(唯一,有序);利用自然排序和比较器排序;根据比较的返回值是否是0来决定来保证元素的唯一性。

(3)Set的选择(元素唯一):

  排序?

  是:TreeSet或LinkedHashSet

  否:HashSet

  知道要用Set,但是不知道是哪个Set,就用HashSet。

4、Map接口:Map接口有三个比较重要的实现类,分别是HashMap、HashTable和TreeMap,LinkedHashMap。

(1)TreeMap,HashMap,HashTable的区别

  1)TreeMap是有序的,HashMap和HashTable是无序的。

  2)Hashtable的方法是同步的,HashMap的方法不是同步的。这是两者最主要的区别。

  3)Hashtable是线程安全的,HashMap不是线程安全的。

  4)HashMap效率较高,Hashtable效率较低。查看Hashtable的源代码就可以发现,除构造函数外,Hashtable的所有 public 方法声明中都有 synchronized关键字,而HashMap的源码中则没有。

  5)Hashtable不允许null值,HashMap允许null值(key和value都允许)

  6)父类不同:Hashtable的父类是Dictionary,HashMap的父类是AbstractMap

(2)LinkedHashMap怎么实现有序的?

  LinkedHashMap内部维护了一个单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和 after用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。

(3)TreeMap怎么实现有序的?

  TreeMap是按照Key的自然顺序或者实现的Comprator接口的比较函数的顺序进行排序,内部是通过红黑树来实现。所以要么key所属的类实现Comparable接口,或者自定义一个实现了Comparator接口的比较器,传给TreeMap用于key的比较。

5、HashMap解析

(1)JDK1.8中HashMap

  1)JDK1.8中HashMap的实现原理(数组+链表红黑树):数组存储的元素是一个Entry类,Entry类有三个数据域,key、value(键值对),next(指向下一个Entry)

  2)HashMap如何设定初始容量大小:一般如果new HashMap() 不传值,默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为大于k的 2的整数次方,例如如果传10,大小为16。负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了16 * 0.75 = 12 就需要将当前 16 的容量进行扩容

  3)HashMap的哈希函数如何设计(如何求hash值):hash函数是先拿到通过key的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作即key.hashCode()^(key.hashCode()>>>6)。这样可以尽可能降低hash碰撞,使hash表越分散越好;同时算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;

  4)为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?

  因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为-2147483648~2147483647,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。

  5)如何通过hash值定位元素的位置:并不是对hash表的长度取余而使用了位运算来得到位置下标,由key的哈希值对数组的长度位运算得到即h & (length-1)

  6)为什么用位运算定位hash以及HashMap的扩容都是以2的次方来进行:假设当前table的length是15,二进制表示为1111,那么length-1就是1110,此时有两个hash值为8和9的key需要计算索引值,计算过程如下:

   8的二进制表示:1000
   8&(length-1)= 1000 & = 1000,索引值即为8;
   9的二进制表示:1001
   9&(length-1)= 1001 & 1110 = 1000,索引值也为8;
  观察发现8和9有相同的索引值,即两个hash值为8和9的key会定位到数组中的同一个位置上形成链表,这就产生了碰撞,降低了查询的效率,HashMap的初始大小和扩容都是以2的次方来进行的,换句话说length-1换成二进制永远是全部为1,比如容量为16,则length-1为1111,大家知道位运算的\’&\’规则是两个1才得1,遇0得0,也就是说length-1中的某一位为1,则对应位置的计算结果才取决于h中的对应位置(h中对应位取0,对应位结果为0,h对应位取1,对应位结果为1。这样就有两个结果),但是如果length-1中某一位为0,则不论h中对应位的数字为几,对应位结果都是0,这样就让两个h取到同一个结果,这就是hash冲突了,恰恰length-1又是全部为1的数,所以结果自然就将hash冲突最小化了。

  7)h%length与h&(length-1)得到的结果其实是一个值,但是为什么hashmap中要用后者呢?

   a、length(2的整数次幂)的特殊性导致了length-1的特殊性(二进制全为1)

   b、位运算快于十进制运算,hashmap扩容也是按位扩容,所以相比较就选择了后者

  8)两个不同key经过key.hashCode()&(length-1)计算后得到相同的数组下标后,如何操作:hashmap在插入元素的时候,会首先检查这个位置上有没有元素,如果已经有了元素,那么就把这个新插入的Entry的next指向本来这个位置上的元素的地址,然后再插入这个位置,这也就是为什么插入多个相同的key的value时,这个位置的value始终是最后插入的那个元素的值。

  9)jdk1.8的HashMap的put方法:

   a、判断数组是否为空,为空进行初始化;

   b、不为空,计算 k 的 hash 值,通过(n – 1) & hash计算应当存放在数组中的下标 index;

   c、查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;

   d、存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据;

   e、如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;

   f、如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8, 大于的话链表转换为红黑树;

   g、插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的2倍。

  10)jdk1.8的HashMap的get方法:

   a、首先将 key hash 之后取得所定位的桶。

   b、如果桶为空则直接返回 null 。

   c、否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。

   d、如果第一个不匹配,则判断它的下一个是红黑树还是链表。

   e、红黑树就按照树的查找方式返回值。

   f、不然就按照链表的方式遍历匹配返回值。

  11)jdk1.8中HashMap的三点主要的优化:

   a、数组+链表改成了数组+链表或红黑树;1.8使用红黑树:防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn);

   b、链表的插入方式从头插法改成了尾插法,是插入时,若数组位置上已经有元素,1.7利用头插法,1.8遍历链表,将元素放置到链表的最后; 因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环

   c、扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变;

   d、在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;

  12)扩容的时候为什么1.8 不用重新hash就可以直接定位原节点在新数据的位置呢?

   由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1,扩容前长度为16,用于计算(n-1) & hash 的二进制n-1为0000 1111,扩容为32后的二进制就高位多了1,为0001 1111。因为是& 运算,1和任何数 & 都是它本身,那就分二种情况:hash比(length-1)大和比(length-1)小。如下图所示。

  13)链表转红黑树的阈值是8,为什么红黑树转链表的阈值是6?

  在hash函数设计合理的情况下,发生hash碰撞8次的几率为百万分之6,概率说话。因为8够用了,至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的互相转化,为了预防这种情况的发生,设置为6。

  14)Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map

   a、HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,锁粒度比较大,

   b、Map m = Collections.synchronizedMap(new HashMap());

   c、ConcurrentHashMap相比HashTable降低了锁粒度,并发度提高。jdk1.7使用分段锁实现ConcurrentHashMap线程安全,jdk1.8使用CAS实现ConcurrentHashMap线程安全。

6、ConcurrentHashMap

  1)ConcurrentHashMap在jdk1.7是如何实现的:jdk1.7中是采用Segment 数组+ HashEntry +成员变量用volatile修饰+ ReentrantLock的方式进行实现的。其中segment继承ReentrantLock;另外成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理。理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组长度)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

  注意:volatile关键字对于基本类型的修改可以在随后对多个线程的读保持一致,但是对于引用类型如数组仅仅保证引用的可见性,但并不保证引用内容的可见性。

  2)ConcurrentHashMap在jdk1.8是如何实现的?

  数据结构类似于hashmap,放弃了Segment臃肿的设计,取而代之的是采用Node数组+ CAS + Synchronized来保证并发安全进行实现。

  jdk1.8 在jdk1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

  3)jdk1.8的ConcurrentHashMap的put操作:

   a、根据 key 计算出 hashcode ;

   b、判断是否需要进行初始化。

   c 、定位出当前 key的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。

   d、如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。

   e、如果都不满足,则利用 synchronized 锁写入数据。

   f、如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

  4)jdk1.8的ConcurrentHashMap 的get操作

   a、根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。

   b、如果是红黑树那就按照树的方式获取值。

   c、就不满足那就按照链表的方式遍历获取值。

  由于 HashEntry 中的value属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。get 方法是非常高效的,因为整个过程都不需要加锁。

posted on
2021-01-03 18:04 
好运来~~ 
阅读(68
评论(0
编辑 
收藏 
举报

 

版权声明:本文为1121cyy原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/1121cyy/p/14226379.html

一、List和Set以及Map的更多相关文章

  1. 一、认识物联网与无线传感网

    无线传感网络是当今在国际上备受关注的、多学科高度交叉的、知识高度集成的前沿热点研究领域。无线传感网络技术涉及纳 […]...

  2. (一) solr的安装与配置

    下载solr文件压缩包,并解压 ,要运行solr服务之前需要先安装jdk,具体安装过程可以参看下面这篇文章: […]...

  3. ABAP的自学之路 ,初步认识ABAP

          由于工作的关系,最近需要对SAP系统进行二次开发,于是开始学习ABAP。鉴于网上对于ABAP的资料 […]...

  4. 聚类分析 一、K-Means

    前言 人们常说“物以类聚,人以群分”,在生物学中也对生物从界门纲目科属种中进行了划分。在统计学中,也有聚类分析 […]...

  5. 一、苹果开发者平台-《测试证书》生成流程

    前言:想在苹果设备上测试开发好的IOS程序或者上传应用程序到APP STORE都需要在苹果开发者平台中生成证书 […]...

  6. Docker 使用指南 (一)—— 基本操作

    版权声明:本文由田飞雨原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/ […]...

  7. 一、frp官方中文文档

    frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp, udp, http, https 协议。 […]...

  8. 一、从GitHub浏览Prism示例代码的方式入门WPF下的Prism

    最近这段时间一直在看一个开源软件PowerToys的源码,里面使用Modules的开发风格让我特别着迷,感觉比 […]...

随机推荐

  1. xss总结漏洞篇 – 东京$

    xss总结漏洞篇 Xss漏洞 Xss漏洞出现在1996年。Xss漏洞又名跨站脚本漏洞   Xss可能造成的危害 […]...

  2. 黄聪:unix时间戳转换工具|unix时间戳在线计算|perl时间戳|php时间戳|mysql时间戳|python时间戳

    如何在不同编程语言中获取现在的Unix时间戳(Unix timestamp)? Java time JavaS […]...

  3. 华为 S5700 交换机 批量修改端口方法

    常常在配置交换机端口的时候需要将多个端口设置为相同的配置,当时各端口逐一去配置不仅慢,而且容易出错,这个时候就 […]...

  4. 树和二叉树

    树和二叉树 什么是树结构 树形结构是一类重要的非线性结构,树形结构中结点之间具有分支,并具有层次结构关系,类似 […]...

  5. USB基础简介

    一、USB2.0   Universal Serial Bus (通用串行总线)  符合USB总线数据通信要求 […]...

  6. Spring MVC系列-(3) Bean的装配

    3. 高级装配Bean 3.1 Bean的作用域 默认情况下,Spring中的bean都是以单例的形式存在的, […]...

  7. ABP 适用性改造 – 精简 ABP CLI 生成的项目结构

    Overview 不管是公司或者个人都会有不同的开发习惯,通过建立项目模板,既可以使开发人员聚焦于业务功能的开 […]...

  8. CMS项目需求分析

      开源Cms系统鱼目混杂,市场上各类的cms系统都以不同的方式存在着,当然,他们都有着自己的价值。但大的趋势 […]...

展开目录

目录导航