浅析Java中的Set接口及实现类

133 阅读5分钟

文章目录


1. Set

Set接口是Collection接口的一个子接口,Set集合中不允许存储重复的元素,而且没有索引。如果要判断两个对象是否相同根据的是equals(),而不是 == 。Set接口主要的实现类有:

  • HashSet
  • LinkedHashSet

2. Hastset

2.1 概念

HashSet具有如下的:

  • 不允许存储重复的元素
  • 没有索引,没有带索引的方法
  • 无序集合
  • 底层实现是哈希表,因此查询非常快
  • 线程不安全
  • 集合元素可以是null
import java.util.HashSet;
import java.util.Iterator;

public class SetMain {
    public static void main(String[] args) {
        HashSet<Integer> set = new HashSet<>();
        set.add(1);
        set.add(2);
        set.add(3);
        System.out.println(set); // [1, 2, 3]
        
        // 遍历元素
        Iterator<Integer> iter = set.iterator();
        while (iter.hasNext()){
            System.out.println(iter.next());
        }

        for(Integer ele : set){
            System.out.println(ele );
        }
    }
}

哈希值:系统随即给出的一个十进制的整数(对象的逻辑地址,而不是数据实际存储的物理地址)。在Object类中可以通过int hashCode()方法得到对象的哈希值

public native int hashCode(){};
public class Hash {
    public static void main(String[] args) {
        String s1 = "Forlogen";
        String s2 = "kobe";

        System.out.println(s1.hashCode()); // 538205156
        System.out.println(s2.hashCode()); // 3297447
    } 
}

2.2 底层实现

HashSet存储数据的结构为哈希表

  • JDK1.8之前哈希表通过数组 + 链表实现
  • JDK1.8之后哈希表通过数组 + 红黑树实现

其中数组存储数据的哈希值,链表和红黑树存储相同哈希值的数据。HashSet如何实现存储的数据不重复呢?原理是HashSet重写了hashCode()equals()当需要判断两个对象是否相等时,不仅需通过hashCode()判断,还需要equals()的返回值也相同。下面给出一个HashSet如何存储元素的例子。

示意图:


在这里插入图片描述

如上所示,list中包含有三个元素 [ F o r l o g e n , F o r l o g e n , k o b e ] [Forlogen, Forlogen, kobe] [Forlogen,Forlogen,kobe],那么在将其添加到HashSet中会有如下过程:

  • 添加第一个"Forlogen":调用hashCode()计算它对应的哈希值538205156,在数组找发现没有这个值,那么将其存储到链表中
  • 添加第二个"Forlogen":调用hashCode()计算它对应的哈希值538205156,在数组找发现已经这个值,然后是使用equals()判断它和相应哈希值存储的元素是否相同,发现相同,那么不存储
  • 添加"kobe":调用hashCode()计算它对应的哈希值3297447,在数组找发现没有这个值,那么将其存储到链表中
  • 添加"重地":调用hashCode()计算它对应的哈希值1179395,在数组找发现没有这个值,那么将其存储到链表中
  • 添加"通话":调用hashCode()计算它对应的哈希值1179395,在数组找发现已经这个值,然后是使用equals()判断它和相应哈希值存储的元素是否相同,发现不相同,故将其存储到链表中

源码分析:HaseSet底层实现依赖于HashMap,依赖于Map中键不能重复的特点来检验元素。

 public HashSet() {
         map = new HashMap<>();
 }

HashSet中的add()使用了HashMap中的put()

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

我们找到HashMap中的put(),发现它又调用了putval()方法进行判断

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

putval()的源码为:

/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
         
         // 通过hash值计算元素的位置,然后判断该位置是否有值
         // 如果没有则直接插入,最后返回null
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 如果该位置上已经有其他的元素
            // 则使用hash和equals方法进行判断是否是重复元素
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

如何利用HashSet来存储不重复的自定义类数据呢?同样我们需要重写hashCode()equals(),假设现有Person类如下所示,如果我们不重写hashCode()equals():

package Set;

import java.util.Objects;

public class Person{
    private int age;
    private String name;

    public Person() {
    }

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

那么在往HashSet中添加元素时就无法去重:

import java.util.HashSet;

public class SetMain {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        set.add(new Person(18, "Forlogen"));
        set.add(new Person(20, "kobe"));
        set.add(new Person(20, "kobe"));

        System.out.println(set); // [Person{age=20, name='kobe'}, Person{age=20, name='kobe'}, Person{age=18, name='Forlogen'}]
    }
}

而如果我们重写hashCode()equals():

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person person = (Person) o;
        return getAge() == person.getAge() &&
                Objects.equals(getName(), person.getName());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getAge(), getName());
    }

那么HashSet就可以正常的进行重复元素的去除:

import java.util.HashSet;

public class SetMain {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        set.add(new Person(18, "Forlogen"));
        set.add(new Person(20, "kobe"));
        set.add(new Person(20, "kobe"));

        System.out.println(set); // [Person{age=20, name='kobe'}, Person{age=18, name='Forlogen'}]
    }
}

3. LinkedhashSet

LinkedhashSet的底层实现是哈希表(数组 + 链表/红黑树) + 链表,多加的双向链表用于记录元素的添加顺序,从而保证元素有序。

4. TreeSet

TreeSet是SortedSet接口的实现类,它可以确保集合中的元素处于排序状态,TreeSet具有如下的特点:

  • 能够保证元素唯一性(根据返回值是否是0来决定的),并且按照某种规则排序

  • 自然排序,无参构造方法(元素具备比较性)

  • 按照compareTo()方法排序,让需要比较的元素所属的类实现自然排序接口Comparable,并重写compareTo()

  • 底层是自平衡二叉树结构

    • 二叉树有前序遍历、后序遍历、中序遍历
    • TreeSet类是按照从根节点开始,按照从左、中、右的原则依此取出元素
  • 当使用无参构造方法,也就是自然排序,需要根据要求重写compareTo()方法,这个不能自动生成