第三十二讲 集合框架——Set接口

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yerenyuan_pku/article/details/84077020

Set接口概述

Set集合不允许存储重复元素,而且不保证元素是有序的(存入和取出的顺序有可能一致[有序],也有可能不一致[无序])。通过查看JDK文档,发现Set集合的功能和Collection的是一致的,所以Set集合取出的方法只要一个,那就是迭代器。

Set接口的常用子类

在这里插入图片描述

HashSet

查阅HashSet集合的API介绍,可发现:

此类实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set的迭代顺序,特别是它不保证该顺序恒久不变。此类允许使用null元素。

通过上面的这句话,我们可以总结出:

  1. HashSet集合采用哈希表结构存储数据,保证元素唯一性的方式依赖于hashCode()与equals()方法(后面会介绍到);
  2. HashSet集合不能保证元素的迭代顺序与元素存储顺序相同。

哈希表

哈希表概述

上面提到了HashSet集合采用哈希表结构存储数据,那什么是哈希表呢? 哈希表底层使用的也是数组机制,数组中也存放对象,而这些对象往数组中存放时的位置比较特殊,当需要把这些对象给数组存放时,会根据这些对象的特有数据结合相应的算法,计算出这个对象在数组中的位置,然后把这个对象存放在数组中。而这样的数组就称为哈希数组,即就是哈希表。

哈希表原理

当向哈希表中存放元素时,需要根据元素的特有数据结合相应的算法,这个算法其实就是Object类中的hashCode方法。由于任何对象都是Object类的子类,所以任何对象都拥有这个方法。即就是在哈希表中存放对象时,会调用对象的hashCode方法,算出对象在表中的存放位置,这里需要注意,如果两个对象hashCode方法算出来的结果一样,这种现象称为哈希冲突,这时会调用对象的equals方法,比较这两个对象是不是同一个对象,如果equals方法返回的是true,那么就不会把第二个对象存放在哈希表中,如果返回的是false,就会把这个对象通过地址链接法或拉链法存放在哈希表中。
哈希表结构存储数据的原理用图来表示:
在这里插入图片描述

总结

保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,想要保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。覆盖hashCode()方法是为了根据元素自身的特点确定哈希值,覆盖equals()方法是为了解决哈希值的冲突。

哈希表存储自定义对象

例,往HashSet中存储学生对象(姓名,年龄)。同姓名,同年龄视为同一个人,不存。

分析:HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一。

创建自定义对象Student:

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

    public Student() {
        super();
    }

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

    /*
    * 覆盖hashCode方法,根据对象自身的特点定义哈希值。
    */
    public int hashCode() {
        final int NUMBER = 37;
        return name.hashCode() + age * NUMBER; // 尽量减小哈希冲突
    }

    /**
     * 还需要定义对象自身判断内容相同的依据,覆盖equals()方法。
     */
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (!(obj instanceof Student)) {
            throw new ClassCastException("类型错误");
        }
        Student stu = (Student) obj;
        return this.name.equals(stu.name) && this.age == stu.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 + "]";
    }

}

创建HashSet集合,存储Student对象:

public class HashSetTest {

    public static void main(String[] args) {
        // 1,创建容器对象
        Set set = new HashSet();

        // 2,存储学生对象
        set.add(new Student("xiaoqiang", 20));
        set.add(new Student("wangcai", 27));
        set.add(new Student("xiaoming", 22));
        set.add(new Student("xiaoqiang", 20));
        set.add(new Student("daniu", 24));
        set.add(new Student("wangcai", 27));

        // 3,获取所有学生
        for (Iterator it = set.iterator(); it.hasNext();) {
            Student stu = (Student) it.next();
            System.out.println(stu.getName() + "::" + stu.getAge());
        }

    }

}

注意:对于判断元素是否存在,以及删除、添加等操作,依赖的方法也是元素的hashCode()和equals()方法。

LinkedHashSet

通过查阅LinkedHashSet的API介绍,我们可知道:

具有可预知迭代顺序的Set接口的哈希表和链接列表实现。此实现与HashSet的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。

