Java 中的 HashSet,内部是如何工作的?

  继续分享一道Java经典面试题:

  题目描述:

Java 中的 HashSet,内部是如何工作的?
HashSet 的内部采用 HashMap来实现。由于 Map 需要 key 和 value,所以HashSet中所有 key 的都有一个默认 value。类似于        HashMap,HashSet 不允许重复的 key,只允许有一个null key,意思就是 HashSet 中只允许存储一个 null 对象。


  答案过于简单 对于Java中的HashSet的实现原理进一步的探索 得到了这些知识,分享给大家:

  

HashSet:

  • 实现了Set接口
  • HashSet依赖的数据结构是哈希表
  • 因为实现的是Set接口,所以不允许有重复的值
  • 插入到HashSet中的对象不保证与插入的顺序保持一致。对象的插入是根据它的hashcode
  • HashSet中允许有NULL值
  • HashSet也实现了Searlizable和Cloneable两个接口

HashSet的构造函数:

   HashSet h = new HashSet();      
   默认初始化大小是16,默认装载因子是0.75.

   HashSet h = new HashSet(int initialCapacity);  
   默认装载因子是0.75

   HashSet h = new HashSet(int initialCapacity, float loadFactor);

   HashSet h = new HashSet(Collection C);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

什么是初始化大小与装载因子:

初始化尺寸就是当创建哈希表(HashSet内部用哈希表的数据结构)的时候桶(buckets)的数量。如果当前的尺寸已经满了,那么桶的数量会自动增长。

装载因子衡量的是在HashSet自动增长之前允许有多满。当哈希表中实体的数量已经超出装载因子与当前容量的积,那么哈希表就会再次进行哈希(也就是内部数据结构重建),这样哈希表大致有两倍桶的数量。

                  表中已经存储的元素的数量
   装载因子 = -----------------------------------------
                       哈希表的大小
  • 1
  • 2
  • 3

例如:如果内部容量为16,装载因子为0.75,那么当表中有12个元素的时候,桶的数量就会自动增长。

性能影响:

装载因子和初始化容量是影响HashSet操作的两个主要因素。装载因子为0.75的时候可以提供关于时间和空间复杂度方面更有效的性能。如果我们加大这个装载因子,那么内存的上限就会减小(因为它减少了内部重建的操作),但是将影响哈希表中的add与查询的操作。为了减少再哈希操作,我们应该选择一个合适的初始化大小。如果初始化容量大于实体的最大数量除以装载因子,那么就不会有再哈希的动作发生了。

HashSet中的一些重要方法:

  1. boolean add(E e):如果不存在则添加,存在则返回false。
  2. void clear() :移除Set中所有的元素
  3. boolean contains(Object o):如果这个元素在set中存在,那么返回true。
  4. boolean remove(Object o):如果这个元素在set中存在,那么从set中删除。
  5. Iterator iterator():返回set中这个元素的迭代器。

简单的程序:

// Java program to demonstrate working of HashSet
import java.util.*;

class Test
{
    public static void main(String[]args)
    {
        HashSet h = new HashSet();

        // adding into HashSet
        h.add("India");
        h.add("Australia");
        h.add("South Africa");
        h.add("India");// adding duplicate elements

        // printing HashSet
        System.out.println(h);
        System.out.println("List contains India or not:" +
                           h.contains("India"));

        // Removing an item
        h.remove("Australia");
        System.out.println("List after removing Australia:"+h);

        // Iterating over hash set items
        System.out.println("Iterating over list:");
        Iterator i = h.iterator();
        while (i.hasNext())
            System.out.println(i.next());
    }
}
  • 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

上述代码的输出:

[Australia, South Africa, India]
List contains India or not:true
List after removing Australia:[South Africa, India]
Iterating over list:
South Africa
India
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

HashSet内部是如何工作的?

所有Set接口的类内部都是由Map做支撑的。HashSet用HashMap对它的内部对象进行排序。你一定好奇输入一个值到HashMap,我们需要的是一个键值对,但是我们传给HashSet的是一个值。

那么HashMap是如何排序的?

实际上我们插入到HashSet中的值在map对象中起的是键的作用,因为它的值Java用了一个常量。所以在键值对中所有的键的值都是一样的。

如果我们在Java Doc中看一下HashSet的实现,大致是这样的:

private transient HashMap map;

// Constructor - 1
// All the constructors are internally creating HashMap Object.
public HashSet()
{
    // Creating internally backing HashMap object
    map = new HashMap<>();
}

// Constructor - 2
public HashSet(int initialCapacity)
{
    // Creating internally backing HashMap object
    map = new HashMap<>(initialCapacity);
}

// Dummy value to associate with an Object in Map
private static final Object PRESENT = new Object();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

如果我们看下HashSet中的add方法:

public boolean add(E e)
{
   return map.put(e, PRESENT) == null;
}
  • 1
  • 2
  • 3
  • 4

我们可以注意到,HashSet类的add()方法内部调用的是HashMap的put()方法,通过你指定的值作为key,常量“PRESENT”作为值传过去。

remove()也是用类似的方法工作。它内部调用的是Map接口的remove。

public boolean remove(Object o)
{
  return map.remove(o) == PRESENT;
}
  • 1
  • 2
  • 3
  • 4

HashSet操作的时间复杂度:

HashSet底层的数据结构是哈希表,所以HashSet的add,remove与查询(包括contain方法)的分摊(平均或者一般情况)时间复杂度是O(1)。

参考文献:

https://docs.oracle.com/javase/7/docs/api/java/util/HashSet.html

http://blog.csdn.net/sinat_36246371/article/details/53366104


你可能感兴趣的:(java面试题)