提示:文章写Collection集合中的Set接口

一、Set接口

1、概述

        Set 集合就像是一个罐子,将数据丢进来就行,因为是丢进来的,Set集合通常不会记忆元素的添加顺序(无序)。Set接口的方法和Collection基本相同,但Set不允许包含重复(无序)的元素。 Set接口的实现类常用的有:HashSet、TreeSet和LinkedHashSet。特点有:元素不可重复

2、Set的常用方法

add(E e) 如果 set 中尚未存在指定的元素,则添加此元素
clear() 移除此 set 中的所有元素
contains(Object o) 如果 set 包含指定的元素,则返回 true
 isEmpty() 如果 set 不包含元素,则返回 true
 size() 返回 set 中的元素数(其容量)

3、案例

import java.util.HashSet;
public class Set_Test {
    public static void main(String[] args) {
        HashSet<String> stringHashSet = new HashSet<>();
        stringHashSet.add("java");
        stringHashSet.add("hello");
        stringHashSet.add("world");
        stringHashSet.add("java");
        System.out.println(stringHashSet);//直接遍历stringHashSet中的元素
        stringHashSet.contains("hello");//判断集合是否为空
        System.out.println(stringHashSet.size());//显示集合中元素的数量
        stringHashSet.clear();
        System.out.println(stringHashSet.size());//判断clear后集合中元素的数量
    }
}

二、HashSet类

1、HashSet概述

        HashSet是Set接口的典型实现,大多时候使用Set集合的时候都是使用这个实现类。因为使用Hash算法来储存元素,因此有很好的存储于查找性能。

2、数据结构       

        数据结构:底层采用了哈希表,哈希表的本质就是“数组+链表”

① 根据对象的哈希值计算(求余)存储位置,如果当前位置没有元素则直接存入
② 如果当前位置有,则拿当前的元素和已经存在的元素比较哈希值,如果哈希值不同,则将当前元素进行存储
 ③ 如果哈希值相同则通过equals()方法比较两个元素的内容,如果内容不相同则将当前元素进行存储,如果内容相同则不存储当前元素

   

3、案例

public class HashSetTest {
    public static void main(String[] args) {
        Set<Student> hs = new HashSet<Student>();
        //创建学生对象
        Student s1 = new Student("林青霞", 30);
        Student s2 = new Student("张曼玉", 35);
        Student s3 = new Student("王祖贤", 33);
        Student s4 = new Student("王祖贤", 33);
        //把学生添加到集合
        hs.add(s1);
        hs.add(s2);
        hs.add(s3);
        hs.add(s4);
        //遍历集合(增强for)
        for (Student s : hs) {
            System.out.println(s.toString());
        }
    }
}
class Student {
    private String name;
    private int age;

    public Student() {
    }

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

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    
     //重写equal方法
    @Override
    public boolean equals(Object o) {
        System.out.println("equals()被调用了......");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        if (age != student.age) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }
	
    /**重写hashCode方法
    @Override
    public int hashCode() {
        System.out.println("hashCode()被调用了......");
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
   }
	*/
}

三、LinkedHashSet类

1、概述

        LinkedHashSet是Set接口基于双向链表的实现,也是 HashSet 的子类。元素的存储和取出顺序是一致的。特点:有序,元素唯一。

2、数据结构

        LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的,如图:

LinkedHashSet 的有序性本质是 LinkedHashMap 的特性,其核心逻辑简化如下:

// LinkedHashMap 核心结构(简化)
class LinkedHashMap<K,V> extends HashMap<K,V> {
    // 双向链表的头尾节点
    transient LinkedHashMap.Entry<K,V> head;
    transient LinkedHashMap.Entry<K,V> tail;
    
    // 将新节点链接到双向链表的尾部
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
       LinkedHashMap.Entry<K,V> last = tail;
       tail = p;
       if (last == null) {
           // 【核心】如果是第一个元素,head 指向该节点
           head = p;
       } else {
           // 非第一个元素:维护双向链表的引用关系
           p.before = last;
           last.after = p;
       }
}
}

3、案例

import java.util.Iterator;
import java.util.LinkedHashSet;

public class LinkedHashSetTest {
    public static void main(String[] args) {
        LinkedHashSet<Object> set = new LinkedHashSet<>();//创建LinkedHashSet集合
        set.add(456);
        set.add(123);
        set.add(123);
        set.add("AA");
        set.add("CC");
        set.add(new Student("Tom",12));
        set.add(new Student("Tom",12));
        set.add(129);

        System.out.print("[ ");
        Iterator<Object> iterator = set.iterator();//使用迭代器遍历
        while (iterator.hasNext()){
            Object next = iterator.next();
                System.out.print(next + " ");//根据遍历后的结果观察特点
        }
        System.out.println("]");
    }
}

四、TreeSet类

1、概述

        TreeSet是Set接口基于红黑树的实现类;默认对元素进行自然排序;

2、数据结构

public class  TreeNode{
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int x){
        val = x;
    }
}