可总结为:LinkedHashSet是一个特殊的Set集合,而且是有序的,底层是一个双向链表+哈希表。

public class LinkedHashSetDemo {

    public static void main(String[] args) {
        // 1,创建一个Set容器对象
        Set set = new LinkedHashSet(); 

        // 2,添加元素
        set.add("abc");
        set.add("heihei");
        set.add("haha");
        set.add("nba");

        // 3,只能用迭代器取出
        for (Iterator it = set.iterator(); it.hasNext();) {
            System.out.println(it.next());
        }

    }

}

运行以上程序,可知LinkedHashSet是有序的。

TreeSet

TreeSet是线程不同步的,可以对Set集合中的元素进行排序,底层数据结构是二叉树(也叫红黑树),保证元素唯一性的依据是:比较方法的返回值是0。更通俗一点说就是比较方法的返回值是否是0,只要是0,就是重复元素,不存。
TreeSet对集合中的元素进行排序的方式有两种,如下:
在这里插入图片描述

TreeSet存储自定义对象,使用TreeSet排序的第一种方式

例1,往TreeSet集合中存储自定义对象学生。想按照学生的年龄进行排序。
先看TreeSet排序的第一种方式——我们自定义的Student类须实现Comparable接口(该接口强制让Student类具备比较性),覆盖compareTo方法。

  • int compareTo(T o):比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

自定义的Student类的代码为:

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

    public Student() {
        super();
    }

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

    /*
     * 覆盖hashCode方法,根据对象自身的特点定义哈希值。
     */
    public int hashCode() {
        final int NUMBER = 37;
        return name.hashCode() + age * NUMBER; // 尽量减小哈希冲突
    }

    /**
     * 还需要定义对象自身判断内容相同的依据,覆盖equals()方法。
     */
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (!(obj instanceof Student)) {
            throw new ClassCastException("类型错误");
        }
        Student stu = (Student) obj;
        return this.name.equals(stu.name) && this.age == stu.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 + "]";
    }

    /**
     * 学生就具备了比较功能。该功能是自然排序使用的方法。
     * 自然排序就以年龄的升序排序为主。
     */
    @Override
    public int compareTo(Object o) {
        Student stu = (Student)o;

        // 验证TreeSet集合的add()方法调用了compareTo()方法
        System.out.println(this.name + ":" + this.age + "......" + stu.name + ":" + stu.age) ;
        if (this.age > stu.age)
            return 1;
        if (this.age < stu.age) 
            return -1;
        return 0;
    }

}

接下来编写一个测试类——TreeSetDemo.java,其代码为:

public class TreeSetDemo {

    public static void main(String[] args) {
        Set set = new TreeSet();

        set.add(new Student("xiaoqiang", 20)); 
        set.add(new Student("daniu", 24));
        set.add(new Student("xiaoming", 22));
        set.add(new Student("tudou", 18));
        set.add(new Student("dahuang", 19));

        // 3,只能用迭代器取出
        for (Iterator it = set.iterator(); it.hasNext();) {
            Student stu = (Student) it.next();
            System.out.println(stu.getName() + "::" + stu.getAge());
        }
    }

}

运行以上程序,会发现TreeSet集合中存储的学生真是按照年龄来升序排序的。
在这里插入图片描述
接着我们面临的需求又发生了变化,同姓名同年龄的学生视为同一个人,是不用存入TreeSet集合中的,而且当年龄相同时,需要按照姓名的自然顺序排序。这时自定义的Student类的代码需要修改为:

package cn.liayun.domain;

public class Student implements Comparable {
	private String name;
	private int age;
	
	public Student() {
		super();
	}
	
	public Student(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	
	/**
	 * 覆盖hashCode方法,根据对象自身的特点定义哈希值。
	 *
	 */
	public int hashCode() {
		final int NUMBER = 37;
		return name.hashCode() + age * NUMBER;
	}

	/**
	 * 需要定义对象自身判断内容相同的依据,覆盖equals方法。
	 */
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (!(obj instanceof Student)) {
			throw new ClassCastException(obj.getClass().getName() + "类型错误");
		}
		Student stu = (Student) obj;
		return this.name.equals(stu.name) && this.age == stu.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 + "]";
	}

