一、集合的框架体系
Java 集合框架提供了一套性能优良,使用方便的接口和类,其位于 java.util 包中, 所以当使用集合框架的时候需要进行导包。
Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合;另一种是图(Map),存储键/值对映射。
- 集合(Collection)体系如下:
- 图(Map)体系如下:
说明:
(1)Collection 接口有两个重要的子接口 List、 Set , 他们的实现子类都是单列集合。
(2)Map 接口的实现子类是双列集合,存放的 K-V(键值对)
1. 常用集合接口概述
如下表:
接口 | 描述 |
---|---|
Collection 接口 | Collection 是最基本的集合接口,一个 Collection 代表一组 Object(即 Collection 的元素), Java不提供直接继承自 Collection的类,只提供继承自 Collection 接口的子接口(如 List和 set)。Collection 接口存储一组不唯一,无序的对象(不能通过索引来访问 Collection 集合中的对象)。 |
List 接口 | List 接口继承自 Collection 接口 ,但 List 接口 是一个有序的集合,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(即元素在 List 中的位置,类似于数组的下标)来访问 List 集合中的元素,第一个元素的索引为 0。而且 List 集合中允许有相同的元素。可以说,List 接口的集合存储一组不唯一,有序(插入顺序)的对象。 |
Set 接口 | Set 接口继承自 Collection 接口,具有与 Collection 完全一样的接口,只是方法上有部分不同,和 Collection 接口 相同,Set 接口存储一组唯一,无序的对象。 |
Map 接口 | Map 接口与 Collection 接口同级(彼此没有继承关系),Map 图存储一组 键-值 对象,提供key(键)到value(值)的映射。 |
Set 和 List 接口的区别:
(1)Set 接口集合存储的是无序的,不重复的数据。List 接口集合存储的是有序的,可以重复的元素。
(2)Set 集合 底层使用的是 链表数据结构,其检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变 (实现子类有 HashSet , TreeSet 等)。
(3)List 结合 底层和数组类似,但是它可以动态增长,根据实际存储的数据的长度自动增长 List 的长度。其检索元素效率高,插入和删除效率低,插入和删除会引起其他元素位置改变 (实现子类有 ArrayList, LinkedList , Vector 等)。
2. 常用 Collection 集合的实现子类
Java 提供了一套实现了 Collection 接口的标准集合类。其中一些是具体类,这些类可以直接拿来使用,而另外一些是抽象类,提供了接口的部分实现。
如下表:
类名 | 描述 |
---|---|
ArrayList 类 | 该类实现了 List 接口,允许存储 null(空值)元素,且可存储重复元素。该类实现了可变大小的数组,随机访问和遍历元素时,提供了更好的性能。该类是非同步的, 在多线程的情况下不要使用。ArrayList 类在扩容时会扩容当前容量的1.5倍。 |
Vector 类 | 该类和 ArrayList 类非常相似,但该类是同步的,可以用在多线程的情况,该类允许设置默认的增长长度,默认扩容方式为原来的2倍。 |
LinkedList 类 | 该类实现了 List 接口,允许存储 null(空值)元素,且可存储重复元素,主要用于创建链表数据结构,该类没有同步方法,如果多个线程同时访问一个 LinkedList,则必须自己实现访问同步,解决方法就是在创建 LinkedList 类 时候再构造一个同步的 LinkedList 。 |
HashSet 类 | 该类实现了 Set 接口,不允许存储重复元素,并且不保证集合中元素的顺序,其允许存储 null (空值)元素,但最多只能存储一个。 |
TreeSet 类 | 该类实现了 Set 接口,不允许存储重复元素,并且不保证集合中元素的顺序,其允许存储 null (空值)元素,但最多只能存储一个。该类可以实现排序等功能。 |
3. 常用的 Map 图的实现子类
如下表:
类名 | 描述 |
---|---|
HashMap 类 | HashMap 类是一个散列表,它存储的内容是键-值对 (key-value) 映射。该类实现了 Map 接口,根据键的 HashCode 值存储y元素,具有很快的访问速度,但最多允许一条记录的键为 null (空值),它不支持线程同步。 |
TreeMap 类 | TreeMap 类继承了AbstractMap ,实现了大部分 Map 接口,并且使用一颗树。 |
Hashtable 类 | Hashtable 继承自 Dictionary(字典) 类,用来存储 键-值对。 |
Properties 类 | Properties 继承自 Hashtable,表示一个持久的属性集,属性列表中每个键及其对应值都是一个字符串。 |
特此说明:由于集合框架的内容繁多,因此本文只介绍 Collection 集合下的 Set 接口及其重要实现子类的内容,其余集合框架的知识将会在下篇博文分享。
二、Collection 接口
1. Collection 接口常用方法
- 说明:所有实现了 Collection 接口的子类集合都可以使用 Collection 接口中的方法。下面使用的是实现了 List 接口的 ArrayList 子类来举例,但实现了 Set 接口的子类都可以使用下列方法。
- 代码实现:
import java.util.ArrayList;
import java.util.List;
public class CollectionMethod {
public static void main(String[] args) {
// 以实现了Collection 接口 的子类 ArrayList 来举例;
ArrayList list = new ArrayList();
// 1. add:添加单个元素
list.add("jack");
list.add(10);// 底层自动装箱:list.add(new Integer(10))
list.add(true);// 同上
System.out.println("list=" + list);// [jack, 10, true]
// 2. addAll:添加多个元素
ArrayList list2 = new ArrayList();// 创建一个新的集合
list2.add("红楼梦");
list2.add("三国演义");
list.addAll(list2);
System.out.println("list=" + list);// [jack, 10, true, 红楼梦, 三国演义]
// 3. remove:删除指定元素,如果不指定则默认删除第一个元素
list.remove(true);// 指定删除某个元素
System.out.println("list=" + list);// [jack, 10, 红楼梦, 三国演义]
// 4. removeAll:删除多个元素
list.add("聊斋");
list.removeAll(list2);
System.out.println("list=" + list);// [jack, 10, 聊斋]
// 5. contains:查找元素是否存在,返回 boolean 值
System.out.println(list.contains("jack"));// T
// 6. containsAll:查找多个元素是否都存在,返回 boolean 值
System.out.println(list.containsAll(list2));// T
// 7. size:获取元素个数
System.out.println(list.size());// 3
// 8. isEmpty:判断是否为空
System.out.println(list.isEmpty());// F
// 9. clear:清空
list.clear();
System.out.println("list=" + list);// []
}
}
2. 迭代器(Iterator)
-
Iterator(迭代器)不是一个集合,它是一种用于访问 Collection 集合的接口,主要用于遍历 Collection 集合中的元素,所有实现了 Collection 接口的子类集合都可以使用迭代器。
-
迭代器的执行原理:
- 迭代器就相当于一个游标,初始时指向集合中的第1个元素的的前一个位置;
- 首先使用 hasNext() 方法来判断迭代器的下一个位置是否还有元素,
- 若下一个位置有元素则使用 next() 方法返回下一个元素,并将迭代器的位置向后移一位。
- 若没有,则不调用 next() 方法,直接退出迭代器。
迭代器常用方法:
(1)调用 coll.next() 会返回迭代器的下一个元素,并且更新迭代器的状态(迭代器下移)。
(2)调用 list.hasNext() 判断集合中是否还有下一个元素。
(3)调用 list.remove() 将迭代器返回的元素删除。
-
注意:在使用 next() 方法前必须使用 hasNext() 方法判断集合中是否还有下一个元素,否则可能会出现异常。
-
代码演示:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class CollectionIterator {
public static void main(String[] args) {
List list = new ArrayList();// 创建一个新的集合,该集合实现了 Collection 接口
list.add(1);
list.add(2);
list.add(3);
// 1. 先得到 list集合 对应的 迭代器
// 使用集合的 iterator() 方法来获得该集合的迭代器;
// 所有实现了 Collection 接口的子类都拥有 iterator() 方法
Iterator iterator = list.iterator();
// 2. 使用 while 循环 + 迭代器 遍历集合
while (iterator.hasNext()) {
// 首先判断集合中(下一个位置)是否还有元素
// 若有,则获取下一个位置的元素,迭代器位置向下移一位
Object obj = iterator.next();
// 输出该元素
System.out.println("obj=" + obj);
}
// 3. 注意:当退出 while 循环后 , 这时 iterator 迭代器,指向的是集合中的最后一个元素;
// 若再次 使用 iterator.next(); 会产生 NoSuchElementException 异常,因为集合中下一个位置没有元素存在了;
// 4. 如果希望再次遍历集合,需要重置 迭代器的状态;
iterator = list.iterator();// 重置迭代器
System.out.println("===第二次遍历===");
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
}
}
3. Collection 集合的遍历
- 说明:所有实现了 Collection 接口的子类集合都可以使用 下面的遍历方法。
(1)使用迭代器遍历(所有集合中都可以使用 迭代器 来遍历)。
(2)使用普通 for 循环遍历(普通 for 循环 是通过元素的索引来获取元素,在无序的集合中不能此方式来获取元素。比如 实现了 Set 接口的子类集合)。
(3)使用增强 for 循环遍历(增强 for 循环的底层其实是实现了 迭代器,因此在 所有集合中都可以使用该方式遍历)。
- 代码实现:
import java.util.*;
public class ListFor {
public static void main(String[] args) {
ArrayList list = new ArrayList();// 创建一个有序的集合
list.add("jack");
list.add("tom");
list.add("鱼香肉丝");
list.add("北京烤鸭子");
//遍历方式:
//1. 迭代器
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
System.out.println("=====增强for=====");
//2. 增强 for 循环
for (Object o : list) {
System.out.println("o=" + o);
}
System.out.println("=====普通for====");
//3. 普通 for 循环
for (int i = 0; i < list.size(); i++) {
System.out.println("对象=" + list.get(i));
}
}
}
三、Set 接口
-
Set 接口是Collection 的子接口,Set 集合存储一组唯一,无序的元素(无序即 Set 接口的元素添加和取出的顺序不一致,且元素没有索引)。
-
Set 接口 Collection 的子接口,因此,其常用方法和 Collection 接口一样。
-
代码实现:
public class SetMethod {
public static void main(String[] args) {
// 1. 以Set 接口的实现类 HashSet 来讲解Set 接口的方法
// 2. set 接口的实现类的对象(Set接口对象), 不能存放重复的元素, 可以添加一个null
// 3. set 接口对象存放元素是唯一和无序的(即添加的顺序和取出的顺序不一致)
// 4. 注意:取出的顺序的顺序虽然不是添加的顺序,但是只要取出一次,它之后取出的顺序就一样了。
Set set = new HashSet();
set.add("john");
set.add("lucy");
set.add("john");// 元素重复,存入失败
set.add("jack");
set.add("hsp");
set.add(null);// 可以添加 null
set.add(null);// 再次添加null 会失败
System.out.println(set);// 添加元素的顺序和打印出来的顺序不同
System.out.println(set);// 但多次打印出来的顺序相同。
}
}
Set 接口的遍历方式
- 同 Collection接口 的遍历方式一样,Set 接口的比遍历方法可以使用:
(1)使用迭代器遍历
(2)使用增强 for 循环
注意,不能使用 普通 for 循环遍历获取元素(因为 Set 中的对象不能通过索引来获取)。
- 代码实现:
public class SetMethod {
public static void main(String[] args) {
Set set = new HashSet();
set.add("john");
set.add("lucy");
set.add("jack");
set.add("hsp");
set.add(null);
// 遍历
// 方式1: 使用迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
// 方式2: 增强for
System.out.println("=====增强for====");
for (Object o : set) {
System.out.println("o=" + o);
}
// set 接口集合,不能通过索引来获取元素,因此不能使用普通 for 循环来获取元素。
// 下面代码会报错!!
for(int i = 0; i < set.size(); i++) {
System.out.println(set.get(i));// 注意,根本没有get 方法
}
}
}
四、HashSet 类(散列表)
1. HashSet 类基本概念
-
HashSet 类实现了 Set 接口,不允许存储重复元素,并且不保证集合中元素的顺序,其允许存储 null (空值)元素,但最多只能存储一个。
-
HashSet 类不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 必须在多线程访问时显式同步对 HashSet 的并发访问。
-
查看HashSet 类的构造器源码,可以发现,HashSet 类的底层其实是基于 HashMap 类来实现的。
-
HashSet 类实现了Set 接口,因此 HashSet 类中的常用方法和 Collection 接口中的一致,且 HashSet 集合的遍历方式和 Set 接口的一致。
-
代码演示:
public class HashSet_ {
public static void main(String[] args) {
/*
1. HashSet 类构造器的源码:
public HashSet() {
map = new HashMap<>();// HashSet 类基于 HashMap 类来创建
}
2. HashSet 可以存放null ,但是只能有一个null,即元素不能重复
*/
Set hashSet = new HashSet();
hashSet.add(null);
hashSet.add(null);// 添加失败
System.out.println("hashSet=" + hashSet);
}
}
2. HashSet 类的底层机制(重难点!!)
HashSet 类的底层是 HashMap 类,其添加的元素是无序且唯一的;HashSet 类的底层维护了一个数组+单向链表(邻接表)的数据结构。
-
简单说明数组+单向链表的数据结构:就是存在一个数组,数组中的每个元素都指向了一条单向链表。如下图:
-
这里我们只分析 HashSet 集合添加元素的底层实现机制和 HashSet 集合的底层扩容机制。先说明结论,再详细分析。
-
结论如下:
一、HashSet 类对象调用 add() 方法添加元素的底层实现机制
(1)在添加一个新元素时,首先根据这个元素的 hashcode 值得到 一个对应的 hash 值,然后将该 hash 值转成一个对应的 索引值。
(2)其次在 HashSet 类对象的领接表中检索,先检索的索引值对应的数组位置,看下该位置是否已经存储了元素。
(3)若数组的该位置未存储元素,则直接将新元素存放进该数组位置。
(4)若数组的该位置已经存储有元素(下面统称为旧元素),则需调用 equals() 方法再进行判断新、旧元素是否相同。
(4.1)若新元素 与 旧元素相同,则不存储新元素,退出 add() 方法。
(4.2)若新元素 与 旧元素不同,则进入数组该位置指向的单向链表,又要再次判断该链表是普通的单项链表,还是一个升级的红黑树。
(4.2.1)若已经是红黑树,则使用红黑树中的方法进行比较,由于红黑树太复杂,就不在此分析其底层。
(4.2.2)若是普通的单向链表,则依次遍历该链表的每个结点;若遍历链表结束,发现所有结点存储的旧元素和新元素都不同,便将新元素加入链表的尾部(将新元素存放进 HashSet 集合中),检查是否要将链表树化,再退出 add 方法;若在遍历过程中,发现存在一个旧元素与新元素相同,则不存储新元素,退出add 方法。
- 源码演示:
public class HashSetSource {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("java");// 到此位置,第1 次add分析完毕,
hashSet.add("php");// 到此位置,第2 次add分析完毕,前两个元素添加在数组的不同位置。
hashSet.add("java");// 在此位置,添加了已存在的元素,它和之前的元素重复了,所以它们 hash 值相同
System.out.println("set=" + hashSet);
对HashSet 的源码解读:
1. 执行 HashSet() 方法:
public HashSet() {
map = new HashMap<>();// 底层是 HashMap 构造器;
}
2. 执行 add() 方法:
public boolean add(E e) {
// e = "java"
return map.put(e, PRESENT)==null;// (static) PRESENT = new Object(); 这个值始终是固定的值,没意义,作用是占位
// 注意:底层 HashMap 的 put()方法需要传入键、值对(两个值),但实际 HashSet 是单列集合,不需要值,所以便将值这个位置的参数设置为静态变量,相当于没有作用。
}
3.执行 put() , 该方法首先会执行 hash(key) 得到key 对应的 hash值,使用 算法 h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value) {
// key == "java", value == PRESENT 静态变量是共享的,在 HashSet 中没有作用。
return putVal(hash(key), key, value, false, true);// hash值不等于 hashcode(),而是做了避免碰撞的处理
}
4.执行 putVal()方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; // 定义了辅助变量
// table 就是 HashMap 的一个属性,类型是 Node[]数组
// if 语句表示如果当前 table 是 null, 或者 大小=0 ;就调用 resize(),进行第一次扩容,到 16个空间.(该方法里面有个缓存算法),然后回到 putVal 方法;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 开始添加元素
// 若添加的索引位置未存放元素,看这个分支
if ((p = tab[i = (n - 1) & hash]) == null) // i 是根据hash 值算出的索引
/*
(1) 根据 key与得到的 hash, 去计算该 key 应该存放到 table表的哪个索引位置,并把这个位置的先前存在的对象,赋给 p
(2) 再判断 p (位置的对象)是否为null
(2.1) 如果p 为 null, 表示该位置还没有存放元素, 就创建一个Node (key="java", value = PRESENT)
(2.2) 然后放在该索引位置 tab[i] = newNode(hash, key, value, null)
*/
tab[i] = newNode(hash, key, value, null); // 最后一个实参 null 就是 next值
// 添加索引位置存在元素 ,看这个分支
else {
// 一个开发技巧提示: 在需要局部变量(辅助变量)时候,再创建
Node<K,V> e; K k; // 辅助变量:辅助结点 e
// 第一种情况,索引位置已存在的元素与新元素 “相同”
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样,并且满足 下面两个条件之一:
// (1) 准备加入的key 和 p 指向的Node 结点的 已存在key 是同一个对象
// (2) p 指向的Node 结点的 已存在key 的 equals() 和准备加入的 key比较后相同
// 否则不能进入
e = p; // 辅助结点 e 指向已存在的对象;
// 第二种情况,索引位置已存在的元素与新元素 “不相同”,但 索引指向了一个红黑树
else if (p instanceof TreeNode)
判断 p 是不是一颗红黑树,如果是一颗红黑树,就调用 putTreeVal , 来进行添加,
否则不能进入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 第三种情况,索引位置已存在的元素与新元素 “不相同”,但 索引指向一个链表
else {
// 如果table 对应索引位置,已经是一个链表, 就使用for 循环比较;
/* (1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后;
注意在把元素添加到链表后,立即判断 该链表是否已经达到8个结点
, 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树);
注意,在转成红黑树时,要进行判断, 判断条件:
*/
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
resize();
// 如果上面条件成立,先 进行table扩容.
// 只有上面条件不成立时,才进行转成红黑树
// (2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break
for (int binCount = 0; ; ++binCount) {
// 死循环
// (1)依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// (2)依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;// 将 p 指针往下移动,可以实现依次比较链表中的每个元素
}
}
//最后判断 e 是否指向空值,如果指向空值,说明该链表中没有元素与新元素相同,跳过下面代码;
//否则,说明 已存在重复元素,进入下面代码,不返回空值
if (e != null) {
// existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue; // 退出putVal 方法,不返回空值
}
}
++modCount;
// size: 每加入一个结点Node(k,v,h,next), size都会增加一次,不管是在数组或是索引位置。
if (++size > threshold) // threshold 属性在resize() 中是一个缓冲值,当超过该值时就需要重新调用 resize 方法扩容
resize();// 再次扩容
afterNodeInsertion(evict);// 对于hashmap 来说,该方法是个空方法,是它留给它的子类去实现的
return null;// 返回 null给 putVal方法,再返回null 给put 方法,代表成功了;如果不为null ,则失败了
}
二、HashSet 集合的底层扩容与链表红黑树化机制
(1)HashSet 的底层是 HashMap,在使用 add 方法添加元素时,会调用 resize() 方法进行数组的扩容。
(2)当第一次往 HashSet 集合中添加元素时,resize()方法会将数组的大小 (size 变量)扩容到16;在方法中设置了一个数组临界值 threshold ,是数组大小的 0.75 倍,当数组中存储的元素到达该临界值的时候,就会再次调用 resize()方法对数组进行扩容,默认是 扩容到之前数组大小的2倍,然后更新临界值 threshold。
(3)当 HashSet 集合中的某个数组位置中的链表的元素个数到达一个固定值时(默认为8个),集合就会将普通的单向链表进行红黑树化,但前提是数组的大小已到达64;否则要先对数组扩容(直到数组大小到达64),再进行树化。
- 代码举例:
public class HashSetIncrement {
public static void main(String[] args) {
/*
HashSet 底层是HashMap, 第一次添加时,table 数组扩容到 16,
临界值(threshold)是 16*加载因子(loadFactor)是0.75 = 12
如果table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,
新的临界值就是 32*0.75 = 24, 依次类推
*/
HashSet hashSet = new HashSet();
// 往集合中添加元素,下列的每个元素都是添加到集合的数组中,不会添加到链表中
// 所以集合将会一直进行数组的扩容。
for(int i = 1; i <= 100; i++) {
hashSet.add(i); // 1,2,3,4,5...100
}
/*
在Java8中, 如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是 8 ),
并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),
否则仍然采用数组扩容机制,意思是说到第11个元素时就会扩容到64了
*/
hashSet = new HashSet(); // 创建一个新的空集合
// 往集合中添加元素,但与上面不同,这次添加的元素会添加到数组的同一个索引位置,
// 因此数组的大小不会改变,依旧是默认的16。
// 由于每个元素都不同,所以这些元素会加入到数组的单向链表中,
// 当链表中的元素增加到8个时,集合就要将普通链表进行树化;
// 但此时数组的大小为16,不满足树化要求的数组大小为64,所以要先进行数据的扩容;
// 则新加入的第9、10个元素依然是添加到单向链表的后面,此时数组大小扩容到64;
// 在添加第11 个元素时,数组大小和链表长度都满足了树化的条件,因此集合将链表进行树化。
// 注意,本例中的每个元素都是不同的,但他们的hash 值相同,因此加入的数组的索引位置相同。
for(int i = 1; i <= 12; i++) {
hashSet.add(new A(i));
}
/*
当我们向hashset增加一个元素,-> Node -> 加入table , 就算是增加了一个size
在 table中 size > threshold ,就会扩容
*/
hashSet = new HashSet(); // 再次创建一个新的空集合
// 在 集合的某一条链表上添加了 7个 A对象
for(int i = 1; i <= 7; i++) {
hashSet.add(new A(i));
}
// 在另一条链表上添加到第4 个 B对象的时候,size = 12,到达临界值,数组会进行 resize()扩容
// 但是由于为满足某条链表的元素个数 = 8,所以不会进行树化。
for(int i = 1; i <= 7; i++) {
hashSet.add(new B(i));
}
}
}
class B {
private int n;
public B(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 200;
}
}
class A {
private int n;
public A(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 100;
}
}
- 上面的解释过程,需要小伙伴们自己动手去 debug ,才能真正理解。
五、LinkedHashSet 类(链式散列表)
1. LinkedHashSet 类的基本概念
(1)LinkedHashSet 类是 HashSet 类的子类,可以使用 HashSet 类中的所有常用方法和遍历方法。
(2)LinkedHashSet 类底层是一个 LinkedHashMap ,底层维护了一个数组+双向链表的数据结构。
(3)LinkedHashSet 类根据元素的 hashCode 值来决定元素在数组中的存储位置(和 HashSet 一样),同时使用双向链表维护了元素的次序,这使得元素看起来是以插入顺序保存的(即元素的添加顺序和输出顺序是相同的),即对 HashSet 进行了扩展。
(4)LinkedHashSet 类不允许添重复元素,允许有null 值,不能使用 索引来获取元素。
2. LinkedHashSet 类的底层机制
- 示意图如下:
- 说明:LinkedHashSet 集合的底层机制其实和 HashSet 集合的差不多,只是将 单向链表换成了 双向链表,所以元素的添加和取出看上去是有序的,但本质上 LinkedHashSet 集合中元素的存放仍是无序的,不能使用索引来获取该集合中的元素。
六、TreeSet 类
- 源码分析
public class TreeSet_ {
public static void main(String[] args) {
// 1. 当我们使用无参构造器,创建 TreeSet 时,仍然是无序的
// 2. 老师希望添加的元素,按照字符串大小来排序
// 3. 使用TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类),并指定排序规则
// TreeSet treeSet = new TreeSet();// 普通构造器
// 带参构造器
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
// return ((String) o2).compareTo((String) o1); 按照字符串大小排序
return ((String) o1).length() - ((String) o2).length();// 按照长度大小排序
}
});
// 添加数据
treeSet.add("jack");
treeSet.add("tom");// 3
treeSet.add("sp");
treeSet.add("a");
treeSet.add("abc");// abc 长度和 tom 相同,加入失败
System.out.println("treeSet=" + treeSet);
源码分析:
1. 构造器把传入的比较器对象,赋给了 TreeSet的底层的 TreeMap的属性this.comparator
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
2. 在 调用 treeSet.add("tom"), 在底层会执行到
if (cpr != null) {
//cpr 就是我们的匿名内部类(对象)
do {
parent = t;
//动态绑定到我们的匿名内部类(对象)compare
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else //如果相等,即返回0,这个Key就没有加入
return t.setValue(value);
} while (t != null);
}
}
}
总结
- 本文是小白博主在学习B站韩顺平老师的Java网课时整理总结的学习笔记,在这里感谢韩顺平老师的网课,如有有兴趣的小伙伴也可以去看看。
- 本文详细介绍了 集合框架 的基本概念,并深入讲解了 Collection 集合中常用的 Set 接口、HashSet 类、LinkedHashSet 类 使用的注意事项和常用方法,并介绍了迭代器的使用;还分析了各个子类实现的源码,举了很多很多例子,希望小伙伴们看后能有所收获!
- 最后,如果本文有什么错漏的地方,欢迎大家批评指正!一起加油!!我们下一篇博文见吧!