当前位置:   article > 正文

Java基础汇总(十八)——HashSet,TreeSet和LinkedHashSet_hashset linkedhashset treeset

hashset linkedhashset treeset

一、HashSet

1.定义

  • HashSet继承AbstractSet类,实现Set、Cloneable、Serializable接口
  • AbstractSet提供 Set 接口的骨干实现,从而最大限度地减少了实现此接口所需的工作
  • Set接口是一种不包括重复元素的Collection,它维持它自己的内部排序,所以随机访问没有任何意义
  1. public class HashSet<E>
  2. extends AbstractSet<E>
  3. implements Set<E>, Cloneable, java.io.Serializable

2.基本属性

  • HashSet是基于HashMap实现,底层使用HashMap保存所有元素
  • 因为HashSet中只需要用到key,而HashMap是key-value键值对,所以向map中添加键值对时,键值对的值固定是PRESENT
  1. private transient HashMap<E,Object> map;
  2. //定义一个Object对象作为HashMap的value
  3. // 定义一个虚拟的Object PRESENT是向map中插入key-value对应的value
  4. // 因为HashSet中只需要用到key,而HashMap是key-value键值对;
  5. // 所以,向map中添加键值对时,键值对的值固定是PRESENT
  6. private static final Object PRESENT = new Object();

3.构造函数

  1. // 默认构造函数 底层创建一个HashMap
  2. public HashSet() {
  3. // 调用HashMap的默认构造函数,创建map
  4. map = new HashMap<E,Object>();
  5. }
  6. // 带集合的构造函数
  7. public HashSet(Collection<? extends E> c) {
  8. // 创建map。
  9. // 为什么要调用Math.max((int) (c.size()/.75f) + 1, 16),
  10. // 从 (c.size()/.75f) + 1 和 16 中选择一个比较大的树呢?
  11. // 首先,说明(c.size()/.75f) + 1
  12. // 因为从HashMap的效率(时间成本和空间成本)考虑,HashMap的加载因子是0.75。
  13. // 当HashMap的“阈值”(阈值=HashMap总的大小*加载因子) < “HashMap实际大小”时,
  14. // 就需要将HashMap的容量翻倍。
  15. // 所以,(c.size()/.75f) + 1 计算出来的正好是总的空间大小。
  16. // 接下来,说明为什么是 16 。
  17. // HashMap的总的大小,必须是2的指数倍。若创建HashMap时,指定的大小不是2的指数倍;
  18. // HashMap的构造函数中也会重新计算,找出比“指定大小”大的最小的2的指数倍的数。
  19. // 所以,这里指定为16是从性能考虑。避免重复计算。
  20. map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
  21. // 将集合(c)中的全部元素添加到HashSet中
  22. addAll(c);
  23. }
  24. // 指定HashSet初始容量和加载因子的构造函数
  25. public HashSet(int initialCapacity, float loadFactor) {
  26. map = new HashMap<E,Object>(initialCapacity, loadFactor);
  27. }
  28. // 指定HashSet初始容量的构造函数
  29. public HashSet(int initialCapacity) {
  30. map = new HashMap<E,Object>(initialCapacity);
  31. }
  32. HashSet(int initialCapacity, float loadFactor, boolean dummy) {
  33. map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
  34. }

