【集合】集合汇总

目录

一、集合汇总图

二、List、Set、Map、Queue

三、ArrayList 和 LinkedList

3.1 ArrayList

3.2 LinkedList

3.3 ArrayList 和 LinkedList 的对比

3.4 Vector

3.4.1 Vector 介绍

3.4.2 ArrayList 和 Vector 对比

3.4.3 实现 List 线程安全

四、Hash

4.1 什么是 hash

4.2 哈希冲突(碰撞)

五、Map 集合类

5.1 HashMap

5.1.1 概念

5.1.2 特点总结

5.1.3 存储过程

5.2 常见问题

5.2.1 计算节点索引值的几种算法

5.2.2 当出现哈希碰撞会怎么样

5.2.3 扩容

5.2.4 Hash 的继承关系

5.2.5 默认容量是多少。为什么必须是2的n次幂?

5.2.6 加载因子的作用。默认加载因子是多少?

5.2.7 为什么 HashMap 桶中的节点个数超过 8 才转为红黑树?

5.2.8 存储结构-字段

5.2.9 遍历 HashMap 的四种方式

5.2.10 HashMap 在 jdk1.7 和 jdk1.8 中的对比

5.3 HashMap添加的对象为什么要重写 equals 和 hashcode

5.3.1 equals 和  == 的区别

5.3.2 为什么要重写 hashCode 和 equals

5.4 HashTable 和 HashMap 的区别

5.5 LinkedHashMap

5.6 TreeMap

5.7 ConcurrentHashMap

六、Set 类

6.1 概念

6.2 HashSet

6.3 TreeSet

6.4 LinkedHashSet

七、Queue 实现类

7.1 概念

7.2 ArrayBlockingQueue

7.2.1 概念

7.2.2 特点

7.2.3 常用方法

7.2.4 结构和源码分析

7.3 LinkedBlockingQueue


一、集合汇总图

 

二、List、Set、Map、Queue

● List

List 是一个有序的容器,允许元素重复,可以插入 null,每个元素有索引,适合搜索和修改节点内容。常用的实现类有 ArrayList、LinkedList 和 Vector

● Set

Set 是一个无序的容器,不允许元素重复,即只能存入一个 NULL 元素。Set 常见的实现类有 HashSet、LinkedHashSet 和 TreeSet

● Map

Map 是一个键值对的集合,其中 Key 是无序且唯一的,但 Value 允许重复。Map 没有继承 Collection 接口,从 Map 中检索数据时,只要给出 Key,就能返回对应的 Value。常见的实现类有 HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

● Queue

一种先进先出的无阻塞结构,先进入的节点必须先弹出,是一种线程安全的数据结构。常用于高并发环境,常见的实现类有 BlockQueue、LinkedBlockQueue、SynchrousQueue、ConcurrentLinkedQueue

 

三、ArrayList 和 LinkedList

3.1 ArrayList

ArrayList 类实现了一个动态扩容的数组,底层是通过数组实现的,允许快速随机存取。其适用于查找和修改数据,能够直接通过索引位置定位数据,但不适合插入、删除数据,插入和删除会导致数组重新排列。且 ArrayList 是线程不安全的。

常用方法:

修饰符和返回类型方法名和参数方法描述
voidadd​(int index, E element)将指定元素插入此列表中的指定位置
booleanadd​(E e)将指定的元素追加到此列表的末尾
abstract Eget​(int index)返回此列表中指定位置的元素。
intindexOf​(Object o)返回此列表中第一次出现的指定元素的下标,如果此列表不包含该元素,则返回-1。
Iterator<E>iterator​()以适当的顺序返回此列表中元素的迭代器。
intlastIndexOf​(Object o)返回此列表中指定元素最后一次出现的索引,如果此列表不包含该元素,则返回-1。
Eremove​(int index)删除此列表中指定位置的元素。
Eset​(int index, E element)使用指定的元素替换此列表中指定位置的元素
List<E>subList​(int fromIndex, int toIndex)返回一个新的集合,新集合元素是从原集合的fromIndex(包含自身)到toIndex(不包含自身)之间的元素
 asList(T[] t)数组转 List
 toArray()List 转数组

