Java中的Set集合
来源:互联网 发布:java记录日志的方式 编辑:程序博客网 时间:2024/06/06 00:07
Set集合,类似于一个罐子,程序依次把多个对象“丢进”Set集合,而Set集合通常不能记住元素的添加顺序。Set集合与Collection基本相同,没有提供额外的方法。实际上Set就是Collection,只是行为略有不同(Set不允许包含重复元素)。
Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则添加操作失败,add()方法返回false,且新元素不会被加入。
上面介绍的是Set集合的通用知识,因此完全适合后面介绍的HashSet、TreeSet和EnumSet三个实现类,只是三个实现类各有特色。
一、HashSet类
HashSet是Set接口的典型实现,大多数时候使用Set集合就是使用这个集合类。
HashSet按Hash算法来存储集合中的元素,因此具有很好地存取和查找性能。
HashSet具有以下特点:
- 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
- HashSet不是同步的,如果多个线程访问一个HashSet,假设有两个或者两个以上的线程同时修改了HashSet集合时,必须通过代码来保证其同步。
- 集合元素可以是null。
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据hashCode值来决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功。
也就是说,HashSet集合判断两个元素相等的标准就是两个对象通过equals()方法比较相等,并且两个对象的hashCode()返回值也相等。
//类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()方法总是返回trueclass 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); }}
程序中向books集合中分别添加了两个A对象,两个B对象,两个C对象,其中C对象重写了equals()方法总是返回true,hashCode()总是返回2,这将导致HashSet把两个C对象当成同一个对象。运行结果:
[B@1,B@1,C@2,A@15db9742,A@6d06d69c]
从上面程序可以看出,即使两个A对象通过equals()返回true,但HashSet依然把它们当成不同的两个对象;即使两个B对象的hashCode()返回相同值1,但HashSet仍把它们当成两个对象。
这里有个注意点:当把一个对象放入HashSet时,如果需要重写该对应类的equals()方法,则也应该重写其hashCode()方法。
规则是:如果两个对象通过equals()方法比较返回true,这两个对象的hashCode值也应该相同。
如果两个对象通过equals()方法比较返回true,但这两个对象的hashCode()方法返回不同的hashCode时,这将导致HashSet会把这两个对象存在不同的位置,从而使两个对象都可以添加成功,这就与Set的规则冲突了。
如果两个对象的hashCode()方法返回hashCode值相同,但它们通过equals()方法返回false时将更麻烦:因为两个对象的hashCode值相同,HashSet试图向把它们保存在同一位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置使用链式结构来保存多个对象;而HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的HashCode值,将会导致性能下降。
重写hashCode()方法的规则:
- 在程序运行过程中,同一个对象多次调用hashCode()值应该返回相等的值
- 当两个程序通过equals()方法比较返回true时,这个两个对象的hashCode()方法也应该返回相等的值。
- 对象中用作equals()方法比较的实例变量,都应该用于计算hashCode值。
下面给出重写hashCode()方法的一般步骤。
- 把对象内每个有意义的实例变量(即每个参数equals()方法比较的标准的实例变量)计算出一个int类型hashCode值。
- 用第一步计算出来的多个hashCode值合计算出一个hashCoe值返回。为了避免直接相加偶然相等,可以通过为各实例变量的hashCode值乘以任意一个质数后相加。
例如:
return f1.hashCode() + (int)f2 * 31;
如果向HashSet中添加一个可变对象后,后面程序修改了该可变对象的实例变量,则可能导致它与集合中的其他元素相同(即两个对象通过equals()方法比较返回true,两个对象的hashCode值也相等),这就有可能导致HashSet中包含两个相同的对象。
下面程序演示了这种情况:
class R{ int count; public R(int count) { this.count = count; } public String toString() { return "R[count:" + count + "]"; } public boolean equals(Object obj) { if(this == obj) return true; if(obj != null && obj.getClass() == R.class) { R r = (R)obj; return this.count == r.count; } return false; } public int hashCode() { return this.count; }}public class HashSetTest { public static void main(String[] args) { HashSet hs = new HashSet(); hs.add(new R(5)); hs.add(new R(-3)); hs.add(new R(9)); hs.add(new R(-2)); //打印HashSet集合,集合元素没有重复 System.out.println(hs); //取出第一个元素 Iterator it = hs.iterator(); R first = (R)it.next(); //为第一个元素的count实例变量赋值 first.count = -3;//① //再次输出HashSet集合,集合元素有重复元素 System.out.println(hs); //删除count为-3的对象 hs.remove(new R(-3));//② //可以看到被删除一个R元素 System.out.println(hs); System.out.println("hs是否包含count为-3的R对象?" + hs.contains(new R(-3)));//输出false System.out.println("hs是否包含count为-2的R对象?" + hs.contains(new R(-2)));//输出false }}
上面代码更改了Set集合中第一个R对象的count值,导致R对象与集合中的其他对象实例变量相同。
HashSet集合中的第1个元素和第2个元素完全相同,这表明两个元素已经重复,此时HashSet会比较混乱,当试图删除count为-3的R对象时先计算HashCode再使用equals()比较——此时只有第二个元素符合,所以第二个元素被删除。第一个元素保存在-2的位置,但使用equals()方法拿它和count为-2的R对象进行比较又返回false——这将导致HashSet不可能准确访问该元素。
二、LinkedHashSet类
HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。
LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时将由很好的性能,因为它以链表来维护内部的次序。
三、TreeSet类
TreeSet是SortedSet接口的实现类,正如SortSet名字所暗示的,TreeSet集合可以保元素处于排序状态。与HashSet集合相比,TreeSet还提供了几个额外的方法。如first()、last()、headSet()等。
看起来方法很多,其实它们很简单:因为TreeSet中的元素是有序的,所以增加了访问第一个、前一个、后一个、最后一个元素的方法,并提供了三个从TreeSet中截取子TreeSet的方法。
下面测试的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());//输出9 System.out.println(nums.last());//输出10 //返回小于4的子集,不包含4 System.out.println(nums.headSet(4));//输出[-9,2] //返回大于5的子集,不包含5 System.out.println(nums.tailSet(5));//输出[5,10] //返回大于等于-3,小于4的自己 [-3,4) System.out.println(nums.subSet(-3, 4));//输出[2] }}
可以看出TreeSet是根据元素的实际大小顺序进行排序的。
TreeSet采用红黑树数据结构来存储集合元素,并且支持两种排序方法:自然排序和定制排序。
1.自然排序
TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将进而元素按升序排列,这种方式就是自然排序。
Java提供了Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法。当一个对象调用该方法与另一个对象进行比较时,例如obj1.compareTo(obj2),返回0表示两个对象相等,返回正整数表明obj1 > obj2;负整数,obj1 < obj2。
Java的一些常用类已经实现了Comparable接口,并提供了比较大小的标准。
- BigDecimal、BigInteger即所有数值型对于的包装类
- Character:按字符的UNICODE值进行比较
- Boolean:true大于false对于的包装类
- String:按字符的UNICODE
- Date、Time:后面的时间、日期比前面的大。
当把一个对象添加到TreeSet时,该对象必须实现Comparable接口,否则程序抛出异常。
class Err()public class TreeSetErrorTest{ public static void main(String[] args) { TreeSet ts = new TreeSet(); //向TreeSet集合中添加两个Err对象 ts.add(new Err()); ts.add(new Err());//① }}
上面程序添加第一个对象时候没有,TreeSet里没有任何元素,所以不会出现问题;当加入第二个Err对象时,TreeSet会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较——如果没有实现Comparable接口,则会引发ClassException异常。
注意:当我们没有实现Comparable接口,即使只添加一个元素,当我们取出该元素时,依然会引发ClassCastException异常。
还有一点需要指出:大部分类在实现compareTo(Object obj)方法时,都需要将被比较对象强制类型转换成相同类型,因为只有相同类型的两个实例才会比较大小,当试图把一个对象加入到TreeSet集合时,TreeSet会调用该对象的compareTo(Object obj)。也就是说,向TreeSet中添加的应该是同一个类的对象,否则也会引发ClassCastException。
public class TreeSetErrorTest2{ public static void main(String[] args) { TreeSet ts = new TreeSet(); //向TreeSet中添加两个对象 ts.add(new String("Java")); ts.add(new Date());///① }}
上述代码在①处引发异常,原因是当添加第二个元素时,无法使用compareTo()来比较大小。
如果向TreeSet中添加的对象时程序员自定义类的对象,则可以向TreeSet中添加多种类型的对象,前提是用户自定义实现了compareTo()接口,且实现compareTo(Object obj)方法没有进行强制类型抓换。
总结:如果希望TreeSet能够正常工作,TreeSet只能添加同一种类型的对象。
当把一个对象加入到TreeSet中石化,TreeSet调用该对象的compareTo(Object obj)方法与容器中的其他对象比较大小,然后根据红黑树找到它的存储位置。如果通过compareTo()方法比较相等,新对象将无法添加到TreeSet集合中。
对于TreeSet而言,判断两个对象是否相等的标准就是:两个对象通过compareTo()方法0。
class Z implements Comparable{ int age; public Z(int age) { this.age = age; } //重写equals方法,总是返回true public boolean equals(Object obj) { return true; } //重写了ComparabTo(Object obj)方法,总是返回1 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变量 System.out.println((Z)(set.last()).age); } }}
程序①代码把同一个对象再次添加到TreeSet中,因为z1对象的compareTo(Object obj)方法总是返回1,虽然它的equals()方法总是返回true,但TreeSet认为z1对象和它自己也不相等,因此TreeSet可以添加两个z1对象。
虽然集合中保存的是两个元素(集合的元素实际上是引用,但习惯上把被引用的对象称为集合元素),实际上是同一个元素。所以当修改TreeSet集合里的第一个元素的age变量后,该TreeSet集合里的最后一个元素的age变量也变了。
由此应该注意一个问题,当需要把一个对象放入TreeSet中时,重写该对象对于类的equals()方法时,应该保证该方法与compareTo(Object obj)方法有一致的结果。
其规则是:如果两个对象通过equals()方法比较返回true,那么通过compareTo()方法比较应该返回。
如果向TreeSet中添加一个可变对象后,并且后面程序修改了该可变对象的实例变量,这将导致它与其他对象的大小顺序发生变化,但TreeSet不会调整他们的顺序,甚至导致TreeSet中保存的这两个对象通过compareTo(Object obj)方法比较返回0。
下面程序演示了这种情况。
class X implements Comparable{ int count; public X(int count) { this.count = count; } public String toString() { return "X[count:" + count + "]"; } //重写equals()方法,根据count来判断 public boolean equals(Object obj) { if(this == obj) { return true; } if(obj != null && obj.getClass() == X.class) { X r = (X)obj; return r.count == this.count; } return false; } //重写compareTo()方法 public int compareTo(Object obj) { X r = (X)obj; return count > r.count ? 1 : count == r.count ? 0 : -1; }}public class TreeSetTest3 { public static void main(String[] args) { TreeSet ts = new TreeSet(); ts.add(new X(5)); ts.add(new X(-3)); ts.add(new X(9)); ts.add(new X(-2)); //打印TreeSet集合,集合元素是有序排列的 System.out.println(ts);//① //取出第一个元素 X first = (X)ts.first(); //对第一个元素的count赋值 first.count = 20; //取出最后一个元素 X last = (X)ts.last(); //对最后一个元素赋值-2,与第二个元素相同 last.count = -2; //再次看到输出TreeSet元素处于无序状态,且有重复元素 System.out.println(ts); //删除实例变量被改变的元素,删除失败 System.out.println(ts.remove(new X(-2)));//false System.out.println(ts.remove(new X(20)));//false System.out.println(ts);//② //删除实例变量没有被改变的元素,成功 System.out.println(ts.remove(new X(5)));//③ System.out.println(ts); //接下来可以删除被修改的元素 System.out.println(ts.remove(new X(-2)));//true System.out.println(ts.remove(new X(-2)));//true System.out.println(ts); }}
程序执行①处代码输出时,将看到集合处于无序状态,而且集合中包含了重复元素。
一旦改变了TreeSet集合里元素的实例变量,当再试图删除该对象时候,TreeSet也会删除失败(甚至集合中原有的、实例变量没被修改但与修改后的元素相等的元素也无法删除),比如②处,删除-2和20都删除失败。
但是当程序执行了③处的代码后,TreeSet会集合中的元素重新索引(并不是重新排序)。接下来就可以删除所有元素了。
与HashSet类似的是,如果TreeSet中包含了可变对象,当那些可变对象的实例变量被修改时,TreeSet在处理这些对象时变得十分复杂,而且容易出错。因此为了让程序更贱健壮,推荐不要修改防卫HashSet和TreeSet集合元素的关键实例变量。
2.定制排序
TreeSet的自然排序是根据元素的大小,升序排列。如果需要实现定制排序,例如降序排序,则可以通过Comparator接口的帮助。该接口里包含一个int compare(T o1,T o2)方法,该方法用于比较o1和o2的大小:如果该方法返回正整数,表明o1 > o2;返回0,则相等,返回负整数o1< o2。
如果我们需要实现定制排序,则需要在创建TreeSet集合时,传入一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑,由于Comparator是一个函数式接口,因此可使用Lambda表达式来代替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) { //此处Lambda表达式的目标类型是Comparator TreeSet ts = new TreeSet((o1 , 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的Lambda表达式,它负责ts集合的排序,所以当把M对象添加到ts集合中时,无须M类实现Comparable接口,因为此时TreeSet无须通过M对象本身来比较大小,而是由TreeSet关联的Lambda表达式负责集合元素的排序。
注意:当通过Comparator对象(Lambda表达式)来实现TreeSet定制排序时,依然不可以向TreeSet中添加类型不同的对象,否则引发ClassCastException异常。TreeSet判断两个元素相当的规则标准是:通过Comparator(或Lambda表达式)比较两个元素返回0,这样TreeSet不会把第二个元素添加到集合中。
四、EnumSet类
EnumSet是一个专门为枚举类设计的集合类,EnumSet中所有元素都必须是指定枚举类的枚举值,该枚举类在创建EnumSet时显式或隐式地指定。EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类内部的定义顺序来决定集合元素的顺序。
EnumSet在内部以位向量的形式存储,这种存储方式非常紧凑高效。因此EnumSet对象占用内存很小,而且运行效率很好。尤其是在进行批量操作时候,如果其参数是EnumSet集合,则该批量操作的速度也非常快。
EnumSet集合不允许加入null元素,如果试图插入null元素,将会报NullPointerException异常。如果只是判断是否包含null元素或者试图删除null元素,则不会报错,只是删除时返回false。
EnumSet类并没有暴露任何构造器来创建该类的实例,程序应该通过它的类方法来创建EnumSet对象。
比如:
- EnumSet allOf(Class elementType)
- EnumSet complement(EnumSet s)
- EnumSet copyOf(Collection c)
- ….
下面示范了如何创建EnumSet实例:
public class EnumSetTest { public static void main(String[] args) { //创建一个EnumSet,集合元素就是Season枚举类的全部枚举值 EnumSet es1 = EnumSet.allOf(Season.class); System.out.println(es1);//输出[SPRING,SUMMER,FALL,WINTER] //创建一个EnumSet空集合,指定其集合元素是Season类的枚举类 EnumSet es2 = EnumSet.noneOf(Season.class); System.out.println(es2);//输出[] //主动添加两个元素 System.out.println(es2.add(Season.SPRING)); System.out.println(es2.add(Season.WINTER)); System.out.println(es2);//输出[SPRING,WINTER] //以指定枚举值创建EnumSet集合 EnumSet es3 = EnumSet.of(Season.SUMMER , Season.WINTER); System.out.println(es3);//输出[SUMMER,WINTER] EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER); System.out.println(es4);//输出[SUMMER,FALL,WINTER] //新创建的EnumSet集合元素和es4集合元素有相同的类型 //es5 + es4的集合元素 = Season枚举类的全部枚举值 EnumSet es5 = EnumSet.complementOf(es4); System.out.println(es4);//输出[SPRING] }}
上面程序代表了EnumSet的常规用法。除此之外,还可以复制另一个Enumset集合中的所有元素,或者赋值另一个Collection集合中的所有元素。当复制Collection集合中的所有元素时,集合中的元素必须是同一个枚举类的枚举值。
public class EnumSetTest2 { public static void main(String[] args) { Collection c = new HashSet(); c.clear(); c.add(Season.FALL); c.add(Season.SPRING); //复制Collection里面的所有元素来创建EnumSet集合 EnumSet enumset = EnumSet.copyOf(c);//① System.out.println(enumset); c.add("JAVA"); c.add("集合"); //下面代码出现异常,因为集合c里面的元素不是全部为枚举值 enumset = EnumSet.copyOf(c);//② }}
程序执行②处代码时,c集合中的元素不全是枚举值所以抛出ClassCastException异常。
五、各Set实现类的性能分析
HashSet和TreeSet是Set的两个典型实现。
HashSet的性能总是要优于TreeSet,特别是最常用的添加、查询等操作。因为TreeSet需要使用额外的红黑树算法来维护集合元素的次序。只有当需要一个排序的Set时,才需要TreeSet。
HashSet还有一个子类,LinkedHashSet,对于普通的插入、删除操作,LinkedHashSet比HashSet要略慢一点,这是由于维护链表所带来的额外开销,但是由于有链表,遍历LinkedHashSet会很快。
EnumSet是所有类中性能最好的,但它只能保存枚举类的枚举值。
Set的三个实现类HashSet、TreeSet、EnumSet都是线程不安全的。如果有多个线程同时访问Set集合,并且有超过一个线程修改了Set集合,必须手动保证该Set集合的同步性。
通常可以通过Collections的工具类synchronizedSortedSet方法来“包装”该Set集合。此操作最好在创建时进行,以防止对Set集合的意外非同步访问。
SortedSet s = Collections.synchronizedSorted(new TreeSet(...));
- java中的Set集合
- Java中的Set集合
- Java中的Set集合
- Java集合中的集:Set
- Java中的Set集合类
- Java中的Set集合类
- JAVA中的Set集合类
- Java中的Set集合类
- 《JAVA集合中的Set接口》
- java中的集合类--set
- java中的Collection集合 List Set Map
- java中的集合框架Set 、List 、Map
- java中的Collection集合 List Set Map
- java中的集合之Set接口(三)
- JAVA中的集合类Map、Set、List
- Java中的集合(List和Set)
- java集合---set集合
- JAVA集合-Set集合
- 修改问题单要仔细阅读提单人的意图,适当时间可以咨询下,以便做对事情
- Java 开发模式-----单例模式
- phpredis中文手册——《redis中文手册》 php版
- Android基础知识(9)—Android绘图基础Canvas、Paint
- android_音乐的播放
- Java中的Set集合
- hdoj 2285 Switches 高斯消元。。。
- Educational Codeforces Round 11 D 计算几何
- DP训练笔记
- 批处理获取当前时间
- Unity之触屏控制_实现模型旋转和缩放—Touch类的使用
- C中大端小端的问题
- 自定义标题栏,让自己以后不那么累
- 大数据IMF传奇行动绝密课程第25课:Spark Sort-Based Shuffle内幕彻底解密