4.方法

  • 返回对HashSet中元素进行迭代的迭代器,返回元素的顺序并不是特定的
  • 底层调用HashMap的keySet返回所有的key,这点反应了HashSet中的所有元素都是保存在HashMap的key中,value则是使用的PRESENT对象,该对象为static final
  1. public Iterator<E> iterator() {
  2. // 实际上返回的是HashMap的“key集合的迭代器”
  3. return map.keySet().iterator();
  4. }
  • 返回此 set 中的元素的数量(set 的容量),底层调用HashMap的size方法,返回HashMap容器的大小
  1. public int size() {
  2. return map.size();
  • 调用HashMap的isEmpty()来判断HaspSet是否为空
  • HashMap为null,对应的HashSet也为空 
  1. public boolean isEmpty() {
  2. return map.isEmpty();
  3. }
  • 调用HashMap的containsKey判断是否包含指定的key
  • HashSet的所有元素就是通过HashMap的key来保存的
  • contains(),判断某元素是否存在于HashSet()中,即(o==null ? e==null : o.equals(e)),存在返回true,否则返回false
  • 底层调用containsKey判断HashMap的key值是否为空,即判断hashset中是否存在指定元素(key)
  1. public boolean contains(Object o) {
  2. return map.containsKey(o);
  3. }
  4. public boolean containsKey(Object key) {
  5. return getNode(hash(key), key) != null;
  6. }
  7. // key和 hash(key)
  8. final Node<K,V> getNode(int hash, Object key) {
  9. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  10. //判断数组不为空 长度不为0 找到数组位置
  11. if ((tab = table) != null && (n = tab.length) > 0 &&
  12. (first = tab[(n - 1) & hash]) != null) {
  13. /*
  14. 判断第一个节点hash等于传进来的hash(key) &&
  15. 第一节点的key=传进来的key ||
  16. first是数组某个位置上的Node节点
  17. */
  18. if (first.hash == hash && // always check first node
  19. ((k = first.key) == key || (key != null && key.equals(k))))
  20. return first;
  21. if ((e = first.next) != null) {
  22. //树节点 就按照树去查找
  23. if (first instanceof TreeNode)
  24. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  25. /*如果不是树 ,遍历链表每个元素 有则返回 从第二个才开始遍历
  26. e是first.next的节点 第二节点
  27. 判断 hash值相同 key相同或者key.equels相同
  28. */
  29. do {
  30. if (e.hash == hash &&
  31. ((k = e.key) == key || (key != null && key.equals(k))))
  32. return e;
  33. } while ((e = e.next) != null);
  34. }
  35. }
  36. return null;
  37. }
  •  hashset只是不允许重复的元素加入,而不是不允许元素连成链表
  • 当两个hashcode相同但key不相等的entry插入时,仍然会连成一个链表,长度超过8时依然会和hashmap一样扩展成红黑树
  • 当两个hashcode相同且key相等的entry插入时,会发生value的替换,只因所有entry的value一样,让人以为相同元素没有插入,事实是将value替换成和原来相同的值,所以和没有插入时一样的
  • 当add方法发生冲突时,如果key相同,则替换value(value值相同),如果key不同,则连成链表
  • add()如果此 set 中尚未包含指定元素,则添加指定元素
  • 如果此Set没有包含满足(e==null ? e2==null : e.equals(e2)) 的e2时,则将e2添加到Set中,否则不添加且返回false
  • 由于底层使用HashMap的put方法将key = e,value=PRESENT构建成key-value键值对,当此e存在于HashMap的key中,则value将会覆盖原有value,但是key保持不变,所以如果将一个已经存在的e元素添加中HashSet中,新添加的元素是不会保存到HashMap中,所以这就满足了HashSet中元素不会重复的特性
  1. public boolean add(E e) {
  2. return map.put(e, PRESENT)==null;
  3. }
  4. public V put(K key, V value) {
  5. return putVal(hash(key), key, value, false, true);
  6. }
  7. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  8. boolean evict) {
  9. Node<K,V>[] tab; Node<K,V> p; int n, i;
  10. // 拿到Node数组 如果数组为空 那么扩容
  11. if ((tab = table) == null || (n = tab.length) == 0)
  12. //扩容看resize() n为新扩容的大小
  13. n = (tab = resize()).length;
  14. // 在新扩容(可能不需要扩容)的实际位置 如果数组位置为空 直接赋值
  15. if ((p = tab[i = (n - 1) & hash]) == null)
  16. tab[i] = newNode(hash, key, value, null);
  17. else {
  18. //如果已经存在值
  19. //对e做赋值的过程
  20. Node<K,V> e; K k;
  21. //e为Node;hash相同;key也相同就赋值
  22. //这里针对的是存在数组位置上的
  23. //Node就和你插入的key相同
  24. if (p.hash == hash &&
  25. ((k = p.key) == key || (key != null && key.equals(k))))
  26. e = p;
  27. //树的处理
  28. else if (p instanceof TreeNode)
  29. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  30. //p是什么
  31. //某个位置上的Node(已经存在) 而且是存在值的(可能next为空)
  32. else {
  33. //开始循环 循环的是链表
  34. for (int binCount = 0; ; ++binCount) {
  35. //如果是链表末尾,也就是没有next链表
  36. if ((e = p.next) == null) {
  37. //在末尾创建新节点并且赋值
  38. p.next = newNode(hash, key, value, null);
  39. /*如果链表长度>=7 那么就创建treeifyBin 在treeifyBin中
  40. if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
  41. resize();
  42. 在判断数组长度<64 时 只会扩容
  43. */
  44. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  45. treeifyBin(tab, hash);
  46. break;
  47. }
  48. //在循环过程中找到了相同的key 和 hash
  49. if (e.hash == hash &&
  50. ((k = e.key) == key || (key != null && key.equals(k))))
  51. break;
  52. //循环过程中 直接覆盖掉p p永远是e的next节点
  53. p = e;
  54. }
  55. }
  56. // 对e赋值完成
  57. //判断是否是覆盖操作,对接到上一个break之前
  58. //如果是覆盖的话
  59. // existing mapping for key
  60. if (e != null) {
  61. V oldValue = e.value;
  62. if (!onlyIfAbsent || oldValue == null)
  63. e.value = value;
  64. //判断accessOrder才会执行
  65. afterNodeAccess(e);
  66. //覆盖操作 返回老的值
  67. return oldValue;
  68. }
  69. }
  70. //设置修改频次
  71. ++modCount;
  72. //如果达到扩容的阈值 那么会扩容 上面的老值就肯定不需要扩容了
  73. if (++size > threshold)
  74. resize();
  75. //判断evict才会执行
  76. afterNodeInsertion(evict);
  77. return null;
  78. }
  •  remove如果指定元素存在于此 set 中,则将其移除。底层使用HashMap的remove方法删除指定的Entry
  1. public boolean remove(Object o) {
  2. return map.remove(o)==PRESENT;
  3. }

  • clear从此 set 中移除所有元素。底层调用HashMap的clear方法清除所有的Entry
  1. public void clear() {
  2. map.clear();
  3. }

  • clone返回此 HashSet 实例的浅表副本:并没有复制这些元素本身
  1. public Object clone() {
  2. try {
  3. HashSet<E> newSet = (HashSet<E>) super.clone();
  4. newSet.map = (HashMap<E, Object>) map.clone();
  5. return newSet;
  6. } catch (CloneNotSupportedException e) {
  7. throw new InternalError();
  8. }
  9. }