示例代码:

public class TestArrayList {
    public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        // 添加元素
        list.add("hello world");
        list.add("super man");
        list.add("good nice");

        // 获取元素
        System.out.println("get(0) = " + list.get(0));

        // 插入元素
        System.out.println("============ 插入元素 ============");
        list.add(1, "monday");

        // 迭代器遍历数组
        System.out.println("\n============ 迭代器遍历 ============");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        // 替换第一个元素为 lowest
        list.set(0, "lowest");

        // 返回最后一次出现的元素的索引
        System.out.println(list.lastIndexOf("good nice"));
        System.out.println(list.indexOf("good nice"));

        // 删除第二个元素
        list.remove(1);

        System.out.println("\n============ 数组分割 ============");
        List<String> arrList = list.subList(0, 2);
        Iterator<String> iterator1 = arrList.iterator();
        while (iterator1.hasNext()) {
            System.out.println(iterator1.next());
        }
    }
}

3.2 LinkedList

LinkedList 类实现了一个双向链表,可以对链表的头尾进行插入和删除操作。与 ArrayList 相比,LinkedList 更适合插入和删除。但 LinkedList 是线程不安全的。

常用方法:

修饰符和返回类型方法名和参数方法描述
voidaddFirst​(E e)在此列表的开头插入指定的元素。
voidaddLast​(E e)将指定的元素追加到此列表的末尾。
EgetFirst​()返回此列表中的第一个元素。
EgetLast​()返回此列表中的最后一个元素。
EremoveFirst​()从此列表中删除并返回第一个元素。
EremoveLast​()从此列表中删除并返回最后一个元素。
ListIterator<E>listIterator​(int index)从列表中的指定位置index开始,返回此列表中元素的列表迭代器。

示例代码:

public class StackTest {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();

        // 向头部添加元素
        list.addFirst("tom");
        list.addFirst("jerry");

        // 向尾部添加元素
        list.addLast("jack");
        list.addLast("petter");

        // 获取首尾元素
        System.out.println(list.getFirst());
        System.out.println(list.getLast());

        // 迭代器
        System.out.println("\n============ 迭代器遍历 ============");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        // 移除第一个节点并返回被删除的这个元素
        System.out.println(list.removeFirst());
        // 移除最后一个节点并返回被删除的这个元素
        System.out.println(list.removeLast());
    }
}

 

3.3 ArrayList 和 LinkedList 的对比

● 数据结构不同

ArrayList 是用动态数组实现的,而 LinkedList 的本质是双向链表

● 随机访问效率

ArrayList 支持 RandomAccess 接口,能够以下标形式直接访问数据,速度更快。而 LinkedList 是一个链表,必须从头部或从尾部移动指针顺序查找

● 插入和删除效率

ArrayList 能够快速在末尾添加或删除数据,但在其他位置插入或删除数据效率不如 LinkedList,因为要重新调整数组的结构。而 LinkedList 能够通过调整指针指向,快速插入或删除节点

● 线程安全

ArrayList 和 LinkedList 都是不同步的,不保证线程安全

 

3.4 Vector

3.4.1 Vector 介绍

Vector 和 ArrayList 大致相同,但 Vector 实现了方法同步,是线程安全的。实现方式是在内部使用了 synchronized 关键字。

由于实现了方法同步,效率不如 ArrayList

查看源码,可以发现 Vector 对关键的几个方法加上了 synchronized 关键字

 

3.4.2 ArrayList 和 Vector 对比

● 线程安全

ArrayList 是非线程安全的,而 Vector 是线程安全的,它的关键方法都加上了 synchronized 关键字

● 性能

Vector 中增加了锁机制,性能不如 ArrayList

● 扩容

ArrayList 和 Vector 都会动态扩容,但 ArrayList 每次增加 50%,而 Vector 每次增加一倍

 

