三句话都能解释清楚的java集合类HashSet,你清楚吗?

简介: HashSet作为一种最简单的java集合类,真的可以用三句话来概括一下:第一句:存放不重复的数据。第二句:底层基于hash表实现。第三句:内部基于HashMap。这也就是说,你想要完完全全彻彻底底地把HashSet吃透,就一定要先吃透HashMap。这篇文章将带着你从特点到存储,再到最后的实现,从源码角度来分析一下。

一、认识


HashSet其实就是一个没有重复数据的集合,基本用法很简单,我们直接给个例子。

public class Test {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        // 将元素添加到Set中
        set.add("a");
        //加入一个存在的则会替换。
        set.add("a");
        //是否包含某个值
        System.out.println("是否包含了a:", set.contains("a"));
        // 删除HashSet中的“e”
        set.remove("e");
        // 将Set转换为数组
        String[] arr = (String[])set.toArray(new String[0]);
        // 遍历HashSet
        for(Iterator iterator = set.iterator();iterator.hasNext();) 
            System.out.println(iterator.next());
        // 清空HashSet
        set.clear();
    }
}

以上只是列出了其最简单的用法。下面我们看看其继承关系。HashSet主要继承了三个接口Serializable、Cloneable、Set,并且实现了抽象类AbstractSet。我们直接看看源码:

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

学过HashMap的人应该都知道HashMap实现的是Map接口,而HashSet是Set接口。

下面我们就从源码的角度来分析一下HashSet。


二、源码分析


1、参数变量


//这个HashMap就是实际保存HashSet元素的容器
private transient HashMap<E,Object> map;
//PRESENT表示的意思很简单,也就是我们的HashSet只使用到了HashMap的key,
//所以此处定义一个静态的常量Object类,来充当HashMap的value
private static final Object PRESENT = new Object();

这里有个问题,那就是既然HashSet只使用到了HashMap的key,为什么不使用null来充当HashMap的value,而使用了PRESENT这个对象呢?

答:想要深入这个问题,我们还需要深入到源码中看看:

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

以上两个是增删方法,在add一个元素的时候,其实调用的就是map.put(e,

PRESENT)==null,HashMap在put元素的时候会出现两种情况:


情况一:put的元素是新的,那么map.put会发现key没有,那么直接插入即可。return结果为true。


情况二:put的元素是旧的,那么map.put会发现key已有,则直接返回相应的value,也就是PRESENT,PRESENT不等于null,return的也就是false了,表示HashSet插入失败。如果我们这里使用null为map.put的参数呢?直接返回相应的value,也就是null,这时候null==null是true。竟然返回了true。很明显就是错误的返回结果呀。

这其实也是去重复的原理。对于删除方法其实也是一样的。


2、构造函数


public HashSet() {
    map = new HashMap<E,Object>();
}
public HashSet(Collection<? extends E> c) {
    map = new HashMap<E,Object>(Math. max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}
public HashSet( int initialCapacity, float loadFactor) {
    map = new HashMap<E,Object>(initialCapacity, loadFactor);
}
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提供的构造方法很多,有5个,在这里我想说明的是每一种构造方法,其实都是创建的HashMap。这也证明了我们文章开头提到的内部基于HashMap。


3、其他方法


增删方法我们已经提到了,在这里我们主要看一下其他方法。

//底层利用的还是HashMap
public boolean contains(Object o) {
    return map .containsKey(o);
}
//检查是否包含指定集合中所有元素
public boolean containsAll(Collection<?> c) {
    Iterator<?> e = c.iterator();
    //只要集合c中有一个元素不属于HashSet,返回false
    while (e.hasNext())
        if (!contains(e.next()))
            return false;
    return true;
}

上面的方法还包含了遍历元素的方式。

HashSet就是这么简单,源码里面几乎所有的方法都是HashMap实现的。

相关文章
|
10天前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
|
27天前
|
存储 安全 Java
java.util的Collections类
Collections 类位于 java.util 包下,提供了许多有用的对象和方法,来简化java中集合的创建、处理和多线程管理。掌握此类将非常有助于提升开发效率和维护代码的简洁性,同时对于程序的稳定性和安全性有大有帮助。
47 17
|
19天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
25 2
|
24天前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
19天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
23天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
76 4
|
24天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
45 2
|
24天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
24天前
|
Java 开发者
|
28天前
|
存储 安全 Java
如何保证 Java 类文件的安全性?
Java类文件的安全性可以通过多种方式保障,如使用数字签名验证类文件的完整性和来源,利用安全管理器和安全策略限制类文件的权限,以及通过加密技术保护类文件在传输过程中的安全。