深入理解 HashSet

什么是 HashSet?

HashSet 是存在 java.util 包中的类,是 Java 中常用的一个集合类,是 Set 接口的实现类,该容器中不能存储重复的对象。对于 HashSet 来说,它是基于 HashMap 实现的,底层采用 HashMap 来存储元素,关于 HashMap 的文章请看这里 深入理解 HashMap

HashSet 的继承类与实现的接口有哪些?

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

从上面的代码中可以知道 HashSet 继承了AbstractSet 类并且实现了 Set、Cloneable、Serializable 接口

HashSet 中的主要成员变量

// 序列ID
static final long serialVersionUID = -5024744406713321676L;

// HashSet 内部使用 HashMap 存储
private transient HashMap<E,Object> map;

// 虚拟对象,用来作为 value 存储到 map 中
private static final Object PRESENT = new Object();

HashSet中的构造函数

一、HashSet()

无参的 HashSet()

// 当调用该构造函数的时候,其实就是实例化了 HashMap 对象
public HashSet() {
    // 实例化
    map = new HashMap<>();
}

二、HashSet(Collection<? extends E> c)

// 把另一个集合的元素全部添加到当前的Set中
public HashSet(Collection<? extends E> c) {
    // 这里初始化的 Map 计算了它的初始容量
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

三、HashSet(int initialCapacity, float loadFactor)

相信看过 深入理解 HashMap 朋友看到这里是不是有种似曾相识的感觉?没错,就是 HashMap 中的 initialCapacity 和 loadFactor

// 指定初始容量和加载因子
public HashSet(int initialCapacity, float loadFactor) {
    // initialCapacity(对应HashMap中的初始容量), loadFactor(对应HashMap中的加载因子)
    map = new HashMap<>(initialCapacity, loadFactor);
}

四、HashSet(int initialCapacity)

// 指定初始容量构造一个 HashSet
public HashSet(int initialCapacity) {
    // initialCapacity 对应 HashMap中的初始容量
    map = new HashMap<>(initialCapacity);
}

五、HashSet(int initialCapacity, float loadFactor, boolean dummy)

// 以指定的参数构造一个新的空链接哈希集合
// initialCapacity:初始容量
// loadFactor:加载因子
// dummy:标记
// 非public修饰,主要是给 LinkedHashSet 使用的
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    // 底层以指定的参数化构造一个 LinkedHashMap 实例来实现
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

HashSet 中常用的方法有哪些?

isEmpty()

如果该集合不包含任何元素,则返回 true

public boolean isEmpty() {
    // 实际调用的是 HashMap 的 isEmpty 方法判断 HashSet 是否为空
    return map.isEmpty();
}

contains(Object o)

contains 判断 HashSet 中是否包含指定元素,有的话则返回 true

public boolean contains(Object o) {
    // 底层实际调用的是 HashMap 的containsKey 来判断是否包含指定的 key,如果包含指定元素则返回 true
    return map.containsKey(o);
}

add(E e)

如果向 HashSet 中添加一个已存在的元素时,新添加的集合元素将不会被存放到 HashMap 中 ,原来的元素也不会有任何改变,也就说明了 HashSet 不能有重复元素的特性

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

add() 方法中的 put 操作实际上是 HashMap的 put 操作,而 put 操作返回的是 putVal

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

关于 hash() 和 putVal() 方法的详细内容请点击这里 深入理解 HashMap ,这里不再讲述

说明:大家可以发现 HashSet 每次放入的值都会被当做key来处理,然后通过 HashMap 中的 put() 方法采用遍历单向链表的形式,对于使用相同的 key 和 相同的 key 生成的 hash值 ,那么就使用指定的值替换旧的值

remove(Object o)

如果指定的元素存在 set 中,则移除

public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}

iterator()

// 获取 HashSet 的迭代器,返回元素的顺序不是特定的
public Iterator<E> iterator() {
    // 底层调用 HashMap 的 keySet 来返回所有的 key
    return map.keySet().iterator();
}

通过上面的代码可以知道 HashSet 中的元素是存放在底层的 HashMap 的 key 上的,可以发现它遍历的是 keySet,HashMap 中的 key 键重复的话就更新该值,可以保证 key 是不重复的,也就是说 HashSet 添加的值是不会重复的

keySet()

public Set<K> keySet() {
    // AbstractMap 中的 keySet;
    Set<K> ks = keySet;
    
    // 如果key的集合为空
    if (ks == null) {
        // 调用 keySet 方法进行初始化
        ks = new KeySet();
        // 赋值
        keySet = ks;
    }
    return ks;
}

size()

返回集合中的元素的数量

public int size() {
    return map.size();
}

clear()

调用 HashMap 的 clear() 方法清空 Node 中的所有节点

public void clear() {
    map.clear();
}

