Set接口及其实现类HashSet、TreeSet的底层结构与区别

(一)Set接口的特点

A collection that contains no duplicate elements. More formally, sets contain no pair of elements e1 and e2 such that e1.equals(e2), and at most one null element. As implied by its name

Set接口是Collection的子接口,通过上面这段官方文档对Set的介绍可以看出Set的两个特点:

1.无序(插入和取出的顺序不一致)

2.不可重复(所有元素只能出现一次,因此null也只能有一个)

Set的所有方法均来自Collection,没有自己的特有方法。HashSet和TreeSet是Set接口最常用的实现类

(二)HashSet的底层结构

首先还是先来看看官方文档对HashSet的介绍:

This class implements the Set interface, backed by a hash table (actually a HashMap instance). It makes no guarantees as to the iteration order of the set; in particular, it does not guarantee that the order will remain constant over time. This class permits the null element.

总结一下:HashSet继承Set接口,底层是一个哈希表,但是实际上是HashMap(这点可以从源码中看出来)。它是无序的(插入和取出的顺序不一致),它允许空节点。

我们先看一下源码:

image.png

HashSet在构造方法中构造了一个HashMap,因此对源码的解读会放到HashMap中,现在主要说一下HashSet的底层实现结构。

2.1 HashSet为什么是不重复和无序的

这个问题其实知道Hash表如何构造就很容易理解。

1.当有一个元素要进入hash表时,先获取该元素的哈希值,再通过一系列运算得到一个索引。(每个元素都能计算出一个索引值,但索引值并非是有序的,因此最终得到一个无序的哈希表。)

2.当该索引处没有元素,则直接插入

3.若该索引处有元素,则需要逐个通过equals判断,如果依旧不相等,则将元素以链表的形式添加在该索引的后面。如果相等,则覆盖原来的元素,实现不重复。

哈希表的链地址法如图所示:

image.png

(三)HashSet的底层实现

为了更加清除的了解HashSet底层实现,我们通过代码来演示一下,按照以往的思路,我们先创建一个简单的类:

public class Book {
    private String name;
    private float price;
    public Book(String name, float price){
        this.name=name;
        this.price=price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public float getPrice() {
        return price;
    }
    public void setPrice(float price) {
        this.price = price;
    }
    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

然后再实现HashSet,在这里我故意让两个元素重复

public class HashSettest1 {
    public static void main(String[] args) {
        HashSet hashSet=new HashSet();
        hashSet.add(new Book("aa",20));
        hashSet.add(new Book("bb",10));
        hashSet.add(new Book("bb",10));
        System.out.println(hashSet);
    }
}

但是结果却并没有把我们认为的重复元素(名称和价格相等就算重复)给去重

image.png

我们前面讲过,HashSet会先计算一个元素的哈希值,然后通过得到的索引用equals和已存在元素进行比较。但是在Book类中没有重写hashcode和equals方法,相当于它并不是按照我们的想法(名称和价格相等就算重复)去判断是否重复,而是根据这个对象计算了一个哈希值。

因此在Book中重写一个HashCode方法和equals方法,这两个方法编译器可以直接生成,为了让大家能理解我自己手写一个简单的HashCode方法和equals方法

@Override
public int hashCode() {
    return name.hashCode()+(int)price;
}
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (getClass() != o.getClass()) return false;
    Book book = (Book) o;
    return price==book.price &&
            name.equals(book.name);
}

我通过名称和价格生成一个hashcode去生成哈希值,equals只有当名称和价格都相等时才算相等。

但是我现在手写的这个hashcode方式存在一个问题,即使name的hash值和price不同,但是加起来是有可能相同的,虽然后续还有equals方法可以防止hashcode的误差,但是增加了计算负担。

idea自动生成的两个方法如下:

@Override
public int hashCode() {
    return Objects.hash(name, price);
}
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Book book = (Book) o;
    return Float.compare(book.price, price) == 0 &&
            Objects.equals(name, book.name);
}

进入hashcode源码后可以看到,对于计算出的值它又乘了一个31,避免哈希值一致但是实际不一致的情况

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;
    int result = 1;
    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());
    return result;
}

为什么选择31作为这个因子,《Effective Java》作了这样的解释:

之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5)- i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。

简而言之,使用 31 最主要的还是为了性能。

(四)TreeSet的结构和运行原理

首先还是来看看官方文档对TreeSet的介绍:

A NavigableSet implementation based on a TreeMap. The elements are ordered using their natural ordering, or by a Comparator provided at set creation time, depending on which constructor is used.

TreeSet底层结构是基于TreeMap的,这一点和HashSet类似。

4.1 TreeSet的特点

1.不允许重复(不允许有null值)

2.可以实现对元素的排序(自然排序、定制排序)

自然排序:添加的元素实现Comparable接口,实现里面的compareTo方法

定制排序:创建TreeMap对象时,传入一个Comparator接口,并实现里面的compare方法

3.底层是一个TreeMap,TreeMap底层是一个红黑树结构,可以实现对元素的排序

4.2 TreeSet的实际操作

还是新建一个最普通的Book类:

public class Book {
    private String name;
    private float price;
    public Book(String name, float price){
        this.name=name;
        this.price=price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public float getPrice() {
        return price;
    }
    public void setPrice(float price) {
        this.price = price;
    }
    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

然后新建一个类去使用TreeSet:

public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet treeSet=new TreeSet();
        treeSet.add(new Book("a",10));
        treeSet.add(new Book("b",5));
        treeSet.add(new Book("c",20));
        System.out.println(treeSet);
    }
}

但是结果是报错的,报错的原因是Book类没有继承Comparable接口

image.png

解决办法一(自然排序):在Book类中继承Comparable接口,重写compareTo方法:

public class Book implements Comparable{
    ......
    @Override
    public int compareTo(Object o) {
        Book book= (Book) o;
        return Float.compare(this.price,book.price);
}
}

解决办法二(定制排序):在建TreeMap对象时,传入一个Comparator接口,并实现里面的compare方法。

public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet treeSet=new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                Book book1= (Book) o1;
                Book book2= (Book) o2;
                return Float.compare(book1.getPrice(),book2.getPrice());
            }
        });
        treeSet.add(new Book("a",10));
        treeSet.add(new Book("b",5));
        treeSet.add(new Book("c",20));
        System.out.println(treeSet);
    }
}

4.3 TreeSet去重的方法

前面讲到hashSet去重的方法是hashcode和equals方法判断相同则覆盖,TreeSet是通过compareTo方法的返回值来判断是否相同,如果返回值为0则认定是重复元素。

(五)总结

最后来总结一些HashSet和TreeSet的区别:

1、TreeSet 是二叉树(红黑树)实现的,Treeset中的数据是自动排好序的,不允许放入null值。

2、HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复。

3、HashSet要求放入的对象实现HashCode()和equals()方法,TreeSet要求放入的对象继承Comparable接口并实现compareTo方法或者在建TreeMap对象时,传入一个Comparator接口,并实现里面的compare方法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容