3.4.3 实现 List 线程安全

即使存在 Vector 实现了线程安全,但其性能较差,还有其他更高效的方式实现线程安全

● Collections.SynchronizedList

SynchronizedList是Collections类的静态内部类,它能把所有 List 接口的实现类包装成线程安全的List,比 Vector 有更好的扩展性和兼容性。但在实际测试中,由于 Vector 和 Collections.SynchronizedList 都使用了 synchronizedList,实际两者性能没有相差很大。

使用方式:

List<String> list = Collections.synchronizedList(new ArrayList<>());

但注意,使用迭代器时需要开发者自己上锁,若骨架同步代码,则会在迭代中,其他的线程对容器的 add 或 remove 也会影响这个迭代效果。故需要在迭代代码外部加上 synchronized

synchronized (lists) {
	
	//获取迭代器
	Iterator<String> iterator = synlist.iterator();
	//遍历
	while (iterator.hasNext()) {
		System.out.println(iterator.next());
	}
}

● CopyOnWriteArrayList

Copy - On - Write ,可以知道,当写操作(add、remove、set)发生时,会进行 copy 操作,而读操作是读的复制前的旧容器,是无锁的,适用于写操作较少,读操作较多的情况。

可以看到写操作,使用 Arrays.copyOf() 来实现数组复制,是十分消耗性能的,故写操作效率较差

缺点:

由于 CopyOnWriteArrayList 的缺点较明显,一般较少使用,缺点有:

内存占用高:每次写操作都要将原容器拷贝一份,数据量大时,对内存压力大,且引起频繁 GC

无法保证实时性:由于 CopyOnWriteArrayList 的读操作是对旧容器,而写操作是对新容器,实现了读写分离,但无法保证读和写的强一致性。

 

四、Hash

4.1 什么是 hash

哈希是将任意长度的输入通过散列算法,变换成固定长度的输出,这个输出就是哈希值。

● 空间压缩

这种转换实际是一种空间映射,哈希值的空间通常小于原输入占用的空间

● 没有唯一性

不同的输入可能转成出同一个哈希值,但不同的哈希值必定对应着不同的输入

 

4.2 哈希冲突(碰撞)

哈希冲突即出现输入不同,转换出的哈希值却相同的情况

 

五、Map 集合类

5.1 HashMap

5.1.1 概念

HashMap 是基于哈希表的 Map 接口的非同步实现,提供键值对的映射操作,允许保存 null,不保证映射的顺序。

在 JDK 1.8 之前是由 链表+数组 组成的,主体是数组,链表这是为了处理哈希冲突

在 JDK 1.8 后,引入了红黑树的方式解决哈希冲突,当链表的长度大于阈值(默认为8)且当前的数组长度大于 64 时,就会将这个节点上的所有数据改为用红黑树存储。若链表长度大于8但数组长度小于64时,则依旧使用链表,且数组扩容

 

● 为什么要数组长度不小于64,节点存储才转变成红黑树

当数组长度较小时,红黑树的深度也会增加,反而会降低效率。由于红黑树需要进行左旋、右旋、变色这些操作来保持平衡,深度越深,重排的耗费也就越高。

 

5.1.2 特点总结

  • 存取是无序的
  • 键和值都可以是null,但键值要求唯一,即只能存一个null
  • 键值唯一
  • jdk1.8 前的数据结构是:数组+链表,1.8后变为:数组+链表+红黑树
  • HashMap 中可能同时存在链表和红黑树,只有当某个数组节点的数据节点大于8,且整个数组的长度大于64时,这个节点的链表才会转换成红黑树

 

5.1.3 存储过程

● 拉链法

  • 先根据 Key 值,使用 hashCode() 方法计算对应的 hash 值,之后结合数组长度,采用算法计算出在数组中存储数据的节点索引值。
  • 若计算后的这个数组节点中没有数据,则直接将原始键值对数据存储到节点中。
  • 若有数据,则会使用 equeals() 方法逐个比较原数据的 key 的 hash 值,和节点中已存在的 key 值是否相等,相等则将 Value 覆盖,不相等则将数据添加到链表后端。

