java源码阅读 - LinkedHashSet

Posted 理想万岁万万岁

tags:

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

往期文章

目录

文章目录

一、介绍

前面文章中我们从源码详细介绍了继承于HashMap的LinkedHashMap,并通过图片示例讲解了LinkedHashMap是如何在HashMap的哈希表上将各个节点通过双向链表串起来的。

也讲解了基于HashMap实现的HashSet,那么是否存在类似于LinkedHashMap原理的一种Set集合?答案是肯定的,而且是我们本篇文章要讲的LinkedHashSet

顾名思义,LinkedHashSet是基于LinkedHashMap实现的一个Set集合。

另外,本片文章虽然不长,但是对前置知识点有着很强的依赖,需要掌握的前置知识有:HashMap(必选)红黑树(可选)LinkedHashMap(必选)HashSet(必选)

二、类的声明

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

从类的声明中可以看到

  • 继承HashSet,表示LinkedHashSet是对HashSet的扩展。
  • 实现set接口,满足Set集合的定义
  • 实现了Cloneable接口,提供了对象克隆方法,但请注意,是浅克隆
  • 实现了Serializable接口,支持序列化

三、构造方法

前面我们在讲HashSet的构造方法时,其中有一个构造方法我们做了特殊对待,如下所示

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

该构造方法创建的map对象的类型是LinkedHashMap,不同于其他构造方法创建的HashMap对象。

而且我们有个关键点不要忽略,在LinkedHashMap中,双向链表的遍历顺序通过构造方法指定,如果没有指定,则使用默认顺序为插入顺序,即accessOrder=false。因此,上面的构造方法所创建的LinkedHashMap对象的双向链表遍历顺序为插入顺序

且该构造方法就是为了给其子类LinkedHashSet使用的。我们往下看

  • 无参构造

    创建LinkedHashMap实例为内部属性,并指定底层哈希表的初始容量为16,加载因子为0.75

    public LinkedHashSet() 
        super(16, .75f, true);
    
    
  • 指定初始容量

    创建LinkedHashMap实例为内部属性,并指定底层哈希表的初始容量为initialCapacity,加载因子为0.75

    public LinkedHashSet(int initialCapacity) 
        super(initialCapacity, .75f, true);
    
    
  • 指定初始容量和加载因子

    创建LinkedHashMap实例为内部属性,并指定底层哈希表的初始容量为initialCapacity,加载因子为loadFactor

    public LinkedHashSet(int initialCapacity, float loadFactor) 
        super(initialCapacity, loadFactor, true);
    
    
  • 通过集合构造

    虽然说LinkedHashSet的底层是LinkedHashMap,但终究还是哈希表+双向链表,需要对哈希表的容量进行计算以避免频繁的扩容。

    创建LinkedHashMap实例作为内部对象后,通过addAll()方法将集合中的元素逐一保存,addAll()方法作为一个批量保存模版由其父类AbstractCollection提供,其中的add()方法由父类HashSet实现,这是设计模式—模版方法的体现。

    public LinkedHashSet(Collection<? extends E> c) 
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    
    
    public boolean addAll(Collection<? extends E> c) 
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    
    

四、最后

看了LinkedHashSet的源码后,发现它只提供了以上几个构造函数,却没有提供各个方法。这是因为它继承于HashSet,因此HashSet中提供的方法都是可以被LinkedHashSet对象调用的,如add()、remove()、contains()等方法。所以不再过多介绍,

五、结论

  • LinkedHashSet内部维护一个LinkedHashMap对象,其底层数据结构为哈希表+链表+红黑树+双向链表
  • LinkedHashSet对内部双向链表的遍历顺序为插入顺序


纸上得来终觉浅,绝知此事要躬行。

————————————————我是万万岁,我们下期再见————————————————

源码阅读(27):Java中主要的Set结构——LinkedHashSetTreeSet等结构

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

1、概述

和HashSet类似,Java中另外两个主要的Set集合结构也做了这样依赖结构,既是LinkedHashSet集合继承了HashSet,并实际应用HashSet集合中构造函数,完成实例化;TreeSet集合内部结构依赖于TreeMap集合,也就是说TreeSet内部数据结构同样是红黑树。

2、LinkedHashSet集合

