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());