ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Set集合以及其实现类

2020-03-16 13:00:33  阅读:193  来源: 互联网

标签:Set HashSet 实现 System println 集合 new out TreeSet


Set集合

Set集合类似于一个罐子,不记录添加元素的添加顺序,只是不允许包含重复元素(重复的判定在不同的实现类中可能有些区别。

HashSet类

HashSet具有很好的存取和查找性能。

HashSet有以下特点:

  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也可能发生变化
  • HashSet 不是同步的,如果多个线程访问同一个HashSet ,并且有两个或两个以上的线程同时修改HashSet,则必须通过代码来保证同步
  • 集合元素值可以是null

Set像是一个罐子,记不住添加元素的顺序,所以查找的时候只能根据元素本身的属性去查找,因此Set不允许包含重复元素(这个重复的判定在不同的实现类中可能有细微的差别)

import java.util.HashSet;

class A{
    @Override
    public boolean equals(Object obj) {
        return true;
    }

    @Override
    public int hashCode() {
        return 1;
    }
}

class B{
    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

class C{
    @Override
    public int hashCode() {
        return 2;
    }
}

public class HashSetTest {
    public static void main(String[] args) {

        HashSet s = new HashSet();
        System.out.println(s.add(null));
        System.out.println(s.add(null));
        s.forEach(ele-> System.out.println(ele));

        HashSet h = new HashSet();
        System.out.println(h.add(new A()));
        System.out.println(h.add(new A()));
        System.out.println(h.add(new B()));
        System.out.println(h.add(new B()));
        System.out.println(h.add(new C()));
        System.out.println(h.add(new C()));
        System.out.println(h);
    }
}

输出结果

true
false
null
true    //可以看出 HashSet集合判断两个元素相等的标准是:两个对象通过equals()方法比较得到true,并且hashCode()方法的返回值也相等 
false
true    //equals()返回true,但hashCode()返回值不相等时,会把它们保存在不同位置,但这违反了Set集合的“不包含重复元素”的规则
true
true    //equals()返回false,但hashCode()返回值相等时,会把它们用链式结构保存在一个位置,这样会导致在访问时性能下降
true    //(两个以上的元素具有相同的hashCode值)
[A@1, C@2, C@2, B@54a097cc, B@36f6e879]

基于hashCode()和equals()方法对于HashSet的重要性,对于HashSet中的对象,要遵循以下重写这两个方法的基本规则:

  • hashCode()每次运行的返回值应该一样
  • 当两个对象通过equals()方法返回true时,这两个对象的hashCode()方法应该返回相同的值
  • 对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值

重写案例

import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;

class R{
    Double ooo;
    public R(double d){
        this.ooo = d;
    }

    @Override
    public String toString() {
        return "R{" +
                "ooo=" + ooo +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        R r = (R) o;
        return Objects.equals(ooo, r.ooo);
    }

    @Override
    public int hashCode() {
        long l = Double.doubleToLongBits(this.ooo);
        return (int)(1^(l>>>32));
    }
}
public class HashSetTest2 {
    public static void main(String[] args) {
        HashSet hs = new HashSet();
        hs.add(new R(1.1));
        // 整一个迭代器,把第一个元素取出来,没有进入迭代过程所以可以修改元素
        // (这也算不上修改吧,迭代过程中也能修改元素,
        // 但除了迭代器的remove()方法外不能用其他方法添加或删减元素)
        Iterator it = hs.iterator();
        R first = (R)it.next();
        first.ooo = 2.2;
        System.out.println(hs);
        // contains(Object o)方法:HashSet集合会先得到o的hashCode,然后到自己集合中对应的地方去找,
        // 找到后在通过equals()方法去比较,得到true后返回
        System.out.println(hs.contains(new R(1.1)));//因为equals返回了false
        System.out.println(hs.contains(new R(2.2)));//压根就没找到R(2.2)对应的hashCode
        /*
            由此可见,对于已经添加进去的对象,尽量不要修改关于该元素参与计算hashCode()和equals()的实例变量,
            否则,像上面的hs中的那一个对象,既无法通过contains(1.1)访问到,也无法通过contains(2.2)访问到
            这样会给集合操作这些元素带来很大麻烦
        */
    }
}

输出结果

[R{ooo=2.2}]
false
false

LinkedHashSet类

相对于HashSet的区别:

LinkedHashSet 是HashSet的子类,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时维护元素存储的次序。
当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合中的元素。

import java.util.LinkedHashSet;

public class LinkedHashSetTest {
    public static void main(String[] args) {
        LinkedHashSet ls = new LinkedHashSet();
        ls.add("a");
        ls.add("b");
        ls.add("c");
        ls.add("d");
        System.out.println(ls);
        ls.remove("c");
        System.out.println(ls);
    }
}
[a, b, c, d]
[a, b, d]

此实现使客户摆脱了HashSet提供的未指定的,通常混乱的排序,而不会导致与TreeSet相关的增加的成本。无论原始集的实现如何,都可以使用它来产生与原始集具有相同顺序的集合副本:

  void foo(Set s) {
         Set copy = new LinkedHashSet(s);
         ...
     }

性能

由于需要维护元素的插入顺序(其实是维护链式列表),LinkedHashSet相对于HashSet会增加开销,因此执行添加、删除等操作时性能会轻微下降,但当迭代的时候LinkedHashSet集合的迭代时间与集合的大小成正比,而HashSet集合的迭代时间与集合的容量成正比,因此LinkedHashSet的迭代性能要好一些。

当构造HashSet集合时,有两个参数,一个是初始化容量(默认是16),一个是负载因子(默认值是0.75),因为HashSet的迭代性能和容量成正比,所以我们通常不会设置太大的容量,默认或者更低。
但当构造LinkedHashSet时,选择初始化容量过高的值的副作用不会那么严重,因为其迭代性能与集合大小成正比。

TreeSet类

public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet s = new TreeSet();
        s.add(1);
        s.add(2);
        s.add(3);
        s.add(4);
        System.out.println(s);
        System.out.println(s.first());  //1
        System.out.println(s.last());   //4
        System.out.println(s.headSet(3));   //1,2
        System.out.println(s.tailSet(2));   //2,3,4
        System.out.println(s.subSet(1,3)); //1,2,3
    }
}