size: 表示 HashMap 中 K-V 的实时数量

threshold (临界值) =  capacity (容量) * loanFactor (加载因子),这个值是当前已占用数组长度的最大值,当 size 超过这个临界值就会进行扩容,扩容后的 HashMap 容量是之前容量的两倍

 

5.2 常见问题

5.2.1 计算节点索引值的几种算法

● 底层默认

使用对 key 的 hashCode值结合数组长度进行无符号右移(>>>)、按位异或(^),按位与(&)计算出索引

● 其他方法

平方取中法,取余数,伪随机法等,但默认的位运算方式效率更高

 

5.2.2 当出现哈希碰撞会怎么样

当两个元素的key的hash值相等时,会产生哈希碰撞,若 key 值相同,则新的 value 值会覆盖旧的 value,若 key 值不同,则会添加到链表的后面,若链表长度超过阈值8且数组长度查过64,则会转为红黑树存储

 

5.2.3 扩容

当超出临界值时,会进行扩容,默认扩容为原容量的两倍,创建一个容量是当前容量一倍的数据,并将原来的数组数据复制过来

扩容后的节点要么在原位置,要么会被分配到 原位置+旧容量 的位置,由此在扩充 HashMap 时,不需要重新计算 hash,只需要看原来的 hash 新增的那个 bit 是 0 还是 1,若是 0 则在原位置,若是 1 则在 原位置+旧容器 的位置,如:

 

5.2.4 Hash 的继承关系

继承关系如下:

Cloneable: 表示可以进行克隆,创建并返回一个 HashMap 对象的副本

Serializable: 序列化接口,表示 HashMap 可以被序列化和反序列化

AbstractMap: 父类提供了 Map 实现接口,以最大限度地减少实现此接口所需的工作

 

5.2.5 默认容量是多少。为什么必须是2的n次幂?

● 默认容量

默认容量为 16(1<<4),可以在初始化时指定容量。

集合的最大容量为 1<< 30,即 2 的 30 次幂

为什么必须是 2 的 n 次幂

HashMap 为了存取高效,尽量减少碰撞,将数据均匀分配到数组中,故一般使用取模的方式进行分配索引。

由于直接取模效率不如位运算,故源码做了优化,使用  hash&(length-1),效果等同于 hash%length,而等价的前提就是 length 是 2 的 n 次幂。

若数组长度不是 2 的 n 次幂会发生什么

计算出的索引十分容易发生先沟通,容易发生哈希碰撞导致数组其他空间空闲

● 若数组初始化时传入的值不是 2 的 n 次幂会怎样

HashMap 允许在初始化时指定 initialCapacity 来指定数组容量

HashMap 会自动找到大于等于 initialCapacity 的最小的 2 的幂(如传入是10, 则会找到12)

源码分析

1) 先对 cap 减一,防止一直输入的是 2 的 n 次幂了。如输入的 cap 为16, 已经是 2 的 n 次幂了,若直接使用 16 则移位后会得到 32.故先减一变为15,移位得到16

2) 通过不断的移位和或操作,使得最高位1之后全部变成1

如 00000000 00000000 00000000 00001001 移位后得到   00000000 00000000 00000000 00001111

3) 最后得到的是一个奇数,故最后再加一

 

5.2.6 加载因子的作用。默认加载因子是多少?

● 为什么要有加载因子

HashMap 扩容并不是数组满了才扩容的,而是存在界限值,界限值 = 数组长度 * 加载因子。

loadFactor(加载因子) 是用来衡量 HashMap 疏密程度的,影响 hash 操作到同一个数组位置的概率。

 

加载因子设置不当会导致什么

loadFactor 太大:会导致数组填充过满,链表中的节点数量增加,也会导致生成更多的红黑树,降低元素的查找效率