LinkedHashSet集合的主要继承体系的示意图已经在上文中给出,这里就不再进行赘述了。LinkedHashSet集合的代码非常简单基本上可以说是java集合框架中(Java Collections Framework )代码最简单的几个类之一——整个类加注释一共不到200行。它之所以那么简单是因为它的功能全部依靠LinkedHashMap容器进行工作,且没有提供除此之外额外的特殊功能。

2.1、LinkedHashSet集合的构造函数

首先我们介绍LinkedHashSet集合的构造函数,相关代码片段如下所示

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable 
  // ......
  
  // 可以看到LinkedHashSet集合中所有的构造函数都调用了父类HashSet中
  // HashSet(int initialCapacity, float loadFactor, boolean dummy) 
  // 这个构造函数,这个构造函数也涉及到我们上文中的一个未解决的疑问。
  public LinkedHashSet(int initialCapacity, float loadFactor) 
    super(initialCapacity, loadFactor, true);
  
  
  // 这些构造函数的作用都不在赘述,initialCapacity代表初始化容量
  // loadFactor代表负载因子
  public LinkedHashSet(int initialCapacity) 
    super(initialCapacity, .75f, true);
  
  
  // 初始化容量16,负载因子0.75
  public LinkedHashSet() 
    super(16, .75f, true);
  

  public LinkedHashSet(Collection<? extends E> c) 
    // 将LinkedHashSet集合的初始化容量设定为当前参照集合“c”的大小的两倍
    // 如果前者小于11,那么设定初始化容量为11
    super(Math.max(2*c.size(), 11), .75f, true);
    addAll(c);
  
  
  // ......

从以上构造函数的代码片段我们可以知道,要了解LinkedHashSet集合的各种构造函数,只需要了解其父类HashSet中的HashSet(int initialCapacity, float loadFactor, boolean dummy)构造函数,为了便于介绍,我们这里再给出相关代码片段:

public class HashSet<E>
  extends AbstractSet<E>
  implements Set<E>, Cloneable, java.io.Serializable 
  
  // ......
  HashSet(int initialCapacity, float loadFactor, boolean dummy) 
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
  
  // ......

  • 该构造函数一共有三个参数,最后一个参数“dummy”中文含义是“虚假的”,而且从代码中我们也可以看出这个参数实际上并没有被使用。这是因为在HashSet集合的构造函数中,已经存在了HashSet(int initialCapacity, float loadFactor)这个构造函数,且后者实例化了一个HashMap容器。为了区别开来,实例化LinkedHashMap容器的构造函数才定义了三个参数。

  • 为什么LinkedHashSet集合并没有像HashSet集合那样直接依赖LinkedHashMap容器,而是选择继承HashSet集合后,再在HashSet集合内部通过一个“包保护”特性的构造函数进行LinkedHashMap容器的初始化呢?这主要是为了充分利用HashMap容器和LinkedHashMap容器的继承关系,以及HashSet集合中已经封装的方法。

// LinkedHashSet是HashSet的子类
public class HashSet<E>
  extends AbstractSet<E>
  implements Set<E>, Cloneable, java.io.Serializable 
  
  // 当使用以下构造时,LinkedHashSet内部依赖的Map实现就会变成LinkedHashMap
  // 而由于map属性本身的访问安全性使用private关键字进行修饰
  // 所以LinkedHashSet不能直接为map赋值,只能在HashSet中描述一个构造函数,然后再在LinkedHashSet中进行引用
  private transient HashMap<E,Object> map;  
 
  HashSet(int initialCapacity, float loadFactor, boolean dummy) 
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
  

2.2、LinkedHashSet集合的主要方法

由于LinkedHashSet继承了HashSet集合,且由此沿用了LinkedHashMap容器的所有特性(如果读不懂这句话,说明上一小节没有看明白,还需要重温一下)。所以LinkedHashSet集合无需额外实现任何其他的方法——因为LinkedHashMap容器已经完成了所有需要完成的功能。

当然有个例外,就是LinkedHashSet集合中被覆盖的spliterator()方法,该方法在JDK 1.8+版本中被定义,用来返回一个可分割容器的(并行)迭代器。相关知识我们会在后文中进行说明。

3、TreeSet集合

TreeSet集合是一个基于红黑树的有序集合,它内部功能依赖于TreeMap容器进行工作(后者的工作原理可参见文章《源码阅读(21):Java中其它主要的Map结构——TreeMap容器(1)》) 。TreeSet的主要继承体系如下图所示:

