Set集合,类似于一个罐子,程序可以把多个对象"丢进"Set集合,而Set集合通常不能记住每个元素的添加顺序.Set集合与Collection基本相同,没有提供任何额外的方法.实际上Set就是Collection,只是行为有所不同(Set不允许有重复元素)

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

  上面介绍Set的通用知识,因此完全适合后面介绍的HashSet,TreeSet和EnumSet三个实现类,只是这三个实现类各有特色.

一  HashSet

  HashSet是Set接口的典型实现,大多数时候使用的Set集合时就是使用这个实现类.HashSet按Hash算法来存储集合中的元素,因此具有良好的存取和查找性能。

  HashSet具有以下特点:

    1.不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。

    2.HashSet不是同步的,如果多个线程同时访问一个HashSet。假设有两个或者以上的线程同时修改了HashSet集合时,则必须通过代码来保证其同步。

    3.集合元素可以是null.

  当使用HashSet集合来存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode()值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但是它们的hashCode()方法返回值不相等,HashSet将会把它们存入不同的位置,依然可以添加成功。

  也就是说,HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。

示例代码:类A,B,C,分别重写了equals()方法,hashCode()方法这两个中的一个或者两个。

package com.j1803.collectionOfIterator;

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

//类A重写了equals()方法,总是返回true,但没有重写hashCode()方法.
class A{
    @Override
    public boolean equals(Object obj){
        return true;
    }
}

//类B重写了hashCode()方法,总是返回2,但没有重写equals()方法
class B{
    @Override
    public int hashCode(){
        return 2;
    }
}
//类C重写了equals()方法,总是返回true,重写了hashCode()方法,总是返回2
class C{
    @Override
    public boolean equals(Object obj){
        return true;
    }
    @Override
    public int hashCode(){
        return 2;
    }
}
public class HashSetTest {
    public static void main(String[] args) {
        Set book=new HashSet();
        HashSet books=new HashSet();
        A a=new A();
        B b=new B();
        C c=new C();
        books.add(a);
        books.add(a);
        books.add(b);
        books.add(b);
        books.add(c);
        books.add(c);
        System.out.println(books);

    }
}
[com.j1803.collectionOfIterator.B@2, com.j1803.collectionOfIterator.A@4554617c]

Process finished with exit code 0

 

package com.j1803.collectionOfIterator;

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

//类A重写了equals()方法,总是返回true,但没有重写hashCode()方法.
class A{
    @Override
    public boolean equals(Object obj){
        return true;
    }
}

//类B重写了hashCode()方法,总是返回2,但没有重写equals()方法
class B{
    @Override
    public int hashCode(){
        return 2;
    }
}
//类C重写了equals()方法,总是返回true,重写了hashCode()方法,总是返回2
class C{
   /* @Override
    public boolean equals(Object obj){
        return true;
    }*/
   @Override
    public int hashCode(){
        return 2;
    }
}
public class HashSetTest {
    public static void main(String[] args) {
        Set book=new HashSet();
        HashSet books=new HashSet();
        A a=new A();
        B b=new B();
        C c=new C();
        books.add(a);
        books.add(a);
        books.add(b);
        books.add(b);
        books.add(c);
        books.add(c);
        book.add(new A());
        book.add(new A());
        book.add(new B());
        book.add(new B());
        Boolean flag1=book.add(new C());
        System.out.println(flag1);
        Boolean flag2=book.add(new C());
        System.out.println(flag2);
        System.out.println(book);
        System.out.println(books);

    }
}
true
true
[com.j1803.collectionOfIterator.B@2, com.j1803.collectionOfIterator.B@2, com.j1803.collectionOfIterator.C@2, com.j1803.collectionOfIterator.C@2, com.j1803.collectionOfIterator.A@74a14482, com.j1803.collectionOfIterator.A@1540e19d]
[com.j1803.collectionOfIterator.B@2, com.j1803.collectionOfIterator.C@2, com.j1803.collectionOfIterator.A@4554617c]

