java基础 集合 之 Set

652 阅读5分钟

Set集合:

集合Set的总结 先看下Set集合的类图

dwJqQx.jpg

Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则添加操作失败,add方法返回false,且新元素不会被加入。

Set判断两个对象相同不是使用==运算符,而是根据equals方法。也就是说,只要两个对象用equals方法比较返回true,Set就不会接受这两个对象;反之,只要两个对象用equals方法比较返回false,Set就会接受这两个对象(甚至这两个对象是同一个对象,Set也可把它们当成两个对象处理,在后面程序中可以看到这种极端的情况

HashSet具有以下特点。

  • 不能保证元素的排列顺序,顺序有可能发生变化。
  • HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步。集合元素值可以是null。
  • 当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该HashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功。
  • 简单地说,HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。
  • 当把一个对象放入HashSet中时,如果需要重写该对象对应类的equals()方法,则也应该重写其hashCode()方法。其规则是:如果两个对象通过equals()方法比较返回true,这两个对象的hashCode值也应该相同。

如果两个对象通过equals()方法比较返回true,但这两个对象的hashCode()方法返回不同的hashCode值时,这将导致HashSet会把这两个对象保存在Hash表的不同位置,从而使两个对象都可以添加成功,这就与Set集合的规则有些出入了。

如果两个对象的hashCode()方法返回的hashCode值相同,但它们通过equals()方法比较返回false时将更麻烦:因为两个对象的hashCode值相同,HashSet将试图把它们保存在同一个位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置用链式结构来保存多个对象;而HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降。

重写hashCode()方法的基本规则。

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

重写equals和hashCOde方法:

public boolean equals(Object obj) {

    if (this == obj) return true;

    if (obj != null && obj.getClass() == R.class) {
        R r = (R) obj;
        if (r.count == this.count) {
            return true;
        }
    }
    return false;
}

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

HashSet所维护的顺序与TreeSet或LinkedHashSet都不同,因为他们实现具有不同的元素存储方式,TreeSet将元素存储在红-黑数据结构中,而HashSet使用的是散列函数,LinkedHashList因为查询速度的原因也使用了散列,但是他使用了链表来维护元素的插入顺序。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable{

    private transient HashMap<E,Object> map;
    //默认构造器
    public HashSet() {
        map = new HashMap<>();
    }
    //将传入的集合添加到HashSet的构造器
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    //明确初始容量和装载因子的构造器
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    //仅明确初始容量的构造器(装载因子默认0.75)
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
   }

可以看到HashSet就是一个Map的包装,底层还是Map进行实现的

看下HashSet的具体的方法:

private static void test() {
      // 新建HashSet
      HashSet<String> set = new HashSet<>();
      // 将元素添加到Set中
      set.add("hello");
      set.add("word");
      set.add("excel");
      set.add("ppt");
      set.add("ops");
      // 打印HashSet的实际大小
      System.out.println("size:"+set.size());
      // 判断HashSet是否包含某个值
      System.out.println("HashSet contains hello:" +set.contains("hello"));
      System.out.println("HashSet contains excel:" + set.contains("excel"));
      // 删除HashSet中的“ppt”
      set.remove("ppt");
      // 将Set转换为数组
      String[] arr = set.toArray(new String[0]);
      System.out.println("---foreach---");
      for (String str:arr)
          System.out.println(str);

      HashSet<String> otherset = new HashSet<>();
      otherset.add("tom");
      otherset.add("pig");
      otherset.add("foll");
      // 克隆一个cloneset,内容和set一模一样
      HashSet<String> cloneset = (HashSet)set.clone();
      // 删除“**cloneset**中,属于otherSet的元素”
      cloneset.removeAll(otherset);
      // 打印cloneset
      System.out.println(cloneset);
      // 克隆内容一样的set
      HashSet<String> retainset = (HashSet)set.clone();
      retainset.retainAll(otherset);
      System.out.println(retainset);
      // 遍历HashSet
      for(Iterator iterator = set.iterator(); iterator.hasNext();)
          System.out.println(iterator.next());
      // 清空HashSet
      set.clear();
      // 输出HashSet是否为空
      System.out.println(set.isEmpty()?"set is empty":"set is not empty");
	}
//输出
size:5
HashSet contains hello:true
HashSet contains excel:true
---foreach---
excel
ops
hello
word
[excel, ops, hello, word]
[]
excel
ops
hello
word
set is empty

LinkedHashSet使用了链表记录集合元素的添加顺序,但LinkedHashSet依然是HashSet,因此它依然不允许集合元素重复

TreeSet

如果向TreeSet中添加一个可变对象后,并且后面程序修改了该可变对象的Field,这将导致它与其他对象的大小顺序发生了改变,但TreeSet不会再次调整它们的顺序,甚至可能导致TreeSet中保存的这两个对象通过compareTo(Object obj)方法比较返回0。下面程序演示了这种情况。

class R implements Comparable {
    int count;
    public R(int count) {
        this.count = count;
    }
    public String toString() {
        return "R[count:" + count + "]";
    }

//重写equals()方法,根据count来判断是否相等

    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj != null && obj.getClass() == Z.class) {
            R r = (R) obj;
            if (r.count == this.count) {
                return true;
            }
        }
        return false;
    }