TreeSet集合充分利用了TreeMap容器中Key键特性,实现了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
  * */
// ....

3.1、TreeSet集合的构造函数

这里先我们介绍TreeSet集合的构造函数,相关代码片段如下所示:

public class TreeSet<E> extends AbstractSet<E>
  implements NavigableSet<E>, Cloneable, java.io.Serializable 
  
  // ......
  /**
   * 这是TreeSet集合中依赖的Map结构,看似是一个抽象的NavigableMap
   * 实则根据后续的代码分析就可以知道,它是一个TreeMap容器
   * The backing map.
   */
  private transient NavigableMap<E,Object> m;
  
  // ......
  /**
   * 这个构造函数的访问安全设定为“包保户”级别,外部调用者无法直接使用
   * 该构造函数接受一个NavigableMap对象,NavigableMap是大多数Map容器的父级接口,它将成为TreeSet集合内部所依赖的Map容器结构。
   * Constructs a set backed by the specified navigable map.
   */
  TreeSet(NavigableMap<E,Object> m) 
    this.m = m;
  

  // 默认的TreeSet集合构造函数,其内部将TreeSet集合依赖的Map容器初始化为TreeMap。
  public TreeSet() 
    this(new TreeMap<E,Object>());
  

  // 该构造函数将允许传入一个比较器,并为内部的TreeMap容器导入这个比较器
  public TreeSet(Comparator<? super E> comparator) 
    this(new TreeMap<>(comparator));
  

  // 该构造函数允许传入一个其他种类的结合,并在内部完成TreeMap容器的初始化后
  // 将传入集合(c)的元素存入TreeSet容器中
  public TreeSet(Collection<? extends E> c) 
    this();
    addAll(c);
  

  // 该构造函数类似于上一个构造函数,不同的是它允许传入的是一个支持有序排列的Set集合
  public TreeSet(SortedSet<E> s) 
    this(s.comparator());
    addAll(s);
  
  // ......

3.2、TreeSet集合中的主要方法

3.2.1、那些直接封装的方法

由于TreeSet集合的内部依赖了TreeMap容器工作,所以在前者提供的大部分方法中直接调用了TreeMap的对应方法(以NavigableMap抽象类的对象实例进行表达),我们首先来看看:

public class TreeSet<E> extends AbstractSet<E>
  implements NavigableSet<E>, Cloneable, java.io.Serializable 
  
  /**
   * The backing map.
   */
  private transient NavigableMap<E,Object> m;
  
  // Dummy value to associate with an Object in the backing Map
  private static final Object PRESENT = new Object();  

  // ......
  // 向TreeSet集合中添加一个元素
  // 实际上就是调用了TreeMap的put方法,其中的value参数采用了一个常量PRESENT
  public boolean add(E e) 
    return m.put(e, PRESENT)==null;
  
  
  // 清理TreeSet集合中的所有元素
  // 实际上就是调用了TreeMap的clear方法
  public void clear() 
    m.clear();
  
  
  // 清理TreeSet集合中的指定元素
  // 实际上就是调用了TreeMap的remove方法
  public boolean remove(Object o) 
    return m.remove(o)==PRESENT;
  
  
  // 该方法在TreeSet集合中,基于指定的fromElement元素和指定的toElement元素寻找一个子集
  // 如果找到了这个子集,则构造一个新的TreeSet集合进行返回。
  // 其中fromInclusive参数表示返回的子集是否包括起始元素;toInclusive参数表示返回的子集是否包括结束元素
  public NavigableSet<E> subSet(E fromElement, boolean fromInclusive, E toElement,   boolean toInclusive) 
    return new TreeSet<>(m.subMap(fromElement, fromInclusive, toElement,   toInclusive));
  
  
  // 该方法在TreeSet集合中,从集合的第一个元素开始,基于指定的toElement元素截止,寻找一个子集
  // inclusive参数代表子集是否包括了指定的元素
  public NavigableSet<E> headSet(E toElement, boolean inclusive) 
    return new TreeSet<>(m.headMap(toElement, inclusive));
  
  
  // 以及诸如这样的返回TreeSet集合当前第一个元素的方法
  public E first() 
    return m.firstKey();
  
  // ......

3.2.2、那些被修改的方法