Process finished with exit code 0

   

package com.j1803.setTest;
import java.util.HashSet;
class A{
	@Override
	public boolean equals(Object arg0) {
		// TODO Auto-generated method stub
		return true;
	}
}
class B{

	@Override
	public int hashCode() {
		// TODO Auto-generated method stub
		return 2;
	}
	
}
class C{

	@Override
	public boolean equals(Object obj) {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public int hashCode() {
		// TODO Auto-generated method stub
		//注意hashCode()返回为1不是与B类中一样返回为2
		return 1;
	}
}
public class HashSetTest {
	
	public static void main(String[] args) {
		HashSet books1=new HashSet();
		books1.add(new A());
		books1.add(new A());

		books1.add(new B());
		books1.add(new B());
		
		books1.add(new C());
		books1.add(new C());
		System.out.println(books1);



		
	}
[com.j1803.setTest.A@7852e922, com.j1803.setTest.C@1, com.j1803.setTest.B@2, com.j1803.setTest.B@2, com.j1803.setTest.A@4e25154f]

 

 

     注意点:当把一个对象放入HashSet中时,如果需要重写该对象对应类的equals()方法,则也应该重写其hashCode()方法。规则是:如果两个对象通过equals()方法比较返回true,则这两个对象的hashCode()值也应该相同。

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

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

  hashCode()方法对于HashSet的重要性(实际上,对象的hashCode值对于后面的HashMap同样重要),下面给出重写hashCode()方法的基本原则。

  在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值。

  当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()方法也应该返回相等的值。

  对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值。

下面给出重写hashCode()方法的一般几个步骤。

 

二  LinkedHashSet类

      HashSet还有一个子类LinkedHashSet,LinkHashSet集合也是根据元素的hashCode的值来决定元素的存储位置的,但它使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按照元素的添加顺序来访问集合里的元素。

  LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时将有很好的性能,因为它是以链表来维护内部顺序的。

package com.j1803.setTest;
import java.util.LinkedHashSet;
public class LinkedHashSetTest {
    public static void main(String[] args) {
        LinkedHashSet book=new LinkedHashSet();
        book.add("AAAAA");
        book.add("BBBBB");
        book.add("CCCCC");
        book.add("DDDDD");
        System.out.println(book);
    }

}
[AAAAA, BBBBB, CCCCC, DDDDD]

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

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

三  TreeSet类

  TresSet是SortedSet接口的实现类,正如SortedSet名字所暗示的,TreeSet可以确保集合元素处于排序状态。与HashSet集合相比,TreeSet还提供了如下几个额外的方法

  Comparator comparator():如果TreeSet采用了定制排序,则该方法返回定制排序所使用的Comparator;如果TreeSet采用了自然排序,则返回null.

  Object first():返回集合的第一个元素。

  Object last():返回集合中的最后一个元素。

  Object lower(Object e):返回集合中位于指定元素之前的元素(也就是小于指定元素的最大元素,参考元素不需要是TreeSet集合里的元素)。

  Object higher(Object e):返回集合中位于指定元素之后的元素(也就是大于指定元素的最小元素,参考元素不需要是TreeSet集合里的元素)。

  Object subSet(Object fromElement,Object toElement):返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)。

  SortedSet headSet(Object toElement):返回此Set集合的子集,由小于toElement的元素组成。

