Java 入门指南:Set 接口

Collection 接口

Collection 接口提供了一系列用于操作和管理集合的方法,包括添加、删除、查询、遍历等。它是所有集合类的根接口,包括 ListSetQueue 等。

![[Collection UML.png]]

Collection 接口常见方法

  • add(E element):向集合中添加元素。

  • addAll(Collection col):将 col 中的所有元素添加到集合中

  • boolean remove(Object obj):通过元素的equals方法判断是否是要删除的那个元素,只删除找到的第一个元素

  • boolean removeAll(Collection col):取两集合差集

  • boolean retain(Collection col):把交集的结果存在当前的集合中,不影响col

  • boolean contains(Object obj):判断集合中是否包含指定的元素。

  • boolean containsAll(Collection col):调用元素的equals方法来比较的。用两个两个集合的元素逐一比较

  • size():返回集合中的元素个数。

  • isEmpty():判断集合是否为空。

  • clear():清空集合中的所有元素。

  • iterator():返回用于遍历集合的迭代器。

  • hashCode(): 获取集合对象的哈希值

  • Object[] toArray():转换成对象数组

Set 接口

  • Set 接口是 Collection 的子接口,Set集合中的元素是无序,同时不可重复的,可以存放null元素

  • Set 接口的实现类有 HashSettreeSetLinkedHashSet

Set 接口是一个不包含重复元素的集合。Set 接口的主要特点是:

  • 无序性Set 中的元素是没有顺序的,除非使用特定的实现类(如LinkedHashSet)。无序性不等于随机性,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。

  • 不可重复性Set 不允许重复的元素。指添加、重写的元素按照 equals() 判断时,返回 false,需要同时重写 equals() 方法和 hashCode() 方法。

Set 接口常用方法

![[Set Methods.png]]

Set 常见实现类

Set 接口在 Java 中有多种实现类,其中最常用的是 HashSetLinkedHashSetTreeSet。Set 的三个实现类都线程不安全

HashSet

HashSet 是 Java 中实现 Set 接口的一个常用类。它基于哈希表实现,不允许包含重复元素,并且不保留元素的插入顺序HashSet 允许存储 null 元素。

HashSet主要特点
  1. 不允许重复元素HashSet 内部使用哈希表来存储元素,利用哈希值来快速定位元素位置,因此不会存储重复的元素。

  2. 无序集合:HashSet 不保持元素的插入顺序,即无法按照添加顺序或元素值的顺序访问元素

  3. 允许存储 null 元素:HashSet 可以存储 null 元素,但只能存储一个 null,因为重复元素不被允许。

HashSet 四种构造方法
  1. HashSet():构建一个空的 HashSet 对象,其初始容量为默认值 16负载因子默认值 0.75
Set<String> set = new HashSet<>(); 

public HashSet(){
	map = new HashMap<>();
}

HashSet 的构造方法中可以看出,底层实际是实现了HashMap

  1. HashSet(Collection <? extends E> col):创建一个包含指定集合 col中的元素的新 HashSet 对象。将其初始化为给定集合中的元素。

  2. HashSet(int initCapacity):创建一个空的 HashSet 对象,同时指定初始容量 initCapacity,负载因子为默认值 0.75

  3. HashSet(int initCapacity,float loadFactor):创建一个空的 HashSet 对象,指定初始容量 initCapacity 和负载因子 loadFactor

负载因子:比如说当前的容器容量是 16,负载因子是 0.75, 16\*0.75 = 12 ,也就是说,当容量达到了12的时候就会进行扩容操作。简单来说相当于扩容机制的一个阈值,当超过这个阈值的时候就会触发扩容。

HashSet 使用示例
import java.util.HashSet;
import java.util.Set;

public class HashSetExample {
    public static void main(String[] args) {
        // 创建一个 HashSet
        Set<String> names = new HashSet<>();

        // 添加元素
        names.add("Alice");
        names.add("Bob");
        names.add("Charlie");

        // 尝试添加重复元素
        names.add("Alice");  // 不会被添加,因为不允许重复元素

        // 检查集合是否包含指定元素
        System.out.println(names.contains("Alice"));  // 输出: true

        // 遍历集合中的所有元素
        for (String name : names) {
            System.out.println(name);
        }

        // 移除元素
        names.remove("Bob");
        System.out.println(names);  // 输出: [Alice, Charlie] (顺序不确定)

        // 清空集合
        names.clear();
        System.out.println(names.isEmpty());  // 输出: true
    }
}
HashSet 扩容机制

