Set集合类似于一个罐子,集合里的多个对象之间没有明显的顺序关系.Set集合与Collection一样,没有提供额外的方法.

Set接口继承于Collection接口.

Set集合里不允许有相同的元素.

Set判断两个对象是否相同的时候使用的是equals方法,而不是==.
示例代码:

import java.util.Set;
import java.util.HashSet;

public class SetTest
{
    public static void main(String[] args)
    {
        Set books=new HashSet();
        //添加一个字符串对象
        books.add(new String("哈利波特"));
        //再次添加一个字符串对象 
        //因为两个字符串对象通过equals方法比较相等
        //所以添加失败,返回false
        boolean result=books.add(new String("哈利波特"));
        //从下面可以看出输出集合中只有一个元素
        System.out.println(result+"--->"+books);
    }
}

代码中,books两次添加对象不是一个对象,但是equals比较返回了true,所以添加失败,最后的输出结果也就只有一个结果.

接下来介绍Set集合接口的三个具体实现类,HashSet, TreeSet, EnumSet.

一. HashSet类

HashSet是Set接口典型实现,大多数时候使用Set集合就是使用这个实现类.
HashSet具有以下特点:

  1. 不能保证元素的排列顺序
  2. HashSet不是同步的,多个线程访问一个HashSet时如果有修改操作必须有代码确保HashSet同步
  3. 集合的元素值可以是null

HashSet是通过调用hashCode()方法调用hashCode值进行equals比较,判断是否相同的.

下面代码重写了equals()和hashCode()两个方法的一个或者全部:

import java.util.HashSet;

//类A的equals()方法总是返回true,但没有重写其hashCode()方法
class A
{
    public boolean equals(Object obj)
    {
        return true;
    }
}
//类B的hashCode()方法总是返回1,但没有重写其equals()方法
class B
{
    public int hashCode()
    {
        return 1;
    }
}
//类C的hashCode()方法总是返回2,且重写了其equals()方法
class C
{
    public int hashCode()
    {
        return 2;
    }
    public boolean equals(Object obj)
    {
        return true;
    }
}
public class HashSetTest
{
    public static void main(String[] args)
    {
        HashSet books=new HashSet();
        //分别向books集合中添加两个A对象,两个B对象,两个C对象
        books.add(new A());
        books.add(new A());
        books.add(new B());
        books.add(new B());
        books.add(new C());
        books.add(new C());
        System.out.println(books);
    }
}

代码中,分别添加了两个A对象,两个B对象,和两个C对象,其中,C重写了equals()总是返回true和hashCode()方法总是返回2,因此HashSet把两个C对象当成了同一个对象,只能添加一个.但是A和B的两个对象都能够添加.

所以,一定要注意:当需要重写类中的equals()方法时,一定要对应着重写HashCode()方法,原则是:如果两个对象通过equals()方法比较返回是true,则两个对象的hashCode值也要一致.

当向HashSet中添加可变的可变对象时,必须十分小心,如果修改HashSet集合中的对象,有可能导致该对象与集合中的其他对象相等,从而导致HashSet无法准确的访问对象!

二. LinkedHashSet类

LinkedHashSet是Has后Set的子类,它也用hashCode值决定元素的存储位置,但是同时使用链表保持元素的插入次序.性能略低于hashCode.

import java.util.LinkedHashSet;

public class LinkedHashSetTest
{
    public static void main(String[] args)
    {
        LinkedHashSet books=new LinkedHashSet();
        books.add("哈利波特");
        books.add("小王子");
        System.out.println(books);
        //删除 哈利波特
        books.remove("哈利波特");
        //重新添加 哈利波特
        books.add("哈利波特");
        System.out.println(books);
    }
}

输出LinkedHashSet集合的元素时,元素顺序总是与添加顺序一致.

三. TreeSet类

TreeSet是SortedSet接口的实现类,可以确保集合元素处于排序状态.
部分功能代码如下:

import java.util.TreeSet;

public class TreeSetTest
{
    public static void main(String[] args)
    {
        TreeSet nums=new TreeSet();
        //向TreeSet中添加四个Integer对象
        nums.add(5);
        nums.add(2);
        nums.add(10);
        nums.add(-9);
        //输出集合元素,看到集合元素已经处于排序状态
        System.out.println(nums);
        //输出集合里的第一个元素
        System.out.println(nums.first());
        //输出集合里的最后一个元素
        System.out.println(nums.last());
        //返回小于4的子集,不包含4
        System.out.println(nums.headSet(4));
        //返回大于5的子集,如果Set中包含5,子集中也包含5
        System.out.println(nums.tailSet(5));
        //返回大于等于-3,小于4的子集
        System.out.println(nums.subSet(-3,4));
    }
}

