Java Review (二十六、集合----- Set 集合)

简介: Java Review (二十六、集合----- Set 集合)

文章目录

Set 集合,它类似于一个罐子 , 程序可以依次把多个对象"丢进" Set 集合,而  Set集合通常不能记住元素的添加顺序 。 Set 集合与 Collection 基本相同,没有提供任何额外的方法。实际上 Set 就是  Collection , 只是行为略有不同( Set不允许包含重复元素) 。

HashSet 类

散列表(hashtable )是一种可以快速地査找所需要的对象的数据结构, 散列表为每个对象计算一个整数, 称为散列码(hashcode)。 散列码是由对象的实例域产生的一个整数。更准确地说, 具有不同数据域的对象将产生不同的散列码。

HashSet是 Set 接口的典型实现 ,大多数时候使用 Set 集合时就是使用这个实现类。 HashSet 按 Hash算法来存储集合中 的元素,因此具有很好的存取和查找性能。

 HashSet 具有以下特点 :

  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化 。
  • HashSet 不是同步的,如果多个线程同时访问 一个 HashSet,假设有两个或者两个以上线程同时修改了 HashSet 集合时,则必须通过代码来保证其同步。
  • 集合元素值可以是 null 。

元素比较

当向 HashSet 集合中存入一个元素时, HashSet 会调用该对象的 hashCode()方法来得到该对象的hashCode  值,然后根据该 hashCode 值决定该对象在 HashSet 中的存储位置。如果有两个元素通过 equals()方法比较返回  true,但它们的 hashCode()方法返回值不相等, HashSet 将会把它们存储在不同的位置,

依然可以添加成功。

HashSet 集合判断两个元素相等的标准是两个对象通过 equals()方法比较相等,并且两个对象的 hashCode()方法返回值也相等。

下面程序分别提供了 三个类 A 、 B 和 C ,它们分别重写了 equals()、 hashCode()两个方法的一个或全部,通过此程序可以了解HashSet 判断集合元素相同的标准 :

HashSetTest.java

// 类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);
  }
}

运行结果:

image.png即使两个 A 对象通过 equals()方法 比较返回 true ,但 HashSet 依然把它们当成两个对象:即使两个 B 对象的 hashCode()返回相 同值〈都是1), 但 HashSet 依然把它们 当成两个对象 。

当把一个对象放入 HashSet 中时 ,如果需要重写该对象对应类的 equalsO方法 ,则也应该重写其 hashCode()方法 。  规则是 :如果两个对象通过 equals()方法比较返回 true , 这两个对象的 hashCode 值也应该相同 。

散列表用链表数组实现。每个列表被称为桶( bucket) (参看图二) 要想査找表中对象的位置, 就要先计算它的散列码, 然后与桶的总数取余, 所得到的结果就是保存这个元素的桶的索引。

例如, 如果某个对象的散列码为 76268, 并且有 128 个桶, 对象应该保存在第 108 号桶中(76268除以 128余 108 )。或许会很幸运, 在这个桶中没有

其他元素,此时将元素直接插人到桶中就可以了。

图二、散列表

image.png

有时候会遇到桶被占满的情况, 这也是不可避免的。这种现象被称为散列冲突( hash collision) 。 这时,  需要用新对象与桶中的所有对象进行比较,査看这个对象是否已经存在。如果散列码是合理且随机分布的, 桶的数目也足够大, 需要比较的次数就会很少。

当向 HashSet 中添加可变对象时,必须十分小心 。 如果修改 HashSet 集合中 的对象,有可能导致该对象与 集合中的其他对象相等,从而导致 HashSet 无法准确访问该对象 。

API:java.util.HashSet

LinkedHashSet 类

HashSet还有一个子类 LinkedHashSet , LinkedHashSet 集合也是根据元素的 hashCode  值来决定元素的存储位置 , 但它同时使用链表维护元素的次序 ,这样使得元素看起来是以插入的顺序保存的 。 也就是说 , 当遍历  LinkedHashSet 集合里的元素时, LinkedHashSet 将会按元素的添加顺序来访问集合里的元素。

LinkedHashSet 需要维护元素 的插入顺序,因此性能略低于 HashSet 的性能,但在迭代访问 Set 里的全部元素时将有很好的性能,因为它以链表来维护内部顺序 。

LinkedHashSetTest.java