resize() 数组扩容方法:

  1. 判断 table 表是否为null,如果为null,则为table表进行容量开辟
    newCap = DEFAULT_INITIAL_CAPACITY;
    默认的初始值为 DEFAULT_INITIAL_CAPACITY(16);

    newThr = (int)(DEFAULT_LOAD_FACTOR (0.75)* DEFAULT_INITIAL_CAPACITY);
    扩容阈值为:newThr= 16 * 0.75 = 12;

    当集合容量达到12时再次调用 resize() 方法进行扩容

  2. 第二步:当进行第二次扩容,以及之后每一次扩容的时候,每次到达扩容阈值的时候,容量扩容到原先的两倍 newCap = oldCap << 1,新的扩容阈值为:newThr = newCap * 0.75 = 24,以此类推(12,24,36,48.....)

HashSet 添加数据底层源码

添加元素,调用 map.put() 方法

 public boolean add(E e) {
	 return map.put(e, PRESENT)==null;
 }

首先进行添加元素时,要先通过计算其 hash 值来确认要添加到数组位置索引

public V put(K key, V value) {
   return putVal(hash(key), key, value, false, true);
} 

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  1. 计算出 key(传进来的元素)的 hashCode 值
  2. 将计算出的 hashCode 值再无符号右移16位得到最终的hash值

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;
  1. 第一次添加元素时先判断 table 表是否为 null
 //如果为null将通过resize()方法扩容给table赋初始容量(16)