loadFactor 太小:由于临界值 = 数组长度 x 影响因子,影响因子过小会导致数组容易扩容,

会导致数组的利用率低,存放的数据分散,

故官方给出了 0.75 这一个较为合适的临界值。

 

● 默认加载因子

默认的加载因子为 0.75,也可以在 HashMap 初始化时自定义

 

5.2.7 为什么 HashMap 桶中的节点个数超过 8 才转为红黑树?

https://mp.weixin.qq.com/s/QgkBRoADcO8Wgj0dHCc9dw?

HashMap 在桶中节点个数达到8,会进行树化,当节点小于等于6个时,会转为链化。这个 8 和 6 是通过衡量时间和空间后得出的。

原因总结:

● 节省内存空间

TreeNodes 占用的空间是普通 Nodes 的两倍

● 触发概率计算

从下图中可以看到,根据泊松分布计算,当临界值是 6 的时候的触发概率,比临界值是 8 的时候的概率大了 200 多倍

效率权衡

类型平均查找长度6个节点情况8个节点情况
红黑树log(n)log(6) = 2.6log(8) = 3
链表n/26/2 = 38/2 = 4

根据计算结果,若是将阈值设为6,由于2.6 和 3 相差不大,树结构的转换和生成也需要额外的时间开销,以及考虑到树节点的占用空间更大,故使用 8 作为阈值

 

5.2.8 存储结构-字段

HashMap 的实现数据结构是以 数组+链表+红黑树的方式实现的,但其具体底层是一个 Node(jdk1.8之前叫 Entry) 节点的数组,即哈希桶数组,其中的 Node 节点是负责存储键值对数据

 

5.2.9 遍历 HashMap 的四种方式

keys() 获取所有 key,values() 获取所有 value

HashMap map = new HashMap();
map.put(1,1);
map.put(2,2);
map.put(3,3);
map.put(4,4);
map.put(5,5);
map.put(6,6);

Set<Integer> keys = map.keySet();
for (Integer key : keys) {
    System.out.println(key);
}
Collection<Integer> values = map.values();
for (Integer value : values) {
    System.out.println(value);
}

● 使用 Iterator 迭代器迭代

Iterator iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<Integer, Integer> mapEntry = (Map.Entry<Integer, Integer>) iterator.next();
    System.out.println(mapEntry.getKey() + " === " + mapEntry.getValue());
}

● 使用 get 方式

不建议使用这种方式,因为会迭代两次。KeySet 获取迭代器一次,get又迭代一次

Set<Integer> keySet = map.keySet();
for(Integer item: keySet) {
    System.out.println( item + "====" + map.get(item));
}

jdk1.8 后使用 Map 接口中的默认方法

map.forEach((key,value) -> {
    System.out.println(key + "========" + value);
});

 

5.2.10 HashMap 在 jdk1.7 和 jdk1.8 中的对比

JDK1.8主要解决或优化了一下问题:

  1. resize 扩容优化
  2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
  3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
不同JDK 1.7JDK 1.8
存储结构数组 + 链表数组 + 链表 + 红黑树
初始化方式单独函数:inflateTable()直接集成到了扩容函数resize()
hash值计算方式扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式头插法(先讲原位置的数据移到后1位,再插入数据到该位置)尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

其他:

https://blog.csdn.net/ThinkWon/article/details/104588551/

 

5.3 HashMap添加的对象为什么要重写 equals 和 hashcode

5.3.1 equals 和  == 的区别

equals 比较的是目标的内容是否相同,而 == 比较的可能是目标的指针指向地址是否相同。

 

5.3.2 为什么要重写 hashCode 和 equals

流程

在将对象作为 hashCode() 的 key 值时,会使用自定义类的 hashCode() 函数计算出对应的数组下标

在找到对应节点后,使用 equals() 来比较当前下标上的节点是否有相同的 key,有则覆盖 value,没有则添加到尾部

● 对象的 hashCode() 的问题

重写 hashCode() 主要是解决参数相同的自定义类,会得到不同的数组下标的问题

