源码阅读(25):Java中主要的Set结构——概述

Posted 说好不能打脸

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码阅读(25):Java中主要的Set结构——概述相关的知识,希望对你有一定的参考价值。

1、概述

从本专题的第15篇文章开始一直到本专题的第24篇文章截止,我们花了大量的篇幅介绍Java中的Map接口以及实现该接口的多个重要容器,其中又以介绍HashMap容器花费的篇幅为最。这是为什么呢?这主要因为HashMap容器中所使用的多个数据结构和算法在Map容器中最具代表性,例如TreeMap容器主要使用红黑树进行实现,而红黑树也应用在HashMap容器中;再例如LinkedHashMap容器在HashMap容器中,增加了一个全局的双向链表结构,使得容器中的K-V键值对对象具有了多种新的遍历方式。

而我们在介绍Set集合前,先介绍Java中多个原生的Map容器,是因为Java中多个原生的Set集合都依赖于对应的Map容器进行实现。例如后文将要介绍的HashSet集合,其内部主要依赖于HashMap进行实现,前者的子类LinkedHashSet也是如此;再例如后文要介绍的TreeMap集合,其内部主要依赖于HashMap进行实现。如下图所示:

Set性质的集合中不存在重复的对象,那么怎么定义两个对象是重复的呢?如果这两个对象分别记为e1和e2,那么"e1.equals(e2) "方法调用的结果为false(反之亦然)。这样的话Set性质的集合更正式的定义为:

任何情况下,某个集合中的两个对象元素k1和k2都不可能出现k1.equals(k2) 结果为true或者k2.equals(k1)结果为true的情况,这样的集合就是Set性质的集合。

这实际上就可以解释为什么会出现多个Java原生提供的Set集合,其内部都依赖于对应的Map容器进行实现,就是因为这些对应的Map容器中判定Key值是否一致的标准,就是依据的equals方法的比较结果。以下是HashMap容器判定Key值是否一致的代码片段:

// ...... 
// 如果两个key值的内存地址相同,或者两个key值的equals方法返回true
// 就认为两个key值相同
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
// ......

2、Set结构中重要的接口和抽象类

在介绍具体的Set集合前,还需要介绍一下Set结构中几个重要的抽象类和接口定义。

2.1、java.util.SortedSet接口

一般来说Set性质集合中的元素对象都是无序的,一个很典型的例子是基于HashSet集合对A、B、C、D等多个对象使用add()方法进行添加,其在Set集合中的顺序位置会受到添加顺序的影响:

//......
// 创建一个HashSet集合,并添加字符串对象
HashSet<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
set.add("1");
set.add("2");
set.add("3");
Iterator<String> itr = set.iterator();
while(itr.hasNext()) 
  String item = itr.next();
  System.out.println(String.format("item = %s" , item));


// 以下代码改变了元素对象的添加顺序,就会造成集合中元素的存储顺序完全不一样
System.out.println("//=======================");
set = new HashSet<>();
set.add("1");
set.add("2");
set.add("3");
set.add("a");
set.add("b");
set.add("c");
itr = set.iterator();
while(itr.hasNext()) 
  String item = itr.next();
  System.out.println(String.format("item = %s" , item));

//......

以下为代码的输出结果:

item = a
item = 1
item = b
item = 2
item = c
item = 3
//=======================
item = 1
item = a
item = 2
item = b
item = 3
item = c

之所以出现这样的情况,是HashSet集合内部的结构所决定的——HashSet集合内部依赖了HashMap集合的结构。出现这种现象的集合称之为无序集合。

但如果某个具体的Set集合实现了java.util.SortedSet接口,就表示其集合中的元素对象将按照某种元素的对比方式,进行全局性的有序排列。一般来说实现了java.util.SortedSet接口的具体集合都提供了两种比较方法,第一种比较方法是使用元素对象自己实现的Comparable接口来进行,换句话说这就要求存储到SortedSet集合的元素对象类实现java.lang.Comparable接口中的compareTo方法;第二种比较方式是通过java.util.SortedSet接口中的定义的comparator()方法返回的Comparator比较器进行元素的比较。如果以上两种比较方式都不可用,那么在调用相关方法或者在构造实例时就会抛出ClassCastException异常。

