集合


1 集合(父接口:Collection)总结

在这里插入图片描述

  • 常见集合

    - List:可重复,可为null
    	- ArrayList:基于数组(索引—)、随机访问快、增删慢
    		- CopyOnWriteArrayList:线程安全	
    	- LinkedList:基于双向链表、随机访问慢、增删快
    	- Vector:基于数组、线程安全(性能差)
    		- Stack:Vector子类,线程安全、后进先出
    
    - Map:键值对,键唯一(不可重复)
    	- HashMap:Key和Value都可为null
    	- LinkedHashMap:链表映射/字典,hashmap特性 + 双向链表的特性
    	- TreeMap:基于红黑树、key有序
    	- SortedMap:Key 升序排列
    	- ConcurrentHashMap:分段锁、支持并发
    	- HashTable:散列表、键值对访问
    
    - Set:由对应 Map 兑现、不可重复
    	- HashSet:基于HashMap,元素为链表形式(插入数据时遍历链表)
    		- LinkedHashSet:HashSet子类,HashSet特性 + LinkedHashMap特性
    	- LinkedSet:链表集合
    	- TreeSet:基于二叉树
    	- SortedSet:有序集合
    
    - Queue
    	- ArrayQueue:基于数组、双端队列、先进先出、随机访问快
    	- Dqueue:双端队列,两端均可入列出列
    	- PriorityQueue:基于数组实现的二叉树小顶堆
    
  • 线程安全集合:Vector、Stack、Hashtable、ConcurrentHashMap、CopyOnWriteArrayList

    • Vector:内部的方法都加了synchronized关键字

    • Stack:栈,继承于Vector。

    • Hashtable:内部的方法都加了synchronized关键字

      缺点是效率低:每次加锁都会锁住全部的数据,期间其他任何线程都得等待。这样就会造成容器越大,对容器数据操作的效率将越低

    • ConcurrentHashMap:Segment(段) + HashEntry(具体的键值对)

      • 底层结构:和HashMap一样,都是数组+链表+红黑树
      • 效率低于hashmap,但远高于hashtable
      • 线程安全机制:分段锁(默认16段)。只有在求size等操作时才需要锁定整个表
        1)在jdk1.7及以前,ConcurrentHashMap会对数组分段,在实际操作时,会根据元素的索引找到其所处段,只将该段位锁住,并不影响其他段位的数据操作。
        2)jdk1.8后,依旧使用分段锁思想,只是将锁的粒度更加细分化。以数组的每个索引为为单位进行加锁
        3)具体来说,ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构,
        一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,
        每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

      在这里插入图片描述


2 List

2.1 ArrayList 列表 ——(容量可变、不安全、数组实现、访问快、插入删除慢)

ArrayList 实现了 RandomAcess 标记接口,如果一个类实现了该接口,那么表示使用索引遍历比迭代器更快。容量可变非线程安全列表,使用数组实现。集合扩容时会创建更大的数组,把原有数组复制到新数组。支持对元素的快速随机访问,但插入与删除速度很慢。

  • elementData是 ArrayList 的数据域,被 transient 修饰,序列化时会调用 writeObject 写入流,反序列化时调用 readObject 重新赋值到新对象的 elementData。原因是 elementData 容量通常大于实际存储元素的数量,所以只需发送真正有实际值的数组元素。size 是当前实际大小,elementData 大小大于等于 size。
  • modCount 记录了 ArrayList 结构性变化的次数,继承自 AbstractList。所有涉及结构变化的方法都会增加该值。expectedModCount 是迭代器初始化时记录的 modCount 值,每次访问新元素时都会检查 modCount 和 expectedModCount 是否相等,不相等就会抛出异常。这种机制叫做 fail-fast,所有集合类都有这种机制

2.2 LinkedList 队列——(双向链表实现、插入删除快、访问慢)

LinkedList 本质是双向链表,与 ArrayList 相比插入和删除速度更快,但随机访问元素很慢。除继承 AbstractList 外还实现了 Deque 接口,这个接口具有队列和栈的性质。成员变量被 transient 修饰,原理和 ArrayList 类似。

  • LinkedList 包含三个重要的成员:size、first 和 last。size 是双向链表中节点的个数,first 和 last 分别指向首尾节点的引用。
  • LinkedList 的优点:在于可以将零散的内存单元通过附加引用的方式关联起来,形成按链路顺序查找的线性结构,内存利用率较高

3 Map

3.1 HashMap

3.1.1 HashMap底层结构——(数组+链表/红黑树实现,键值对、扩容2的幂次方)

JDK8 之前底层实现是数组 + 链表,JDK8 改为数组 + 链表/红黑树,节点类型从Entry 变更为 Node。主要成员变量包括存储数据的 table 数组、元素数量 size、加载因子 loadFactor。

  • table 数组记录 HashMap 的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表
  • Node/Entry 节点包含四个成员变量:key、value、next 指针和 hash 值。