	/**
	 * 学生就具备了比较功能。该功能是自然排序使用的方法。
	 * 自然排序就以年龄的升序排序为主。
	 */
	@Override
	public int compareTo(Object o) {
		Student stu = (Student) o;
		
//		System.out.println(this.name + ":" + this.age + "......." + stu.name + ":" + stu.age);
		
		/*
		 * 既然是同姓名同年龄是同一个人,视为重复元素,要判断的要素有两个。
		 * 既然是按照年龄进行排序,所以先判断年龄,再判断姓名。
		 */
		int temp = this.age - stu.age;
		
		return temp == 0 ? this.name.compareTo(stu.name) : temp;
		
//		return 1;
	}
}

这时测试类——TreeSetDemo.java的代码应改为:

public class TreeSetDemo {

    public static void main(String[] args) {
        Set set = new TreeSet();

        set.add(new Student("xiaoqiang", 20));
        set.add(new Student("daniu", 24));
        set.add(new Student("xiaoming", 22));
        set.add(new Student("tudou", 18));
        set.add(new Student("daming", 22));
        set.add(new Student("dahuang", 19));

        // 3,只能用迭代器取出
        for (Iterator it = set.iterator(); it.hasNext();) {
            Student stu = (Student) it.next();
            System.out.println(stu.getName() + "::" + stu.getAge());
        }
    }

}

运行结果为:
在这里插入图片描述

图解TreeSet存储元素的自然排序和唯一性

在这里插入图片描述

在这里插入图片描述
思考一个这样的问题:元素变为怎么存进去的就怎么取出来的,怎么做呢?

这时可依据二叉树原理来实现,只要让compareTo()方法返回正数即可。

import java.util.*;

class Student implements Comparable { // 该接口强制让学生具备比较性
    private String name;
    private int age;

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
    public int compareTo(Object obj) {
        return 1;
    }
}
class TreeSetDemo {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();
        ts.add(new Student("lisi02", 22));
        ts.add(new Student("lisi007", 20));
        ts.add(new Student("lisi09", 19));
        ts.add(new Student("lisi08", 19));

        Iterator it = ts.iterator();
        while(it.hasNext()) {
            Student stu = (Student)it.next();
            System.out.println(stu.getName()+"..."+stu.getAge());
        }
    }
}

TreeSet存储自定义对象,使用TreeSet排序的第二种方式

例,将Student对象存储到TreeSet集合中,同姓名同年龄视为同一个人,不存,按照学生的姓名进行升序排序,而且当姓名相同时,需要按照学生的年龄进行升序排序。

分析:当元素自身不具备比较性时,或者具备的比较性不是所需要的,这时就需要让容器自身具备比较性。定义一个比较器,将比较器对象作为参数传递给TreeSet集合的构造函数。当两种排序都存在时,以比较器为主。

我们自定义的Student类的代码没必要修改。接着,自定义一个比较器实现Comparator接口,覆盖compare()方法。

package cn.liayun.comparator;

import java.util.Comparator;

import cn.liayun.domain.Student;

/**
 * 自定义了一个比较器,用来对学生对象按照姓名进行升序排序。
 * @author liayun
 *
 */
public class ComparatorByName /*extends Object*/ implements Comparator {

	@Override
	public int compare(Object o1, Object o2) {
		Student s1 = (Student)o1;
		Student s2 = (Student)o2;
		int temp = s1.getName().compareTo(s2.getName());
		return temp == 0 ? s1.getAge() - s2.getAge() : temp;
	}

}

最后编写一个测试类——TreeSetDemo2.java进行测试。

package cn.liayun.set.demo;

import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

import cn.liayun.comparator.ComparatorByName;
import cn.liayun.domain.Student;

public class TreeSetDemo2 {