//接下来每一次都是当集合容量达到扩容阈值时调用resize()方法进行扩容
 if ((tab = table) == null || (n = tab.length) == 0)
	n = (tab = resize()).length;
  1. 每一次向集合中添加元素的时候,会调用该元素的 hashCode() 方法得到一个地址值,接着将得到的地址值放进 tab 数组中进行查询,若当前位置为 null 直接将元素添加到当前位置。
 if ((p = tab[i = (n - 1) & hash]) == null)
	tab[i] = newNode(hash, key, value, null);
  1. 如果当前位置已经存放元素,那么会先判断当前传进来的对象和已有对象是否是同一对象,或者调用equals方法进行比较,如果满足其一,新的元素将会覆盖原先对象的值
 else {
	Node<K,V> e; 
	K k;
 if (p.hash == hash && ((k = p.key) == key || 
        (key != null && key.equals(k))))
	 e = p;

 //这里主要是用来判断当前对象是否已经树化,如果树化将会调用红黑树的添加方法进行元素添加
	 else if (p instanceof TreeNode)
		 e = ((TreeNode<K,V>)p).
					 putTreeVal(this, tab, hash, key, value);

 //经过比较当前传入元素与当前元素所处tab数组位置处的元素不是同一对象,
 //则与当前位置对象next所以指的对象一一比较
 //如果p.next==null就直接将当前元素添加去。
	 else {
		 for (int binCount = 0; ; ++binCount) {
			 
			if ((e = p.next) == null) {
				p.next = newNode(hash, key, value, null);
			
			if (binCount >= TREEIFY_THRESHOLD - 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;
  1. 判断当前集合容量是否达到扩容阈值,若果到达扩容阈值就先进行扩容,然后再将元素添加进去, 反之直接添加即可。
	 
 if (++size > threshold)
	 resize();
	 afterNodeInsertion(evict);
	 return null;
 }
LinkedHashSet

LinkedHashSet 是 Java 中的一个实现了 Set 接口的类,它是 HashSet 的子类。与 HashSet 不同,LinkedHashSet 保留了元素的插入顺序,因此可以按照插入顺序迭代访问元素。它基于 HashTable 实现,同时使用链表来维护元素的插入顺序。

LinkedHashSet 主要特点
  1. 不允许重复元素:与 HashSet 一样,在 LinkedHashSet 中不能存储重复的元素。

  2. 有序集合LinkedHashSet 保留了元素插入的顺序,因此迭代遍历 LinkedHashSet 可以按照插入顺序访问元素。

  3. 允许存储 null 元素:LinkedHashSet 可以存储一个 null 元素,但只能存储一个 null,重复的 null 元素不被允许

LinkedHashSet 的四种构造方法
  1. LinkedHashSet():创建一个具有默认容量(16),负载因子(0.75)的新的空连接散列集。
Set<String> set = new LinkedList<>(); 
  1. LinkedHashSet(Collection <? extends E> col):创建一个包含指定集合 col中的元素的新 LinkedHashSet 对象。将其初始化为给定集合中的元素。

  2. LinkedHashSet(int initCapacity):创建一个空的 LinkedHashSet 对象,同时指定初始容量 initCapacity,负载因子为默认值 0.75

  3. LinkedHashSet(int initCapacity, float loadFactor):创建一个空的 LinkedHashSet 对象,指定初始容量 initCapacity 和负载因子 loadFactor

LinkedHashSet 使用示例
import java.util.LinkedHashSet;
import java.util.Set;

public class LinkedHashSetExample {
    public static void main(String[] args) {
        // 创建一个 LinkedHashSet
        Set<String> names = new LinkedHashSet<>();

        // 添加元素
        names.add("Alice");
        names.add("Bob");
        names.add("Charlie");

        // 尝试添加重复元素
        names.add("Alice");  // 不会被添加,因为不允许重复元素

        // 检查集合是否包含指定元素
        System.out.println(names.contains("Alice"));  // 输出: true

        // 遍历集合中的所有元素
        for (String name : names) {
            System.out.println(name);
        }

        // 移除元素
        names.remove("Bob");
        System.out.println(names);  // 输出: [Alice, Charlie] (按插入顺序)

        // 清空集合
        names.clear();
        System.out.println(names.isEmpty());  // 输出: true
    }
}
LinkedHashSet 底层机制
  1. 底层扩容机制与 HashSet 扩容机制相同

  2. LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组+双向链表 它根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序, 其遍历顺序和插入顺序一致

  3. LinkedHashSet 中有 headtail, 分别指向链表的头和尾。每一个节点有before 和 after 属性

    在添加一个元素时,先求 hashCode 值,再求索引,确定该元素在table表中的位置,然后将添加的元素加入到双向链表(如果该元素已经存在,则不添加)p.next= newElement; newElement.pre = p;

添加元素底层源码
 public boolean add(E e) {
         return map.put(e, PRESENT)==null;
      }

LinkedHashSet 的底层大多实现原理与 HashSet 相同,同时实现了 LinkedHashMap

table[] 数组的类型为 HashMap $Node [],且数组里每一个结点的类型为 LinkedHashMap $Entry

当传进元素时,会先将元素创建为 Node<K,V> ,然后将Node<K,V>里的K-V封装到数据类型为 Entry<k,v>entrySet<Entry<k,v>> 集合中去

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
       LinkedHashMap.Entry<K,V> p =
       new LinkedHashMap.Entry<K,V>(hash, key, value, e);
       linkNodeLast(p);
       return p;
}
// LinkedHashMap 的静态内部类 Entry 继承自 HashMap 的静态内部类 Node
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
       Entry(int hash, K key, V value, Node<K,V> next) {
       super(hash, key, value, next);
    }
}

//底层Node是没有任何直接遍历方法,因此会将Node<k,v>实现Entry<k,v>接口,
//通过Entry<k,v>里的getKey()和getValue()方法来获取元素 
static class Node<K,V> implements Map.Entry<K,V> {
    
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    
    Node(int hash, K key, V value, Node<K,V> next) {
   	 this.hash = hash;
   	 this.key = key;
   	 this.value = value;
   	 this.next = next;
    }
    
    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
   	 LinkedHashMap.Entry<K,V> last = tail;
   	 tail = p;
   	 if (last == null)
   	 head = p;
   	 else {
   	 p.before = last;
   	 last.after = p;
    }
}
TreeSet

TreeSet 是 Java 中的一个实现了 SortedSet 接口的类,它基于红黑树(Red-Black Tree) 的数据结构实现。与 HashSet 和 LinkedHashSet 不同,TreeSet 是有序的集合,可以保持元素的自然排序(例如,数字按升序,字符串按字典序)

SortedSet 接口是 Java 集合框架中的一种有序集合,它继承自 Set 接口,并添加了一些与集合元素排序相关的方法。SortedSet 保证集合中的元素按照特定的顺序排列,并且不允许出现重复的元素。

SortedSet 接口的常用实现类是 TreeSet,它基于红黑树数据结构实现了有序集合,可以自动对元素进行排序。

TreeSet 相较于 HashSet 性能较差

TreeSet 主要特点
  1. 不允许重复元素:与其他 Set 实现类一样,TreeSet 中不能存储重复的元素。

  2. 有序集合:TreeSet 会根据元素的自然顺序进行排序。如果元素不具备自然顺序,则需要在创建 TreeSet 时提供一个 Comparator 对象来指定排序规则。

  3. 支持高效的查找和遍历:由于 TreeSet 内部使用红黑树,它的插入、删除和查找操作的时间复杂度都是 O(log n)

  4. 不是线程安全的

  5. JDK8 以后,集合中的元素不可以是 null(如果为空,则会抛出异常 java.lang.NullPointerException)

TreeSet 的四种构造方法
  1. TreeSet():创建一个空的 TreeSet,它按照元素的自然顺序进行排序。
Set<Stirng> set = new TreeSet<>();
TreeSet<String> set = new TreeSet<>();
  1. TreeSet(Comparator<? super E> comparator):创建一个空的 TreeSet,并使用指定的比较器对元素进行排序。比较器可以自定义,用于指定元素的排序规则。
TreeSet<String> set = new TreeSet<>(new MyComparator());
  1. TreeSet(Collection <? extends E> col):创建一个 TreeSet,并将指定集合 col 中的元素添加到 TreeSet 中。元素将按照自然顺序进行排序。

  2. TreeSet(Sorted <E> sortedSet):创建一个 TreeSet,并使用指定排序集合的比较器对元素进行排序。这样可以将一个已经排序好的集合转换为 TreeSet。

SortedSet<String> sortedSet = new TreeSet<>();
sortedSet.add("apple");
sortedSet.add("banana");
sortedSet.add("orange");

TreeSet<String> treeSet = new TreeSet<>(sortedSet);

在使用自定义对象作为 TreeSet 元素时,需要确保对象实现了 Comparable 接口或传入了合适的比较器。否则可能会抛出 ClassCastException 异常。

TreeSet 使用示例
import java.util.TreeSet;
import java.util.Set;

public class TreeSetExample {
    public static void main(String[] args) {
        // 创建一个 TreeSet
        Set<String> names = new TreeSet<>();

        // 添加元素
        names.add("Alice");
        names.add("Bob");
        names.add("Charlie");

        // 尝试添加重复元素
        names.add("Alice");  // 不会被添加,因为不允许重复元素

        // 检查集合是否包含指定元素
        System.out.println(names.contains("Alice"));  // 输出: true

        // 遍历集合中的所有元素
        for (String name : names) {
            System.out.println(name);
        }

        // 移除元素
        names.remove("Bob");
        System.out.println(names);  // 输出: [Alice, Charlie] (按字母顺序)

        // 清空集合
        names.clear();
        System.out.println(names.isEmpty());  // 输出: true
    }
}
TreeSet 底层实现机制
  • TreeSet 底层使用的是红黑树实现,对于元素之间排序,如果不指定自定义的外部比较器 ——Comparator,那么插入的对象必须实现内部比较器——Comparable 接口,元素按照实现此接口的 compareTo() 方法去排序。

  • TreeSet 判断两个对象不相等的方式:两个对象通过 equals 方法返回false,或者通过 CompareTo 方法比较没有返回0

  • 在不使用默认排序的情况下,可以重写 compare() 方法来实现自定义排序

compare() 底层源码
![[compare Source Code.png]]

TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String)o1).compareTo((String)o2);
            }