  SortedSet tailSet(Object fromElement):返回此Set集合的子集,由大于或等于fromElement的元素组成。

package com.j1803.setTest;
import java.util.TreeSet;
public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet num=new TreeSet();
        num.add(-5);
        num.add(45);
        num.add(78);
        num.add(-13);
        num.add(40);
        num.add(99);
        num.add(0);
        System.out.println(num);
        //输出集合的第一个元素
        System.out.println("输出集合的第一个元素"+num.first());
        //输出集合的最后一个元素
        System.out.println("输出集合的最后一个元素"+num.last());
        //输出小于50最大的元素
        System.out.println("输出小于50的最大的元素"+num.lower(50));
        //输出大于50的最小元素
        System.out.println("输出大于50的最小元素"+num.higher(50));
        //输出50到80之间的元素
        System.out.println("输出10到80之间的元素"+num.subSet(10, 80));
        //输出小于50的元素treeSet集合
        System.out.println("输出小于50的元素"+num.headSet(50));
        //输出大于50的元素的treeSet集合
        System.out.println("输出大于50的元素的treeSet集合"+num.tailSet(50));
    }
}
[-13, -5, 0, 40, 45, 78, 99]
输出集合的第一个元素-13
输出集合的最后一个元素99
输出小于50的最大的元素45
输出大于50的最小元素78
输出10到80之间的元素[40, 45, 78]
输出小于50的元素[-13, -5, 0, 40, 45]
输出大于50的元素的treeSet集合[78, 99]

TreeSet并不是根据元素的插入顺序来排序的,而是根据元素实际值的大小来进行排序的。

与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合数据。TreeSet支持两种排序方法:自然排序和定制排序,默认为自然排序。

  1.自然排序

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

  Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较是。例如obj1.compareTo(obj2),如果改方法返回0,则表明这两个对象相等,如果该方法返回一个正整数,则表明obj1大于obj2;如果该方法返回一个负整数,则表明obj1小于obj2.

  Java的一些常用类已经实现了Comparable接口,并提供了比较大小的标准。下面是实现了Comparable接口的常用类。

  BigDecimal,Character,Boolean,String,Date,Time.

  如果把一个自定义的类的对象添加到treeSet中,则该对象对应的类必须实现Comparable接口,否则程序将会抛出异常ClassCastException.

  大部分类在实现compareTo(Object obj)方法时,都需要将被比较对象obj强制类型转换成相同类型,因为只有相同类的两个实例才会比较大小。

  如果向TreeSet中添加的对象是程序员自定义的类的对象,则可以向TreeSet中添加多种类型的对象,前提是用户自定义的类实现了Comparable接口,且实现compareTo(Object obj)方法没有进行强制类型转换。但是当试图取出TreeSet集合元素时,不同类型的元素依然会发生ClassCastException异常。

  总结:如果希望TreeSet能正常运作,TreeSet只能添加同一种类型的对象。

  当一个对象加入TreeSet集合中时,TreeSet调用该对象的compareTo(Object obj)方法与容器中的其他对象比较,然后根据红黑树结构找到它的存储位置。如果两个对象通过compareTo(Object obj)方法比较相等,新对象将无法添加到TreeSet集合中。

  对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较是否返回0,如果返回0,则认为相等,否则就认为它们不相等。

 

package com.j1803.setTest;

import java.util.TreeSet;

/**
 * @author zhou_oyster
 *
 */
class Person implements Comparable{
    private int age;
    @Override
    public boolean equals(Object obj) {
        return true;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public int compareTo(Object arg0) {
        return 1;
    }
    public Person(int age) {
        super();
        this.age = age;
    }
}
public class TreeSetTest1 {
    public static void main(String[] args) {
    TreeSet book=new TreeSet();
    Person person=new Person(45);
    book.add(person);
    System.out.println(book.add(person));//输出true,添加成功
    System.out.println(book);//显示所有的元素
    //取出book集合中的第一个元素并修改年龄
    ((Person)book.first()).setAge(12);
    //查看第一个元素的年龄和最后一个元素的年龄
    System.out.println(((Person)book.first()).getAge()+"===================="+((Person)book.last()).getAge());    
    }

}
true
[com.j1803.setTest.Person@7852e922, com.j1803.setTest.Person@7852e922]
12====================12

  可以看到虽然修改Comparable的compareTo()方法,误让程序以为person和他本身不相等,从而可以添加成功,集合中保存对象的引用指的是同一个对象,所以修改了第一个age,后面的age也修改了。

  故:当需要把一个对象放入TreeSet中,重写该对象对应类的equals()方法时,应保证该方法与compareTo(Object obj)方法有一致结果,其规则是:如果两个对象通过equals()方法比较返回true,这两个对象通过compareTo()方法应该返回0.

