JAVA数据结构之Set集合

一、Set集合概论和特点

Set集合特点

  • 不包含重复元素的集合
  • 没有带索引的方法,所以不能使用普通for循环遍历

Set集合是一个接口,不能实体化,所以若要实体化,则需要找到它的实现类——HashSet

二、HashSet

该类实现Set接口,由哈希表(实际为HashMap实例)支持。 对集合的迭代顺序不作任何保证; 特别是,它不能保证订单在一段时间内保持不变。 这个类允许null元素。

遍历字符串演示

package Set;

import java.util.HashSet;
import java.util.Set;

public class SetDemo {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();
        // 增加数据
        set.add("Hello");
        set.add("World");
        set.add("java");
        //增强for循环输出
        for (String s : set){
            System.out.println(s);
        }
    }
}

输出结果

java
Hello
World

输入数据的时候,按照的顺序是Hello、World、java,但是输出的顺序是java、Hello、World,因此这个例子就验证了HashSet对集合的迭代顺序不作任何保证

因此,HashSet的特点是

  1. 底层数据结构是哈希表
  2. 对集合的迭代不作任何保证,也就是说不保证存储和取出来的元素顺序一样
  3. 没有带索引方法,所以不可能用for循环遍历
  4. 由于是Set集合,所以不包含重复元素的集合

三、HashSet集合保证元素唯一性的源码分析

package Set;

import java.util.HashSet;
import java.util.Set;

public class SetDemo {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();
        // 增加数据
        set.add("Hello");
        set.add("World");
        set.add("java");
        //增强for循环输出
        for (String s : set){
            System.out.println(s);
        }
    }
}

//------------------------------------------------
/*
	源码
	比如添加字符串Hello
*/
	//1、传输参数过来,e就是"Hello",调用一个put方法,然后返回
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

	//2、在put方法中,先调用一个hash方法,计算出key(也就是add方法里面的参数e)的哈希值
	//4、接收到hash方法返回的"Hello"的哈希值后,然后调用putVal方法  
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
	
	//3、key(也就是add方法中的e)通过hashCode方法,计算出key的哈希值,返回到put方法中
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

	//5、这是的形参hash就是指"Hello"的哈希值,而key就是"Hello"
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    	//6、如果这个哈希表未初始化,就对其初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    	//7、根据对象的哈希值计算对象的存储位置,如果该位置为null(说明有元素),则存储元素(new一个结点存储进去)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            /*
            	8、通过前面比较如果执行到这里,说明该存储位置有元素,那么就比较存入的元素和以前的元素的哈希值
            		这是一个短路运算符,如果前面为false,那么后面不会执行
            		8.1、如果这两个哈希值不同,那么返回false,if不成立,继续向下执行,把元素添加到集合
            		8.2、如果哈希值相同,那么会执行(k = p.key) == key || (key != null && key.equals(k)))
            			首先进行对象的比较(比较地址),如果两个地址值相同,那么说明这两个是同一个元素。
            			如果地址值不相同,那就调用equal方法,判断这两个元素的内容是否一样(两个元素哈希值一样),如果一样则						不存入集合,如果不一样,那么则把元素添加到集合中
            */
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //这个else if先不用看
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            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;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

简单概括来说就是以下几步

  1. 先如果两个元素存储位置不相同,那么直接把元素添加到集合即可
  2. 如果两个元素存储位置相同,那么比较两个元素的哈希值
  3. 如果两个元素的哈希值不一样,那么存储进去
  4. 如果两个元素的哈希值一样,那么比较两个元素的内容
  5. 如果两个元素的内容一样,那么不添加进集合
  6. 如果两个元素的内容不一样,那么不用添加进集合

HashSet集合之所以可以一个位置存储多个元素,那是因为HashSet集合整体是数组结构,而每个位置上是链式结构,所以一个位置能存储多个元素


四、哈希值

哈希值:是JDK根据对象地址或者字符串或者数字算出来的int类型的数值

因此在Object类中有一个方法可以获取对象的哈希值

  • public int hashCode() :返回对象的哈希码值

