Java学习笔记——Set集合

Set集合

Set集合概述

  • Set集合类似于一个罐子,他只管把对象放进罐子中,而不管放入的次序
  • Set集合与Collection集合基本相同,没有提供任何额外的方法
  • Set集合不允许添加重复元素,若试图添加两个相同元素进入同一个Set集合中,则添加失败,add()方法返回false, 且新元素不会被加入

Set集合的实现类——HashSet类

  • HashSet是Set接口的典型实现
  • HashSet按Hash算法来存储集合中的元素,具有很好的存取和查找性能
  • HashSet的特点:
    • 不能保证元素的排列顺序
    • HashSet不是同步的,若多个线程同时访问同一个HashSet,假设两个或以上线程同时修改HashSet集合时,必须通过代码来保证其同步。
    • 集合元素可以是null
  • HashSet集合判断两个元素是否相等是根据equals()方法的返回值和hashCode()的返回值来判断的,当equals()方法的返回值为true且hashCode()返回值相等时会认为两个元素相等
  • HashSet集合通过hashCode()方法的返回值来决定元素的位置,若两个元素通过equals()方法返回true,但是hashCode()方法返回值不同,HashSet()集合会将这两个元素存储到不同的位置

    若需要将某个类的对象保存到HashSet集合时,建议重写equals()方法的同时也重写hashCode()方法,并且应尽量保证equals()方法返回true时它们的hashCode()方法返回值也相等

  • HashSet中每个能存储元素的“槽位“”称为“桶”, 如果有多个hashCode值相同,但equals()却返回false,就需要在一个“桶”中放多个元素,这样会导致性能下降。

  • 重写hashCode()方法的基本规则:
    • 在程序运行过程中,同一对象多次调用hashCode()方法应返回相同的值
    • 当两个对象equals()方法返回true时两个hashCode值应相等
    • 对象中用作equals()方法比较标准的实例变量,都应用于计算hashCode值
  • 如果像HashSet集合中添加一个可变对象后,然后修改了可变对象的引用值(实例对象),则可能导致它与集合中另一个元素相等,这就导致了HashSet集合中有两个相同的元素,导致性能下降。
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;

class R {
    int count;

    public R(int count) {
        this.count = count;
    }

    @Override
    public String toString() {
        return "R[count:" + count + "]";
    }

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

    @Override
    public int hashCode() {
        return this.count;
    }
}

public class HashSetTest {
    public static void main(String[] args) {
        HashSet hs = new HashSet();
        hs.add(new R(5));
        hs.add(new R(3));
        hs.add(new R(-2));
        hs.add(new R(20));
        System.out.println(hs);
        Iterator it = hs.iterator();
        R first = (R)it.next();
        first.count = 5;
        System.out.println(hs);
        hs.remove(new R(5));
        System.out.println("hs是否包含一个count值为-2的对象:"+hs.contains(new R(-2)));
        System.out.println("hs是否包含一个count值为5的对象:"+hs.contains(new R(5)));
        System.out.println(hs);
    }
}

/*
运行结果:
[R[count:-2], R[count:3], R[count:20], R[count:5]]
[R[count:5], R[count:3], R[count:20], R[count:5]]
hs是否包含一个count值为-2的对象:false
hs是否包含一个count值为5的对象:false
[R[count:5], R[count:3], R[count:20]]
*/

当试图删除count值为5的元素时,HashSet会计算出count值为5的hashCode值,从而找到该对象的保存位置,然后通过equals()方法进行比较,如果相同,则删除。
由此可见,当可变对象添加到HashSet集合中后尽量不要修改,否则会导致无法正常操作集合中元素

LinkedHashSet类

LinkedHashSet类是HashSet类的子类,他在HashSet类的基础上使用链表维护元素的次序,当遍历LinkedHashSet集合时,将会暗元素的添加顺序来访问集合里的元素,因为要维护添加次序,因此会导致性能略差,但在迭代访问Set集合里的全部元素时会有很好的性能,因为它以链表维护内部顺序。