//重写compareTo()方法,根据count来比较大小

    public int compareTo(Object obj) {
        R r = (R) obj;
        return count > r.count ? 1 :count < r.count ? -1 : 0;
    }

}

public class TreeSetTest3 {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();
        ts.add(new R(5));
        ts.add(new R(-3));
        ts.add(new R(9));
        ts.add(new R(-2));
       //打印TreeSet集合,集合元素是有序排列的
        System.out.println(ts); //①
       //取出第一个元素R first=(R)ts.first();//对第一个元素的count赋值first.count=20;//取出最后一个元素R last=(R)ts.last();//对最后一个元素的count赋值,与第二个元素的count相同last.count=-2; //再次输出将看到TreeSet里的元素处于无序状态,且有重复元素
        System.out.println(ts); //②
	//删除Field被改变的元素,删除失败
        System.out.println(ts.remove(new R(-2))); //③
        System.out.println(ts);
	//删除Field没有改变的元素,删除成功
        System.out.println(ts.remove(new R(5))); //④
        System.out.println(ts);

    }

}  

上面程序中的R对象对应的类正常重写了equals()方法和compareTo()方法,这两个方法都以R对象的count实例变量作为判断的依据。当程序执行①行代码时,看到程序输出的Set集合元素处于有序状态;因为R类是一个可变类,因此可以改变R对象的count实例变量的值,程序通过粗体字代码行改变了该集合里第一个元素和最后一个元素的count实例变量的值。当程序执行②行代码输出时,将看到该集合处于无序状态,而且集合中包含了重复元素。

Set和存储顺序

Set:存入的Set的每一个元素必须是唯一的,因为Set不保存重复的元素。加入Set的元素必须定义equals()方法以确保对象的唯一性,Set与Conllection有完全一样的接口。Set接口不保证维护单元的次序。

HashSet:为快速查找而设计的Set,存入的Hashset的元素必须定义HashCode()

TreeSet:保持次序的Set。底层是树结构,使用它可以从Set中提取有序的序列元素必须实现Comparable接口

LInkedHashSet:具有与HashSet的查询速度,且内部使用链表维护元素的顺序,于是在使用迭代器遍历Set时结果会按元素插入的次序显示。元素也必须定义hashCode()方法。

HasSet应该是默认的选择因为他对速度进行了优化

SortedSet

SortedSet的元素可以保证出于排序状态,这使得他可以通过在SortedSet接口中的下列方法提供附加的功能:Comparator comparator()返回当前Set使用的Comparator;或者返回null,

Object first() 返回容器的第一个元素

Object last() 返回容器的最末一个元素

SortedSet subSet(fromElemny,toElement) 生成set的子集,范围含前不含后

SortedSet headSet(toElement) 生成此Set的子集,由小于toElement的元素组成

SortedSet tailset(fromElemnt) 生成此Set的子集 由大于等于fromElement的元素组成。

// A Java program to demonstrate working of SortedSet
import java.util.SortedSet;
import java.util.TreeSet;
 
public class Main
{
    public static void main(String[] args)
    {
        // Create a TreeSet and inserting elements
        SortedSet<String> sites = new TreeSet<>();
        sites.add("practice");
        sites.add("geeksforgeeks");
        sites.add("quiz");
        sites.add("code");
 
        System.out.println("Sorted Set: " + sites);
        System.out.println("First: " + sites.first());
        System.out.println("Last: " + sites.last());
 
        // Getting elements before quiz (Excluding) in a sortedSet
        SortedSet<String> beforeQuiz = sites.headSet("quiz");
        System.out.println(beforeQuiz);
 
        // Getting elements between code (Including) and
        // practice (Excluding)
        SortedSet<String> betweenCodeAndQuiz =
                                  sites.subSet("code","practice");
        System.out.println(betweenCodeAndQuiz);
 
        // Getting elements after code (Including)
        SortedSet<String> afterCode = sites.tailSet("code");
        System.out.println(afterCode);
    }
}
// 输出
Sorted Set: 
First: code
Last: quiz