从上面代码的结果中可以看出,TreeSet不是根据元素插入的顺序排序的,而是根据元素的实际值的大小来排序的.TreeSet支持两种排序方法:自然排序和定制排序,默认为自然排序.

1.自然排序.
TreeSet是调用的Comparable接口中的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按照升序排列,这就是自然排序.所以,如果试图将一个对象添加到TreeSet中时,该对象所属类必须实现Comparable接口,否则会抛出异常!

同时,向TreeSet集合中添加的对象必须是同一类型的对象,否则也会引发异常,因为不同的类型对象不具有可比性.

当然,两个通过compareTo(Object obj)方法进行比较后相等的对象,新对象也是无法添加到TreeSet集合中的.
实例代码如下:

import java.util.TreeSet;

class Z implements Comparable
{
    int age;
    public Z(int age)
    {
        this.age=age;
    }
    //重写equals方法,总是返回true
    public boolean equals(Object obj)
    {
        return true;
    }
    //重写了compareTo(Object obj)方法,总是返回正整数
    public int compareTo(Object obj)
    {
        return 1;
    }
}

public class TreeSetTest2
{
    public static void main(String[] args)
    {
      TreeSet set=new TreeSet();
      Z z1=new Z(6);
      set.add(z1);
      //输出true,表明添加成功
      System.out.println(set.add(z1));
      //下面输出set集合,将看到有两个元素
      System.out.println(set);
      //修改set集合的第一个元素的age变量
      ((Z)(set.first())).age=9;
      //输出set集合的最后一个元素的age变量,将看到也变成了9
      System.out.println(((Z)(set.last())).age);
    }
}

虽然代码中添加了两个z1对象,但是因为z1对象的compareTo(Object obj)方法总是返回1,虽然它的equals()方法总是返回true,但TreeSet会认为z1对象和它自己也不是相等的,所以可以添加两个.但因为始终是z1一个元素,所以一旦z1中的成员的值被改变了,无论添加了几次,都会发生变化.

所以,当需要把一个对象放入TreeSet中时,重写equals()方法和compareTo(Object obj)方法应保持一致的结果,其规则为:如果两个对象通过equals()方法比较返回true时,这两个对象通过compareTo()方法返回的值应该为0.

同时,在TreeSet集合中放入可变类对象一旦对象中的值发生改变,TreeSet在处理这些对象时将变得非常复杂,容易出错.所以,为了程序的健壮性,推荐在使用HashSet和TreeSet集合中只放入不可变量.

2.定制排序.
TreeSet自然排序是根据元素大小按升序排序.但有时想定制排序方法,如降序,则要通过Comparator接口的帮助.该接口中的int compare(T o1,T o2)方法,如果该方法返回正整数,则o1大于o2;如果该方法返回0,则o1和o2相等;如果返回负整数,则o1小于o2.

实现定制排序时,需要在创建TreeSet对象时提供一个Comparator对象与该TreeSet集合关联,由Comparator对象管理排序逻辑.
代码如下:

import java.util.TreeSet;
import java.util.Comparator;

class M
{
    int age;
    public M(int age)
    {
        this.age=age;
    }
    public String toString()
    {
        return "M[age:"+age+"]";
    }
}
public class TreeSetTest4
{
    public static void main(String[] args)
    {
        TreeSet ts=new TreeSet(new Comparator()
        {
            //根据M对象的age属性来决定大小
            public int compare(Object o1,Object o2)
            {
                M m1=(M)o1;
                M m2=(M)o2;
                return m1.age>m2.age?-1:m1.age<m2.age?1:0;
            }
        });
        ts.add(new M(5));
        ts.add(new M(-3));
        ts.add(new M(9));
        System.out.println(ts);
        }
}

代码中,创建了一个Comparator接口的匿名内部类,负责对象ts集合的排序.当把M对象添加到ts集合中时,无需M类实现Comparator接口,而是由与TreeSet关联的Comparator对象来负责排序.

参考资料: 《疯狂Java讲义》