对于对象,默认的 hashCode() 方法会根据对象的引用计算出一个散列码(整形值),并被处理形成数组下标。

若存入 HashMap 中的是不同的对象,即使内部的值都是相等的,但是不同对象的引用是不同的,则根据引用得出的 hash 值也是不同,这两个对象都会被存入 HashMap 中,存储在不同的索引位置

得到的 hash 值是不同的


 

● 对象的 equals 的问题

equals() 的问题主要是父类 Object 类的 equals() 方法之只比较了地址,会导致属性相同的不同自定义对象比较时返回 false

在自定义类中,父类为 Object 类,可以查看源码,父类的 equals 只比较了地址:

故当传入的是两个属性相同,但不是同一个对象的自定义类对象时,equals 会返回 false

 

修改方案:

一般 hashCode() 方法和 equals() 方法会一起被重写,使用内部的属性值来作为比较依据

public class MapTest {
    public static void main(String[] args) {
        HashMap<Person, String> map = new HashMap<Person, String>();
        map.put(new Person("001"), "tom");
        map.put(new Person("002"), "juddy");
        map.put(new Person("003"), "alic");
        map.put(new Person("003"), "pink");

        System.out.println(map.toString());
        System.out.println(map.get(new Person("001")));
        System.out.println(map.get(new Person("002")));
        System.out.println(map.get(new Person("003")));
    }
}

class Person {
    private String id;

    public Person(String id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        return id != null?id.hashCode():0;
    }

    @Override
    public boolean equals(Object obj) {
        //测试两个对象的索引是否相同,索引相同就返回true
        if(this==obj) {
            return true;       
        }
        //测试检测的对象是否为空,是就返回false
        if(obj==null) {
            return false;       
        }
        //测试两个对象所属的类是否相同,否则返回false
        if(getClass()!=obj.getClass()) {
            return false;       
        }

        //对obj进行类型转换以便和类A的对象进行比较
        Person person = (Person)obj;       
        //对于值可能为null的属性,检测时应使用Object的equals方法,不为null的可以直接使用==检测
        return Objects.equals(id, person.id);
    }
}
public class Student {
    //姓名、学号、年纪
    private String name;
    private int sid;
    private int age;

