第十五章 Set集合(中)

1.Set介绍

        Set接口是Collection的子接口,set接口没有提供额外的方法。Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法。

     |-----Collection接口:单列集合,用来存储一个一个的对象
        |------Set接口:存储无序的、不可重复的数据。----》高中的集合
            |----HashSet:作为Set接口的主要实现类,线程不安全,可以存null值
                |----LinkedHashSet:作为hashSet的子类,遍历其内部数据时,可以按照添加的顺序遍历
                        对于频繁遍历操作,LinkedHashSet效率高于HashSet。
            |----TreeSet:可以按照添加对象的指定属性,进行排序。
      1.Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法。
      2.要求:向Set中添加的数据,其所在的类一定要重写hashCode()和equals()
       要求:重写的hashCode()和equals()尽可能保持一致性:相等的对象必须具有相等的散列码
        重写两个方法的小技巧:对象中作equals()方法比较的Field,都应该用来计算hashCodeSet:存储无序的,不同重复的数据
  // hashSet
  1.无序的:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的hash值。

  2.不可重复的:保证添加的元素按照equals()判断时,不能返回true。即:相同的元素只能添加一个。
二、添加元素的过程:以HashSet为例
  我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,
  此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已有元素:
  如果此位置上没有其他元素,则元素a添加成功。----》情况1
  如果此位置上有其他元素b(后以链表形式存在的多个元素),则比较元素a于元素b的hash值:
      如果hash值不同,则元素a添加成功---》情况2
      如果hash值相同,进而调用元素所在类的equals()方法:
          equals()返回true,元素添加失败
          equals()返回false,则元素添加成功。----》情况2
  对于添加成功的情况2和情况3而言:元素a于已经存在指定索引位置上数据以链表的方式存储。
  jdk7:元素a方法数组中,指向原来的元素。
  jdk8:原来的元素在数组中,指向元素a
  总结:七上八下
  HashSet底层:数组+链表

2.实现类HashSet

        HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
        HashSet 具有以下特点:不能保证元素的排列顺序,HashSet 不是线程安全的,集合元素可以是 null。
        HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
        对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
       
 向HashSet中添加元素的过程:当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好)。
        如果两个元素的hashCode()值相等,会再继续调用equals方法,如果equals方法结果为true,添加失败;如果为false,那么会保存该元素,但是该数组的位置已经有元素了,那么会通过链表的方式继续链接。
       如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功。 

        底层也是数组,初始容量为16,当如果使用率超过0.75,(16*0.75=12)就会扩大容量为原来的2倍。(16扩容为32,依次为64,128....等)

public void test() {
        Set set = new HashSet();
        set.add(123);
        set.add(456);
        set.add("CC");
        set.add(new Person("Jerry", 20));
        set.add(new String("TOm"));
        Iterator iterator1 = set.iterator();
        while (iterator1.hasNext()) {
            System.out.print(iterator1.next() + "\t");
        }

    }

        Set里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用==还是equals()? 它们有何区别:Set里的元素是不能重复的,用equals()方法判读两个Set是否相等equals()和==方法决定引用值是否指向同一对象equals()在类中被覆盖,为的是当两个分离的对象的内容和类型相配的话,返回真值。

2.1.重写 hashCode() 方法原则

        在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

2.2.重写 equals() 方法的基本原则

        以自定义的Customer类为例,何时需要重写equals()?当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法,它们仅仅是两个对象。因此,违反了“相等的对象必须具有相等的散列码”
        结论:复写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

2.3.复写hashCode方法有31原因

        选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突);并且31只占用5bits,相乘造成数据溢出的概率较小;31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率);31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)。

3.实现类LinkedHashSet

        LinkedHashSet 是 HashSet 的子类。LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
        LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能,LinkedHashSet 不允许集合元素重复。

