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

Posted 说好不能打脸

tags:

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

1、概述

在全面理解了HashMap结构后,理解HashSet数据结构也就足够简单了——HashSet内部就是对HashMap数据结构的依赖。HashMap结构的Key就是HashSet存储的数据,HashMap结构的Value则是一个固定对象记为“PRESENT”:

// ...
private static final Object PRESENT = new Object();
// ...

下图为HashSet的主要继承体系:

从上图可以看出HashSet继承了AbstractSet,后者包括了相当一部分关于Set集合通用的方法逻辑。这些方法包括了equals(Object o)、hashCode()、isEmpty()、contains(Object o)等方法。

This class provides a skeletal implementation of the Setinterface to minimize the effort required to implement thisinterface.

在上一篇文章中《源码阅读(25):Java中主要的Set结构——概述》我们已经介绍过HashSet结构的使用效果,简单来说其使用效果可以概要为以下几个要点:

  • HashSet中不允许存储“相同”的元素。

如何确认“相同”元素呢?就是使用当前存储到HashSet集合中的对象定义的hashCode()方法的返回结果作为确认依据(关于对象定义中的equals(Object anObject)方法和hashCode()方法如何作用于对象“相等”的判定属于最基础知识,读者可参考其它资料)。例如当前HashSet中存储了java.lang.String类型的对象,那么将使用“boolean String.hashCode()”这个方法判定对象是否“相同”。实际上这个原理也是HashMap集合的工作原理。

  • 由于内部存储原理依赖于HashMap的原因,HashSet集合中存储的元素并不是有序的。最显著的效果是:同样的元素集合,使用不同的添加顺序添加到集合中,这些元素在集合中存储的位置可能是不同的。这段代码已经在前文中给出:
//......
// 创建一个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

2、结构特点

2.1、HashSet的主要结构

上文已经多次强调,HashSet集合内部依赖于HashMap集合,下面我们给出HashSet源代码中关键属性的定义代码,进行分析:

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable 
  // ......
  // 这是一个不参与序列化过程的HashMap集合,作为HashSet中一个重要属性存在。
  private transient HashMap<E,Object> map;

  // Dummy value to associate with an Object in the backing Map
  // 在HashSet基于HashMap添加元素时,后者每一个K-V键值对结点的Value值,都采用这个常量进行描述
  private static final Object PRESENT = new Object();
  
  // ......

2.2、HashSet中的构造函数

HashSet中的构造函数也很简单,主要就是对其内部的HashMap属性进行实例化,代码片段如下所示:

/**
 * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
 * default initial capacity (16) and load factor (0.75).
 * 完成一个默认的HashMap对象的实例化
 */
 public HashSet() 
   map = new HashMap<>();
 

 /**
  * 参考一个指定集合中的元素进行内部HashMap集合的初始化
  * 注意,HashMap的初始化大小基于当前两个数值较大的一个:源集合大小的175%和固定值16
  *
  * @param c the collection whose elements are to be placed into this set
  * @throws NullPointerException if the specified collection is null
  */
 public HashSet(Collection<? extends E> c) 
   map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
   addAll(c);
 

 /**
  * 该构造函数可以为内部使用的HashMap指定两个关键参数,一个是HashMap的初始化大小,另一个是HashMap的负载因子
  * @param      initialCapacity   he initial capacity of the hash map
  * @param      loadFactor        the load factor of the hash map
  * @throws     IllegalArgumentException if the initial capacity is less
  *             than zero, or if the load factor is nonpositive
  */
 public HashSet(int initialCapacity, float loadFactor) 
   map = new HashMap<>(initialCapacity, loadFactor);
 

 /**
  * 该构造函数可以为内部使用的HashMap指定一个关键参数,是HashMap的初始化大小;负载因子将设定为默认的0.75
  * @param      initialCapacity   the initial capacity of the hash table
  * @throws     IllegalArgumentException if the initial capacity is less
  *             than zero
  */
 public HashSet(int initialCapacity) 
   map = new HashMap<>(initialCapacity);
 

 /**
  * 这个构造函数很有趣,这是一个访问控制级别为“包内访问”的构造函数,事实上,它是专门给
  * HashSet的子类LinkedHashSet进行实例化使用的构造函数。我们将在后续内容中介绍LinkedHashSet容器。
  * 那么问题来了,为什么不在LinkedHashSet类中定义这个构造函数呢?
  */
 HashSet(int initialCapacity, float loadFactor, boolean dummy) 
   map = new LinkedHashMap<>(initialCapacity, loadFactor);
 

由以上分析可以看出,基本上所有我们直接使用进行HashSet集合对象初始化的构造函数,其内部都是对HashMap对象的初始化。

3、HashSet的主要方法

HashSet的主要方法都是对内部HashMap集合主要方法的封装

请看HashSet集合的主要方法代码:

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable 
  // ......
  /**
   * 集合的添加操作亦是调用了HashMap的添加操作,请注意写入的value对象,是一个常量
   * @param e element to be added to this set
   * @return <tt>true</tt> if this set did not already contain the specified
   * element
   */
  public boolean add(E e) 
    return map.put(e, PRESENT)==null;
  
  
  /**
   * 集合的移除操作亦是调用HashMap的移除操作。
   *
   * @param o object to be removed from this set, if present
   * @return <tt>true</tt> if the set contained the specified element
   */
  public boolean remove(Object o) 
      return map.remove(o)==PRESENT;
  

  /**
   * 集合的元素清除操作,亦是调用HashMap集合的清除操作
   */
  public void clear() 
    map.clear();
  

  // 这里不再列举
  // ......

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

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

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

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

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

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

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