可以看出(好像也看不太出。。没事),TreeSet并不是根据元素的插入顺序进行排序的,而是根据元素的实际值大小来排序的
与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合元素。
TreeSet支持两种排序规则:自然排序和定制排序。在默认情况下,TreeSet采用自然排序。

TreeSet自然排序

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

a.compareTo(b)方法来自Comparable接口,该方法返回一个整数:
返回0:两个对象相等
返回1:a大于b
返回-1:a小于b
Java的一些类已经实现了Comparable接口,并提供了比较大小的标准:

  • BigDemical、BigInteger以及所有的数值型对应的包装类:按他们的大小进行比较
  • Character:按字符的Unicode值进行比较
  • Boolean:true对应的包装类实例大于false对应的包装类实例
  • String:按字符串中字符的Unicode值进行比较
  • Data、Time:(后面的时间、日期) 比 (前面的时间、日期大)
    //下面程序试图向TreeSet集合中添加两个Err对象,添加第二个对象时,TreeSet会调用
    //该对象的compareTo(Object o)方法与集合中的其他元素进行比较——
    //如果其对应类没有实现Comparable接口,则会引发ClassCastException异常
    TreeSet ts = new TreeSet();
    ts.add(new Error());
    ts.add(new Error());
    /**
    *而且,大部分类在实现compareTo(Object o)方法的时候,都需要将被比较的对象o强转为相同类型,因为只有两个相同类型才能比较大小。
    *因此,向TreeSet里添加的应该是同一个类的对象,不然容易引发ClassCastException异常
    **/

