Set集合 类似于一个罐子,丢进"Set",集合里的过个对象之间没有明显的顺序。
Set集合与Collection集合基本上完全一样,他没有提供任何额外的方法。
实际上Set就是Collection,只是行为略有不同,Set不允许包含重复元素。允许包含值为null的元素,但最多只能有一个null元素。
常用方法
按照定义,Set 接口继承 Collection 接口,所有原始方法都是现成的,没有引入新方法。具体的 Set 实现类依赖添加的对象的 equals() 方法来检查等同性。
各个方法的作用描述:
public int size() :返回set中元素的数目,如果set包含的元素数大于Integer.MAX_VALUE,返回Integer.MAX_VALUE;
public boolean isEmpty() :如果set中不含元素,返回true ;
public boolean contains(Object o) :如果set包含指定元素,返回true ;
public Iterator iterator() : 返回set中元素的迭代器,元素返回没有特定的顺序,除非set提高该保证的某些类的实例 ;
public boolean add(Object o) :如果set中不存在指定元素,则向set加入 ;
public boolean remove(Object o) :如果set中存在指定元素,则从set中删除 ;
public boolean removeAll(Collection c) :如果set包含指定集合,则从set中删除指定集合的所有元素 ;
public void clear() :从set中删除所有元素;
原理分析
HashSet的元素存放顺序和添加进去时候的顺序没有任何关系;而LinkedHashSet 则保持元素的添加顺序;TreeSet则是对我们的Set中的元素进行排序存放。
一般来说,当要从集合中以有序的方式抽取元素时,TreeSet 实现就会有用处。为了能顺利进行,添加TreeSet 的元素必须是可排序的。 而同样需要对添加到TreeSet中的类对象实现 Comparable 接口的支持。
对于Comparable接口的实现。假定一棵树知道如何保持 java.lang 包装程序器类元素的有序状态。一般说来,先把元素添加到 HashSet,再把集合转换为 TreeSet 来进行有序遍历会更快。这点和HashMap的使用非常的类似。
其实Set的实现原理是基于Map上面的。Set中很多实现类和Map中的一些实现类的使用上非常的相似。Map中的“键值对”,其中的 “键”是不能重复的。这个和Set中的元素不能重复一致,其实Set利用的就是Map中“键”不能重复的特性来实现的。
HashSet的巧妙实现:就是建立一个“键值对”,“键”就是我们要存入的对象,“值”则是一个常量。这样可以确保, 我们所需要的存储的信息之是“键”。而“键”在Map中是不能重复的,这就保证了我们存入Set中的所有的元素都不重复。而判断是否添加元素成功,则是通 过判断我们向Map中存入的“键值对”是否已经存在,如果存在的话,那么返回值肯定是常量:PRESENT ,表示添加失败。如果不存在,返回值就为null 表示添加成功。
下面是一个测试类
SetTest.java
package com.haixu.set; import java.util.HashSet; import java.util.Set; /* * Set 集合测试! * */ public class SetTest { public static void main(String[] args) { //HashSet是Set的典型实现 Set<String> str1 = new HashSet<String>(); //添加一个字符串 str1.add(new String("you will succcess!")); //再次添加一个字符串对象 //因为两个字符串对象通过equals方法比较相等 //添加失败将返回false boolean result = str1.add(new String("Hold on Haixu!")); System.out.println(result +"------->" + str1); } }
HashSet类:
1、HashSet不能重复存储equals相同的数据 。原因就是equals相同,数据的散列码也就相同(hashCode必须和equals兼容)。大量相同的数据将存放在同一个散列单元所指向的链表中,造成严重的散列冲突,对查找效率是灾难性的。
2、HashSet的存储是无序的 ,没有前后关系,他并不是线性结构的集合。
3、hashCode必须和equals必须兼容
当向HashSet集合中存入一个元素师,HsahSet会调用对象的hashCode方法来得到对象的hashCode值,然后根据该HashCode值决定该对象的HashSet的位置。
如果两个元素通过equals()方法比较返回true,但他们的哈市COde方法返回值不相等,HashSet将会把他们存放在不同的位置!
这张图片价值不菲呀!好好阅读呀!
在网上找了好多的资料都没有仔细详细的讲解HashSet 类,只能总计总结了!
HashSetTest.java
package com.haixu.set; /* * HashSet类的练习,测试对象的存储位置 * */ import java.util.HashSet; class A{ public boolean equals(Object obj){ return true; } } class B{ public int hashCode(){ return 1; } } class C{ public int hashCode(){ return 2; } public boolean equals(Object obj){ return true; } } public class HashSetTest { public static void main(String[] args) { HashSet<Object> str1 = new HashSet<Object>(); str1.add(new A()); str1.add(new A()); str1.add(new B()); str1.add(new B()); str1.add(new C()); str1.add(new C()); System.out.println(str1); } }
运行结果“[[email protected], [email protected],[email protected],
[email protected],
[email protected]]
HashSetTest2.java
package com.haixu.set; import java.util.HashSet; import java.util.Iterator; 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; if(r.count == this.count){ return true; } } return false; } public int hashCode(){ return this.count; } } public class HashSetTest2 { public static void main(String[] args) { HashSet<Object> hs =new HashSet<Object>(); 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 = (Iterator) hs.iterator(); R first = (R)it.next(); //为第一个元素count实例变量赋值 first.count = -3; //再次输出HashSet集合,集合没有重复元素 System.out.println(hs); //删除count为-3的R对象 hs.remove(new R(-3)); //可以看出被删除类一个对象 System.out.println(hs); System.out.println("hs是否包含count为-3的R对象?" + hs.contains(new R(-3))); System.out.println("hs是否包含count为5的R对象?" + hs.contains(new R(5))); } }
TreeSet原理:
/*
* TreeSet存储对象的时候, 可以排序, 但是需要指定排序的算法
*
* Integer能排序(有默认顺序), String能排序(有默认顺序), 自定义的类存储的时候出现异常(没有顺序)
*
* 如果想把自定义类的对象存入TreeSet进行排序, 那么必须实现Comparable接口
* 在类上implement Comparable
* 重写compareTo()方法
* 在方法内定义比较算法, 根据大小关系, 返回正数负数或零
* 在使用TreeSet存储对象的时候, add()方法内部就会自动调用compareTo()方法进行比较, 根据比较结果使用二叉树形式进行存储
*/
TreeSet是依靠TreeMap来实现的。
TreeSet是一个有序集合,TreeSet中的元素将按照升序排列,缺省是按照自然排序进行排列,意味着TreeSet中的元素要实现Comparable接口。或者有一个自定义的比较器。
我们可以在构造TreeSet对象时,传递实现Comparator接口的比较器对象。
import java.util.Iterator;
import java.util.*;
public class TreeSetTest {
public static void main(String[] args) {
Set ts = new TreeSet();
ts.add("Haixu");
ts.add("success");
ts.add("you can!");
Iterator it = ts.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
输出结果“Haixu, success,you can!
打印结果不是和先前加入的顺序一样,它是按照一个字母的排序法进行排序的。这是因为String 类实现了Comparable接口。
如果我们自己定义的一个类的对象要加入到TreeSet当中,那么这个类必须要实现Comparable接口。
比较器
在使用Arrays对数组中的元素进行排序的时候,可以传递一个比较器。
在使用Collections对集合中的元素进行排序的时候,可以传递一个比较器。
那么在使用TreeSet对加入到其中的元素进行排序的时候可以传入一个比较器吗?
public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<E,Object>(comparator)); }
通过查看它的构造方法就知道可以传入一个比较器。
EnumSet 是一个与枚举类型一起使用的专用 Set 实现。枚举set中所有元素都必须来自单个枚举类型(即必须是同类型,且该类型是Enum的子类)。
枚举类型在创建 set 时显式或隐式地指定。枚举 set 在内部表示为位向量。 此表示形式非常紧凑且高效。此类的空间和时间性能应该很好,
足以用作传统上基于 int 的“位标志”的替换形式,具有高品质、类型安全的优势。
如果指定的 collection 也是一个枚举 set,则批量操作(如 containsAll 和 retainAll)也应运行得非常快。
由 iterator 方法返回的迭代器按其自然顺序 遍历这些元素(该顺序是声明枚举常量的顺序)。
返回的迭代器是弱一致的:它从不抛出 ConcurrentModificationException,也不一定显示在迭代进行时发生的任何 set 修改的效果。
不允许使用 null 元素。试图插入 null 元素将抛出 NullPointerException。但是,
试图测试是否出现 null 元素或移除 null 元素将不会抛出异常。
像大多数 collection 一样,EnumSet 是不同步的。如果多个线程同时访问一个枚举 set,
并且至少有一个线程修改该 set,则此枚举 set 在外部应该是同步的。这通常是通过对自然封装该枚举 set 的对象执行同步操作来完成的。
如果不存在这样的对象,则应该使用 Collections.synchronizedSet(java.util.Set) 方法来“包装”该 set。
最好在创建时完成这一操作,以防止意外的非同步访问:
Set<MyEnum> s = Collections.synchronizedSet(EnumSet.noneOf(Foo.class));
实现注意事项:所有基本操作都在固定时间内执行。虽然并不保证,但它们很可能比其 HashSet 副本更快。
如果参数是另一个 EnumSet 实例,则诸如 addAll() 和 AbstractSet.removeAll(java.util.Collection) 之类的批量操作
也会在固定时间内执行。
注意1:不允许使用 null 元素。试图插入 null 元素将抛出 NullPointerException。但是,
试图测试是否出现 null 元素或移除 null 元素将不会抛出异常。
注意2:EnumSet是不同步的。不是线程安全的。
注意3:EnumSet的本质就为枚举类型定制的一个Set,且枚举set中所有元素都必须来自单个枚举类型。
注意4:关于EnumSet的存储,文档中提到是这样的。 “枚举 set 在内部表示为位向量。”
我想它应该是用一个bit为来表示的于之对应的枚举变量是否在集合中。
比如:0x1001
假如约定从低位开始,就表示第0个,和第三个枚举类型变量在EnumSet中。
这样的话空间和时间性能也就应该很好。
注意5:至于Enum的枚举常量的位置(序数)可以用Enum的ordinal()方法得到。
注意6:在jdk内部可以一个数组的形式一个枚举的枚举常量。
下面是来自来JDK中RegularEnumSet.java的一个示例
private static <E extends Enum<E>> E[] getUniverse(Class<E> elementType) {
return SharedSecrets.getJavaLangAccess()
.getEnumConstantsShared(elementType);
}
注意7:元素属于哪种枚举类型必须在创建 set 时显式或隐式地指定.
注意8:关于枚举类型的更多知识可参考《枚举类型》
Enumset是个虚类,我们只能通过它提供的静态方法来返回Enumset的实现类的实例。
返回EnumSet的两种不同的实现:如果EnumSet大小小于64,
就返回RegularEnumSet实例(当然它继承自EnumSet),这个EnumSet实际上至用了一个long来存储这个EnumSet。
如果 EnumSet大小大于等于64,则返回JumboEnumSet实例,它使用一个long[]来存储。这样做的好处很明显: 大多数情况下返回的RegularEnumSet效率比JumboEnumSet高很多。
实例1 import java.util.Comparator; import java.util.EnumSet; import java.util.Random; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; public class Test { /** * @param args */ public static void main(String[] args) { System.out.println("EnumSet.noneOf"); EnumSet<Student> set=EnumSet.noneOf(Student.class); set.add(Student.HARRY); set.add(Student.ROBBIE); set.add(Student.ROBIN); for(Student p:set) System.out.println(p); set.clear(); System.out.println("EnumSet.allOf"); set=EnumSet.allOf(Student.class); for(Student p:set) System.out.println(p); set.clear(); System.out.println("EnumSet.Of one"); set=EnumSet.of(Student.ROBIN); for(Student p:set) System.out.println(p); System.out.println("EnumSet.Of two"); set=EnumSet.of(Student.ROBIN,Student.HARRY); for(Student p:set) System.out.println(p); } } enum Student { ROBIN("robin"), HARRY("harry",40), ROBBIE("robbie"); String name; int age; private Student(String name) { this(name,0); } private Student(String name,int age) { this.name=name; this.age=age; } public String toString() { return name; } }