前面我们花了一定的篇幅学习了HashMap的一些底层原理,以及简单了解了HashSet和HashMap两种集合的渊源,现在我们从HashSet源码入手,来学习HashSet更细节的地方。

对于HashSet而言,它是基于HashMap实现的。HashSet底层采用HashMap来保存元素,因此HashSet底层其实比较简单。

package java.util;

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

// HashSet是通过map(HashMap对象)保存内容的
    private transient HashMap<E,Object> map;

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

// 默认构造函数 底层创建一个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);
    }

// 返回HashSet的迭代器
    public Iterator<E> iterator() {
        // 实际上返回的是HashMap的“key集合的迭代器”
        return map.keySet().iterator();
    }
   //调用HashMap的size()方法返回Entry的数量,得到该Set里元素的个数
    public int size() {
        return map.size();
    }
   //调用HashMap的isEmpty()来判断HaspSet是否为空
   //HashMap为null。对应的HashSet也为空
    public boolean isEmpty() {
        return map.isEmpty();
    }
    //调用HashMap的containsKey判断是否包含指定的key
    //HashSet的所有元素就是通过HashMap的key来保存的
    public boolean contains(Object o) {
        return map.containsKey(o);
    }

// 将元素(e)添加到HashSet中,也就是将元素作为Key放入HashMap中
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

// 删除HashSet中的元素(o),其实是在HashMap中删除了以o为key的Entry
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
     //清空HashMap的clear方法清空所有Entry
    public void clear() {
        map.clear();
    }

// 克隆一个HashSet,并返回Object对象
    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();
        }
    }

// java.io.Serializable的写入函数
    // 将HashSet的“总的容量,加载因子,实际容量,所有的元素”都写入到输出流中
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out any hidden serialization magic
        s.defaultWriteObject();

// Write out HashMap capacity and load factor
        s.writeInt(map.capacity());
        s.writeFloat(map.loadFactor());

// Write out size
        s.writeInt(map.size());

// Write out all elements in the proper order.
        for (Iterator i=map.keySet().iterator(); i.hasNext(); )
            s.writeObject(i.next());
    }

// java.io.Serializable的读取函数
    // 将HashSet的“总的容量,加载因子,实际容量,所有的元素”依次读出
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in any hidden serialization magic
        s.defaultReadObject();

// Read in HashMap capacity and load factor and create backing HashMap
        int capacity = s.readInt();
        float loadFactor = s.readFloat();
        map = (((HashSet)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));

// Read in size
        int size = s.readInt();

// Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
从上述HashSet源代码可以看出,它其实就是一个对HashMap的封装而已。所有放入HashSet中的集合元素实际上由HashMap的key来保存,而HashMap的value则存储了一个PRESENT,它是一个静态的Object对象。

HashSet的绝大部分方法都是通过调用HashMap的方法来实现的,因此HashSet和HashMap两个集合在实现本质上是相同的。

根据HashMap的一个特性: 将一个key-value对放入HashMap中时,首先根据key的hashCode()返回值决定该Entry的存储位置,如果两个key的hash值相同,那么它们的存储位置相同。如果这个两个key的equalus比较返回true。那么新添加的Entry的value会覆盖原来的Entry的value,key不会覆盖。因此,如果向HashSet中添加一个已经存在的元素,新添加的集合元素不会覆盖原来已有的集合元素。

现在我们通过一个实际的例子来看看是否真正理解了HashMap和HashSet存储元素的细节:

class Name
{
    private String first;
    private String last;
    public Name(String first, String last) 
    {
        this.first = first;
        this.last = last;
    }
    public boolean equals(Object o) 
    {
        if (this == o)
        {
            return true;
        }
        if (o.getClass() == Name.class)
        {
            Name n = (Name)o;
            return n.first.equals(first)
                && n.last.equals(last);
        }
        return false;
    }
}
public class HashSetTest
{
    public static void main(String[] args) 
    {
        Set<Name> s = new HashSet<Name>();
        s.add(new Name("abc", "123"));
        System.out.println(
            s.contains(new Name("abc", "123")));
    } 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
上面程序中向HashSet里添加了一个new Name(“abc”,”123”)对象之后,立即通过程序判断该HashSet里是否包含一个new Name(“abc”,”123”)对象。粗看上去,很容易以为该程序会输出true。

实际上会输出false。因为HashSet判断两个对象相等的标准是想通过hashCode()方法计算出其hash值,当hash值相同的时候才继续判断equals()方法。而如上程序我们并没有重写hashCode()方法。所以两个Name类的hash值并不相同,因此HashSet会把其当成两个对象来处理。

所以,当我们要将一个类作为HashMap的key或者存储在HashSet的时候。通过重写hashCode()和equals(Object object)方法很重要,并且保证这两个方法的返回值一致。当两个类的hashCode()返回一致时,应该保证equasl()方法也返回true。当给上述Name类增加如下方法:

public void hashCode(){

return first.hashCode()+last.hashCode();
}
1
2
3
4
此时我们测试的方法会返回true。
--------------------- 
作者:不能说的秘密go 
来源:CSDN 
原文:https://blog.csdn.net/canot/article/details/51240251 
版权声明:本文为博主原创文章,转载请附上博文链接!

HashSet源码解析相关推荐

