Set接口深入剖析之HashSet、LinkedHashSet和TreeSet

 更新时间:2023年09月18日 09:33:59   作者:努力的小鳴人  
这篇文章主要介绍了Set接口深入剖析之HashSet、LinkedHashSet和TreeSet,LinkedHashSet是HashSet的子类,实现了Set接口,LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表,需要的朋友可以参考下

一、基础知识

  • Set接口:存储无序、不可重复的数据
  • HashSet:主要实现类,线程不安全,可以存储null值
  • LinkedHashSet:是HashSet的子类,遍历内部的数据时,可以按照添加的顺序遍历
  • TreeSet:可以按照添加的对象指定属性,进行排序

二、HashSet

基于JDK 1.8 分析

定义

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

HashSet继承AbstractSet类,实现Set、Cloneable、Serializable接口

基本属性

private transient HashMap<E,Object> map;
//定义一个Object对象作为HashMap的value
private static final Object PRESENT = new Object();

HashSet中数据是存放在HashMap中,HashSet基于HashMap,对数据的访问基本用HashMap的方法,另外存入Set中的数据本身是无序的,维护访问顺序没有意义

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }

HashSet是可克隆对象和进行序列化 ,其内部的数据存储区通过一个transient修饰的HashMap维护,调用了HashMap的构造函数完成。主要的参数是基础容量为16个单位,加载因子是0.75

方法

public int size() {
        return map.size();
    }
public boolean isEmpty() {
        return map.isEmpty();
    }
public boolean contains(Object o) {
        return map.containsKey(o);
    }    
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);
	}
 /**
     * Returns an iterator over the elements in this set.  The elements
     * are returned in no particular order.
     *
     * @return an Iterator over the elements in this set
     * @see ConcurrentModificationException
     */
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }    

contains()判断某个元素是否存在于HashSet()中,存在返回true,否则返回false,底层调用containsKey判断HashMap的key值是否为空 底层调用HashMap的keySet返回所有的key,反映了HashSet中的所有元素都是保存在HashMap的key中,value则是使用的PRESENT对象,该对象为static final

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();
        }
    }

clone返回此 HashSet 实例的浅表副本,并没有复制这些元素

put方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //确认初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //如果桶为空,直接插入新元素,也就是entry
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //如果冲突,分为三种情况
        //key相等时让旧entry等于新entry即可
        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);
        else {
            //如果key不相等,则连成链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

hashset不允许重复的元素加入,因为只要key的equals方法判断为true时会发生value的替换,即当两个hashcode相同但key不相等的entry插入时,仍然会连成一个链表,长度超过8时依然会和hashmap一样扩展成红黑树,当add方法发生冲突时,如果key相同,则替换value,如果key不同,则连成链表

小结

  1. HashSet有以下特点
    • 不能保证元素的排列顺序,顺序有可能发生变化
    • 不是同步的
    • 集合元素可以是null,但只能放入一个null
  2. 当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置
  3. HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值相等

注:如果要把一个对象放入HashSet中,重写该对象对应类equals方法,也应该重写其hashCode()方法:其规则是如果两个对象通过equals方法比较返回true时,其hashCode也应该相同。另对象中用作equals比较标准的属性,都应用来计算 hashCode的值

三、LinkedHashSet

LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起 来像是以插入顺序保存的,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素

构造函数

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

四个构造函数调用的是同一个父类构造函数,该构造函数是一个包内私有构造函数,它只能被LinkedHashSet使用,该构造函数需要初始容量,负载因子和 boolean类型的哑值等参数,这个哑参数只是用来区别这个构造函数与HashSet的其他拥有初始容量和负载因子参数的构造函数

注:哑值:没有什么用处的参数,作为标记

小结

LinkedHashSet直接继承自HashSet,能够维护基础的有序性,LinkedHashSet并没有自己的方法,所有方法都继承自它的父类HashSet,因此对LinkedHashSet的所有操作方式就像对HashSet操作一样,唯一的不同是内部使用不同的对象去存储元素。在HashSet中,插入的元素是被当做HashMap的键来保存的,而在LinkedHashSet中被看作是LinkedHashMap的键

换句话说 LinkedHashSet继承自HashSet,源码更少、更简单,唯一的区别是LinkedHashSet内部使用的是LinkHashMap

特点:

  • 有序,按照插入顺序
  • 不是同步的
  • 集合元素可以是null,但只能放入一个null
  • 没有set和get方法 只能通过迭代器取值

LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet

四、TreeSet

定义

TreeSet与TreeMap实现类似

TreeMap是一个有序的二叉树,TreeSet同样也是一个有序的二叉树,它的作用是提供有序的Set集合。TreeSet继承自AbstractSet,实现了Set接口、Cloneable和Serializable、NavigableSet接口。其内部主要是通过一个NavigableMap的map维护数据存储

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable

构造方法

//默认构造方法
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;
}