HashMap 中数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,如果两个元素 key 的 hash 值一样,就会发生哈希冲突,被放到同一个链表上,为使查询效率尽可能高,键的 hash 值要尽可能分散。

HashMap 默认初始化容量为 16,扩容容量必须是 2 的幂次方 . 最大容量为 1<< 30 、默认加载因子为 0.75。

  • 为什么必须扩容为原来的2倍?
    查看源码即可发现在计算存储位置时,计算式为:(n-1)&hash(key)容量n为2的幂次方,n-1的二进制会全为1(例如3的二进制为 11),位运算时可以充分散列,使得添加的元素均匀分布在HashMap的每个位置上,避免不必要的哈希冲突。所以扩容必须2倍就是为了维持容量始终为2的幂次方。
  • 哈希函数的实现?将键与上自身的无符号右移7位和4位得到的值
static int hash(int h) {
    return h ^ (h >>> 7) ^ (h >>> 4);
}

3.1.2 HashMap 相关方法(JDK8)

  • hash:计算元素 key 的散列值

    如果 key 为 null 返回 0,否则就将 key 的 hashCode 方法返回值高低16位异或,让尽可能多的位参与运算,让结果的 0 和 1 分布更加均匀,降低哈希冲突概率。

  • put:添加元素

    ① 调用 putVal 方法添加元素。
    ② 如果 table 为空或长度为 0 就进行扩容,否则计算元素下标位置,不存在就调用 newNode 创建一个节点。
    ③ 如果存在且是链表,如果首节点和待插入元素的 hash 和 key 都一样,更新节点的 value。
    ④ 如果首节点是 TreeNode 类型,调用 putTreeVal 方法增加一个树节点,每一次都比较插入节点和当前节点的大小,待插入节点小就往左子树查找,否则往右子树查找,找到空位后执行两个方法:balanceInsert 方法,插入节点并调整平衡、moveRootToFront 方法,由于调整平衡后根节点可能变化,需要重置根节点。
    ⑤ 如果都不满足,遍历链表,根据 hash 和 key 判断是否重复,决定更新 value 还是新增节点。如果遍历到了链表末尾则添加节点,如果达到建树阈值 7,还需要调用 treeifyBin 把链表重构为红黑树。
    ⑥ 存放元素后将 modCount 加 1,如果 ++size > threshold ,调用 resize 扩容。

  • get :获取元素的 value 值

    ① 调用 getNode 方法获取 Node 节点,如果不是 null 就返回其 value 值,否则返回 null。
    ② getNode 方法中如果数组不为空且存在元素,先比较第一个节点和要查找元素的 hash 和 key ,如果都相同则直接返回。
    ③ 如果第二个节点是 TreeNode 类型则调用 getTreeNode 方法进行查找,否则遍历链表根据 hash 和 key 查找,如果没有找到就返回 null。

  • resize:扩容数组

    重新规划长度和阈值,如果长度发生了变化,部分数据节点也要重新排列。


    重新规划长度
    ① 如果当前容量 oldCap > 0 且达到最大容量,将阈值设为 Integer 最大值,return 终止扩容。
    ② 如果未达到最大容量,当 oldCap << 1 不超过最大容量就扩大为 2 倍
    ③ 如果都不满足且当前扩容阈值 oldThr > 0,使用当前扩容阈值作为新容量。
    ④ 否则将新容量置为默认初始容量 16,新扩容阈值置为 12。


    重新排列数据节点
    ① 如果节点为 null 不进行处理。
    ② 如果节点不为 null 且没有next节点,那么通过节点的 hash 值和 新容量-1 进行与运算计算下标存入新的 table 数组。
    ③ 如果节点为 TreeNode 类型,调用 split 方法处理,如果节点数 hc 达到6 会调用 untreeify 方法转回链表。
    ④ 如果是链表节点,需要将链表拆分为 hash 值超出旧容量的链表和未超出容量的链表。对于hash & oldCap == 0 的部分不需要做处理,否则需要放到新的下标位置上,新下标 = 旧下标 + 旧容量。


3.1.3 HashMap 为什么线程不安全?

  • JDK7 :存在死循环数据丢失数据覆盖问题。
  • 数据丢失

    • 并发赋值被覆盖(多线程put同一处):HashMap底层是一个Entry数组,put操作时假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在链表的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失

    • 已遍历区间新增元素丢失(迁移时): 当某个线程在 transfer 方法迁移时,其他线程新增的元素可能落在已遍历过的哈希槽上。遍历完成后,table 数组引用指向了 newTable,新增元素丢失。

    • 新表被覆盖(多线程同时扩容): 如果 resize 完成,执行了 table = newTable,则后续元素就可以在新表上进行插入。但如果多线程同时 resize ,每个线程都会 new一个数组,这是线程内的局部对象,线程之间不可见。迁移完成后resize 的线程会赋值给 table 线程共享变量,可能会覆盖其他线程的操作,在新表中插入的对象都会被丢弃。

  • 死循环: 扩容时 resize 调用 transfer 使用头插法迁移元素,虽然 newTable 是局部变量,但原先 table 中的 Entry 链表是共享的,问题根源是 Entry 的 next 指针并发修改,某线程还没有将 table 设为newTable 时用完了 CPU 时间片,导致数据丢失或死循环。

  • JDK8 :在 resize 方法中完成扩容,并改用尾插法,不会产生死循环,但并发下仍可能数据覆盖。可用 ConcurrentHashMap 或 Collections.synchronizedMap 包装成同步集合。