import java.util.LinkedHashSet;
public class LinkedHashSetTest {
    public static void main(String[] args) {
        LinkedHashSet lhs = new LinkedHashSet();
        lhs.add("a");
        lhs.add("s");
        lhs.add("d");
        lhs.add("f");
        System.out.println(lhs);
    }
}
//运行结果:[a, s, d, f]

TreeSet类

  • TreeSet是SortedSet接口的实现类
  • TreeSet可以保证集合元素处于排序状态
  • TreeSet集合使用红黑树的数据结构来存储集合元素
  • 一下是TreeSet集合相对于HashSet集合的额外方法的代码体现
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Time;
import java.util.Scanner;
import java.util.TreeSet;

class Err {
}

public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();
        ts.add("y");
        ts.add("j");
        ts.add("h");
        ts.add("yjh");
        ts.add("hjy");
        //TreeSet集合要求自然排序的集合必须都实现该接口,
        // 添加未实现Comparable接口的类时抛出ClassCastException异常
        /*实现该接口的常用类有:
                BigDecimal,BigInteger,Character,Boolean,String,Date, Time*/
        //ts.add(new Err());
        //向TreeSet集合中添加不同类时会引发ClassCastException异常
        //ts.add(new Scanner(System.in));
        System.out.println(ts);
        //返回该集合的第一个元素
        System.out.println(ts.first());
        //返回该集合的最后一个元素
        System.out.println(ts.last());
        //返回该集合在参数之前的所有元素(不包括参数本身),参数不一定是该集合中的元素
        System.out.println(ts.headSet("j"));
        //返回该集合在参数之后的所有元素(包括参数本身),参数不一定是该集合中的元素
        System.out.println(ts.tailSet("j"));
        //返回该集合的一个大于第一个参数且小于第二个参数的元素子集合,参数不一定是该集合中的元素
        System.out.println(ts.subSet("hjy", "yjh"));
        System.out.println(ts.subSet("a", "yjh"));
        System.out.println(ts.subSet("a", "z"));
        //返回集合中比参数小的最大元素,参数不一定是该集合中的元素
        System.out.println(ts.lower("l"));
        //返回集合中比参数大的最小元素,参数不一定是该集合中的元素
        System.out.println(ts.higher("l"));
    }
}

TreeSet集合的排序方式

自然排序

  • TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将几何元素按照升序排列,这就是自然排序
  • 将一个对象放入TreeSet中,重写该对象对应类的equals()方法时,应保证该方法与compareTo(Object obj)方法有一致的结果,否则会出现下面代码演示的结果
import java.util.Objects;
import java.util.TreeSet;

class Z implements Comparable {
    int count;

    public Z(int count) {
        this.count = count;
    }

    @Override
    public boolean equals(Object o) {
        return true;
    }

    @Override
    public String toString() {
        return "Z{" + "count=" + count + '}';
    }

    @Override
    public int compareTo(Object o) {
        return 1;
    }
}

public class TreeSetTest2 {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();
        Z z = new Z(7);
        ts.add(z);
        System.out.println(ts);
        //TreeSet集合判断两个元素是否相同的唯一标准是compareTo()返回0
        //因为Z对象的compareTo()永远返回1,因此认为两个同意元素不等,因此可以添加同一元素
        ts.add(z);
        System.out.println(ts);
        ((Z) ts.first()).count = 9;
        System.out.println(((Z) ts.last()).count);
    }
}
/*
运行结果:
[Z{count=7}]
[Z{count=7}, Z{count=7}]
9
*/
  • 向TreeSet集合中添加可变对象,修改可变对象的实例变量,可能会导致与其他元素的大小发生了改变,但TreeSet集合不会再次调整他们的顺序,甚至可能导致TreeSet的compareTo()方法返回0。
import java.util.TreeSet;

class T implements Comparable{
    int count;

    public T(int count) {
        this.count = count;
    }

    @Override
    public String toString() {
        return "T{" + "count=" + count + '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        T t = (T) o;
        return count == t.count;
    }

    @Override
    public int compareTo(Object o) {
        T t = (T) o;
        return count > t.count ? 1 :
                count < t.count ? -1 : 0;
    }
}

