Set集合通用知识:
- set集合类似一个罐子,程序可以一次把对象丢进set集合,而set集合通常不能记住元素的添加顺序。Set集合与Collection基本相同,没有提供任何额外的方法。实际上Set就是Collection,只是行为略有不同。
- set集合不允许包含相同元素,Set在每次添加元素(值)的时候(使用add()方法添加),都会把前面的元素和新增的元素进行比较,如果返回的是false,则重复,丢弃;返回的是true,添加到集合当中。
- 上面介绍的是set集合的通用知识,因此完全适合后面介绍的HashSet、TreeSet和EnumSet三个实现类。只是三个实现类还各有特色。
HashSet类:
import java.util.*;
// 类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()方法总是返回true
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);
}
}
上面程序中向books集合中分别添加了两个A对象 、两个B对象、两个C对象,其中C类重写了equals()方法和hashCode()方法,导致HashSet把两个C对象当成同一对象。
如果两个对象的hashCode()方法返回的hasCode值相同,但他们通过equals()方法比较返回false时更加麻烦:因为两个对象的hasCode()值相同,HashSet将试图把他们保存在同一个位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置用链式结构来保存多个对象;而HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降。
重写hashCode()方法的基本规则:
在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值。
当两个对象通过equals()方法返回true时,这两个对象的hashCode()方法应该返回相同的值。
对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode。
- HashSet是Set接口的典型实现,大多时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能
- 前面说过,Set集合是不允许重复元素的,否则将会引发各种奇怪的问题。那么HashSet如何判断元素重复呢?
- 当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该HashCode值来决定该对象在HashSet中存储位置
- 如果有两个元素通过equals方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同位置,也就可以添加成功
- 如果两个元素通过equals为true,并且两个元素的hashCode相等,则这两个元素相等(即重复)
- 如果要重写保存在HashSet中的对象的equals方法,也要重写hashCode方法,重写前后hashCode返回的结果相等(即保证保存在同一个位置),所有参与计算 hashCode() 返回值的关键属性,都应该用于作为 equals() 比较的标准。如果重写了equals方法但不重写hashCode方法,即相同equals结果的两个对象将会被HashSet当作两个元素保存起来,这与我们设计HashSet的初衷不符(元素不重复)
- 另外如果两个元素哈市Code相等但equals结果不为true,HashSet会将这两个元素保存在同一个位置,并将超过一个的元素以链表方式保存,这将影响HashSet的效率
- 如果重写了equals方法但没有重写hashCode方法,则HashSet可能无法正常工作,比如下面的例子。
HashSet的特征:
- 不能保证元素的排列顺序,顺序可能与元素的添加顺序不同,元素的顺序可能变化
- HashSet不是同步的,如果多个线程同时访问一个HashSet,如果有2条或者2条以上线程同时修改了HashSet集合时,必须通过代码来保证其同步
- 集合元素值可以是null
LinkedHashSet类:
HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。
LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问的Set里的全部元素是有很好的性能,因为它以链表来维护内部顺序。
public class LinkedHashSetTest
{
public static void main(String[] args)
{
LinkedHashSet books = new LinkedHashSet();
books.add("a");
books.add("b");
System.out.println(books);
// 删除 a
books.remove("a");
// 重新添加 a
books.add("a");
System.out.println(books);
}
}
输出元素的顺序总是与添加的顺序一致:
[a,b]
[b,a]
TreeSet类
TreeSet类的特征
TreeSet是SortedSet接口的唯一实现,正如SortedSet名字所暗示的,TreeSet可以确保集合元素处于排序状态。与前面HashSet集合相比,TreeSet还提供了如下几个额外的方法:
- Object first():返回集合中的第一个元素。
- Object last():返回集合中的最末一个元素。
- Object lower(Object e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素不需要是TreeSet的元素)。
- Object higher(Object e):返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素不需要是TreeSet的元素)。
- SortedSet subSet(fromElement, toElement):返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)。
- SortedSet headSet(toElement):返回此Set的子集,由小于toElement的元素组成。
- SortedSet tailSet(fromElement):返回此Set的子集,由大于或等于fromElement的元素组成。
TreeSet采用红黑树的数据结构对元素进行排序。TreeSet支持两种排序方法:自然排序和定制排序。
- 自然排序:TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间大小关系,然后将集合元素按升序排列,这种方式就是自然排列。
- 定制排序:TreeSet借助于Comparator接口的帮助。该接口里包含一个的int compare(T o1, T o2)方法,该方法用于比较o1和o2的大小。
自然排序(在元素中写排序规则)
TreeSet 会调用compareTo方法比较元素大小,然后按升序排序。所以自然排序中的元素对象,都必须实现了Comparable接口,否则会跑出异常。对于TreeSet判断元素是否重复的标准,也是调用元素从Comparable接口继承而来额compareTo方法,如果返回0则是重复元素(两个元素I相等)。Java的常见类都已经实现了Comparable接口,下面举例说明没有实现Comparable存入TreeSet时引发异常的情况。
package collection.Set;
import java.util.TreeSet;
class Err {
}
public class TreeSets {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add(new Err());
ts.add(new Err());
System.out.println(ts);
}
}
运行程序会抛出如下异常
Exception in thread "main" java.lang.ClassCastException: collection.Set.Err cannot be cast to java.lang.Comparable
at java.util.TreeMap.compare(Unknown Source)
at java.util.TreeMap.put(Unknown Source)
at java.util.TreeSet.add(Unknown Source)
at collection.Set.TreeSets.main(TreeSets.java:13)
将上面的Err类实现Comparable接口之后程序就能正常运行了
class Err implements Comparable {
@Override
public int compareTo(Object o) {
// TODO Auto-generated method stub
return 0;
}
}
还有个重要问题是,因为TreeSet会调用元素的compareTo方法,这就要求所有元素的类型都相同,否则也会发生异常。也就是说,TreeSet只允许存入同一类的元素。例如下面这个例子就会抛出类型转换异常
package collection.Set;
import java.util.TreeSet;
class Err implements Comparable {
@Override
public int compareTo(Object o) {
// TODO Auto-generated method stub
return 0;
}
}
public class TreeSets {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add(1);
ts.add("2");
System.out.println(ts);
}
}
运行结果
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at java.lang.String.compareTo(Unknown Source)
at java.util.TreeMap.put(Unknown Source)
at java.util.TreeSet.add(Unknown Source)
at collection.Set.TreeSets.main(TreeSets.java:18)
定制排序(在集合中写排序规则)
TreeSet还有一种排序就是定制排序,定制排序时候,需要关联一个 Comparator对象,由Comparator提供排序逻辑。下面就是一个使用Lambda表达式代替Comparator对象来提供定制排序的例子。 下面是一个定制排序的列子
package collection.Set;
import java.util.Comparator;
import java.util.TreeSet;
class M {
int age;
public M(int age) {
this.age = age;
}
public String toString() {
return "M[age:" + age + "]";
}
}
class MyCommpare implements Comparator{
public int compare(Object o1, Object o2){
M m1 = (M)o1;
M m2 = (M)o2;
return m1.age > m2.age ? 1 : m1.age < m2.age ? -1 : 0;
}
}
public class TreeSets {
public static void main(String[] args) {
TreeSet ts = new TreeSet(new MyCommpare());
ts.add(new M(5));
ts.add(new M(3));
ts.add(new M(9));
System.out.println(ts);
}
}
当然将Comparator直接写入TreeSet初始化中也可以。如下。
package collection.Set;
import java.util.Comparator;
import java.util.TreeSet;
class M {
int age;
public M(int age) {
this.age = age;
}
public String toString() {
return "M[age:" + age + "]";
}
}
public class TreeSets {
public static void main(String[] args) {
TreeSet ts = new TreeSet(new Comparator() {
public int compare(Object o1, Object 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);
}
}
EnumSet特征
- EnumSet是一个专为枚举类设计的集合类,EnumSet中所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定。EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类的定义顺序来决定集合元素的顺序。
- EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是当进行批量操作(如调用containsAll 和 retainAll方法)时,如其参数也是EnumSet集合,则该批量操作的执行速度也非常快。
- EnumSet集合不允许加入null元素。如果试图插入null元素,EnumSet将抛出 NullPointerException异常。如果仅仅只是试图测试是否出现null元素、或删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。
package collection.Set;
import java.util.EnumSet;
enum Season
{
SPRING, SUMMER, FALL, WINTER
}
public class EnumSets {
public static void main(String[] args) {
//必须用元素对象的类类型来初始化,即Season.class
EnumSet es1 = EnumSet.allOf(Season.class);
System.out.println(es1);
EnumSet es2 = EnumSet.noneOf(Season.class);
es2.add(Season.WINTER);
es2.add(Season.SUMMER);
System.out.println(es2);
EnumSet es3 = EnumSet.of(Season.WINTER, Season.SUMMER);
System.out.println(es3);
EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
System.out.println(es4);
EnumSet es5 = EnumSet.complementOf(es4);
System.out.println(es5);
}
}
执行结果
[SPRING, SUMMER, FALL, WINTER]
[SUMMER, WINTER]
[SUMMER, WINTER]
[SUMMER, FALL, WINTER]
[SPRING]
各种集合性能分析
HashSet和TreeSet是Set集合中用得最多的I集合。HashSet总是比TreeSet集合性能好,因为HashSet不需要额维护元素的顺序。
LinkedHashSet需要用额外的链表维护元素的插入顺序,因此在插入时性能比HashSet低,但在迭代访问(遍历)时性能更高。因为插入的时候即要计算hashCode又要维护链表,而遍历的时候只需要按链表来访问元素。
EnumSet元素是所有Set元素中性能最好的,但是它只能保存Enum类型的元素