Java基础汇总(十八)——HashSet,TreeSet和LinkedHashSet

一、HashSet

1.定义

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

2.基本属性

  • HashSet是基于HashMap实现,底层使用HashMap保存所有元素
  • 因为HashSet中只需要用到key,而HashMap是key-value键值对,所以向map中添加键值对时,键值对的值固定是PRESENT
private transient HashMap<E,Object> map;

//定义一个Object对象作为HashMap的value
// 定义一个虚拟的Object PRESENT是向map中插入key-value对应的value
// 因为HashSet中只需要用到key,而HashMap是key-value键值对;
// 所以,向map中添加键值对时,键值对的值固定是PRESENT
private static final Object PRESENT = new Object();

3.构造函数

// 默认构造函数 底层创建一个HashMap
    public HashSet() {
        // 调用HashMap的默认构造函数,创建map
        map = new HashMap<E,Object>();
    }



    // 带集合的构造函数
    public HashSet(Collection<? extends E> c) {
        // 创建map。
        // 为什么要调用Math.max((int) (c.size()/.75f) + 1, 16),
        // 从 (c.size()/.75f) + 1 和 16 中选择一个比较大的树呢? 

      
        // 首先,说明(c.size()/.75f) + 1
        // 因为从HashMap的效率(时间成本和空间成本)考虑,HashMap的加载因子是0.75。
        // 当HashMap的“阈值”(阈值=HashMap总的大小*加载因子) < “HashMap实际大小”时,
        // 就需要将HashMap的容量翻倍。
        // 所以,(c.size()/.75f) + 1 计算出来的正好是总的空间大小。
        // 接下来,说明为什么是 16 。
        // HashMap的总的大小,必须是2的指数倍。若创建HashMap时,指定的大小不是2的指数倍;
        // HashMap的构造函数中也会重新计算,找出比“指定大小”大的最小的2的指数倍的数。
        // 所以,这里指定为16是从性能考虑。避免重复计算。

        map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
        // 将集合(c)中的全部元素添加到HashSet中
        addAll(c);
    }



    // 指定HashSet初始容量和加载因子的构造函数
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<E,Object>(initialCapacity, loadFactor);
    }

    // 指定HashSet初始容量的构造函数
    public HashSet(int initialCapacity) {
        map = new HashMap<E,Object>(initialCapacity);
    }

    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
    }

4.方法

  • 返回对HashSet中元素进行迭代的迭代器,返回元素的顺序并不是特定的
  • 底层调用HashMap的keySet返回所有的key,这点反应了HashSet中的所有元素都是保存在HashMap的key中,value则是使用的PRESENT对象,该对象为static final
  public Iterator<E> iterator() {
        // 实际上返回的是HashMap的“key集合的迭代器”
        return map.keySet().iterator();
    }
  • 返回此 set 中的元素的数量(set 的容量),底层调用HashMap的size方法,返回HashMap容器的大小