  反之如果compareTo(Object obj)返回0而equals()返回false,则会与Set规则产生冲突。

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

package com.j1803.setTest;
import java.util.TreeSet;
public class Book implements Comparable{
    private int price;
    public Book(int price) {
        super();
        this.price = price;
    }
    @Override
    public String toString() {
        return "Book [price=" + price + "]";
    }
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Book other = (Book) obj;
            if (price != other.price)
                return false;
            return true;
        }
        @Override
        public int compareTo(Object obj) {
            Book book=(Book)obj;
            return this.price>book.getPrice()?1:this.price<book.getPrice()?-1:0;
        }
    public static void main(String[] args) {
        TreeSet set=new TreeSet();
        set.add(new Book(12));
        set.add(new Book(10));
        set.add(new Book(-10));
        set.add(new Book(-5));
        set.add(new Book(5));
        set.add(new Book(8));
        //打印set集合
        System.out.println(set);
        //修改第一个元素
        Book book1=(Book)set.first();
        book1.setPrice(13);
        //修改最后一个元素,使其与第二个元素的price相同
        Book book2=(Book)set.last();
        book2.setPrice(-5);
        //打印,可以看到无序且有重复元素
        System.out.println(set);
        //删除实例变量被改变的元素,删除失败。
        System.out.println(set.remove(new Book(13)));
        //打印
        System.out.println(set);
        //删除实例变量没有被改变的元素,删除成功。
        System.out.println(set.remove(new Book(10)));
        //打印
        System.out.println(set);    
    }

}
[Book [price=-10], Book [price=-5], Book [price=5], Book [price=8], Book [price=10], Book [price=12]]
[Book [price=13], Book [price=-5], Book [price=5], Book [price=8], Book [price=10], Book [price=-5]]
false
[Book [price=13], Book [price=-5], Book [price=5], Book [price=8], Book [price=10], Book [price=-5]]
true
[Book [price=13], Book [price=-5], Book [price=5], Book [price=8], Book [price=-5]]

  可以删除没有被修改实例变量,且不与其他修改实例变量的对象重复的对象。

  当执行了红色代码后TreeSet会对集合中的元素重新索引(不是重新排序),接下来可以删除TreeSet所有元素,推荐不要修改放入HashSet和TreeSet集合中元素的关键实例变量。

//删除元素
System.out.println(set.remove(new Book(-5)));
System.out.println(set);
[Book [price=13], Book [price=-5], Book [price=5], Book [price=8], Book [price=-5]]
true
[Book [price=13], Book [price=5], Book [price=8], Book [price=-5]]

  2.定制排序

  TreeSet的自然排序是根据元素的大小,TreeSet将它们以升序排列。如果需要实现定制排序,例如以降序排列。则可以通过Comparator接口的帮助。,该接口里包含了一个int compare(T o1,T o2)方法。该方法用于比较o1和o2的大小:如果该方法中返回正整数,则表明o1大于o2;如果该方法返回0,则表明o1等于o2;如果该方法返回负整数,则表明o1大于o2.

  如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。由于Comparator是一个函数式接口,因此可用Lambda表达式来代替Comparator对象。