在TreeSet集合中,还有一部分原由TreeMap提供的,但是由TreeSet集合进行了“调整”的方法,例如:

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

  // ......
  public  boolean addAll(Collection<? extends E> c) 
    // Use linear-time version if applicable
    // 如果当前传入的集合有元素,并且当前传入的集合是一个实现了SortedSet接口的有序集合
    if (m.size()==0 && c.size() > 0 && c instanceof SortedSet && m instanceof TreeMap) 
      SortedSet<? extends E> set = (SortedSet<? extends E>) c;
      TreeMap<E,Object> map = (TreeMap<E, Object>) m;
      Comparator<?> cc = set.comparator();
      Comparator<? super E> mc = map.comparator();
      // 且当前TreeSet集合的对比器和传入的集合c使用的对比器“相同”
      // 请注意这个“相同”的定义,除了可以是两者的内存地址相等,还可以是通过equals方法得到了true的返回值
      // 在这些条件都满足的情况下,就是用TreeMap的addAllForTreeSet方法进行元素添加
      if (cc==mc || (cc != null && cc.equals(mc))) 
        map.addAllForTreeSet(set, PRESENT);
        return true;
      
    
    
    // 否则进行集合“c”中的元素一个一个的添加操作
    return super.addAll(c);
  
  // ......


3.2.3、TreeSet集合的序列化方法

和java集合框架中大多数集合/容器的序列化思路一样,为了尽可能保证不进行多余的序列化/反序列化处理,TreeSet集合也重新定义了序列化/反序列化处理过程,代码片段如下所示:

public class TreeSet<E> extends AbstractSet<E>
  implements NavigableSet<E>, Cloneable, java.io.Serializable 
  
  // ......
  // 该方法定义了TreeSet的序列化过程
  private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException 
    // Write out any hidden stuff
    // 这个方法之前介绍过,既是将对象中各种普通属性(没有标识transient关键字)进行序列化
    s.defaultWriteObject();
    // 将TreeSet集合中使用的TreeMap对象的比较器进行序列化
    // Write out Comparator
    s.writeObject(m.comparator());
    // Write out size
    // 将当前TreeSet集合的大小(size)属性进行序列化
    s.writeInt(m.size());
    // Write out all elements in the proper order.
    // 将TreeSet集合中的有序元素依次进行序列化
    for (E e : m.keySet())
      s.writeObject(e);
  
  
  // 该方法定义了TreeSet的反序列化过程
  private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException 
    // Read in any hidden stuff
    // 通过该方法,将没有标识transient关键字的普通属性进行反序列化
    s.defaultReadObject();
    // Read in Comparator
    @SuppressWarnings("unchecked")
    // 读取当前的比较器对象,并赋予给这个新的TreeSet集合,
    Comparator<? super E> c = (Comparator<? super E>) s.readObject();
    // Create backing TreeMap
    // 为这个TreeSet集合初始化其内部依赖使用的TreeMap容器
    TreeMap<E,Object> tm = new TreeMap<>(c);
    m = tm;
    // Read in size
    // 读取反序列化前集合的大小值,并基于这个值
    // 通过TreeMap容器中提供的readTreeSet方法,依次进行每个元素的反序列化操作
    int size = s.readInt();
    tm.readTreeSet(size, s, PRESENT);
  
  // ......

3、总述

总的来说Java提供的多种原生Set集合其实现都非常简单,都是基于对应的Map集合完成其功能。例如TreeSet内部依赖TreeMap进行工作,HashSet集合内部依赖TreeMap进行工作。所以只需要认证理解阅读本专题之前介绍的多种Map容器,就不难理解这些Set集合了。这也是为什么本专题只花费很少的篇幅就介绍了多种Set集合。

截止这篇文章,JDK1.8中主要的List集合、Queue集合、Map键值对结构和Set集合就介绍完了,从下一片文章开始,我们将进入Java线程、并发特点,以及线程安全的集合框架的讲解。

以上是关于java源码阅读 - LinkedHashSet的主要内容,如果未能解决你的问题,请参考以下文章

源码阅读Java集合 - ArrayList深度源码解读

AtomicInteger 源码分析阅读

Java源码阅读

JAVA——底层源码阅读——集合ArrayList的实现底层源码分析

Java多线程——ReentrantReadWriteLock源码阅读

JAVA——底层源码阅读——包装数据类型Integer.valueOf()自动装箱方法底层源码分析