主要方法

1.add:将指定的元素添加到此 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);
}

与put的流程类似,只把插入换成了查找

ceiling:返回此 set 中大于等于给定元素的最小元素;如果不存在这样的元素,则返回 null

太多了直接上脑图:

在这里插入图片描述

自然排序与定制排序

TreeSet支持两种排序方式,自然排序 和 定制排序

小结

  1. TreeSet内部是通过TreeMap构造出来的
    • 有序,按照比较器排序,并非按照插入顺序
    • 不是同步的
    • 集合元素可以是null,但只能放入一个null
    • 没有set和get方法 只能通过迭代器取值
  2. TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态

总结:HashSet、LinkedHashSet和TreeSet,三者其内部都是基于Map来实现的。TreeSet和LinkedHashSet分别使用了TreeMap和LinkedHashMap来控制访问数据的有序性。都属于Set的范畴,都是没有重复元素的集合。基于HashMap和TreeMap,也都是非线程安全的

到此这篇关于Set接口深入剖析之HashSet、LinkedHashSet和TreeSet的文章就介绍到这了,更多相关Set接口深入剖析内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBoot中对SpringMVC的自动配置详解

    SpringBoot中对SpringMVC的自动配置详解

    这篇文章主要介绍了SpringBoot中的SpringMVC自动配置详解,Spring MVC自动配置是Spring Boot提供的一种特性,它可以自动配置Spring MVC的相关组件,简化了开发人员的配置工作,需要的朋友可以参考下
    2023-10-10
  • Java深入分析Iterator迭代器与foreach循环的使用

    Java深入分析Iterator迭代器与foreach循环的使用

    这篇文章主要介绍了Java-Iterator迭代器与foreach循环,主要包括Iterator迭代器接口的操作方法和foreach 循环语法解析,需要的朋友可以参考下
    2022-05-05
  • Java基于BIO实现文件上传功能

    Java基于BIO实现文件上传功能

    这篇文章主要为大家详细介绍了Java基于BIO实现文件上传功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11
  • Java中的@Repeatable注解使用详解

    Java中的@Repeatable注解使用详解

    这篇文章主要介绍了Java中的@Repeatable注解使用详解,@Repeatable注解是java8为了解决同一个注解不能重复在同一类/方法/属性上使用的问题,本文提供了解决思路和部分实现代码,需要的朋友可以参考下
    2024-02-02
  • 浅谈Java中Lock和Synchronized的区别

    浅谈Java中Lock和Synchronized的区别

    这篇文章主要介绍了Java中Lock和Synchronized的区别,Lock和Synchronized都是java中去用来解决线程安全问题的一个工具,但是具体有什么区别呢?下面我们一起进入文章了解具体详细内容吧,需要的朋友可以参考一下
    2022-04-04
  • JAVA集合框架Map特性及实例解析

    JAVA集合框架Map特性及实例解析

    这篇文章主要介绍了JAVA集合框架Map特性及实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • Java加密 消息摘要算法MAC实现详解

    Java加密 消息摘要算法MAC实现详解

    这篇文章主要介绍了Java 消息摘要算法MAC实现详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-07-07
  • Java中的适配器模式详解

    Java中的适配器模式详解

    这篇文章主要介绍了Java中的适配器模式详解,适配器模式(Adapter Pattern)将某个类的接口转换成客户端期望的另一个接口表示,主的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作,需要的朋友可以参考下
    2023-09-09
  • idea中如何集成http请求

    idea中如何集成http请求

    这篇文章主要介绍了idea中如何集成http请求问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-10-10
  • Windows下使用Graalvm将Springboot应用编译成exe大大提高启动和运行效率(推荐)

    Windows下使用Graalvm将Springboot应用编译成exe大大提高启动和运行效率(推荐)

    这篇文章主要介绍了Windows下使用Graalvm将Springboot应用编译成exe大大提高启动和运行效率,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-02-02

最新评论