java.util.SortedSet接口中主要的定义方法介绍如下:

public interface SortedSet<E> extends Set<E> 
  // 该方法返回一个比较器,这个比较器用于在集合的各个操作中进行元素对象的比较
  // 该返回值允许为null,如果为null则表示该set集合在进行元素对象比较时,将采用元素对象自身实现的java.lang.Comparable接口
  // 如果以上两种情况都不成立,则会抛出ClassCastException异常
  Comparator<? super E> comparator();
  
  // 该方法将返回一个有序的子级
  // 这个有序的子级中的元素对象,将包括从“from”对象开始(包含)到“to”对象结束(不包含)的所有元素对象
  // 请注意,如果from对象和to对象相同(如何定义相同,上文已进行了说明),那么该方法将返回空集合
  // 另外,from或者to对象也不用一定要存在于集合中,因为取子集的比较主要参照对象值进行(这句话不好理解?后文有相关代码)
  // 最后需要注意的是,这个返回的子集合中的元素对象全是源集合中各元素对象的引用,所以对子集合进行的操作,都将反映到源集合中。
  // 并且不能进行写性质的操作,否则会抛出IllegalArgumentException异常。
  SortedSet<E> subSet(E from, E to);

  // 返回有序集合中元素对象值小于“to”对象的子集
  // 该方法定义的注意事项和以上subSet方法注意事项类似
  SortedSet<E> headSet(E to);
  
  // 返回有序集合中元素对象值大于或者等于“from”对象的子集
  // 该方法定义的注意事项和以上subSet方法注意事项类似
  SortedSet<E> tailSet(E from);
  
  // 返回当前有序集合中,值最小的元素对象
  // 如果当前集合中没有任何元素,则抛出NoSuchElementException异常
  E first();
  
  // 返回当前有序集合中,值最大的元素对象
  // 如果当前集合中没有任何元素,则抛出NoSuchElementException异常
  E last();

2.2、java.util.NavigableSet接口

java.util.NavigableSet接口是java.util.SortedSet接口的一个子级接口,直译可以理解成支持“基于参照元素进行引导操作”的Set集合,也就是说在满足集合有序的前提下,可以参照指定的元素进行集合中各元素对象的读写操作。举例说明:可以参考集合中已有的元素对象X,查询集合中所有值小于或等于该对象X的值最大的那个元素对象。

保证这种“基于参照元素进行引导操作”的前提是集合中的元素对象按照一定顺序进行排列,这也就可以解释为什么java.util.NavigableSet接口是java.util.SortedSet接口的一个子级接口。java.util.NavigableSet接口中主要的定义方法介绍如下:

public interface NavigableSet<E> extends SortedSet<E> 
  // 返回有序集合中,那些值小于指定元素对象中,值最大的一个元素对象
  // 如果没有任何元素对象符合要求,则返回null
  E lower(E e);
  
  // 返回有序集合中,那些值小于或等于指定元素对象中,值最大的一个元素对象
  // 如果没有任何元素对象符合要求,则返回null
  // lower方法和floor方法最大的区别就是是否包括了“值相等”的判定
  E floor(E e);
  
  // 返回有序集合中,那些值大于指定元素对象中,值最小的一个元素对象
  // 如果没有任何元素对象符合要求,则返回null 
  E higher(E e);  

  // 返回有序集合中,那些值大于或等于指定元素对象中,值最小的一个元素对象
  // 如果没有任何元素对象符合要求,则返回null
  // higher方法和ceiling方法最大的区别就是是否包括了“值相等”的判定
  E ceiling(E e);
  
  // 返回并删除有序集合中的第一个(值最小的)元素。
  // 如果当前集合中没有元素,则返回null
  E pollFirst();
  
  // 返回并删除有序集合中的最后一个(值最大的)元素。
  // 如果当前集合中没有元素,则返回null
  E pollLast();
  
  // 返回一个迭代器,这个迭代器可以按照元素对象值从小到大、从第一个到最后一个的顺序进行集合中元素对象的遍历。
  Iterator<E> iterator();

  // 返回一个迭代器,这个迭代器基于当前集合的反序集合。
  // 也就是说这个迭代器可以按照元素对象值从大到小、从最后一个到第一个的顺序进行当前集合中元素对象的遍历。
  Iterator<E> descendingIterator();  

  // 该方法返回当前有序集合的反序集合,注意这个反序集合中的各个元素对象并不是新的实例,而是原集合中元素对象的引用
  // 只是这些元素对象间的关联方向发生了变化。所以对原集合中对象的修改操作结果也会反应到反序集合中。
  // 如果在对任一集合进行迭代时修改了任一集合(通过迭代器自己的移除操作除外),则会抛出诸如ConcurrentModificationException这样的异常
  NavigableSet<E> descendingSet();
  
  // subSet方法的操作意义已经在介绍SortedSet接口时描述过了,这里不再进行赘述
  // 其中有两个多出的参数,意义如下:
  // fromInclusive:该参数描述返回的集合中是否包括“from”元素对象
  // toInclusive:该参数描述返回的集合中是否包括“to”元素对象
  NavigableSet<E> subSet(E from, boolean fromInclusive, E to,   boolean toInclusive);
  
  // 返回有序集合中元素对象值小于“to”对象的子集
  // inclusive:该参数描述返回的集合中是否包括“to”元素对象
  NavigableSet<E> headSet(E to, boolean inclusive);
  
  // 返回有序集合中元素对象值大于“from”对象的子集
  // inclusive:该参数描述返回的集合中是否包括“from”元素对象
  NavigableSet<E> tailSet(E from, boolean inclusive);
  
  // ......

后文将要讲解的java.util.TreeSet类,即实现了NavigableSet接口也实现了SortedSet接口,我们通过一些TreeSet类中方法的使用效果来进行观察:

  • TreeSet集合的基本使用
// ......
// 请注意实例代码中,向TreeSet集合放入了java.lang.String类的实例
// java.lang.String类实现了Comparable接口,所以集合中各元素的比较操作将使用
// String.compareTo(String anotherString)方法进行比较
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("b");
treeSet.add("d");
treeSet.add("a");
treeSet.add("c");
treeSet.add("1");
treeSet.add("3");
treeSet.add("5");
treeSet.add("4");
treeSet.add("2");

// 进行正序遍历
Iterator<String> itr = treeSet.iterator();
while(itr.hasNext()) 
  String item = itr.next();
  System.out.println(String.format("item = %s" , item));

// ......

以上代码的输出结果,如下所示

item = 1
item = 2
item = 3
item = 4
item = 5
item = a
item = b
item = c
item = d
  • 获取反序集合的操作
// 同样基于以上代码片段中创建的treeSet集合
// ......
NavigableSet<String> descendingSet = treeSet.descendingSet();
Iterator<String> descItr = descendingSet.iterator();
while(descItr.hasNext()) 
  String item = descItr.next();
  System.out.println(String.format("item = %s" , item));

// ......

以上代码的输出结果,如下所示:

item = d
item = c
item = b
item = a
item = 5
item = 4
item = 3
item = 2
item = 1
  • 获取子集的操作
// 同样基于以上代码片段中创建的treeSet集合
// ......
SortedSet<String> subSetOne = treeSet.subSet("3", "c");
Iterator<String> subItr = subSetOne.iterator();
while(subItr.hasNext()) 
  String item = subItr.next();
  System.out.println(String.format("item = %s" , item));