public class LinkedHashSetTest
{
  public static void main(String[] args)
  {
    LinkedHashSet books = new LinkedHashSet();
    books.add("疯狂Java讲义");
    books.add("轻量级Java EE企业应用实战");
    System.out.println(books);
    // 删除 疯狂Java讲义
    books.remove("疯狂Java讲义");
    // 重新添加 疯狂Java讲义
    books.add("疯狂Java讲义");
    System.out.println(books);
  }
}

虽然 LinkedHashSet 使用了链表记录集合元素的添加顺序,但 LinkedHashSet 依然是HashSet ,因此它依然不九许集合元素重复 。

API:java.util.LinkedHashSet

TreeSet 类

image.png

TreeSet 是 SortedSet 接口的实现类,正如 SortedSet 名字所暗示的, TreeSet 可以确保集合元素处于排序状态。与 HashSet 集合相比, TreeSet 还提供了如下几个额外的方法 :

  • Comparator comparator(): 如 果 TreeSet 采用了定制排序,则该方法返回定制排序所使用的Comparator; 如 果 TreeSet 采用了自然排序,则返回 null 。
  • Object ftrst(): 返回集合中的第一个元素。
  • Object last(): 返回集合中的最后一个元素 。
  • Object lower(Object e): 返回集合中位于指定元素之前的元素( 即小于指定元素的最大元素 ,参考元素不需要是 TreeSet 集合里的元素)。
  • Object higher (Object e): 返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素不需要是 TreeSet 集合里的元素) 。
  • SortedSet subSet(Object fromElement, Object toElement): 返回此 Set 的子集合,范围从 fromElement(包含〉到 toElement (不包含)。
  • SortedSet headSet(Object toElement): 返回此 Set 的子集 ,由小于toElement 的元素组成 。
  • SortedSet tailSet(Object 仕omElement): 返回此Set 的子集 , 由大于或等于企omElement 的元素组成 。

下面程序测试了 TreeSet 的通用用法:

TreeSetTest.java

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的子集,如果Set中包含5,子集中还包含5
    System.out.println(nums.tailSet(5)); // 输出 [5, 10]
    // 返回大于等于-3,小于4的子集。
    System.out.println(nums.subSet(-3 , 4)); // 输出[2]
  }
}

运行结果:

与 HashSet 集合采用 hash 算法来决定元素 的存储位置不同, TreeSet 采用红黑树的数据结构来存储集合元素。TreeSet 支持两种排序方法 : 自然排序和定制排序。在默认情况下, TreeSet 采用自然排序。

要使用树集, 必须能够比较元素。这些元素必须实现 Comparable 接口, 或者构造集时必须提供一个 Comparator 。

自然排序

TreeSet 会调用集合元素的 compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然排序 。

Java 提供了 一个 Comparable 接口,该接口里定义了一个 compareTo(Object obj  )方法,该方法返回一个整数值,实现该接口的类必须实现该方法 , 实现了 该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行  比较时,例如 obj 1.compareTo(obj2) ,如果该方法返回 0 ,则表明这两个对象

相等 :如果该方法返回一个正整数, 则表明 objl 大于 obj2; 如果该方法返回一个负整数, 则表明 objl小于 obj2 。

Java 的 一些常用类已经实现了 Comparable 接口,并提供了比较大小的标准。

 下面是实现了Comparable 接口的常用类:

  • BigDecimal 、 BigInteger 以及所有的数值型对应的包装类 : 按它们对应的数值大小进行比较。
  • Character: 按字符的 UNICODE 值进行比较。
  • Boolean: true 对应的包装类实例大于 false 对应的包装类实例。
  • String: 按字符串中字符的UNICODE 值进行 比较。
  • Date 、 Time: 后面的时间、日期比前面的时间、日期大。

如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口,否则程序将会抛出异常。如下程序示范了这个错误:

TreeSetTestError.java

class Err{}
public class TreeSetErrorTest
{
  public static void main(String[] args)
  {
    TreeSet ts = new TreeSet();
    // 向TreeSet集合中添加Err对象
    // 自然排序时,Err没实现Comparable接口将会引发错误
    ts.add(new Err());
  }
}

向 TreeSet 中添加的应该是同一个类的对象,否则也会引发ClassCastException 异常,因为大部分类在实现 compareTo(Object obj)方法时,都需要将被比较对象 obj 强制类型转换成相同类型。