public void test1() {
        // LinkedHashSet()的使用
        // LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个和后一个数据
        Set set = new LinkedHashSet();
        set.add(456);
        set.add(123);
        set.add(123);
        set.add("AA");
        set.add(new Person("Jerry", 20));
        set.add(new Person("Jerry", 20));
        set.add(new String("TOm"));
        Iterator iterator1 = set.iterator();
        while (iterator1.hasNext()) {
            // 456	123	AA	Person{name='Jerry', age=20}	TOm
            System.out.print(iterator1.next() + "\t");
        }

    }

4.实现类TreeSet

        TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。TreeSet底层使用红黑树结构存储数据。

1.向TreeSet中添加的数据,要求是相同类的对象
2.两种排序方式:自然排序和定制排序
3.自然排序中,比较两个对象是否相同的标准为:compareTo()返回0.不再equals().
3.定制排序中,比较两个对象是否相同的标准为:compare()返回0.不再equals().

       新增的方法如下:Comparator comparator();Object first();Object last();Object lower(Object e);Object higher(Object e);SortedSet subSet(fromElement, toElement);SortedSet headSet(toElement);SortedSet tailSet(fromElement) 。
        TreeSet和后面要讲的TreeMap采用红黑树的存储结构;特点:有序,查询速度比List快。

4.1.红黑树五大性质

        节点必须是红色或黑色;根节点是黑色;叶子节点(外部节点、空节点)都是黑色;红色节点的子节点都是黑色(推论:红色节点的父节点都是黑色;从根节点到叶子节点的所有路径上不能有两个连续的红色节点);从任意节点到叶子节点的所有路径都包含相同数据的黑色节点。

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

    public Person() {
    }

    public Person(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 boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

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

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

    // 按照姓名从大到小排列,年龄从小到大排列
    @Override
    public int compareTo(Object o) {
        if (o instanceof Person) {
            Person person = (Person) o;
            //  return this.name.compareTo(person.name);
            int compare = -this.name.compareTo(person.name);
            if (compare != 0) {
                return compare;
            } else {
                return Integer.compare(this.age, person.age);
            }
        } else {
            throw new RuntimeException("输入的类型不匹配");
        }

    }
}
@Test
    public void test2(){
        TreeSet set = new TreeSet();
        // 失败,不能添加不同类的对象
//        set.add(456);
//        set.add(123);
//        set.add("AA");
//        set.add(new Person("Jerry", 20));
        // 举例1
//        set.add(23);
//        set.add(45);
//        set.add(43);
//        set.add(3);
//        set.add(78);
//        Iterator iterator = set.iterator();
//        while (iterator.hasNext()){
//            // 3	23	43	45	78
//            System.out.print(iterator.next() + "\t");
//        }

        set.add(new Person("Jerry", 20));
        set.add(new Person("Tom", 20));
        set.add(new Person("Hi", 20));
        set.add(new Person("Nd", 20));
        set.add(new Person("CH", 20));
        Iterator iterator = set.iterator();
        while (iterator.hasNext()){
            // Person{name='Tom', age=20}	Person{name='Nd', age=20}	Person{name='Jerry', age=20}	Person{name='Hi', age=20}	Person{name='CH', age=20}
            System.out.print(iterator.next() + "\t");
        }

    }
    @Test
    public void test3(){
        // 定制排序
        Comparator com = new Comparator() {
            // 按照年龄从小到大排序
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof Person && o2 instanceof Person){
                    Person p1 = (Person)o1;
                    Person p2 = (Person)o2;
                    return Integer.compare(p1.getAge(), p2.getAge());
                }else {
                    throw new RuntimeException("输入的类型不匹配");
                }
            }
        };
        TreeSet set = new TreeSet(com);
        set.add(new Person("Jerry", 20));
        set.add(new Person("Tom", 23));
        set.add(new Person("Hi", 25));
        set.add(new Person("Nd", 50));
        set.add(new Person("CH", 60));
        Iterator iterator = set.iterator();
        while (iterator.hasNext()){
            // Person{name='Jerry', age=20}	Person{name='Tom', age=23}	Person{name='Hi', age=25}	Person{name='Nd', age=50}	Person{name='CH', age=60}
            System.out.print(iterator.next() + "\t");
        }