	public static void main(String[] args) {
		//初始化TreeSet集合时,明确一个比较器。
		Set set = new TreeSet(new ComparatorByName());
		
		set.add(new Student("xiaoqiang", 20));
		set.add(new Student("daniu", 24));
		set.add(new Student("xiaoming", 22));
		set.add(new Student("tudou", 18));
		set.add(new Student("daming", 22));
		set.add(new Student("dahuang", 19));

		for (Iterator it = set.iterator(); it.hasNext();) {
			Student stu = (Student) it.next();
			System.out.println(stu.getName() + "::" + stu.getAge());
		}
	}

}

练习

练习一、对多个字符串(不重复)按照长度排序(由短到长)。

分析:字符串本身具备比较性,但是它的比较方式不是所需要的,这时就只能使用比较器。

先自定义一个比较器。

package cn.liayun.test;

import java.util.Comparator;

public class ComparatorByLength implements Comparator {

	@Override
	public int compare(Object arg0, Object arg1) {
		//对字符串按照长度比较
		String s1 = (String) arg0;
		String s2 = (String) arg1;
		//比较长度,
		int temp = s1.length() - s2.length();
		//长度相同,再按照字典顺序比较
		return temp == 0 ? s1.compareTo(s2) : temp;
	}

}

再编写一个测试类Test.java进行测试:

public class Test {

    public static void main(String[] args) {
        sortStringByLength();
    }

    /*
     * 练习一:对多个字符串(不重复)按照长度排序(由短到长)
     * 
     * 思路:
     *     1,多个字符串,需要容器存储。
     *     2,选择哪个容器。字符串是对象,可以选择集合,而且不重复,选择Set集合
     *     3,还需要排序,可以选择TreeSet集合。
     *     
     */
    public static void sortStringByLength() {
        Set set = new TreeSet(new ComparatorByLength()); // 自然排序的方式

        set.add("haha");
        set.add("abc");
        set.add("zz");
        set.add("nba");
        set.add("xixixi");

        for (Object obj : set) {
            System.out.println(obj);
        }
    }
}

练习二、对多个字符串(重复),按照长度排序。

分析:自然排序可以使用String类中的compareTo方法,但是现在要的是长度排序,这就需要比较器了。所以须定义一个按照长度排序的比较器对象。

package cn.liayun.test;

import java.util.Comparator;

public class ComparatorByLength implements Comparator {

	@Override
	public int compare(Object arg0, Object arg1) {
		//对字符串按照长度比较
		String s1 = (String) arg0;
		String s2 = (String) arg1;
		//比较长度,
		int temp = s1.length() - s2.length();
		//长度相同,再按照字典顺序比较
		return temp == 0 ? s1.compareTo(s2) : temp;
	}

}

接下来编写一个测试类Test.java进行测试:

public class Test {

    public static void main(String[] args) {
        sortStringByLength();
    }

    /*
     * 练习二:对多个字符串(重复),按照长度排序。
     * 
     * 思路:
     *     1,能使用TreeSet吗?不能。
     *     2,可以存储到数组,List。这里先选择数组。
     */
    public static void sortStringByLength() {
        String[] strs = {"nba", "haha", "abccc", "zero", "xixi", "nba", "abccc", "cctv", "zero"};
        // 自然排序可以使用String类中的compareTo方法,
        // 但是现在要的是长度排序,这就需要比较器。
        // 定义一个按照长度排序的比较器对象
        Comparator comp = new ComparatorByLength();

        // 排序就需要嵌套循环,位置置换
        for (int x = 0; x < strs.length - 1; x++) {
            for (int y = x + 1; y < strs.length; y++) {
                // if (strs[x].compareTo(strs[y]) > 0) { // 按照字典顺序
                if (comp.compare(strs[x], strs[y]) > 0) { // 按照长度顺序
                    swap(strs, x, y);
                }
            }
        }

        for (String s : strs) {
            System.out.println(s);
        }
    }

    private static void swap(String[] strs, int x, int y) {
        String temp = strs[x];
        strs[x] = strs[y];
        strs[y] = temp;
    }
}

猜你喜欢

转载自blog.csdn.net/yerenyuan_pku/article/details/84077020