如下程序示范了这个错误 :

TreeSetErrorTest2.java

public class TreeSetErrorTest2
{
  public static void main(String[] args)
  {
    TreeSet ts = new TreeSet();
    // 向TreeSet集合中添加两个对象
    ts.add(new String("疯狂Java讲义"));
    ts.add(new Date());   // ①
  }
}

当把一个对象加入 TreeSet 集合中时, TreeSet 调用该对象的compareTo(Object  obj)方法与容器中的其他对象比较大小,然后根据红黑树结构找到它的存储位置 。 如果两个对象通过 compareTo(Object  obj)方法比较相等,新对象将无法添加到 TreeSet 集合中 。

TreeSetTest2.java

class Z implements Comparable
{
  int age;
  public Z(int age)
  {
    this.age = age;
  }
  // 重写equals()方法,总是返回true
  public boolean equals(Object obj)
  {
    return true;
  }
  // 重写了compareTo(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变量,将看到也变成了9
    System.out.println(((Z)(set.last())).age);
  }
}

运行结果:

image.png

程序中①代码行把同 一个对象再次添加到 TreeSet 集合中,因为 zl 对象的

ompareTo(Object obj)方法总是返回 1, 虽然它的 equalsO方法总是返回 true ,但 TreeSet会认为 z1对 象和它自己也不相等,因此TreeSet 可以添加两个 z1 对 象。

TreeSet 及 Z 对象在内存中的存储示意图

image.png

由此应该注意一个问题 : 当需要把一个对象放入 TreeSet中, 重写该对象对应类的 equals()方法时,应保证该方法与  compareTo(Object obj)方法有一致的结果,其规则 是 : 如果两个对象通过 equals()方法比较返回 true  时,这两个对象通过 compareTo(Object obj)方法比较应返回 0 。

定制排序

TreeSet 的自 然排序是根据集合元素 的大小, TreeSet 将它们以升序排列 。 如果需要实现定制排序,例如以降序排列,  则可以通过 Comparator 接口 的帮助 。该接口里包含一个 int compare(T 01 , T 02)方法,该方法用于比较 01 和  02 的大小:如果该方法返 回正整数 ,则表明 01 大于 02; 如果该方法返回 0 ,则表明 01 等于 02; 如果该方法返回负整数,  则表 明 01 小于 02 。

如果需要实现定制排序,则需要在创建 TreeSet 集合对象时,提供一个Comparator 对象与该TreeSet集合关联 , 由该  Comparator 对象负责集合元素 的排序逻辑。由于 Comparator 是一个函数式接口,因此可使用 Lambda 表达式来代替  Comparator 对象 。

TreeSetTest4.java

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;
      // 根据M对象的age属性来决定大小,age越大,M对象反而越小
      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);
  }
}

运行结果:

image.png

API:java.util.TreeSet

EnumSet 类

EnumSet 是一个专为枚举类设计的集合类, EnumSet 中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建 EnumSet 时显式或隐式地指定。

EnumSet 的集合元素也是有序的, EnumSet 以枚举值在 Enum 类内的定义顺序来决定集合元素的顺序。

EnumSet 在内部以位向 量 的形式存储,这种存储形式非常紧凑 、 高效 ,因此 EnumSet  对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用 containsAll() 和 retainAll()方法〉时,如果其参数也是  EnumSet 集合,则该批量操作的执行速度也非常快。

EnumSet 集合不允许加入 null 元素,如果试图插入 null 元素, EnumSet 将抛出  NullPointerException异常。如果只是想判断 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集合。

下面程序示范了 如何使用 EnumSet来保存枚举类的多个枚举值 :

EnumSetTest.java

enum Season
{
  SPRING,SUMMER,FALL,WINTER
}
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); // 输出[]
    // 手动添加两个元素
    es2.add(Season.WINTER);
    es2.add(Season.SPRING);
    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(es5); // 输出[SPRING]
  }
}

API:java.util.EnumSet

各 Set 实现类的性能分析

HashSet 和 TreeSet 是 Set 的两个典型实现 , 到底如何选择 HashSet 和 TreeSet 呢?

HashSet 的性能总是 比 TreeSet 好(特别是最常用的添加 、 查询元素等操作) , 因为 TreeSet  需要额外的红黑树算法来维护集合元素的次序 。 只有当需要一个保持排序 的 Set 时,才应该使用 TreeSet,否则都应该使用 HashSet  。

