一、HashSet 概述

  1、HashSet Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。

  2、HashSet Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。

  3、HashSet 具有以下特点

    ① 不能保证元素的排列顺序;

    ② HashSet 不是线程安全的;

    ③ 集合元素可以是 null

  4、HashSet 集合判断两个元素相等的标准两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。

  5、对于存放在Set容器中的对象, 对应的类一定要重写equals()hashCode(Object obj)方法,以实现对象相等规则。即: “相等的对象必须具有相等的散列码” 。

  6、HashSet的继承关系

    (1)HashSet继承了 AbstractSet 抽象类并实现了 Set 接口,AbstractSet 的子类还包括 TreeSet,里面实现了两个类公共的一部分方法,后面也会略有介绍。

    (2)那么HashSet到底一个怎么样的存在呢?HashSet顾名思义就是通过Hash表的方式存储数据,既然提到hash,那么肯定少不了HashMap,其实HashSet很聪明,他只需在内部维护了一个HashMap实例,将数据存储在了map中,并且集合元素不能重复。因此HashSet可以说是集合类中实现最简单的一个,他基本就是实现了List接口中定义的几个方法。

 

二、HashSet 结构

  1、HashSet 声明

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

  

  2、HashSet 结构

    

 

  3、HashSet 方法

    

 

三、HashSet 创建

  1、构造器

    HashSet 提供了四种构造器

 1 public HashSet() {
 2     map = new HashMap<>();
 3 }
 4 
 5 public HashSet(Collection<? extends E> c) {
 6     map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
 7     addAll(c);
 8 }
 9 
10 public HashSet(int initialCapacity, float loadFactor) {
11     map = new HashMap<>(initialCapacity, loadFactor);
12 }
13 
14 public HashSet(int initialCapacity) {
15     map = new HashMap<>(initialCapacity);
16 }

 

    HashSet的创建其实就是实例化一个HashMap,可以创建默认大小的HashMap实例,也可以指定initialCapacity和loadFactor,这两个参数的具体含义会在介绍HashMap中讲解。

  2、HashSet和HashMap有什么区别呢?

    (1)HashMap提供键值对的方式存储数据,而HashSet仅仅提供数据存储,并没有键值对应。他获取元素的方式也只能通过遍历的方式逐个获取;

    (2)HashMap在存入数据的时候是更加key值的hash值判断,而HashSet需要重写hashCode和equals两个方法,如果不重写则会调用默认的实现,用户在使用HashSet的时候要特别注意元素的euqals判断,有必要的话要重写一个,以免出现问题。

  3、

 

四、HashSet中添加元素的过程

  1、HashSet 如何存储数据的呢?

    通过构造函数可以看到HashSet中真正存放元素的地方就是HashMap,但是HashMap是K-V的形式,而HashSet中没有Key,那么他们是如何将HashSet中的元素映射到HashMap中的呢?

    原来是将HashSet中的元素存放到了map的key中,而value则存放一个无意义的Object对象。这么做的好处还可以保证HashSet中的value值唯一。

    了解HashMap的同学知道,HashMap中校验key值唯一性的方式是通过hash值,然后根据hash值定位数组中的位置,但是也存在hash冲突的情况,那么解决hash冲突的方式就是在原来数组的位置增加一个链表,将hash值冲突,但是key值不同的元素存放在链表中。那么在判断key值是否相同的时候就用到了equals方法。

    从上面的描述中可以抓出几个关键点,第一hash值,第二equals方法。这就是为什么我们需要将存入HashSet的元素重写hashCode和euqals方法的根本原因。

    源码:

1 // Dummy value to associate with an Object in the backing Map
2 private static final Object PRESENT = new Object();
3 
4 public boolean add(E e) {
5     return map.put(e, PRESENT)==null;
6 }

 

  2、HashSet  添加元素步骤

    (1)当向 HashSet 集合中存入一个元素时, HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值, 然后根据 hashCode 值, 通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。 (这个散列函数会与底层数组的长度相计算得到在数组中的下标, 并且这种散列函数计算还尽可能保证能均匀存储元素, 越是散列分布,该散列函数设计的越好);

    (2)如果两个元素的hashCode()值相等, 会再继续调用equals方法, 如果equals方法结果为true, 添加失败; 如果为false, 那么会保存该元素, 但是该数组的位置已经有元素了,那么会通过链表的方式继续链接。

    (3)如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等, hashSet 将会把它们存储在不同的位置,但依然可以添加成功。

        

 

       底层也是数组, 初始容量为16, 当如果使用率超过0.75, (16*0.75=12)就会扩大容量为原来的2倍。 (16扩容为32, 依次为64,128....等)

 

      ① 我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,
      ② 此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素:

        ③ 如果此位置上没有其他元素,则元素a添加成功。 --->情况1
        ④ 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
          ⑤ 如果hash值不相同,则元素a添加成功。--->情况2
          ⑥ 如果hash值相同,进而需要调用元素a所在类的equals()方法:
            ⑦ equals()返回true,元素a添加失败
            ⑧ equals()返回false,则元素a添加成功。--->情况2

      对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。
      jdk 7 :元素a放到数组中,指向原来的元素。
      jdk 8 :原来的元素在数组中,指向元素a
      总结:七上八下

 