3.1.4 hashmap的key为null放在那里?

null作为key时被放在了链表table下标为0的位置.

3.1.5 Hashtable和hashmap区别?🚩

  1. 同步性)HashTable的方法是同步的,HashMap不能同步。

  2. 继承的父类不同)HashTable是继承自Dictionary类,而HashMap是继承自AbstractMap类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。

  3. 对null key和null value的支持不同).HashTable不允许null值(key和value都不可以),HashMap允许使用null值(key和value)都可以。这样的键只有一个,可以有一个或多个键所对应的值为null。

  4. 遍历方法不同)HashTable使用Enumeration(枚举)遍历,HashMap使用Iterator(迭代器)进行遍历。

  5. 初始化大小和扩容方式不同)HashTable中hash数组初始化大小及扩容方式不同。

    • Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。创建时,如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,
    • 而HashMap会将其扩充为2的幂次方大小。也就是说Hashtable会尽量使用素数、奇数。而HashMap则总是使用2的幂作为哈希表的大小。
  6. 计算hash值的方法不同

    • Hashtable直接使用key对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数法来获得最终的位置。
    • HashMap为了得到元素的位置,首先需要根据元素的 Key计算出一个hash值,然后再用这个hash值来计算得到最终的位置

3.2 TreeMap🚩——(红黑树实现、key有序)

TreeMap 基于红黑树实现,增删改查的平均和最差时间复杂度均为 O(logn)最大特点是 Key 有序。Key 必须实现 Comparable 接口或提供的 Comparator 比较器,所以 Key 不允许为 null

HashMap 依靠 hashCode 和 equals 去重,而 TreeMap 依靠 Comparable 或 Comparator去重。 TreeMap 排序时,如果比较器不为空就会优先使用比较器的 compare 方法,否则使用 Key 实现的 Comparable 的 compareTo 方法,两者都不满足会抛出异常。

TreeMap 通过 put 和 deleteEntry 实现增加和删除树节点。插入新节点的规则有三个:

  • ① 需要调整的新节点总是红色的。
  • ② 如果插入新节点的父节点是黑色的,不需要调整。
  • ③ 如果插入新节点的父节点是红色的,由于红黑树不能出现相邻红色,进入循环判断,通过重新着色或左右旋转来调整。TreeMap 的插入操作就是按照 Key 的对比往下遍历,大于节点值向右查找,小于向左查找,先按照二叉查找树的特性操作,后续会重新着色和旋转,保持红黑树的特性。

4 Set

Set 不允许元素重复且无序,常用实现有 HashSet、LinkedHashSet 和 TreeSet

4.1 HashSet

  • HashSet通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,所有 Key 都使用相同的 Value ,一个名为 PRESENT 的 Object 类型常量。使用 Key 保证元素唯一性,但不保证有序性。由于 HashSet 是 HashMap 实现的,因此线程不安全

HashSet 判断元素是否相同时

  • 对于包装类型:直接按值比较
  • 对于引用类型:先比较 hashCode是否相同,不同则代表不是同一个对象,相同则继续比较 equals,都相同才是同一个对象。

4.2 LinkedHashSet

  • LinkedHashSet :继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。

4.3 TreeSet-有序

  • TreeSet通过 TreeMap 实现的,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序

🚩补充:哈希冲突的四种解决方法?优缺点?

  • 链地址法(hashmap、hashset):将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。

    • 链地址法的优点:与开放定址法相比,拉链法有如下几个优点

      • ①链地址法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
      • ②由于链地址法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况
      • ③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而链地址法中可取α≥1,且结点较大时,链地址法中增加的指针域可忽略不计,因此节省空间;
      • ④在用链地址法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点
    • 链地址法的缺点:适合节点规模更大的情况。指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

  • 开放定址法:即发生冲突时,去寻找下一个空的哈希地址。只要哈希表足够大,总能找到空的哈希地址。

    • 1)线性探测:按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。
    • 2)再平方探测:按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位(+1,4,9,16…),若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。和线性探测相比就是改变探测了步长。因为如果都是+1来探测在数据量比较大的情况下,效率会很差。
    • 3)伪随机探测:按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发生哈希冲突。
  • 再哈希法:即发生冲突时,由其他的函数再计算一次哈希值

  • 建立公共溢出区:将哈希表分为基本表和溢出表,发生冲突时,将冲突的元素放入溢出表


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值