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