Set接口是Collection的子接口,set接口没有提供额外的方法。Set体系中的类或接口一般都包含"Set"字眼。
Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
**Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals 方法。**也就是说,我们在加入一个新元素的时候,如果这个新元素对象和Set中已有对象进行注意equals比较都返回false,则Set就会接受这个新元素对象,否则拒绝。
因为Set的这个制约,在使用Set集合的时候,应该注意两点:
为Set集合里的元素的实现类实现一个有效的equals(Object)方法、对Set的构造函数,传入的Collection参数不能包含重复的元素map中的所有key,即为一个set;所有value,即为一个collection。
HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取和查找性能。
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该HashCode值决定该对象在HashSet中的存储位置。
值得注意的是,HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法的返回值相等 。
HashSet 具有以下特点:
不能保证元素的排列顺序HashSet 不是线程安全的集合元素可以是 null import java.util.*; //类A的equals方法总是返回true,但没有重写其hashCode()方法。不能保证当前对象是HashSet中的唯一对象 class A { public boolean equals(Object obj) { return true; } } //类B的hashCode()方法总是返回1,但没有重写其equals()方法。不能保证当前对象是HashSet中的唯一对象 class B { public int hashCode() { return 1; } } //类C的hashCode()方法总是返回2,且有重写其equals()方法 class 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); } }result:
[B@1, B@1, C@2, A@3bc257, A@785d65]可以看到,如果两个对象通过equals()方法比较返回true,但这两个对象的hashCode()方法返回不同的hashCode值时,这将导致HashSet会把这两个对象保存在Hash表的不同位置,从而使对象可以添加成功,这就与Set集合的规则有些出入了。
所以,我们要明确的是: equals()决定是否可以加入HashSet、而hashCode()决定存放的位置,它们两者必须同时满足才能允许一个新元素加入HashSet。但是要注意的是: 如果两个对象的hashCode相同,但是它们的equlas返回值不同,HashSet会在这个位置用链式结构来保存多个对象。而HashSet访问集合元素时也是根据元素的HashCode值来快速定位的,这种链式结构会导致性能下降。
所以如果需要把某个类的对象保存到HashSet集合中,我们在重写这个类的equlas()方法和hashCode()方法时,应该尽量保证两个对象通过equals()方法比较返回true时,它们的hashCode()方法返回值也相等
如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功。
对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。
重写 hashCode() 方法的基本原则:
在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值;
当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等;
对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
LinkedHashSet 是 HashSet 的子类。LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时(遍历)将有很好的性能(链表很适合进行遍历) 。
LinkedHashSet 同样不允许集合元素重复–Set集合的通用定义。
TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合元素。
TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet<E>,Cloneable,java.io.Serializable接口。
其主要方法如下所示:
Comparator comparator() Object first() Object last() Object lower(Object e) Object higher(Object e) SortedSet subSet(fromElement, toElement) SortedSet headSet(toElement) SortedSet tailSet(fromElement)TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
① 自然排序
TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序排列。
这也就意味着,如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。
实现 Comparable接口 的类必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。
java.lang.Comparable<T>接口源码如下:
public interface Comparable<T> { public int compareTo(T o); //即a.compare(b); a b是同一个类的两个不同实例 }Comparable 的典型实现:
BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较Character:按字符的 unicode值来进行比较Boolean:true 对应的包装类实例大于 false 对应的包装类实例String:按字符串中字符的 unicode 值进行比较Date、Time:后边的时间、日期比前面的时间、日期大向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。
对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较返回值。
当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果。
如果两个对象通过 equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。
代码实例如下:
public class TreeSetDemo { public static void main(String[] args) { // 创建集合对象 // 自然顺序进行排序 TreeSet<Integer> ts = new TreeSet<Integer>(); // 创建元素并添加 // 20,18,23,22,17,24,19,18,24 ts.add(20); ts.add(18); ts.add(23); ts.add(22); ts.add(17); ts.add(24); ts.add(19); ts.add(18); ts.add(24); // 遍历 for (Integer i : ts) { System.out.println(i); } } }② 定制排序
TreeSet的自然排序是根据集合元素的compareTo方法,进行元素升序排列。
如果需要定制排序,比如降序排列,可通过java.util.Comparator<T>接口的帮助,重写compare(T o1,T o2)方法。
利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
此时,仍然只能向TreeSet中添加类型相同的对象,否则发生ClassCastException异常。
使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。
实例如下:
public class TreeSetDemo { public static void main(String[] args) { // 如果一个方法的参数是接口,那么真正要的是接口的实现类的对象 // 而匿名内部类就可以实现这个东西 TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() { @Override public int compare(Student s1, Student s2) { // 姓名长度 int num = s1.getName().length() - s2.getName().length(); // 姓名内容 int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num; // 年龄 int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2; return num3; } }); // 创建元素 Student s1 = new Student("linqingxia", 27); Student s2 = new Student("zhangguorong", 29); Student s3 = new Student("wanglihong", 23); // 添加元素 ts.add(s1); ts.add(s2); ts.add(s3); // 遍历 for (Student s : ts) { System.out.println(s.getName() + "---" + s.getAge()); } } }Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。而Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
实现了comparable的对象直接就可以成为一个可以比较的对象,不过得在类中进行方法定义。comparator在对象外比较,不修改实体类。
故而,有一种说法叫做Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
Comparable接口如下,方法名为compareTo,只有一个参数:
public interface Comparable<T> { public int compareTo(T o); }Comparator是一个函数式接口,方法名为compare,参数有两个,用泛型T修饰:
//注意这个注解 @FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); //... )Comparator方法如下:
EnumSet是一个专门为枚举类设计的集合类,EnumSet中所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式、或隐式地指定。EnumSet的集合元素也是有序的,它们以枚举值在Enum类内的定义顺序来决定集合元素的顺序
import java.util.*;enum Season { SPRING,SUMMER,FALL,WINTER } public class EnumSetTest { public static void main(String[] args) { //创建一个EnumSet集合,集合元素就是Season枚举类的全部枚举值 EnumSet es1 = EnumSet.allOf(Season.class); //输出[SPRING,SUMMER,FALL,WINTER] System.out.println(es1); //创建一个EnumSet空集合,指定其集合元素是Season类的枚举值。 EnumSet es2 = EnumSet.noneOf(Season.class); //输出[] System.out.println(es2); //手动添加两个元素 es2.add(Season.WINTER); es2.add(Season.SPRING); //输出[SPRING,WINTER] System.out.println(es2); //以指定枚举值创建EnumSet集合 EnumSet es3 = EnumSet.of(Season.SUMMER , Season.WINTER); //输出[SUMMER,WINTER] System.out.println(es3); EnumSet es4 = EnumSet.range(Season.SUMMER , Season.WINTER); //输出[SUMMER,FALL,WINTER] System.out.println(es4); //新创建的EnumSet集合的元素和es4集合的元素有相同类型, //es5的集合元素 + es4集合元素 = Season枚举类的全部枚举值 EnumSet es5 = EnumSet.complementOf(es4); //输出[SPRING] System.out.println(es5); } }HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet。
对于普通的插入、删除操作,LinkedHashSet比HashSet要略慢一点,这是由维护链表所带来的开销造成的。不过,因为有了链表的存在,遍历LinkedHashSet会更快。
EnumSet是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。
HashSet、TreeSet、EnumSet都是"线程不安全"的,通常可以通过Collections工具类的synchronizedSortedSet方法来"包装"该Set集合。
SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));