    //定义构造方法,给对象初始化
    public Student(){

    }
    public Student(String name,int sid,int age){
        this.name=name;
        this.sid=sid;
        this.age=age;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getSid() {
        return sid;
    }
    public void setSid(int sid) {
        this.sid = sid;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public int hashCode() {
        return sid != null?sid.hashCode():0;
    }


    //重写equlas方法,判断姓名、学号相等,就是同一个学生
    //obj是给我们的一个学生对像
    //this是我们自己的学生对像equals
    public boolean equals(Object obj){
        boolean flag=false;
        //判断对像是否相同,基本是不可能的
        if(obj==this){
            flag=true;
        }
        //
        if(!(obj instanceof Student)){
            flag=false;
        }else{
            Student stu=(Student)obj;
            if(stu.name.equals(this.name) && stu.sid==this.sid){
                flag=true;
            }
        }
        System.out.println(flag);
        return flag;
    }
}

 

5.4 HashTable 和 HashMap 的区别

Hashtable 和 HashMap 的使用方式相同,但 HashTable 已经不建议使用了,要保证线程安全应使用 ConcurrentHashMap

区别是什么:

● 线程安全

HashMap 是非线程安全的,而 HashTable 是线程安全的。HashTable 内部的关键方法都经过 synchronized。

● 效率

由于线程安全问题,HashTable 效率低于 HashMap

● 对 NULL 的支持

HashMap 支持存储 NULL,而 HashTable 不支持

初始容量大小和扩充容量不同

HashMap 默认大小为16,每次扩容为扩大一倍。而 HashTable 默认大小为11,每次扩充为 2n+1

底层数据结构

HashMap 在 jdk1.8 之后引入了红黑树,而 HashTable 没有

● 父类不同

HashTable 继承自 Dictionary 类,而 HashMap 是 Map 接口的一个实现

 

5.5 LinkedHashMap

LInkedHashMap 结合了 HashMap 和 LinkedList,实现了一个有序的 Map。虽然其增加了时间和空间上的开销,但通过维护一个运行于所有条目的双向链表,LinkedHashMap 保证了元素迭代的顺序,该迭代顺序可以是插入顺序,也可以是访问顺序。

 

5.6 TreeMap

TreeMap 是完全的一个红黑树,适用于对一个有序key进行遍历。而 HashMap 更适用于插入、删除、定位元素

 

5.7 ConcurrentHashMap

ConcurrentHashMap 通过分段锁机制实现线程安全,效率高于 HashTable。

由于 HashTable 中只有一把锁,所有的并发线程都必须同时竞争一把锁。而 ConcurrentHashMap 将数据分段,每段数据持有一把自己的锁,这样就允许多个并发线程同时访问,同时也保证了线程安全。

 

六、Set 类

6.1 概念

Set 是一个不存储重复元素的集合,有无序、值不能重复的特点

 

6.2 HashSet

HashSet 是 Set 的实现类,存储无序、不重复的元素的集合,但与 HashMap 类似,由于 HashSet 判断值是否相等是通过比较其hash 值是否相同来判断的,故如果要存储的是自定义类的话,需要重写自定义类的 hashCode() 和 equals() 方法

● HashSet 如何检查重复

先对传入的对象调用 hashCode() 来获取 hash 值,并以此确定元素在内存中的位置。

但是一个存储位置上可能存在多个元素,这时候需要使用 equals() 方法对位置上已存储的元素,与传入的新对象进行比较。

若相同,则返回存入失败,若不相同,则存入 HashSet 对象中

 

6.3 TreeSet

TreeSet 的本质是一个"有序的,并且没有重复元素"的集合,它是通过TreeMap实现的。

使用方式:

public class TestList2 {

    public static void main(String[] args) {
              testTreeSetAPIs();
            }

           // 测试TreeSet的api
           public static void testTreeSetAPIs() {
               String val;
               // 新建TreeSet
                 TreeSet tSet = new TreeSet();
                // 将元素添加到TreeSet中
                tSet.add("aaa");
                 // Set中不允许重复元素,所以只会保存一个“aaa”
                 tSet.add("aaa");
                 tSet.add("bbb");
                 tSet.add("eee");
                 tSet.add("ddd");
                 tSet.add("ccc");
                 System.out.println("TreeSet:"+tSet);
                // 打印TreeSet的实际大小
               System.out.printf("size : %d\n", tSet.size()); // 导航方法
               // floor(小于、等于)
               System.out.printf("floor bbb: %s\n", tSet.floor("bbb"));
               // lower(小于)
               System.out.printf("lower bbb: %s\n", tSet.lower("bbb"));
               // ceiling(大于、等于)
                 System.out.printf("ceiling bbb: %s\n", tSet.ceiling("bbb"));
                System.out.printf("ceiling eee: %s\n", tSet.ceiling("eee"));
                 // ceiling(大于)
                System.out.printf("higher bbb: %s\n", tSet.higher("bbb"));
                // subSet()
                 System.out.printf("subSet(aaa, true, ccc, true): %s\n", tSet.subSet("aaa", true, "ccc", true));
                 System.out.printf("subSet(aaa, true, ccc, false): %s\n", tSet.subSet("aaa", true, "ccc", false));
                 System.out.printf("subSet(aaa, false, ccc, true): %s\n", tSet.subSet("aaa", false, "ccc", true));

                 System.out.printf("subSet(aaa, false, ccc, false): %s\n", tSet.subSet("aaa", false, "ccc", false));
                 // headSet()
                 System.out.printf("headSet(ccc, true): %s\n", tSet.headSet("ccc", true));
                System.out.printf("headSet(ccc, false): %s\n", tSet.headSet("ccc", false));
                 // tailSet()
                 System.out.printf("tailSet(ccc, true): %s\n", tSet.tailSet("ccc", true));
                 System.out.printf("tailSet(ccc, false): %s\n", tSet.tailSet("ccc", false));

                 // 删除“ccc”
                 tSet.remove("ccc");
                 // 将Set转换为数组
                 String[] arr = (String[])tSet.toArray(new String[0]);
                 for (String str:arr)
                         System.out.printf("for each : %s\n", str);

                 // 打印TreeSet
                 System.out.printf("TreeSet:%s\n", tSet);
                         // 遍历TreeSet
                 for(Iterator iter = tSet.iterator(); iter.hasNext(); ) {
                         System.out.printf("iter : %s\n", iter.next());
                     }

                 // 删除并返回第一个元素
                 val = (String)tSet.pollFirst();
                 System.out.printf("pollFirst=%s, set=%s\n", val, tSet);

                 // 删除并返回最后一个元素
                val = (String)tSet.pollLast();
                 System.out.printf("pollLast=%s, set=%s\n", val, tSet);

                 // 清空HashSet
                 tSet.clear();

                 // 输出HashSet是否为空
                 System.out.printf("%s\n", tSet.isEmpty()?"set is empty":"set is not empty");
             }
}

 

6.4 LinkedHashSet

LInkedHashSet 结合了 HashSet 和 LinkedList,实现了一个有序的 Set。虽然其增加了时间和空间上的开销,但通过维护一个运行于所有条目的双向链表,LinkedHashSet 保证了元素迭代的顺序,该迭代顺序可以是插入顺序,也可以是访问顺序。

 

七、Queue 实现类

7.1 概念

Queue 是一种先进先出的结构,是线程安全的

 

7.2 ArrayBlockingQueue

7.2.1 概念

ArrayBlockingQueue 是一个数据结构的有界阻塞队列

 

7.2.2 特点

  • 线程安全
  • 初始化时必须指定大小

 

7.2.3 常用方法

方法名描述临界行为
队列尾部添加元素
add添加一个元素若队列已满,抛出 IIIegaISlabEepeplian 异常
offer添加一个元素若队列已满,返回 false。添加成功则返回 true
put添加一个元素若队列已满,则阻塞,等待有空闲位置
队列头部弹出元素
remove   移除并返回头部元素若队列为空,则抛出 NoSuchElementException 异常
poll移除并返回头部元素若队列为空,返回 null
take移除并返回头部元素若队列为空,则阻塞
获取队列头部元素(不移除)
element  返回头部元素(不移除)若队列为空,则抛出 NoSuchElementException 异常
peek 返回头部元素(不移除)若队列为空,返回 null

 

7.2.4 结构和源码分析

查看 ArrayBlockingQueue 的源码,可以发现他的设计关键点:

  • 使用一个锁,写入和弹出都需要操作锁对象
  • 数组的容量有限,故内部节点会循环使用,使用一个写指针,和一个存指针,通过不断移动指针来实现存取

● 结构分析

当队列的数组没有满:

取指针始终指向第一个节点,即arr[0],取指针 takeIndex = 0。

而随着元素被推入队列,存指针始终是下一个要存入数据的数组索引,图中 putIndex = 5。

   

当数组被填满时:

存指针重新移动到了数组第一位,putIndex = 0。

若此时有新元素要入队,根据调用的方法,可能被拒绝或阻塞。阻塞则是使用了 Condition 的方式等待

此时取走了三个节点:

当头部元素被弹出后,取指针也会不断后移,始终保存入对时间最长的元素的索引

如图中,数组中的 0 ,1,2 都被弹出了,取指针索引指向了3

    

若原本有要入队的元素被阻塞,或是有新元素要入队:

由于存指针已经到了数组末尾,为了循环利用,则重新指向函数头。如插入了一个元素,则重新插入到 arr[0]的位置,且

修改存指针 putIndex = 1

          

 

查看源码,列出关键属性

 

7.3 LinkedBlockingQueue

 

 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值