public int size() {
        return map.size();
  • 调用HashMap的isEmpty()来判断HaspSet是否为空
  • HashMap为null,对应的HashSet也为空 
public boolean isEmpty() {
        return map.isEmpty();
    }
  • 调用HashMap的containsKey判断是否包含指定的key
  • HashSet的所有元素就是通过HashMap的key来保存的
  • contains(),判断某元素是否存在于HashSet()中,即(o==null ? e==null : o.equals(e)),存在返回true,否则返回false
  • 底层调用containsKey判断HashMap的key值是否为空,即判断hashset中是否存在指定元素(key)
 public boolean contains(Object o) {
        return map.containsKey(o);
    }


public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}
 // key和 hash(key)
 final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //判断数组不为空 长度不为0 找到数组位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            /*
            判断第一个节点hash等于传进来的hash(key) &&
            第一节点的key=传进来的key ||
            first是数组某个位置上的Node节点
            
            */ 
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                //树节点 就按照树去查找
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                /*如果不是树 ,遍历链表每个元素 有则返回 从第二个才开始遍历
                  e是first.next的节点 第二节点
                  判断 hash值相同 key相同或者key.equels相同
                */
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
  •  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中元素不会重复的特性
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;

    	// 拿到Node数组 如果数组为空 那么扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            //扩容看resize() n为新扩容的大小
            n = (tab = resize()).length;

    	// 在新扩容(可能不需要扩容)的实际位置 如果数组位置为空 直接赋值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        
        else {
            //如果已经存在值
            //对e做赋值的过程
            Node<K,V> e; K k;

            //e为Node;hash相同;key也相同就赋值 
            //这里针对的是存在数组位置上的
            //Node就和你插入的key相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

            //树的处理
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //p是什么 
            //某个位置上的Node(已经存在) 而且是存在值的(可能next为空)

            else {

                //开始循环 循环的是链表
                for (int binCount = 0; ; ++binCount) {

                    //如果是链表末尾,也就是没有next链表
                    if ((e = p.next) == null) {
                        //在末尾创建新节点并且赋值
                        p.next = newNode(hash, key, value, null);

              /*如果链表长度>=7 那么就创建treeifyBin 在treeifyBin中
                if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                    resize();		
                在判断数组长度<64 时 只会扩容
                        */
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }


                    //在循环过程中找到了相同的key 和 hash
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;

                    //循环过程中 直接覆盖掉p  p永远是e的next节点
                    p = e;
                }
            }


            // 对e赋值完成  
            //判断是否是覆盖操作,对接到上一个break之前 
            //如果是覆盖的话
            // existing mapping for key
            if (e != null) { 
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;

                //判断accessOrder才会执行
                afterNodeAccess(e);

                //覆盖操作 返回老的值
                return oldValue;
            }
        }


    	//设置修改频次
        ++modCount;

    	//如果达到扩容的阈值 那么会扩容 上面的老值就肯定不需要扩容了
        if (++size > threshold)
            resize();

    	//判断evict才会执行
        afterNodeInsertion(evict);
        return null;
    }
  •  remove如果指定元素存在于此 set 中,则将其移除。底层使用HashMap的remove方法删除指定的Entry
public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}

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

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

二、TreeSet

1.定义

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

2.TreeSet中的变量

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

3.构造方法

//默认构造方法,根据其元素的自然顺序进行排序

public TreeSet() {
    this(new TreeMap<E,Object>());
}

//构造一个包含指定 collection 元素的新 TreeSet,它按照其元素的自然顺序进行排序。
public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
}

//构造一个新的空 TreeSet,它根据指定比较器进行排序。
public TreeSet(Collection<? extends E> c) {
    this();
    addAll(c);
}

//构造一个与指定有序 set 具有相同映射关系和相同排序的新 TreeSet。
public TreeSet(SortedSet<E> s) {
    this(s.comparator());
    addAll(s);
}

TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}

4.方法

  • add:将指定的元素添加到此 set(如果该元素尚未存在于 set 中)
public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }

 
public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
    //空树时,判断节点是否为空
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    //非空树,根据传入比较器进行节点的插入位置查找
    if (cpr != null) {
        do {
            parent = t;
            //节点比根节点小,则找左子树,否则找右子树
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
//如果key的比较返回值相等,直接更新值(一般compareto相等时equals方法也相等)
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
    //如果没有传入比较器,则按照自然排序
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    //查找的节点为空,直接插入,默认为红节点
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
        //插入后进行红黑树调整
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}    
  • get:获取元素
public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}
  • ceiling:返回此 set 中大于等于给定元素的最小元素;如果不存在这样的元素,则返回 null
public E ceiling(E e) {
        return m.ceilingKey(e);
    }
  • clear:移除此 set 中的所有元素
public void clear() {
        m.clear();
    }
  • clone:返回 TreeSet 实例的浅表副本。属于浅拷贝