我们可以观察到它的根节点为18,左侧节点为小于根节点的元素,右侧是大于根节点的元素。左侧元素14,它的分支左侧为小于14的元素,右侧为大于14的元素,依次排序。根节点右侧同左侧一样。当我们插入数据时可以提高效率。它可以根据红黑二叉树进行排序。

3、案例

public class TreeSetTest {
    public static void main(String[] args) {
        Set<Integer> set = new TreeSet<Integer>();
        set.add(10);
        set.add(40);
        set.add(30);
        set.add(50);
        set.add(20);
        
        set.add(30);

        for (Integer i : set) {
            System.out.println(i);
        }
    }
}

四、Comparator比较器

        如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。

import java.util.Arrays;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

public class TreeSetTest2 {
    public static void main(String[] args) {
        TreeSet<Integer> intSet = new TreeSet<>();
        intSet.add(5);
        intSet.add(2);
        intSet.add(8);
        intSet.add(2);
        intSet.add(9);
        intSet.add(1);//添加数据
        System.out.println(intSet);
        System.out.println(intSet.first());//打印第一个数据
        System.out.println(intSet.last());//打印第二个数据

        //使用内部类实现Comparator接口
        Set<String> strSet = new TreeSet<>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                if (o1.length() != o2.length()){
                    return o1.length() - o2.length();//根据比较集合长度进行升序排序
                }
                return o1.compareTo(o2);//根据比较集合内容进行升序排序
            }
        });
        strSet.addAll(Arrays.asList("张三","李四","王五","赵六儿","孙七","周八","张三"));
        System.out.println(strSet);
    }
}

五、Iterator迭代器

1、Iterator接口概述

        在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.IteratorIterator接口也是Java集合中的一员,但它与CollectionMap接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历)Collection中的元素,因此Iterator对象也被称为迭代器。

2、方法

public Iterator iterator() 获取集合对应的迭代器,用来遍历集合中的元素的
public E next() 返回迭代的下一个元素。
public boolean hasNext() 如果仍有元素可以迭代,则返回 true。

3、案例

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class TestIterator {
    public void test01(){
        Collection coll = new ArrayList();
        coll.add("张三");
        coll.add("李四");
        coll.add("王五");

        Iterator iterator = coll.iterator();
        System.out.println(iterator.next());
        System.out.println(iterator.next());
        System.out.println(iterator.next());
        System.out.println(iterator.next());
    }

    public void test02(){
        Collection coll = new ArrayList();
        coll.add("张三");
        coll.add("李四");
        coll.add("王五");
        Iterator iterator = coll.iterator();//获取迭代器对象
        while(iterator.hasNext()) {//判断是否还有元素可迭代
            System.out.println(iterator.next());//取出下一个元素
        }
    }
}

4、 使用Iterator迭代器删除元素

        方法:void remove() ;

5、Iterator迭代器的快速失败(fail-fast)机制

        如果在Iterator、ListIterator迭代器创建后的任意时间从结构上修改了集合(通过迭代器自身的 remove 或 add 方法之外的任何其他方式),则迭代器将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就完全失败,而不是冒着在将来不确定的时间任意发生不确定行为的风险。

        这样设计是因为,迭代器代表集合中某个元素的位置,内部会存储某些能够代表该位置的信息。当集合发生改变时,该信息的含义可能会发生变化,这时操作迭代器就可能会造成不可预料的事情。因此,果断抛异常阻止,是最好的方法。这就是Iterator迭代器的快速失败(fail-fast)机制。

5.1、ConcurrentModificationException异常
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class TestConcurrentModificationException {
    public static void main(String[] args) {
        Collection coll = new ArrayList();
        coll.add("hello");
        coll.add("world");
        coll.add("java");
        coll.add("haha");
        coll.add("mysql");

        Iterator iterator = coll.iterator();
        while(iterator.hasNext()){
            String str = (String)iterator.next();
            if(str.contains("a")){
                coll.remove(str);//foreach遍历集合过程中,调用集合的remove方法
            }
        }

        /*for (Object o : coll) {
            String str = (String) o;
            if(str.contains("a")){
                coll.remove(o);//foreach遍历集合过程中,调用集合的remove方法
            }
        }*/
    }
}
5.2、modCount变量

迭代器如何实现快速失败(fail-fast)机制的呢?

  • 在ArrayList等集合类中都有一个modCount变量。它用来记录集合的结构被修改的次数。

  • 当我们给集合添加和删除操作时,会导致modCount++。

  • 然后当我们用Iterator迭代器遍历集合时,创建集合迭代器的对象时,用一个变量记录当前集合的modCount。例如:int expectedModCount = modCount;,并且在迭代器每次next()迭代元素时,都要检查 expectedModCount != modCount,如果不相等了,那么说明你调用了Iterator迭代器以外的Collection的add,remove等方法,修改了集合的结构,使得modCount++,值变了,就会抛出ConcurrentModificationException。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