public class TreeSetTest3 {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();
        ts.add(new T(3));
        ts.add(new T(1));
        ts.add(new T(-5));
        ts.add(new T(10));
        System.out.println(ts);
        ((T)(ts.first())).count = 7;
        ((T)(ts.last())).count = -3;
        System.out.println(ts);
    }
}

定制排序

  • 实现定制排序可以借助Comparator接口的int compare(T o1, T o2)方法
  • 在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该方法负责集合元素的排序逻辑,Comparator是一个函数式接口,可以用Lambda表达式进行替代
import java.util.TreeSet;

class T {
    int count;

    public T(int count) {
        this.count = count;
    }

    @Override
    public String toString() {
        return "T{" + "count=" + count + '}';
    }
}

public class TreeSetTest4 {
    public static void main(String[] args) {
        //此处Lambda表达式的目标类型是Comparator
        TreeSet ts = new TreeSet((o1, o2)->{
            T t1 = (T)o1;
            T t2 = (T)o2;
            //根据T对象的count属性来决定大小,count越大,T对象反而越小
            return t1.count > t2.count ? -1 :
                    t1.count < t2.count ? 1 : 0;
        });
        ts.add(new T(3));
        ts.add(new T(1));
        ts.add(new T(-5));
        ts.add(new T(10));
        System.out.println(ts);
    }
}

EnumSet类

  • EnumSet类是专门为枚举类设置的一个集合类,EnumSet集合中的所有元素都必须是指定枚举类型的枚举值
  • EnumSet集合的元素也是有序的,它是根据枚举值在Enum类内的定义顺序来决定的
  • EnumSet集合在内部以位向量的形式存储,这使EnumSet对象占用内存小,并且运行效率很好。尤其是进行批量操作时,如果参数也是 EnumSet集合,执行速度会非常快
  • EnumSet集合不允许加入null元素,若试图加入null元素,会抛出NullPointerException异常
  • EnumSet集合没有提供构造器,只能通过类方法来创建对象
import java.util.EnumSet;

enum Season {
    SPRING, SUMMER, FALL, WINTER
}

public class EnumSetTest {
    public static void main(String[] args) {
        //创建一个枚举集合,集合元素为Season枚举类所有元素
        EnumSet es = EnumSet.allOf(Season);
        System.out.println(es);
        //创建一个空枚举集合,集合元素类型为Season
        EnumSet es2 = EnumSet.noneOf(Season);
        System.out.println(es2);
        es2.add(Season.SPRING);
        es2.add(Season.WINTER);
        System.out.println(es2);
        //以指定枚举值创建一个枚举集合
        EnumSet es3 = EnumSet.of(Season.SPRING, Season.FALL);
        System.out.println(es3);
        //创建一个枚举集合,元素是枚举类中的一个范围中的元素
        EnumSet es4 = EnumSet.range(Season.SPRING, Season.WINTER);
        System.out.println(es4);
        //创建一个枚举集合,集合中的元素与集合es4是补集关系
        EnumSet es5 = EnumSet.complementOf(es4);
        System.out.println(es5);
    }
}

各Set集合性能分析以及选择


  • HashSet和TreeSet是Set的典型实现
  • HashSet的性能总比TreeSet性能好,因为TreeSet集合需要维护集合元素的次序
  • HashSet的一个子类LinkedHashSet,在HashSet的基础上按添加顺序来保存,所以比HashSet性能略差
  • EnumSet性能是最好的,但是他只能存储同一类型的枚举值
  • 仅仅只需要插入查找的话可以选择HashSet集合
  • 若需要保持排序的时候,可以使用TreeSet集合
  • 若想要遍历集合更快的话可以选择LinkedHashSet集合

Set接口的三个实现类都是线程不安全的,若有多个线程同时参与同一个集合,并且有一个以上线程同时对集合进行修改时必须手动保证该Set集合的同步性。
通常可以通过Collection工具类的synchronizedSortedSet方法来“包装”该集合。
此操作最好在创建时进行,以防止对Set集合的意外非同步访问
SortSet ss = Collection.synchronizedSortedSet(new HashSet());

猜你喜欢

转载自blog.csdn.net/yjh728/article/details/80876186
今日推荐