// 当在子集中添加数据时,会抛出IllegalArgumentException异常
//subSetOne.add("0");
// 抛出IllegalArgumentException异常
//subSetOne.add("10");
// 抛出IllegalArgumentException异常
//subSetOne.add("1");

// 以下是不推荐的取子集的方式,例如to或者from对象不存在于集合中
// 以下方式将返回一个空集合
SortedSet<String> subSetTwo = treeSet.subSet("00", "00");
Validate.isTrue(subSetTwo.isEmpty() , "subSetTwo确实是空集合");
subSetTwo = treeSet.subSet("1", "1");
// 以下方式将取得的子级元素包括:2、3、4、5、a,请注意,from参数传入的字符串“10”并不存在于集合中
// 这是因为字符串和字符串比较时,字符串“10”的值介于字符串“1”和“2”之间
subSetTwo = treeSet.subSet("10", "b");
// ......

为了更好解释为什么字符串“10”的值介于字符串“1”和“2”之间,我们再给出String.compareTo(String anotherString)方法的源代码,如下所示:

// 从该方法的过程可以看出字符串的对比是按照字符串中每个字符进行了
// 如果字符的比较没有得到两个字符串的大小,就再按照字符串的长度进行比较
public int compareTo(String anotherString) 
  int len1 = value.length;
  int len2 = anotherString.value.length;
  int lim = Math.min(len1, len2);
  char v1[] = value;
  char v2[] = anotherString.value;

  int k = 0;
  // 基于两个字符串的每一个字符进行比较
  while (k < lim) 
    char c1 = v1[k];
    char c2 = v2[k];
    if (c1 != c2) 
      return c1 - c2;
    
    k++;
  
  // 如果没有比较出大小,在基于长度进行比较
  return len1 - len2;

2.3、java.util.AbstractSet抽象类

该类存在的意义和java.util.AbstractMap类存在的意义相似,主要为了有效减少实际的Set集合的实现复杂度。其中提供了一些通用方法的实现逻辑,包括:equals方法、hashCode方法、removeAll方法。以下是该类的部分源代码介绍:

public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> 
  public boolean equals(Object o) 
    // 如果两个对象的内存地址相同,对人认为两个对象相等
    if (o == this)
      return true;
    
    // 如果当前做对比参考的对象并不是一种Set性质的集合
    // 则返回false
    if (!(o instanceof Set))
      return false;
    Collection<?> c = (Collection<?>) o;
    // 如果参与比较的两个集合大小不一样,则认为两个对象不相同,返回false
    if (c.size() != size())
      return false;
    try 
      // containsAll方法的意义在于如果c集合中所有元素都同时存在于当前集合中,则返回true
      // 其它情况返回false
      return containsAll(c);
     catch (ClassCastException unused)   
      return false;
     catch (NullPointerException unused) 
      return false;
    
  
  
  // 根据前文的介绍我们知道equals方法和hashCode方法一般都需要做对应的实现
  public int hashCode() 
    int h = 0;
    Iterator<E> i = iterator();
    while (i.hasNext()) 
      E obj = i.next();
      if (obj != null)
        h += obj.hashCode();
    
    return h;
  

后文,本专题将详细介绍Java中几个关键的原生Set集合:HashSet、LinkedHashSet和TreeSet集合。

============
(接后文《源码阅读(26):Java中主要的Set结构——HashSet集合》)

以上是关于源码阅读(25):Java中主要的Set结构——概述的主要内容,如果未能解决你的问题,请参考以下文章

源码阅读(26):Java中主要的Set结构——HashSet

源码阅读(15):Java中主要的Map结构——概述

源码阅读(10):Java中主要的QueueDeque结构——ArrayDeque集合(上)

源码阅读(12):Java中主要的QueueDeque结构——PriorityQueue集合(上)

源码阅读(11):Java中主要的QueueDeque结构——ArrayDeque集合(下)

源码阅读(13):Java中主要的QueueDeque结构——PriorityQueue集合(中)