一、前言

这一篇里,我将对HashSet、LinkedHashSet、TreeSet进行汇总分析,并不打算一一进行详细介绍,因为JDK对Set的实现进行了取巧。我们都知道Set不允许出现相同的对象,而Map也同样不允许有两个相同的Key(出现相同的时候,就执行更新操作)。所以Set里的实现实际上是调用了对应的Map,将Set的存放的对象作为Map的Key。

二、源码分析

这里笔者就以最常用的HashSet为例进行分析,其余的TreeSet、LinkedHashSet类似,就不赘述了。

2.1 结构概览

关系也很简单,实现了Set的接口,继承了AbstractSet抽象类,抽象类里面定义了集合的常见操作,与我们之前分析过的ArrayList之类的相似。

2.2 成员变量

// HashMap就是HashSet里实现具体操作的对象
private transient HashMap<E,Object> map;
// 将对象作为Value存进去
private static final Object PRESENT = new Object();

由于使用Map进行操作,把E作为Key,要定义一个PRESENT对象作为Value,每个存入的对象都使用它来作为Value。

2.3 构造方法

2.3.1 HashSet()

public HashSet() {
map = new HashMap<>();
}

看了这个,相信园友们应该就知道它是怎么实现的了,我们平时构建HashSet的时候,其实它是在里面new了一个HashMap。

2.3.2 HashSet(Collection<? extends E> c)

public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}

调用传集合的构造方法则是使用了HashMap里指定初始化容量的构造方法,然后再调用addAll()。

public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}

addAll方法很简单,其实就是遍历集合c,然后调用add方法,实现插入HashMap。

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

add方法则是调用了map的put()方法,将对象作为Key,之前域里的PRESENT作为Value,插入到HashMap中。

2.3.3 HashSet(int initialCapacity, float loadFactor, boolean dummy)

最后值得一提的是这个构造方法:

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

注意它是包访问权限的,而不是public,因为这个构造方法是提供给LinkedHashSet使用的,所以里面初始化的也是LinkedHashMap,有兴趣的园友们也可以去LinkedHashSet里看一下它的构造方法。

三、Set的使用注意事项

笔者前端时间恰好碰到了因为HashSet的底层事项导致的一个bug,在此跟大家分享一下:

/**
* @author joemsu 2018-02-04 上午10:33
*/
public class UserInfo {
private Long id;
private String name;
private Integer age; public UserInfo(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
} public void setName(String name) {
this.name = name;
} @Override
public String toString() {
return "UserInfo{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
} @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserInfo info = (UserInfo) o;
return Objects.equals(id, info.id) &&
Objects.equals(name, info.name) &&
Objects.equals(age, info.age);
} @Override
public int hashCode() {
return Objects.hash(id, name, age);
} }

UserInfo为当时笔者要处理的一个pojo对象,由第三方提供,重写了equals和hashCode方法(当时没有发现)。而笔者当时要获取所有的UserInfo对象,放入集合当中进行复杂的逻辑处理,出于可能出现重复对象的考虑(使用递归遍历不同部门下的人员信息,可能存在一个人在多个部门),选择使用HashSet。然后在遍历HashSet集合a的时候,会将每个遍历到的对象加入另一个集合b作记录,事后会将a,b做一个差集,取出其中没有访问到的元素。这个过程中可能会涉及到更新某个对象,具体过程简化如下:

public static void main(String[] args) {
// 将对象加入set
UserInfo info1 = new UserInfo(1L, "zhangsan", 22);
UserInfo info2 = new UserInfo(2L, "lisi", 23);
UserInfo info3 = new UserInfo(3L, "wangwu", 24);
Set<UserInfo> userInfoSet = new HashSet<>();
userInfoSet.add(info1);
userInfoSet.add(info2);
userInfoSet.add(info3); // 对访问到的元素加入集合
List<UserInfo> visited = new ArrayList<>();
visited.add(info1);
visited.add(info2);
visited.add(info3);
// 假设对其中一个对象进行修改
info1.setName("liliu");
// 去掉访问过的元素
userInfoSet.removeAll(visited);
userInfoSet.stream().forEach(System.out::println);
}

最后的输出结果:

UserInfo{id=1, name='liliu', age=22}

是的,所有修改过的元素都无法移除。当笔者通过debug发现这一现象后立刻就意识到,可能就是hashCode导致被修改过的元素无法访问到,为什么呢,我们可以回顾一下HashMap的remove操作:

final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
// 省略
}
return null;
}

在HashMap中,是通过Key的hash值来定位桶的位置,当笔者修改了对象的name属性之后,由于重写的hashCode方法里包括了name这一字段,所以,hash值也发生了改变,导致在对应的桶里找不到该对象,也就无法实现remove操作(虽然两个是同一个引用)。

四、总结

Set的各种底层实现都是对应的Map,熟悉了Map里的各种方法,相信对于Set的了解也会更加深入。最后谢谢各位园友观看,如果有描述不对的地方欢迎指正,与大家共同进步!