二、TreeSet

1.定义

  • TreeSet提供有序的Set集合
  • TreeSet是基于TreeMap实现的 
  • TreeMap是一个有序的二叉树,那么同理TreeSet同样也是一个有序的
  • TreeSet基于AbstractSet,实现NavigableSet、Cloneable、Serializable接口
  • AbstractSet提供 Set 接口的骨干实现,从而最大限度地减少了实现此接口所需的工作
  • NavigableSet是扩展的 SortedSet,具有了为给定搜索目标报告最接近匹配项的导航方法,这就意味着它支持一系列的导航方法。比如查找与指定目标最匹配项
  1. public class TreeSet<E> extends AbstractSet<E>
  2. implements NavigableSet<E>, Cloneable, java.io.Serializable

2.TreeSet中的变量

  1. private transient NavigableMap<E,Object> m;
  2. //PRESENT会被当做Map的value与key构建成键值对
  3. private static final Object PRESENT = new Object();

3.构造方法

  1. //默认构造方法,根据其元素的自然顺序进行排序
  2. public TreeSet() {
  3. this(new TreeMap<E,Object>());
  4. }
  5. //构造一个包含指定 collection 元素的新 TreeSet,它按照其元素的自然顺序进行排序。
  6. public TreeSet(Comparator<? super E> comparator) {
  7. this(new TreeMap<>(comparator));
  8. }
  9. //构造一个新的空 TreeSet,它根据指定比较器进行排序。
  10. public TreeSet(Collection<? extends E> c) {
  11. this();
  12. addAll(c);
  13. }
  14. //构造一个与指定有序 set 具有相同映射关系和相同排序的新 TreeSet。
  15. public TreeSet(SortedSet<E> s) {
  16. this(s.comparator());
  17. addAll(s);
  18. }
  19. TreeSet(NavigableMap<E,Object> m) {
  20. this.m = m;
  21. }