// String、Integer等类均已实现Comparable接口,无需另外实现
compareTo()方法

compareTo 方法定义在 Comparable 接口中,用于比较一个对象与另一个对象的顺序。

int compareTo(T object)

@Override 
public int compareTo(Person o) { 
	if (this.age > o.getAge()) {
		 return 1; 
	} 
	if (this.age < o.getAge()) {
		 return -1; 
		 
	} 
	return 0; 
}

  • 如果返回值 < 0,则表示当前对象小于与其比较的对象。
  • 如果返回值 == 0,则表示当前对象等于与其比较的对象。
  • 如果返回值 > 0,则表示当前对象大于与其比较的对象

在实际使用中,当需要对自定义对象进行排序时,通常实现 Comparable 接口,并在其中重写 compareTo 方法。该方法的具体实现根据业务需求来决定。

添加元素
public boolean add(E e) {
        return m.put(e, PRESENT)==null;
 }
  1. 创建一个 Entry<K,V> 类型的 root(根)结点,之后每次添加的子结点类型都为 Entry<K,V>
public V put(K key, V value) {
	Entry<K,V> t = root;
	if (t == null) {
		compare(key, key); // type (and possibly null) check
	
		root = new Entry<>(key, value, null);
		size = 1;
		modCount++;
		return null;
	}
  1. 每次添加元素的时候,都会调用 compare() 方法判断当前添加的元素与集合中已有元素是否为同一元素
    如果不是则直接添加,同时根据 compare() 方法返回值来判断添加的位置
	int cmp;
	Entry<K,V> parent;
// split comparator and comparable paths
	Comparator<? super K> cpr = comparator;
	if (cpr != null) {
		do {
			parent = t;
			cmp = cpr.compare(key, t.key);
			if (cmp < 0)
				t = t.left;
			else if (cmp > 0)
				t = t.right;
			else
				return t.setValue(value);
		} while (t != null);
	}
  1. 传进来的子节点先与根结点进行判断:

    • 如果大于根结点,则让结点与根结点的子结点进行比较
    • 如果传入元素小于任意子结点的左右结点其中一个结点,则让该结点作为该元素的双亲结点
  2. 传入元素与双亲结点进行比较,如果大于双亲结点添加到右子树,如果小于双亲结点,则添加到左子树,否则直接返回值

	Comparable<? super K> k = (Comparable<? super K>) key;

	do {
		parent = t;
		cmp = k.compareTo(t.key);
		if (cmp < 0)
			t = t.left;
		else if (cmp > 0)
			t = t.right;
		else
			return t.setValue(value);
		} while (t != null);
	}
