目录
3.3 ArrayList 和 LinkedList 的对比
5.2.7 为什么 HashMap 桶中的节点个数超过 8 才转为红黑树?
5.2.10 HashMap 在 jdk1.7 和 jdk1.8 中的对比
5.3 HashMap添加的对象为什么要重写 equals 和 hashcode
5.3.2 为什么要重写 hashCode 和 equals
一、集合汇总图
二、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 是线程不安全的。
常用方法:
修饰符和返回类型 | 方法名和参数 | 方法描述 |
void | add(int index, E element) | 将指定元素插入此列表中的指定位置 |
boolean | add(E e) | 将指定的元素追加到此列表的末尾 |
abstract E | get(int index) | 返回此列表中指定位置的元素。 |
int | indexOf(Object o) | 返回此列表中第一次出现的指定元素的下标,如果此列表不包含该元素,则返回-1。 |
Iterator<E> | iterator() | 以适当的顺序返回此列表中元素的迭代器。 |
int | lastIndexOf(Object o) | 返回此列表中指定元素最后一次出现的索引,如果此列表不包含该元素,则返回-1。 |
E | remove(int index) | 删除此列表中指定位置的元素。 |
E | set(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 是线程不安全的。
常用方法:
修饰符和返回类型 | 方法名和参数 | 方法描述 |
void | addFirst(E e) | 在此列表的开头插入指定的元素。 |
void | addLast(E e) | 将指定的元素追加到此列表的末尾。 |
E | getFirst() | 返回此列表中的第一个元素。 |
E | getLast() | 返回此列表中的最后一个元素。 |
E | removeFirst() | 从此列表中删除并返回第一个元素。 |
E | removeLast() | 从此列表中删除并返回最后一个元素。 |
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.6 | log(8) = 3 |
链表 | n/2 | 6/2 = 3 | 8/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主要解决或优化了一下问题:
- resize 扩容优化
- 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
- 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
不同 | JDK 1.7 | JDK 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