五、

六、HashSet遍历

    在HashSet的源码中找了一通,然并没有找到get方法,那么我们将如何获取元素呢?我发现了iterator接口,这个接口返回的Iterator并不是HashSet自己维护的Iterator,而是通过返回HashMap的keySet().Iterator,这个迭代器遍历的是HashMap的key值。也就是HashSet中保存的value。

    源码:

1 public Iterator<E> iterator() {
2     return map.keySet().iterator();
3 }

 

七、重写 hashCode() 方法的基本原则

  (1)在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。

  (2)当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。

  (3)对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

 

  Eclipse/IDEA工具里hashCode()的重写,Eclipse/IDEA为例,在自定义类中可以调用工具自动重写equalshashCode
  问题: 为什么用Eclipse/IDEA复写hashCode方法,有31这个数字

  (1)选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)

  (2)并且31只占用5bits,相乘造成数据溢出的概率较小。
  (3)31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。 (提高算法效率)

  (4)31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除! (减少冲突)

八、重写 equals() 方法的基本原则

  (1)当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是, 根据Object.hashCode()方法,它们仅仅是两个对象。
  (2)因此,违反了“相等的对象必须具有相等的散列码”。
  (3)结论:复写equals方法的时候一般都需要同时复写hashCode方法。 常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

 

九、常见面试题

  (1)题目1

 1     @Test
 2     public void test() {
 3         HashSet set = new HashSet();
 4         Person p1 = new Person(1001,"AA");
 5         Person p2 = new Person(1002,"BB");
 6         set.add(p1);
 7         set.add(p2);
 8         System.out.println(set);
 9 
10         p1.name = "CC";
11         set.remove(p1);
12         System.out.println(set);
13 
14         set.add(new Person(1001,"CC"));
15         System.out.println(set);
16 
17         set.add(new Person(1001,"AA"));
18         System.out.println(set);
19     }

  Person类:

 1 public class Person {
 2     int id;
 3     String name;
 4 
 5     public Person() {
 6     }
 7 
 8     public Person(int id, String name) {
 9         this.id = id;
10         this.name = name;
11     }
12 
13     @Override
14     public String toString() {
15         return "Person{" +
16                 "id=" + id +
17                 ", name='" + name + '\'' +
18                 '}';
19     }
20 
21     @Override
22     public boolean equals(Object o) {
23         if (this == o) return true;
24         if (o == null || getClass() != o.getClass()) return false;
25         Person person = (Person) o;
26         return id == person.id &&
27                 Objects.equals(name, person.name);
28     }
29 
30     @Override
31     public int hashCode() {
32         return Objects.hash(id, name);
33     }
34 }

 

  当 Person没有重写 equals() 和 hashCode()运行结果为:

 [Person{id=1001, name='AA'}, Person{id=1002, name='BB'}]
 [Person{id=1002, name='BB'}]
 [Person{id=1001, name='CC'}, Person{id=1002, name='BB'}]
 [Person{id=1001, name='CC'}, Person{id=1001, name='AA'}, Person{id=1002, name='BB'}]

   当Person没有根据属性进行这两个方法重写时,可以根据 p1计算hashCode()进行移除,再添加新对象时,也是本身的 hashCode()方法。

  

  当 Person重写 equals() 和 hashCode()后运行结果为:

 [Person{id=1002, name='BB'}, Person{id=1001, name='AA'}]
 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]
 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}]
 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]

  当根据属性进行了equals() 和 hashCode() 重写后,

   p1的name改成 "cc",当移除时,会根据p1(id='1001',name='cc')此时重新计算hashCode()来查找在 数组中的位置,没有查找到就删除失败了。

  再次添加时还是根据自身的属性来计算的,所以(1001,"CC")仍然能存进来。

  再次添加时(1001,"AA")时,虽然与 p1的 hashCode 是一样的,但是 equals()不一样,仍然可以存进来。

  

十、

十一、

 

posted on 2021-04-19 14:31  格物致知_Tony  阅读(478)  评论(0编辑  收藏  举报