public Object clone() {
        TreeSet<E> clone = null;
        try {
            clone = (TreeSet<E>) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }

        clone.m = new TreeMap<>(m);
        return clone;
    }
  • comparator:返回对此 set 中的元素进行排序的比较器;如果此 set 使用其元素的自然顺序,则返回 null
public Comparator<? super E> comparator() {
        return m.comparator();
    }
  • contains:如果此 set 包含指定的元素,则返回 true
public boolean contains(Object o) {
        return m.containsKey(o);
    }
  • descendingIterator:返回在此 set 元素上按降序进行迭代的迭代器
public Iterator<E> descendingIterator() {
        return m.descendingKeySet().iterator();
    }
  • descendingSet:返回此 set 中所包含元素的逆序视图
public NavigableSet<E> descendingSet() {
        return new TreeSet<>(m.descendingMap());
    }
  • first:返回此 set 中当前第一个(最低)元素
public E first() {
        return m.firstKey();
    }
  • floor:返回此 set 中小于等于给定元素的最大元素;如果不存在这样的元素,则返回 null
public E floor(E e) {
        return m.floorKey(e);
    }
  • headSet:返回此 set 的部分视图,其元素严格小于 toElement
public SortedSet<E> headSet(E toElement) {
        return headSet(toElement, false);
    }
  • higher:返回此 set 中严格大于给定元素的最小元素;如果不存在这样的元素,则返回 null
public E higher(E e) {
        return m.higherKey(e);
    }
  • isEmpty:如果此 set 不包含任何元素,则返回 true
public boolean isEmpty() {
        return m.isEmpty();
    }
  • iterator:返回在此 set 中的元素上按升序进行迭代的迭代器
public Iterator<E> iterator() {
        return m.navigableKeySet().iterator();
    }
  • last:返回此 set 中当前最后一个(最高)元素
public E last() {
        return m.lastKey();
    }
  • lower:返回此 set 中严格小于给定元素的最大元素;如果不存在这样的元素,则返回 null
public E lower(E e) {
        return m.lowerKey(e);
    }
  • pollFirst:获取并移除第一个(最低)元素;如果此 set 为空,则返回 null
public E pollFirst() {
        Map.Entry<E,?> e = m.pollFirstEntry();
        return (e == null) ? null : e.getKey();
    }
  • pollLast:获取并移除最后一个(最高)元素;如果此 set 为空,则返回 null
public E pollLast() {
        Map.Entry<E,?> e = m.pollLastEntry();
        return (e == null) ? null : e.getKey();
    }
  • remove:将指定的元素从 set 中移除(如果该元素存在于此 set 中)
public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }
  • size:返回 set 中的元素数(set 的容量)
public int size() {
        return m.size();
    }
  • subSet:返回此 set 的部分视图
/**
     * 返回此 set 的部分视图,其元素范围从 fromElement 到 toElement。
     */
     public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
             E toElement,   boolean toInclusive) {
             return new TreeSet<>(m.subMap(fromElement, fromInclusive,
                  toElement,   toInclusive));
     }
     
     /**
      * 返回此 set 的部分视图,其元素从 fromElement(包括)到 toElement(不包括)。
      */
     public SortedSet<E> subSet(E fromElement, E toElement) {
         return subSet(fromElement, true, toElement, false);
     }
  • tailSet:返回此 set 的部分视图
/**
     * 返回此 set 的部分视图,
     *其元素大于(或等于,如果 inclusive 为 true)fromElement。
     */
    public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
        return new TreeSet<>(m.tailMap(fromElement, inclusive));
    }
    
    /**
     * 返回此 set 的部分视图,其元素大于等于 fromElement。
     */
    public SortedSet<E> tailSet(E fromElement) {
        return tailSet(fromElement, true);
    }

 三、LinkedHashSet

1.定义

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

2.构造函数

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

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保存的是这个双向链表的头节点
private static class Entry<K,V> extends HashMap.Entry<K,V>
{
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;
 
        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }
}

附:

访问级别:

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博客_构造方法的访问权限

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值