哈希值的特点在代码中体现

package CodeDemo;
/*
	学生对象类
*/
public class Student {
    private int age;
    private String name;

    public  Student(){}

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
package CodeDemo;

public class CodeDemo {
    public static void main(String[] args) {
        Student s1 = new Student(18, "刘德华");
        Student s2 = new Student(18, "刘德华");
        Student s3 = new Student(20, "周星驰");

        //同一对象多次调用hashCode()方法返回的哈希值是相同的
        System.out.println(s1.hashCode());
        System.out.println(s1.hashCode());
        System.out.println("----------------------");

        //默认情况下,不同对象的哈希值是不同的
        //可以通过方法重写,可以实现不同对象的哈希值相同
        System.out.println(s2.hashCode());
        System.out.println(s3.hashCode());
        System.out.println("----------------------");

        //不同的字符串对应的哈希值也是不同的
        System.out.println("Hello".hashCode());
        System.out.println("World".hashCode());
        System.out.println("----------------------");

        //但相同的字符串对应的哈希值是相同的
        System.out.println("JAVA".hashCode());
        System.out.println("JAVA".hashCode());
        System.out.println("----------------------");

        //不同汉字的哈希值可能一样
        System.out.println("重地".hashCode());
        System.out.println("通话".hashCode());
        System.out.println("中秋".hashCode());
    }
}

运行结果

149928006
149928006
----------------------
713338599
168423058
----------------------
69609650
83766130
----------------------
2269730
2269730
----------------------
1179395
1179395
651582

五、HashSet学生实例应用

需求:创建一个存储学生对象的集合,存储多个学生对象,使用程序遍历

要求:学生对象的成员变量值相同,认为是同一个对象

思路

  1. 定义学生类
  2. 创建HashSet集合对象
  3. 创建学生对象
  4. 把学生添加到集合
  5. 遍历集合(增强for)
  6. 在学生类中重写两个方法(hashCode()和equals())

代码实现

package CodeDemo;
/*
	学生类
*/
import java.util.Objects;

public class Student {
    private int age;
    private String name;

    public  Student(){}

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age &&
                Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }
}
package CodeDemo;
/*
	测试类
*/
import java.util.HashSet;
import java.util.Iterator;

public class HashDemo {
    public static void main(String[] args) {
        Student s1 = new Student(19, "成龙");
        Student s2 = new Student(20, "刘德华");
        Student s3 = new Student(20, "刘德华");

        HashSet<Student> hashSet = new HashSet<Student>();

        hashSet.add(s1);
        hashSet.add(s2);
        hashSet.add(s1);
        hashSet.add(s3);

        for (Student s : hashSet){
            System.out.println(s);
        }

        Iterator<Student> iterator = hashSet.iterator();
        while (iterator.hasNext()){
            Student s = iterator.next();
            System.out.println(s);
        }
    }
}

运行结果

Student{age=19, name='成龙'}
Student{age=20, name='刘德华'}
Student{age=19, name='成龙'}
Student{age=20, name='刘德华'}

为什么要重写hashCode方法呢???

那是因为hashcode重写的好还可以提升hashset的效率

六、LinkedHashSet集合概述和特点

特点

  1. 哈希表和链式实现的Set接口,具有可预测的迭代次序
  2. 由链式保证元素有序,也就是说元素的存储和取出顺序是一致的
  3. 由哈希表保证元素唯一,也就是说没有重复的元素
package CodeDemo;

import java.util.LinkedHashSet;

public class LinkDemo {
    public static void main(String[] args) {
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
        linkedHashSet.add("Hello");
        linkedHashSet.add("World");
        linkedHashSet.add("java");
        linkedHashSet.add("Hello");

        for (String s : linkedHashSet){
            System.out.println(s);
        }
    }
}
Hello
World
java

七、TreeSet集合概述和特点

TreeSet集合特点

TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序排列

  1. 元素有序,但是这里的顺序值得不是存储和取出顺序,而是按照一定的规则进行排序,具体排序方式取决于构造方法
    TreeSet():根据其元素的自然排序进行排序(自然排序是指ABCD这样的顺序)
    TreeSet(Comparator comparator):根据指定的比较器进行排序
  2. 没有带索引的方法,所以不能使用普通的for循环遍历
  3. 由于是Set集合,所以不包含重复元素的集合
package CodeDemo;

import java.util.TreeSet;

public class TreeDemo {
    public static void main(String[] args) {
        //创建集合对象
        //集合存储的是对象(引用类型),所以存储整数应该存储Inerger
        TreeSet<Integer> tree = new TreeSet<Integer>();

        //添加数据
        tree.add(10);
        tree.add(100);
        tree.add(1000);
        tree.add(10);
        tree.add(1);

        for (int num : tree){
            System.out.println(num);
        }
    }
}

输出结果

1
10
100
1000

由结果可见,输出的数据没有重复的数据

并且输入的顺序是10、100、1000、1,但是输出的顺序是1、10、100、1000,所以可证明TreeSet( )根据其元素的自然排序进行排序.

八、自然排序Comparable的使用

从例子中体会其作用

  • 存储学生对象并遍历,创建TreeSet集合使用无参构造方法
  • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
package CodeDemo;
/*
    学生类
 */
import java.util.Objects;

public class Student implements Comparable<Student> {
    private int age;
    private String name;

    public  Student(){}

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age &&
                Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }

    @Override
    public int compareTo(Student o) {
        //return 0; 表示相同
        //return 1; 表示从小到大排序
        //return -1; 表示从大到小排序
        //按照年龄从小到大排序
        int num = this.age - o.age;
        //年龄相同时候,按照姓名字母顺序排序
        int num2 = num == 0? this.name.compareTo(o.name) : num;
        return num2;
    }
}
package CodeDemo;

import java.util.TreeSet;

public class ComparableDemo {
    public static void main(String[] args) {
        //创建集合对象
        TreeSet<Student> treeSet = new TreeSet<Student>();

        //创建学生对象
        Student s1 = new Student(19, "小明");
        Student s2 = new Student(20, "小红");
        Student s3 = new Student(20, "小白");

        //添加到集合中
        treeSet.add(s1);
        treeSet.add(s2);
        treeSet.add(s3);
        treeSet.add(s1);

        for (Student s : treeSet){
            System.out.println(s);
        }
    }
}

n’n’n

运行结果

Student{age=19, name='小明'}
Student{age=20, name='小白'}
Student{age=20, name='小红'}

结论

  • 用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序
  • 自然排序,就是让元素所有的类实现Comparable接口,重写compareTo(T o)方法
  • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写

九、比较器排序Comparator

TreeSet(Comparator<? super E> comparator)

构造一个新的,空的树集,根据指定的比较器进行排序。

为了方便,这里可以运用匿名内部类的知识去解决

package TEXT;
/*
    学生类
 */

public class Student {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
package TEXT;

import java.util.Comparator;
import java.util.TreeSet;

public class Demo {
    public static void main(String[] args) {
        //创建集合对象
        TreeSet<Student> treeSet = new TreeSet<Student>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                int num = o1.getAge() - o2.getAge();
                int num2 = num == 0? o1.getName().compareTo(o2.getName()) : num;
                return num2;
            }
        });

        //创建学生对象
        Student s1 = new Student("小明", 19);
        Student s2 = new Student("小红", 20);
        Student s3 = new Student("小白", 20);

        //添加到集合中
        treeSet.add(s1);
        treeSet.add(s2);
        treeSet.add(s3);
        treeSet.add(s1);

        for (Student s : treeSet){
            System.out.println(s);
        }
    }
}

运行结果

Student{name='小明', age=19}
Student{name='小白', age=20}
Student{name='小红', age=20}

结论

  • 用TreeSet集合存储自定义对象,带参构造方法使用是比较器排序对元素进行排序的
  • 比较器排序,就是让集合构造方法接收Comparator的实现类对象重写compare(T o1, T o2)方法
  • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写