总结

为什么HashSet不允许出现重复值?

因为 HashSet 的内部是使用 HashMap 的 key来存储元素的

public class HashSetDemo {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add("hello");
        set.add("hello");
        System.out.println("set ------- " + set);
        
        List<String> list = new ArrayList<>();
        list.add("hello");
        list.add("hello");
        System.out.println("list ------ " + list);
    }
}

输出:

set ------ [hello]
list ------ [hello, hello]

再举个例子

public class HashSetDemo {
    public static void main(String[] args) {
        Random random = new Random();
        Set<Integer> set = new HashSet<>();
        
        // 生成 100 个20以内的数字
        for (int i = 0; i < 100; i++) {
            // 添加到set中
            set.add(random.nextInt(20));
        }
        System.out.println(set);
    }
}

输出:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

根据输出内容我们可以知道100 个 20 以内的数字中没有重复的

HashSet 可以存储为 null 元素吗?

可以,因为 HashSet 底层是由 HashMap实现的,HashMap 允许 key 为 null,所以 HashSet 固然也可以

public class HashSetDemo {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add(null);
        System.out.println(set);
    }
}

输出:
null

HashSet 到底是有序的还是无序的?

HashSet 是无序的,因为 HashMap 的 key 是无序的,而 HashSet 的底层实现是 HashMap,所以 HashSet 是无序的

有序跟无序怎么理解?

答:如果插入内容的顺序与输出内容的顺序一致表示是有序的,否则是无序的

拿上面的例子继续探讨

public class HashSetDemo {
    public static void main(String[] args) {
        Random random = new Random();
        Set<Integer> set = new HashSet<>();
        for (int i = 0; i < 100; i++) {
            set.add(random.nextInt(20));
        }
        System.out.println(set);
    }
}

输出:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

谁?谁说是无序的?这不是很明显的是有序的嘛,执着的你们肯定也觉得很奇怪,metoo,OK,继续探讨

再看下另外一个例子

public class HashSetDemo {
    public static void main(String[] args) {
        Set<Integer> set = new HashSet<>();
        set.add(1);
        set.add(2);
        set.add(3);
        set.add(4);
        set.add(5);
        for (Integer integer : set) {
            System.out.print(integer + " ");
        }
    }
}

输出:

1 2 3 4 5

还是有序的呀,到底怎么回事?

故意换一下数字的位置再试试

public class HashSetDemo {
    public static void main(String[] args) {
        Set<Integer> set = new HashSet<>();
        set.add(1);
        set.add(3);
        set.add(2);
        set.add(4);
        set.add(5);
        for (Integer integer : set) {
            System.out.print(integer + " ");
        }
    }
}

输出:

1 2 3 4 5

OK,这回是无序的了,再试一个

public class HashSetDemo {
    public static void main(String[] args) {
        Set<Integer> set = new HashSet<>();
        set.add(5);
        set.add(4);
        set.add(3);
        set.add(2);
        set.add(1);
        for (Integer integer : set) {
            System.out.print(integer + " ");
        }
    }
}

输出:

1 2 3 4 5

OK,你们要的无序来了

那么第一个例子那个是怎么回事?说明如下:

说明: 由于我们所创建的 HashSet 是 Integer 类型的,这也是最巧的一点,Integer 类型 hashCode() 的返回值就是其 int 值本身,而存储的时候元素通过一些运算后会得出自己在数组中所处的位置。由于在这一步,其本身即下标(只考虑这一步),其实已经实现了排序功能,由于int类型范围太广,内存放不下,所以对其进行取模运算,为了减少哈希冲突,又在取模前进行了,扰动函数的计算,得到的数作为元素下标,按照 JDK8 下的 hash 算法,以及 loadFactor 及扩容机制,这就导致数据在经过 HashMap 的 hash() 运算后仍然是自己本身的值,且没有发生哈希冲突

总结: 所以HashSet 只是不保证有序并不是保证无序

HashSet 是线程安全的吗?

不是,HashSet 不是线程安全的

如何从 HashSet 中获取指定的元素?

通过将 Set 集合转换为 List 列表即可使用 get 操作获取指定元素

public class HashSetDemo {

    // 自定义一个获取变量的类型方法
    public static String getType(Object obj) {
        return obj.getClass().getName().toString();
    }

    public static void main(String[] args) {
        Set<Integer> set = new HashSet<>();
        set.add(1);
        set.add(2);
        set.add(3);
        set.add(4);
        set.add(5);
        List<Integer> list = new ArrayList<>(set);
        System.out.println(list.get(1)); // 获取指定元素
        System.out.println(getType(list)); // 打印变量类型
    }
}

输出:

2
java.util.ArrayList

发布了184 篇原创文章 · 获赞 864 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/Woo_home/article/details/103594152
今日推荐