  1. Java HashSet源码解析

    本解析源码来自JDK1.7,HashSet是基于HashMap实现的,方法实现大都直接调用HashMap的方法 另一篇HashMap的源码解析文章 概要 实现了Set接口,实际是靠HashMap实现的 ...

  2. HashSet源码解析(最好先看HashMap的源码解析)

    HashMap的源码解析:https://mp.csdn.net/console/editor/html/106188425 HashSet:Java中的一个集合类,该容器不允许包含重复的数值 pub ...

  3. java集合-HashSet源码解析

    HashSet 无序集合类 实现了Set接口 内部通过HashMap实现 // HashSet public class HashSet<E>extends AbstractSet< ...

  4. Java - HashSet源码解析

    java提高篇(二四)-----HashSet 一.定义 public class HashSet<E> extends AbstractSet<E> implements S ...

  5. Java集合深入学习 - HashSet源码解析(基于jdk1.8)

    HashSet...感觉就像是一个阉割版的HashMap.. /*** 定义HashSet类 继承 AbstractSet 实现Set,Cloneable,Serializable*/ public ...

  6. HashSet源码解析——基于JDK1.8

    前言 HashSet 是一个不允许存储重复元素的集合,它的实现比较简单,只要理解了 HashMap,HashSet 就水到渠成了.当然了解HashMap可以参考我的这篇文章 1 常量介绍 //使用Ha ...

  7. Java 集合系列16之 HashSet详细介绍(源码解析)和使用示例

    概要 这一章,我们对HashSet进行学习. 我们先对HashSet有个整体认识,然后再学习它的源码,最后再通过实例来学会使用HashSet.内容包括: 第1部分 HashSet介绍 第2部分 Has ...

  8. 面试官系统精讲Java源码及大厂真题 - 11 HashSet、TreeSet 源码解析

    11 HashSet.TreeSet 源码解析 更新时间:2019-09-16 19:37:35 成功的奥秘在于目标的坚定. --迪斯雷利 引导语 HashSet.TreeSet 两个类是在 Map ...

  9. EventBus源码解析

    前面一篇文章讲解了EventBus的使用,但是作为开发人员,不能只停留在仅仅会用的层面上,我们还需要弄清楚它的内部实现原理.所以本篇博文将分析EventBus的源码,看看究竟它是如何实现"发 ...

最新文章

  1. 关于R语言plyr包的安装问题
  2. bs4爬取的时候有两个标签相同_python爬虫初体验,爬取中国最好大学网大学名次...
  3. 谈谈我开发过的几套语音通信解决方案
  4. 织梦dedecms dede plus文件作用介绍及安全设置
  5. u盘复制不进去东西_限制电脑只能识别自己指定的U盘
  6. 原生安卓苹果APP-java抢单派单系统平台源码
  7. 断言java_Java几种常用的断言风格你怎么选?
  8. ubuntu jdk tomcat mysql_Ubuntu下JDK+Tomcat+MySql环境的搭建
  9. C++的掐拷贝、深拷贝【面向对象程序设计细节】
  10. Javascript ES6 Promise同步读取文件(使用async、await)
  11. ALV 行、列、单元格颜色设置
  12. 所有的Python库,我都整理在这里了
  13. JS 常见的 6 种继承方式
  14. 一文让你理清导数、方向导数、梯度向量之间的关系~
  15. 程序逸的Java项目之旅-图书管理系统之项目搭建
  16. 揭秘经典案例炼成之道 微信开发者大会精华回顾
  17. CUBEMX配置STM32实现FTP文件传输以及使用SNTP获取网络时间并写入RTC
  18. camera杂项---两种shutter
  19. 计算机应用的毕业论文论文,计算机应用本科毕业论文
  20. NetBeans--员工信息管理系统

热门文章

  1. Linux下history命令详解---转载
  2. 深入redis内部之redis启动过程之二
  3. 【机器听觉】初探语音识别技术
  4. 【推荐系统】推荐系统整体框架概览
  5. 他用几个公式解释了现金贷业务的风控与运营 (下) 2017-09-18 22:04 风控/运营/违约 “金额如此小的业务,成本极度敏感,刚开始的时候我们在数据成本和坏账成本之间特别纠结。” 以上是许
  6. 高效计算基础与线性分类器
  7. 分布式MySQL数据库TDSQL架构分析
  8. 评价并不精辟,感觉没有什么深度
  9. C语言有三个电阻r1r2r3,[VR虚拟现实]ARM硬件试题库及答案(37页)-原创力文档
  10. linux怎么运行cli,linux脚本 直接用cli模式运行脚本