Java集合专题-Set集合

文章详细介绍了Java中的Set接口以及其常见实现类:HashSet、LinkedHashSet和TreeSet。HashSet基于HashMap实现,允许一个null元素,不保证元素顺序;LinkedHashSet保持插入顺序;TreeSet则能按特定规则排序,可通过比较器定制排序规则。文章还探讨了这些类的底层数据结构和遍历方式。
摘要由CSDN通过智能技术生成

@[TOC](文章目录)

    • Set集合基本介绍

1)无序(添加和取出的顺序不一致),但是取出的顺序是固定的,没有索引;

2)不允许重复元素,所以最多包含一个null;

3)JDK API中Set接口的实现类有(常用): HashSet、LinkedHashSet、TreeSet等。

    • Set接口的遍历方式

  1. 可以使用迭代器进行遍历;

Iterator iterator = set.iterator();

while (iterator.hasNext()) {

Object obj = iterator.next();

System.out.println("obj=" + obj);

}

  1. 使用增强for循环进行遍历;

for (Object o : set) {

System.out.println("o=" + o);

}

3.1 Set接口的实现类-HashSet介绍

3.1.1 HashSet的基本介绍

1) HashSet实现了Set接口;

2) HashSet实际上是HashMap,看下源码:

public Hashset() {

map = new HashMap<>();

}

3)可以存放null值,但是只能有一个null;

4) HashSet不保证元素是有序的,取决于hash后,再确定索引的结果.(即,不保证存放元素的顺序和取出顺序一致);

5)不能有重复元素/对象(Set接口的特性);

3.1.2 HashSet底层机制说明

HashSet底层是HashMap,HashMap底层是:数组table+链表+红黑树

1.HashSet中的add方法底层源码分析:

(1)执行new HashSet():

public HashSet(){

map = new HashMap<>();

}

(2)执行add():

public boolean add(E e) {

return map.put(e, PRESENT)==null;//(static) PRESENT = new Object(); }

Set为单列元素,而Set的底层是使用HashMap储存(双列),将添加进来的元素放到map的key的位置,value则使用PRESENT对象进行占位。

(3)执行map.put(e, PRESENT),该方法会执行hash(e)得到e对应的hash值,具体hash的计算为(h = key.hashCode()) ^ h >>> 16

static final int hash(Object key) {

int h;

return key == null ? 0 : (h = key.hashCode()) ^ h >>> 16;

}

public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } (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,就是第一次扩容到 16 个空间.

if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;

//(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)

if ((p = tab[i = (n - 1) & hash]) == null)

tab[i] = newNode(hash, key, value, null);

else {

Node<K,V> e; K k;

//如果当前索引位置对应的链表的第一个元素和准备添加的 key 的 hash 值一样

//并且满足 下面两个条件之一:

//(1) 准备加入的 key 和 p 指向的 Node 结点的 key 是同一个对象

//(2) p 指向的 Node 结点的 key 的 equals() 和准备加入的 key 比较后相同就不能加

if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))

e = p;

//再判断 p 是不是一颗红黑树, 如果是一颗红黑树,就调用 putTreeVal , 来进行添加

else if (p instanceof TreeNode)

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) {

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;

}

if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))

break;

p = e;

}

}

if (e != null) { // existing mapping for key

V oldValue = e.value;

if (!onlyIfAbsent || oldValue == null) {

e.value = value;

afterNodeAccess(e);

return oldValue;

}

}

++modCount; //size 就是我们每加入一个结点 Node(k,v,h,next), size++

if (++size > threshold)

resize();//扩容

afterNodeInsertion(evict);

return null;

}

(5)HashSet的扩容和转成红黑树的机制:

HashSet底层HashMap数组扩容的情况:(初始数组长度为16)

a. 当table数组中的元素达到临界值threshold=数组长度 * 0.75,就会扩容到原来数组的2倍;

b. 当table数组中的某一条链表的元素个数达到8时,再向该链表中每加入一个元素时就会将数组的长度变为原来的2倍。

每次进行上述的数组扩容时,table中元素存储的位置都会改变(length变了,i = (n - 1) & hash]

HashMap底层进行树化的条件:

如果一条链表的元素个数达到8个并且table数组的大小达到64,才进行将链表转化为红黑树;否则只进行上述的数组扩容。

3.2 Set接口实现类-LinkedHashSet

3.2.1 LinkedHashSet的介绍

1) LinkedHashSet是 HashSet的子类;

2) LinkedHashSet底层是一个 LinkedHashMap,底层维护了一个数组+双向链表

3) LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。

4) LinkedHashSet不允许添重复元素。

5) 在LinkedHashSet中每个节点除了含有next、item外,还含有before和after属性,这样就形成了双向链表;具有该双向链表就使得可以确保插入顺序和遍历顺序一致

其他特性同上述的HashSet。

3.3 Set接口实现类-TreeSet

3.3.1 TreeSet的介绍

TreeSet最大的特点就是可以在创建构造器的时候,可以传入一个比较器,进行指定Set集合中的元素的排列顺序。

3.3.2 TreeSet底层排序规则的实现

1. 构造器把传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap 的属性 this.comparator public TreeMap(Comparator comparator) { this.comparator = comparator; } 2. 在 调用 treeSet.add("xxx"), 在底层会执行到 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);

}

4. 总结

综上,在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,如下:

1)先判断存储的类型(一组对象[单列或一组键值对[双列)

2)一组对象[单列]:Collection接口

允许重复:List

增删多:LinkedList[底层维护了一个双向链表]

改查多:ArrayList[底层维护 Object类型的可变数组]

不允许重复:Set

无序:HashSet[底层是HashMap,维护了一个哈希表即(数组+链表+红黑树)

排序:TreeSet,需要指定Set中元素的排序规则

插入和取出顺序一致:LinkedHashSet,维护数组+双向链表

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值