4.方法

  • add:将指定的元素添加到此 set(如果该元素尚未存在于 set 中)
  1. public boolean add(E e) {
  2. return m.put(e, PRESENT)==null;
  3. }
  4. public V put(K key, V value) {
  5. Entry<K,V> t = root;
  6. if (t == null) {
  7. //空树时,判断节点是否为空
  8. compare(key, key); // type (and possibly null) check
  9. root = new Entry<>(key, value, null);
  10. size = 1;
  11. modCount++;
  12. return null;
  13. }
  14. int cmp;
  15. Entry<K,V> parent;
  16. // split comparator and comparable paths
  17. Comparator<? super K> cpr = comparator;
  18. //非空树,根据传入比较器进行节点的插入位置查找
  19. if (cpr != null) {
  20. do {
  21. parent = t;
  22. //节点比根节点小,则找左子树,否则找右子树
  23. cmp = cpr.compare(key, t.key);
  24. if (cmp < 0)
  25. t = t.left;
  26. else if (cmp > 0)
  27. t = t.right;
  28. //如果key的比较返回值相等,直接更新值(一般compareto相等时equals方法也相等)
  29. else
  30. return t.setValue(value);
  31. } while (t != null);
  32. }
  33. else {
  34. //如果没有传入比较器,则按照自然排序
  35. if (key == null)
  36. throw new NullPointerException();
  37. @SuppressWarnings("unchecked")
  38. Comparable<? super K> k = (Comparable<? super K>) key;
  39. do {
  40. parent = t;
  41. cmp = k.compareTo(t.key);
  42. if (cmp < 0)
  43. t = t.left;
  44. else if (cmp > 0)
  45. t = t.right;
  46. else
  47. return t.setValue(value);
  48. } while (t != null);
  49. }
  50. //查找的节点为空,直接插入,默认为红节点
  51. Entry<K,V> e = new Entry<>(key, value, parent);
  52. if (cmp < 0)
  53. parent.left = e;
  54. else
  55. parent.right = e;
  56. //插入后进行红黑树调整
  57. fixAfterInsertion(e);
  58. size++;
  59. modCount++;
  60. return null;
  61. }
  • get:获取元素
  1. public V get(Object key) {
  2. Entry<K,V> p = getEntry(key);
  3. return (p==null ? null : p.value);
  4. }
  • ceiling:返回此 set 中大于等于给定元素的最小元素;如果不存在这样的元素,则返回 null
  1. public E ceiling(E e) {
  2. return m.ceilingKey(e);
  3. }
  • clear:移除此 set 中的所有元素
  1. public void clear() {
  2. m.clear();
  3. }
  • clone:返回 TreeSet 实例的浅表副本。属于浅拷贝
  1. public Object clone() {
  2. TreeSet<E> clone = null;
  3. try {
  4. clone = (TreeSet<E>) super.clone();
  5. } catch (CloneNotSupportedException e) {
  6. throw new InternalError();
  7. }
  8. clone.m = new TreeMap<>(m);
  9. return clone;
  10. }
  • comparator:返回对此 set 中的元素进行排序的比较器;如果此 set 使用其元素的自然顺序,则返回 null
  1. public Comparator<? super E> comparator() {
  2. return m.comparator();
  3. }
  • contains:如果此 set 包含指定的元素,则返回 true
  1. public boolean contains(Object o) {
  2. return m.containsKey(o);
  3. }
  • descendingIterator:返回在此 set 元素上按降序进行迭代的迭代器
  1. public Iterator<E> descendingIterator() {
  2. return m.descendingKeySet().iterator();
  3. }
  • descendingSet:返回此 set 中所包含元素的逆序视图
  1. public NavigableSet<E> descendingSet() {
  2. return new TreeSet<>(m.descendingMap());
  3. }
  • first:返回此 set 中当前第一个(最低)元素
  1. public E first() {
  2. return m.firstKey();
  3. }
  • floor:返回此 set 中小于等于给定元素的最大元素;如果不存在这样的元素,则返回 null
  1. public E floor(E e) {
  2. return m.floorKey(e);
  3. }
  • headSet:返回此 set 的部分视图,其元素严格小于 toElement
  1. public SortedSet<E> headSet(E toElement) {
  2. return headSet(toElement, false);
  3. }
  • higher:返回此 set 中严格大于给定元素的最小元素;如果不存在这样的元素,则返回 null
  1. public E higher(E e) {
  2. return m.higherKey(e);
  3. }
  • isEmpty:如果此 set 不包含任何元素,则返回 true
  1. public boolean isEmpty() {
  2. return m.isEmpty();
  3. }
  • iterator:返回在此 set 中的元素上按升序进行迭代的迭代器
  1. public Iterator<E> iterator() {
  2. return m.navigableKeySet().iterator();
  3. }
  • last:返回此 set 中当前最后一个(最高)元素
  1. public E last() {
  2. return m.lastKey();
  3. }
  • lower:返回此 set 中严格小于给定元素的最大元素;如果不存在这样的元素,则返回 null
  1. public E lower(E e) {
  2. return m.lowerKey(e);
  3. }
  • pollFirst:获取并移除第一个(最低)元素;如果此 set 为空,则返回 null
  1. public E pollFirst() {
  2. Map.Entry<E,?> e = m.pollFirstEntry();
  3. return (e == null) ? null : e.getKey();
  4. }
  • pollLast:获取并移除最后一个(最高)元素;如果此 set 为空,则返回 null
  1. public E pollLast() {
  2. Map.Entry<E,?> e = m.pollLastEntry();
  3. return (e == null) ? null : e.getKey();
  4. }
  • remove:将指定的元素从 set 中移除(如果该元素存在于此 set 中)
  1. public boolean remove(Object o) {
  2. return m.remove(o)==PRESENT;
  3. }
  • size:返回 set 中的元素数(set 的容量)
  1. public int size() {
  2. return m.size();
  3. }
  • subSet:返回此 set 的部分视图
  1. /**
  2. * 返回此 set 的部分视图,其元素范围从 fromElement 到 toElement。
  3. */
  4. public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
  5. E toElement, boolean toInclusive) {
  6. return new TreeSet<>(m.subMap(fromElement, fromInclusive,
  7. toElement, toInclusive));
  8. }
  9. /**
  10. * 返回此 set 的部分视图,其元素从 fromElement(包括)到 toElement(不包括)。
  11. */
  12. public SortedSet<E> subSet(E fromElement, E toElement) {
  13. return subSet(fromElement, true, toElement, false);
  14. }
  • tailSet:返回此 set 的部分视图
  1. /**
  2. * 返回此 set 的部分视图,
  3. *其元素大于(或等于,如果 inclusive 为 true)fromElement。
  4. */
  5. public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
  6. return new TreeSet<>(m.tailMap(fromElement, inclusive));
  7. }
  8. /**
  9. * 返回此 set 的部分视图,其元素大于等于 fromElement。
  10. */
  11. public SortedSet<E> tailSet(E fromElement) {
  12. return tailSet(fromElement, true);
  13. }

 三、LinkedHashSet