总结:如果希望TreeSet能够正常工作,TreeSet集合中只能存同一类对象。

HashSet和TreeSet中可变元素的实例变量尽量不要修改(参与相等判断的)

对于TreeSet而言,判断两个元素是否相等的唯一标准是:两个对象通过compareTo(Object o)返回的整数是不是0。

import java.util.TreeSet;

class P implements Comparable{

    int num;

    public P(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "P{" +
                "num=" + num +
                '}';
    }

    @Override
    public int compareTo(Object o) {
        P p = (P)o;
        return this.num>p.num? 1: this.num==p.num?0:-1;
    }
}

public class TreeSet2 {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();
        ts.add(new P(1));
        ts.add(new P(5));
        ts.add(new P(7));
        ts.add(new P(9));
        System.out.println(ts);
        P first = (P)ts.first();
        first.num = 100;
        P last = (P)ts.last();
        last.num = 7;
        System.out.println(ts);

        System.out.println(ts.remove(new P(1)));
        System.out.println(ts.remove(new P(100)));
        System.out.println(ts);
        System.out.println(ts.remove(new P(5)));//①
        System.out.println(ts);
        /**
         * 添加后的元素如果实例变量被修改(而且这个实例变量能够影响compareTo()方法的判断,TreeSet也不会重新排序
         * 反而可能会造成出现重复的元素,导致两个都无法被删除,给操作带来困难
         * 
         * 一旦修改了TreeSet集合里可变元素的实例对象,当再试图删除该对象时,TreeSet也会删除失败
         * 甚至连集合中原有的、实例变量没有被修改但与修改后的元素相等的元素也无法被删除
         *
         * 说明TreeSet可以删除没有被修改实例变量、且不与其他修改实例变量的对象重复的对象
         * 
         * 当执行上面①行代码时,TreeSet会对集合中的元素重新索引(不是重新排序)。接下来
         * 就可以删除TreeSet中的其他元素了。包括那些被修改过的实例变量的元素。
         */
    }
}
[P{num=1}, P{num=5}, P{num=7}, P{num=9}]
[P{num=100}, P{num=5}, P{num=7}, P{num=7}]
false
false
[P{num=100}, P{num=5}, P{num=7}, P{num=7}]
true
[P{num=100}, P{num=7}, P{num=7}]

与HashSet类似的是,如果TreeSet中包含了可变对象,当可变对象的实例变量被修改时,TreeSet在处理这些对象将非常复杂,而且容易出错.为了让程序更加健壮,不要修改放入HashSet和TreeSet集合中的关键实例变量(与CompareTo()方法相关的).

定制排序

TreeSet如果想要实现定制排序,则可以通过Comparator接口的帮助。该接口里包含一个int compare(Object o1,Object o2)方法,o1 == o2 返回0;o1>o2 返回1;o1<o2 返回-1;
通过提供一个Comparator对象与TreeSet关联,由该Comparator对象负责集合的排序逻辑。
由于Comparator是一个函数式接口,因此可使用Lambda表达式来代替Comparator对象。

import java.util.TreeSet;

class N{
    int age;

    public N(int age) {
        this.age = age;
    }
}

class  M{
    int age;

    public M(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "M{" +
                "age=" + age +
                '}';
    }
}

public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet((o1,o2)->{
            M m1 = (M)o1;
            M m2 = (M)o2;
            //根据M对象的age属性来决定大小,age越大,M对象反而越小
            return m1.age>m2.age? -1:m1.age<m2.age?1:0;
        });
        ts.add(new M(5));
        ts.add(new M(9));
        ts.add(new M(10));
        ts.add(new M(1));
//        ts.add(new N(90));  ClassCastException异常 所以还是不要添加不同类的对象
        System.out.println(ts);
    }
}
[M{age=10}, M{age=9}, M{age=5}, M{age=1}]

标签:Set,HashSet,实现,System,println,集合,new,out,TreeSet
来源: https://www.cnblogs.com/woshi123/p/12503154.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有