HashSet 还有一个子类 : LinkedHashSet , 对于普通的插入、删除操作 , LinkedHashSet 比  HashSet要略微慢一点 , 这是由维护链表所带来的额外开销造成的 , 但由于有了链表,遍历 LinkedHashSet 会更快 。

EnumSet 是所有 Set 实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。

必须指出的是, Set 的三个实现类 HashSet 、 TreeSet 和 EnumSet 都是线程不安全的 。如果有多个线程同时访问  一个 Set 集合,并且有超过一个线程修改了该 Set 集合 ,则必须手动保证该Set 集合的同步性。通常可以通过 Collections  工具类的 syncbronizedSortedSet 方法来 "包装"该 Set 集合。 此操作最

好在创建时进行 , 以防止对 Set 集合的意外非同步访问 。

SortedSet s = Collections.synchronizedSortedSet(new TreeSet(.. .));


参考:

【1】:《疯狂Java讲义》

【2】:《Java核心技术 卷一》

【3】:廖雪峰的官方网站:使用Set


目录
相关文章
|
17天前
|
存储 算法 Java
Java Set深度解析:为何它能成为“无重复”的代名词?
Java的集合框架中,Set接口以其“无重复”特性著称。本文解析了Set的实现原理,包括HashSet和TreeSet的不同数据结构和算法,以及如何通过示例代码实现最佳实践。选择合适的Set实现类和正确实现自定义对象的hashCode()和equals()方法是关键。
25 4
|
17天前
|
Java
那些与Java Set擦肩而过的重复元素,都经历了什么?
在Java的世界里,Set如同一位浪漫而坚定的恋人,只对独一无二的元素情有独钟。重复元素虽屡遭拒绝,但通过反思和成长,最终变得独特,赢得了Set的认可。示例代码展示了这一过程,揭示了成长与独特性的浪漫故事。
20 4
|
17天前
|
Java 开发者
Java Set:当“重复”遇见它,秒变“独宠”!
在Java编程中,Set接口确保集合中的元素不重复,每个元素都是独一无二的“独宠”。本文介绍了Set的两种常见实现:HashSet和TreeSet。HashSet基于哈希表实现,提供高效的添加、删除和查找操作;TreeSet基于红黑树实现,不仅去重还能对元素进行排序。通过示例代码,展示了这两种集合的具体应用,帮助开发者更好地理解和使用Set。
22 4
|
19天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
25 2
|
19天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
22天前
|
存储 Java 开发者
Java Set:无序之美,不重复之魅!
在Java的集合框架中,Set接口以其“无序之美”和“不重复之魅”受到开发者青睐。Set不包含重复元素,不保证元素顺序,通过元素的hashCode()和equals()方法实现唯一性。示例代码展示了如何使用HashSet添加和遍历元素,体现了Set的高效性和简洁性。
28 4
|
22天前
|
存储 算法 Java
为什么Java Set如此“挑剔”,连重复元素都容不下?
在Java的集合框架中,Set是一个独特的接口,它严格要求元素不重复,适用于需要唯一性约束的场景。Set通过内部数据结构(如哈希表或红黑树)和算法(如哈希值和equals()方法)实现这一特性,自动过滤重复元素,简化处理逻辑。示例代码展示了Set如何自动忽略重复元素。
26 1
|
22天前
|
存储 算法 Java
Java中的Set,你真的了解它的“无重复”奥秘吗?
在Java的广阔天地里,Set以其独特的“无重复”特性,在众多数据结构中脱颖而出。本文将揭秘Set的“无重复”奥秘,带你领略其魅力。Set通过哈希算法和equals()方法协同工作,确保元素不重复。通过一个简单的案例,我们将展示HashSet如何实现这一特性。
29 1
|
24天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
23天前
|
存储 Java 开发者
Java中的集合框架深入解析
【10月更文挑战第32天】本文旨在为读者揭开Java集合框架的神秘面纱,通过深入浅出的方式介绍其内部结构与运作机制。我们将从集合框架的设计哲学出发,探讨其如何影响我们的编程实践,并配以代码示例,展示如何在真实场景中应用这些知识。无论你是Java新手还是资深开发者,这篇文章都将为你提供新的视角和实用技巧。
22 0