5.排序

5.1.自然排序

        自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列。如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable接口。实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。
Comparable 的典型实现:

实现方法描述
BigDecimal、BigInteger 以及所有的数值型对应的包装类按它们对应的数值大小进行比较
Character按字符的 unicode值来进行比较
Charactertrue 对应的包装类实例大于 false 对应的包装类实例
String按字符串中字符的 unicode 值进行比较
Date、Time后边的时间、日期比前面的时间、日期大

        向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较返回值。
        当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。否则,让人难以理解。

5.2.定制排序

        TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表
示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
        要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。

5.3.TreeSet的自然排序与定制排序

//定义一个 Employee 类。
//该类包含:private 成员变量 name,age,birthday,其中 birthday 为
public class Employee implements Comparable {
    private String name;
    private int age;
    private MyData birthday;

    public Employee(String name, int age, MyData birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    public Employee() {
    }

    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;
    }

    public MyData getBirthday() {
        return birthday;
    }

    public void setBirthday(MyData birthday) {
        this.birthday = birthday;
    }

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

    // 按照姓名顺序排
    @Override
    public int compareTo(Object o) {
        if (o instanceof Employee) {
            Employee e = (Employee) o;
            return this.name.compareTo(e.name);
        } else {
            throw new RuntimeException("类型不匹配");
        }

    }
}
//创建该类的 5 个对象,并把这些对象放入 TreeSet 集合中(下一章:
//TreeSet 需使用泛型来定义)
//分别按以下两种方式对集合中的元素进行排序,并遍历输出:
//使 Employee 实现 Comparable 接口,并按 name 排序
//创建 TreeSet 时传入 Comparator 对象,按生日日期的先后排序。
public class EmployeeTest {
    @Test
    public void test1() {
        // 使用自然排序
        TreeSet set = new TreeSet();
        Employee e1 = new Employee("Thy", 24, new MyData(1999, 1, 5));
        Employee e2 = new Employee("CH", 23, new MyData(1998, 3, 15));
        Employee e3 = new Employee("Hgx", 22, new MyData(1996, 8, 23));
        Employee e4 = new Employee("Lyt", 21, new MyData(1997, 5, 28));
        Employee e5 = new Employee("Wty", 34, new MyData(1990, 10, 05));
        set.add(e1);
        set.add(e2);
        set.add(e3);
        set.add(e4);
        set.add(e5);
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

    @Test
    public void test2() {
        // 按生日日期的先后排序
        TreeSet set = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if (o1 instanceof Employee && o2 instanceof Employee) {
                    Employee e1 = (Employee) o1;
                    Employee e2 = (Employee) o2;
                    MyData b1 = e1.getBirthday();
                    MyData b2 = e2.getBirthday();
                    int minusYear = b1.getYear() - b2.getYear();
                    if (minusYear != 0) {
                        return minusYear;
                    }
                    int minusMonth = b1.getMonth() - b2.getMonth();
                    if (minusMonth != 0) {
                        return minusMonth;
                    }
                    return b1.getDay() - b2.getDay();
                } else {
                    throw new RuntimeException("类型不匹配");
                }
            }
        });
        Employee e1 = new Employee("Thy", 24, new MyData(1999, 1, 5));
        Employee e2 = new Employee("CH", 23, new MyData(1998, 3, 15));
        Employee e3 = new Employee("Hgx", 22, new MyData(1996, 8, 23));
        Employee e4 = new Employee("Lyt", 21, new MyData(1997, 5, 28));
        Employee e5 = new Employee("Wty", 34, new MyData(1990, 10, 05));
        set.add(e1);
        set.add(e2);
        set.add(e3);
        set.add(e4);
        set.add(e5);
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值