【Java集合类】之 HashSet以及底层逻辑分析

简介: 【Java集合类】之 HashSet以及底层逻辑分析

HashSet集合🎋


1.1 HashSet集合的概述和特点🎐🎐🎐


首先先来研究一下帮助文档



HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。

HashSet 允许有 null 值。

HashSet 是无序的,即不会记录插入的顺序。

HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。


HashSet 实现了 Set 接口。


public class HashSet< E >

extends AbstractSet< E>

implements Set< E>, Cloneable, Serializable


此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用 null 元素。


此类为基本操作提供了稳定性能,这些基本操作包括 add、remove、contains 和 size,假定哈希函数将这些元素正确地分布在桶中。对此 set 进行迭代所需的时间与 HashSet 实例的大小(元素的数量)和底层 HashMap 实例(桶的数量)的“容量”的和成比例。因此,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。


注意,此实现不是同步的。如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:


Set s = Collections.synchronizedSet(new HashSet(...));


此类的 iterator 方法返回的迭代器是快速失败 的:在创建迭代器之后,如果对 set 进行修改,除非通过迭代器自身的 remove 方法,否则在任何时间以任何方式对其进行修改,Iterator 都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒将来在某个不确定时间发生任意不确定行为的风险。


注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器在尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误做法:迭代器的快速失败行为应该仅用于检测 bug。


总结:HashSet集合特点


底层数据结构是哈希表

对集合的迭代顺序不做任何的保证,也就是说不保证存储和取出的元素顺序一致

没有带索引的方法,所以不能使用普通for循环来遍历(可以使用迭代器和增强for遍历)

由于是Set 集合,所以不包含重复元素的集合


1.2 HashSet 集合 Demo✨✨✨


代码示例:存储字符串并遍历


package com.ithmm_06;
import java.util.HashSet;
import java.util.Iterator;
/**
 * HashSet集合特点
 *
 * 底层数据结构是哈希表
 * 对集合的迭代顺序不做任何的保证,也就是说不保证存储和取出的元素顺序一致
 * 没有带索引的方法,所以不能使用普通for循环来遍历(可以使用迭代器和增强for遍历)
 * 由于是Set 集合,所以不包含重复元素的集合
 */
public class HashSetDemo {
    public static void main(String[] args) {
        //创建集合对象
        HashSet<String> hs = new HashSet<String>();
        //添加元素
        hs.add("hello");
        hs.add("world");
        hs.add("java");
//        hs.add("hello");
        //遍历
        for(String s:hs){
            System.out.println(s);//world java  hello
        }
        //迭代器  Iterator<E> iterator() 
        //          返回对此 set 中元素进行迭代的迭代器 
        Iterator<String> it = hs.iterator();
        while (it.hasNext()) {
            String str = it.next();
            System.out.println(str);
        }
    }
}


1.3 HashSet集合保证元素唯一性底层逻辑(源码分析)🎊🎊🎊


HashSet集合添加一个元素的过程:




HashSet底层源码:


//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package java.util;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap.KeySpliterator;
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
    static final long serialVersionUID = -5024744406713321676L;
    private transient HashMap<E, Object> map;
    private static final Object PRESENT = new Object();
    public HashSet() {
        this.map = new HashMap();
    }
    public HashSet(Collection<? extends E> var1) {
        this.map = new HashMap(Math.max((int)((float)var1.size() / 0.75F) + 1, 16));
        this.addAll(var1);
    }
    public HashSet(int var1, float var2) {
        this.map = new HashMap(var1, var2);
    }
    public HashSet(int var1) {
        this.map = new HashMap(var1);
    }
    HashSet(int var1, float var2, boolean var3) {
        this.map = new LinkedHashMap(var1, var2);
    }
    public Iterator<E> iterator() {
        return this.map.keySet().iterator();
    }
    public int size() {
        return this.map.size();
    }
    public boolean isEmpty() {
        return this.map.isEmpty();
    }
    public boolean contains(Object var1) {
        return this.map.containsKey(var1);
    }
    public boolean add(E var1) {
        return this.map.put(var1, PRESENT) == null;
    }
    public boolean remove(Object var1) {
        return this.map.remove(var1) == PRESENT;
    }
    public void clear() {
        this.map.clear();
    }
    public Object clone() {
        try {
            HashSet var1 = (HashSet)super.clone();
            var1.map = (HashMap)this.map.clone();
            return var1;
        } catch (CloneNotSupportedException var2) {
            throw new InternalError(var2);
        }
    }
    private void writeObject(ObjectOutputStream var1) throws IOException {
        var1.defaultWriteObject();
        var1.writeInt(this.map.capacity());
        var1.writeFloat(this.map.loadFactor());
        var1.writeInt(this.map.size());
        Iterator var2 = this.map.keySet().iterator();
        while(var2.hasNext()) {
            Object var3 = var2.next();
            var1.writeObject(var3);
        }
    }
    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        int var2 = var1.readInt();
        if (var2 < 0) {
            throw new InvalidObjectException("Illegal capacity: " + var2);
        } else {
            float var3 = var1.readFloat();
            if (!(var3 <= 0.0F) && !Float.isNaN(var3)) {
                int var4 = var1.readInt();
                if (var4 < 0) {
                    throw new InvalidObjectException("Illegal size: " + var4);
                } else {
                    var2 = (int)Math.min((float)var4 * Math.min(1.0F / var3, 4.0F), 1.07374182E9F);
                    this.map = (HashMap)(this instanceof LinkedHashSet ? new LinkedHashMap(var2, var3) : new HashMap(var2, var3));
                    for(int var5 = 0; var5 < var4; ++var5) {
                        Object var6 = var1.readObject();
                        this.map.put(var6, PRESENT);
                    }
                }
            } else {
                throw new InvalidObjectException("Illegal load factor: " + var3);
            }
        }
    }
    public Spliterator<E> spliterator() {
        return new KeySpliterator(this.map, 0, -1, 0, 0);
    }
}


1.4 常见数据结构之哈希表🎐🎐🎐


HashSet底层是哈希表结构的


哈希表


JDK8之前,底层采用数组+链表实现。

JDK8以后,底层进行了优化。由数组+链表+红黑树实现。


一、HashSet1.7版本原理解析🎐🎐🎐


当我们用空参构造 创建一个Hashset 时


HashSet<String> haset1 = new HashSet<>();


底层 创建一个默认长度16,默认加载因子0.75的数组,数组名table



第一次添加元素时🎐🎐🎐


根据元素的哈希值跟数组的长度计算出应存入的位置 例如:4

在判断当前 4 索引位置是否为null,如果是null直接存入



第二次添加元素时🎐🎐🎐

根据元素的哈希值跟数组的长度计算出的结果 是 10 索引位置

在判断当前位置是否为null,如果是null直接存入



第三次添加元素时🎐🎐🎐

根据元素的哈希值跟数组的长度计算出的结果 还是 4 索引位置

此时 4 索引上 已经有了一个元素 不为null ,则会调用equals方法比较 内部的属性值

如果一样,则不存,如果不一样,则存入数组,老元素挂在新元素下面 形成链表结构



第四次添加元素时🎐🎐🎐

根据元素的哈希值跟数组的长度计算出的结果 还是 4 索引位置

此时 4 索引上 已经有了两个元素

会通过equals方法比较 从上到下 一 一比较

如果一样,则不存,如果不一样,则存入数组,老元素挂在新元素下面



扩容

当数组里面存了16*0.75 =12个元素的时候,数组就会扩容为原先的两倍 32


总结


底层结构∶哈希表。(数组+链表)

数组的长度默认为16,加载因子为0.75

首先会先获取元素的哈希值,计算出在数组中应存入的索引

判断该索引处是否为null

如果是null,直接添加

如果不是null,则与链表中所有的元素,通过equals方法比较属性值只要有一个相同,就不存,如果都不一样,才会存入集合。

目录
相关文章
|
17天前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
39 8
|
26天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
27 2
|
1月前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
26天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
1月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
86 4
|
1月前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
1月前
|
Java 开发者
|
1月前
|
存储 Java 开发者
Java中的集合框架深入解析
【10月更文挑战第32天】本文旨在为读者揭开Java集合框架的神秘面纱,通过深入浅出的方式介绍其内部结构与运作机制。我们将从集合框架的设计哲学出发,探讨其如何影响我们的编程实践,并配以代码示例,展示如何在真实场景中应用这些知识。无论你是Java新手还是资深开发者,这篇文章都将为你提供新的视角和实用技巧。
28 0
|
Java
Java底层学习
最近在看几本Java的书,也做了很多笔记,主要是关于Java虚拟机、Java GC、Java 并发编程等方面,参考的主要几本书籍有: 《深入理解Java虚拟机》——周志明 《深入理解Java虚拟机 第二版》——美 Bi...
1664 0
|
20天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####