1.定义

  • LinkedHashSet是HashSet的一个“扩展版本”,会维护“插入顺序”,而HashSet并不管什么顺序
  • LinkedHashSet内部使用LinkedHashMap对象来存储和处理它的元素

2.构造函数

  • 在LinkedHashSet类中一共有4个构造函数
  • 这些构造函数都只是简单地调用父类构造函数(如HashSet类的构造函数)
  1. //Constructor - 1
  2. public LinkedHashSet(int initialCapacity, float loadFactor)
  3. {
  4. super(initialCapacity, loadFactor, true); //Calling super class constructor
  5. }
  6. //Constructor - 2
  7. public LinkedHashSet(int initialCapacity)
  8. {
  9. super(initialCapacity, .75f, true); //Calling super class constructor
  10. }
  11. //Constructor - 3
  12. public LinkedHashSet()
  13. {
  14. super(16, .75f, true); //Calling super class constructor
  15. }
  16. //Constructor - 4
  17. public LinkedHashSet(Collection<? extends E> c)
  18. {
  19. super(Math.max(2*c.size(), 11), .75f, true); //Calling super class constructor
  20. addAll(c);
  21. }
  • 上述4个构造函数调用的是同一个父类的构造函数
  • 这个父类构造函数是一个包内私有构造函数(见下面的代码,HashSet的构造函数没有使用public公开),它只能被LinkedHashSet使用(构造函数默认访问级别:同一类的成员和同包的其他类可以访问)
  • 这个父类构造函数需要初始容量,负载因子和一个boolean类型的哑值(没有什么用处的参数,作为标记)等参数。这个哑参数只是用来区别这个构造函数与HashSet的其他拥有初始容量和负载因子参数的构造函数
  • 这个父类构造函数内部初始化了一个LinkedHashMap对象,这个对象恰好被LinkedHashSet用来存储它的元素
  • LinkedHashSet并没有自己的方法,所有的方法都继承自它的父类HashSet,因此,对LinkedHashSet的所有操作方式就好像对HashSet操作一样
  • 唯一的不同是内部使用不同的对象去存储元素。在HashSet中,插入的元素是被当做HashMap的键来保存的,而在LinkedHashSet中被看作是LinkedHashMap的键,这些键对应的值都是常量PRESENT(PRESENT是HashSet的静态成员变量)
  1. HashSet(int initialCapacity, float loadFactor, boolean dummy)
  2. {
  3. map = new LinkedHashMap<>(initialCapacity, loadFactor);
  4. }