四  EnumSet类

  EnumSet是一个专为枚举类设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值。该枚举类型在创建EnumSet时显示或隐式地指定。EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序。

  EnumSet在内部以位向量的形式存储,这种存储方式非常紧凑,高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用containsAll()和remainAll()方法)时,如果其参数也是EnumSet集合,则该批量操作的执行速度也非常快。

  EnumSet集合不允许加入null元素,如果试图插入null元素,EnumSet将抛出NullPointException异常。如果只是想判断EnumSet是否包含null元素或者试图删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。

  EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的类方法来创建EnumSet对象。EnumSet类它提供了如下常用的类方法来创建EnumSet对象。

  EnumSet allOf(Class elementType):创建一个包含指定枚举类里所有枚举值的EnumSet集合.

  EnumSet complementOf(Enumset s):创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,新EnumSet集合包含原EnumSet集合所不包含的,此枚举类剩下的枚举值(也就是新EnumSet集           合和原来EnumSet集合的集合元素加起来都是该枚举类的所有枚举值)。

  EnumSet copyOf(Collection c):使用一个普通集合来创建EnumSet集合。

  EnumSet copyOf(EnumSet s):创建一个与指定EnumSet具有相同元素类型,相同集合元素的EnumSet集合。

  EnumSet noneOf(Class elementType):创建一个元素类型为指定枚举类型的空EnumSet.

  EnumSet of(E first,E...rest):创建一个包含一个或多个枚举值的EnumSet集合,传入的多个枚举值必须属于同一个枚举类。

  EnumSet range(E from,E to):创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。

 

package com.j1803.EnumSetTest1;
import java.util.EnumSet;
enum Season{
    SPRING,SUMMER,FALL,WINTER
}
public class EnumSetTest {
    public static void main(String[] args) {
        //创建一个EnumSet集合,集合元素就是Season枚举类的全部枚举值。
        EnumSet esl= EnumSet.allOf(Season.class);
        //输出[SPRING,SUMMER,FALL,WINTER]
        System.out.println(esl);
        //创建一个EnumSet空集合,指定集合元素是Season类的枚举值。
        EnumSet es2=EnumSet.noneOf(Season.class);
        //输出[]
        System.out.println(es2);
        //手动添加元素。
        es2.add(Season.FALL);
        es2.add(Season.SPRING);
        //输出[FALL,SPRING]
        System.out.println(es2);
        //以指定枚举值创建EnumSet集合
        EnumSet es3=EnumSet.of(Season.SUMMER,Season.FALL,Season.WINTER);
        //输出[SUMMER,FALL,WINTER]
        System.out.println(es3);
        EnumSet es4=EnumSet.of(Season.SUMMER,Season.WINTER);
        //新创建的EnumSet集合元素和es4集合元素有相同的类型
        //es5集合元素+es4集合元素=Season枚举类的全部枚举值
        EnumSet es5= EnumSet.complementOf(es4);
        System.out.println(es5);
    }
}
[SPRING, SUMMER, FALL, WINTER]
[]
[SPRING, FALL]
[SUMMER, FALL, WINTER]
[SPRING, FALL]

要求复制另一个Collection集合中的所有元素到新创建的

package com.j1803.EnumSetTest1;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
enum Season{
    SPRING,SUMMER,FALL,WINTER
}
public class EnumSetTest {
    public static void main(String[] args) {
        Collection c1=new HashSet();
        c1.add(Season.SUMMER);
        c1.add(Season.SPRING);
        c1.add(Season.FALL);
        //复制Collection集合中的所有元素来创建EnumSet集合
        EnumSet enumSet= EnumSet.copyOf(c1);
        //输出[SUMMER,SPRING,FALL]
        System.out.println(enumSet);
        //运行报ClassCastException错误
        //enumSet.add("PHP");
        //enumSet.add("C++");
        enumSet.add(Season.WINTER);
        System.out.println(enumSet);
    }
}
[SPRING, SUMMER, FALL]
[SPRING, SUMMER, FALL, WINTER]

 五  各Set实现类的性能分析

  HashSet与TreeSet:HashSet的性能是比TreeSet要好,因为TreeSet需要额外的红黑树算法来维护集合元素的次序,当需要一个注重保持排序的Set时,才使用TreeSet。

  EnumSet是所有Set实现类中性能最好的,但它只能保持同一个枚举类的枚举值作为集合元素。

  HashSet,TreeSet和EnumSet都是线程不安全的。如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性。通常可以通过Collection工具类的synchronizedSortedSet方法来"包装"该Set集合。在创建时进行,以防止对Set集合的意外非同步访问。

SortedSet sortedSet= Collections.synchronizedSortedSet(new TreeSet());