//将传进来的值封装成​​Entry<K,V>类型,然后根据判断放进双亲结点的子节点中
	Entry<K,V> e = new Entry<>(key, value, parent);
	
	if (cmp < 0)
		parent.left = e;
	else
		parent.right = e;
	
	fixAfterInsertion(e);
	size++;
	modCount++;
	return null;
}
Comparable 和 Comparator的区别

Comparable 接口和 Comparator 接口都是 Java 中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用:

  • Comparable 接口出自 java.lang 包 它有一个 compareTo(Object obj)方法用来排序

  • Comparator 接口 出自 java.util 包它有一个 compare(Object obj1, Object obj2)方法用来排序

一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法,当我们需要对某一个集合实现两种排序方式:

比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,可以:

  • 重写compareTo() 方法
  • 使用自制的 Comparator 方法
  • 以两个 Comparator 来实现歌名排序和歌星名排序,使用两个参数版的 Collections.sort()
HashSet、LinkedHashSet 和 TreeSet 三者的异同
  • HashSetLinkedHashSetTreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。

  • HashSetLinkedHashSetTreeSet 的主要区别在于底层数据结构不同:

    • HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。

    • LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。

    • TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。

  • 底层数据结构不同又导致这三者的应用场景不同:

    • HashSet 用于不需要保证元素插入和取出顺序的场景

    • LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景

    • TreeSet 用于支持对元素自定义排序规则的场景。

总结

Set 接口是 Java 集合框架中的一个重要接口,它提供了存储唯一元素的能力。通过不同的实现类,可以满足不同的需求场景。在使用 Set 接口时,需要注意元素的唯一性是基于 hashCode()equals() 方法的,因此必须确保这两个方法被正确实现。同时,也需要根据实际需求选择合适的实现类。

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值