3.LinkedHashSet如何维护插入顺序(具体参见Java基础汇总(十六)——LinkedHashMap)

参见文章:Java基础汇总(十六)——LinkedHashMap_我爱豆子的博客-CSDN博客

  • LinkedHashSet使用LinkedHashMap对象来存储它的元素,插入到LinkedHashSet中的元素实际上是被当作LinkedHashMap的键保存起来的
  • LinkedHashMap的每一个键值对都是通过内部的静态类Entry<K, V>实例化的。这个 Entry<K, V>类继承了HashMap.Entry类
  • 这个静态类增加了两个成员变量,before和after来维护LinkedHasMap元素的插入顺序。这两个成员变量分别指向前一个和后一个元素,这让LinkedHashMap也有类似双向链表的表现
  • LinkedHashMap内部类的前面两个成员变量——before和after负责维护LinkedHashSet的插入顺序
  • LinkedHashMap定义的成员变量header保存的是这个双向链表的头节点
  1. private static class Entry<K,V> extends HashMap.Entry<K,V>
  2. {
  3. // These fields comprise the doubly linked list used for iteration.
  4. Entry<K,V> before, after;
  5. Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
  6. super(hash, key, value, next);
  7. }
  8. }

附:

访问级别:

Java四种访问权限的范围
private默认protectedpublic
同一类的成员
同一个包的其他类(包括子类)×
不同包的子类××
不同包的非子类××
  •  public 公开,对外部访问不做限制。
  • protected保护,只对子类和同一个包下的类公开。
  • 默认级保护,不加修饰符,只对同一个包下的类公开。
  • private私有保护,只有自己才能访问,不对外公开。

其中:以上访问级别只适用于类和类的成员,不适用于局部变量。

成员变量、成员方法、构造方法都可以使用上面的四种访问级别

参考文章:

Java-Tutorial/Java集合详解7:HashSet,TreeSet与LinkedHashSet.md at master · h2pl/Java-Tutorial · GitHub

深入源码分析HashSet_不能说的秘密go的博客-CSDN博客

HashMap详解_多啦哀梦的博客-CSDN博客

一文搞懂Java的 构造方法 和 访问权限_Designer 小郑的博客-CSDN博客_构造方法的访问权限

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/blog/article/detail/67270
推荐阅读
相关标签
  

闽ICP备14008679号