【JDK1.8】JDK1.8集合源码阅读——Set汇总的更多相关文章

  1. 【JDK1.8】Java 8源码阅读汇总

    一.前言 ​ 万丈高楼平地起,相信要想学好java,仅仅掌握基础的语法是远远不够的,从今天起,笔者将和园友们一起阅读jdk1.8的源码,并将阅读重点放在常见的诸如collection集合以及concu ...

  2. 【JDK1.8】JDK1.8集合源码阅读——IdentityHashMap

    一.前言 今天我们来看一下本次集合源码阅读里的最后一个Map--IdentityHashMap.这个Map之所以放在最后是因为它用到的情况最少,也相较于其他的map来说比较特殊.就笔者来说,到目前为止 ...

  3. java1.7集合源码阅读: Stack

    Stack类也是List接口的一种实现,也是一个有着非常长历史的实现,从jdk1.0开始就有了这个实现. Stack是一种基于后进先出队列的实现(last-in-first-out (LIFO)),实 ...

  4. java1.7集合源码阅读: Vector

    Vector是List接口的另一实现,有非常长的历史了,从jdk1.0开始就有Vector了,先于ArrayList出现,与ArrayList的最大区别是:Vector 是线程安全的,简单浏览一下Ve ...

  5. 【JDK1.8】JDK1.8集合源码阅读——总章

    一.前言 今天开始阅读jdk1.8的集合部分,平时在写项目的时候,用到的最多的部分可能就是Java的集合框架,通过阅读集合框架源码,了解其内部的数据结构实现,能够深入理解各个集合的性能特性,并且能够帮 ...

  6. 【JDK1.8】JDK1.8集合源码阅读——HashMap

    一.前言 笔者之前看过一篇关于jdk1.8的HashMap源码分析,作者对里面的解读很到位,将代码里关键的地方都说了一遍,值得推荐.笔者也会顺着他的顺序来阅读一遍,除了基础的方法外,添加了其他补充内容 ...

  7. 【JDK1.8】JDK1.8集合源码阅读——ArrayList

    一.前言 在前面几篇,我们已经学习了常见了Map,下面开始阅读实现Collection接口的常见的实现类.在有了之前源码的铺垫之后,我们后面的阅读之路将会变得简单很多,因为很多Collection的结 ...

  8. 【JDK1.8】JDK1.8集合源码阅读——LinkedList

    一.前言 这次我们来看一下常见的List中的第二个--LinkedList,在前面分析ArrayList的时候,我们提到,LinkedList是链表的结构,其实它跟我们在分析map的时候讲到的Link ...

  9. Java集合源码阅读之HashMap

    基于jdk1.8的HashMap源码分析. 引用于:http://blog.stormma.me/2017/05/31/Java%E9%9B%86%E5%90%88%E6%BA%90%E7%A0%81 ...

随机推荐

  1. org.springframework.expression.spel.SpelEvaluationException: EL1004E: Method call: Method service() cannot be found on com.my.blog.springboot.thymeleaf.util.MethodTest type

    前言 本文中提到的解决方案,源码地址在:springboot-thymeleaf,希望可以帮你解决问题. 至于为什么已经写了一篇文章thymeleaf模板引擎调用java类中的方法,又多此一举的单独整 ...

  2. 初次了解struts的action类

    Action类真正实现应用程序的事务逻辑,它们负责处理请求.在收到请求后,ActionServlet会为这个请求选择适当的Action 如果需要,创建Action的一个实例 调用Action的perf ...

  3. Android基础_ContentProvider组件

    一.了解Contentprovider组件 1.1Contentprovider是数据的提供者,Android四大组件之一,程序之间数据共享的接口 1.2activity系统中对数据的访问限制十分严格 ...

  4. 基于Grafana+SimpleJson的灵活报表解决方案

    在时序分析及监控展现领域,Grafana无疑是开源解决方案中的翘楚,其灵活的插件机制,支持各种漂亮的面板.丰富的数据源以及强大的应用.典型的面板有Graph.Text.Singlestat.PieCh ...

  5. java基础,集合,Arraylist,源码解析(基础)

    ArrayList 是什么,定义? 这是动态的数组,它提供了动态的增加和减少元素,实现了List接口(List实现Collection,所以也实现Collection接口)灵活的设置数组的大小等好处 ...

  6. Java异常抛出及try,catch应用实例

    class lanpingException extends Exception { lanpingException(String msg) { super(msg); } } class maoy ...

  7. NYoj289苹果(0-1背包)

    苹果 时间限制:3000 ms  |  内存限制:65535 KB 难度:3 描述 ctest有n个苹果,要将它放入容量为v的背包.给出第i个苹果的大小和价钱,求出能放入背包的苹果的总价钱最大值. 输 ...

  8. angular-dragon-drop.js 双向数据绑定拖拽的功能

    在做公司后台物流的时候,涉及到34个省市分为两个部分,一部分为配送区域,另一部分为非配送区域,想利用拖拽的功能来实现,最好两部分的数组能自动更新. 刚好找到angular-dragon-drop.js ...

  9. spring cloud-zuul的Filter详解

    在前面我们使用zuul搭建了网关http://blog.csdn.net/liuchuanhong1/article/details/59056278 关于网关的作用,这里就不再次赘述了,我们今天的重 ...

  10. 如何开发由Create-React-App 引导的应用(三)

    此文章是翻译How to develop apps bootstrapped with Create React App 官方文档 系列文章 如何开发由Create-React-App 引导的应用 如 ...