List、Set集合系列之剖析HashSet存储原理(HashMap底层)

2022-10-13,,,,

目录

  • list接口
    • 1.1 list接口介绍
    • 1.2 list接口中常用方法
  • list的子类
    • 2.1 arraylist集合
    • 2.2 linkedlist集合
  • set接口
    • 3.1 set接口介绍
  • set接口子类
    • 4.1 hashset集合介绍
    • 4.2 hashset集合存储数据的结构(哈希表)
    • 4.4 hashset存储自定义类型元素
    • 4.5 linkedhashset

前言

在之前的博客文章中已经介绍了collection接口使用,本篇将介绍collection接口中的子类的用法,至于为啥要讲它的子类这种小白问题就不要问我了。啥?有小白在看我写的文章...不好意思不好意思,原谅我刚才说的话,请允许博主我重新组织一下语言...咳咳,至于为啥要讲collection接口的子类呢?小白童鞋啊,collection接口他是接口哇,接口的目的是啥?就是定义一套规范,没有具体类去实现接口,接口就毫无意义了!小白童鞋你何什左咩鸭。

还有一点就是如果对collection接口还不熟悉的小白童鞋强烈建议先去了解collection接口之后再看这篇文章,不然只会事倍功半!好吧,我就知道就是博主我强烈建议过了肯定还有小白童鞋不去找,没事,博主没别的目的,就是想让小白童鞋好好学java,所以我已经准备好了下面这篇文章~点击蓝色字体即可进入~collection集合以及iterator迭代器实现原理

@

list接口

接下来,我们一起学习collection中的常用两个子类java.util.list集合、java.util.set集合。

1.1 list接口介绍

java.util.list接口继承自collection接口,在list集合元素可重复元素有序。所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素,而且元素的存入顺序和取出顺序一致。

list接口特点分析:

  1. 元素存取有序:例如,存元素的顺序是“我”、“是”、“佩”、“奇”,那么集合中,元素的存储就是按照“我”、“是”、“佩”、“奇”的顺序完成的。
  2. 带有索引的集合:与数组的索引是一个道理
  3. 元素重复:通过元素的equals方法,来比较是否为重复的元素。

1.2 list接口中常用方法

list作为collection集合的子接口,不但继承了collection接口中的全部方法,还有一些根据元素索引操作集合特有方法,如下:

public void add(int index, e element): 将指定的元素,添加到该集合中的指定位置上。- public e get(int index):返回集合中指定位置的元素。
public e remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
public e set(int index, e element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。

list集合特有的方法都是跟索引相关,代码如下:

public class listdemo {
    public static void main(string[] args) {
        // 创建list集合对象
        list<string> list = new arraylist<string>();
        
        // 往 尾部添加 指定元素
        list.add("安琪拉屎");
        list.add("刘备胎");
        list.add("廉颇妇");
        
        system.out.println(list);
        // add(int index,string s) 往指定位置添加
        list.add(1,"猪脚亮");
        
        system.out.println(list);
        // string remove(int index) 删除指定位置元素  返回被删除元素
        // 删除索引位置为2的元素 
        system.out.println("删除索引位置为2的元素");
        system.out.println(list.remove(2));
        
        system.out.println(list);
        
        // string set(int index,string s)
        // 在指定位置 进行 元素替代(改) 
        // 修改指定位置元素
        list.set(0, "东皇太二");
        system.out.println(list);
        
        // string get(int index)  获取指定位置元素
        
        // 跟size() 方法一起用  来 遍历的 
        for(int i = 0;i<list.size();i++){
            system.out.println(list.get(i));
        }
        //还可以使用增强for
        for (string string : list) {
            system.out.println(string);
        }   
    }
}

list的子类

2.1 arraylist集合

java.util.arraylist集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以arraylist是最常用的集合。

2.2 linkedlist集合

java.util.linkedlist集合数据存储的结构是链表结构。元素增删快,查找慢的集合。

linkedlist是一个双向链表,那么双向链表是什么样子的呢?

实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而linkedlist提供了大量首尾操作的方法。下面这些方法我们作为了解即可

  • public void addfirst(e e):将指定元素插入此列表的开头。
  • public void addlast(e e):将指定元素添加到此列表的结尾。
  • public e getfirst():返回此列表的第一个元素。
  • public e getlast():返回此列表的最后一个元素。
  • public e removefirst():移除并返回此列表的第一个元素。
  • public e removelast():移除并返回此列表的最后一个元素。
  • public e pop():从此列表所表示的堆栈处弹出一个元素。
  • public void push(e e):将元素推入此列表所表示的堆栈。
  • public boolean isempty():如果列表不包含元素,则返回true。

linkedlist是list的子类,list中的方法linkedlist都是可以使用,这里就不做详细介绍,我们只需要了解linkedlist的特有方法即可。在开发时,linkedlist集合也可以作为堆栈,队列的结构使用。(了解即可)

方法代码如下:

public class linkedlistdemo {
    public static void main(string[] args) {
        linkedlist<string> link = new linkedlist<string>();
        //添加元素
        link.addfirst("大乔");
        link.addfirst("小桥");
        link.addfirst("老乔");
        system.out.println(link);
        // 获取元素
        system.out.println(link.getfirst());
        system.out.println(link.getlast());
        // 删除元素
        system.out.println(link.removefirst());
        system.out.println(link.removelast());

        while (!link.isempty()) { //判断集合是否为空
            system.out.println(link.pop()); //弹出集合中的栈顶元素
        }

        system.out.println(link);
    }
}

好了,到这里,list集合就先告一段落。

set接口

3.1 set接口介绍

java.util.set接口和java.util.list接口一样,同样继承自collection接口,它与collection接口中的方法基本一致,并没有对collection接口进行功能上的扩充,只是比collection接口更加严格了。与list接口不同的是,set接口中元素无序不重复,刚好全与list相反,set会以某种规则保证存入的元素不出现重复。

set集合有多个子类,这里我们介绍其中的java.util.hashsetjava.util.linkedhashset这两个集合。

set集合取出元素的方式可以采用:迭代器、增强for。

set接口子类

4.1 hashset集合介绍

java.util.hashsetset接口的一个实现类,它所存储的元素是不可重复、无序(即存取顺序不一致)。java.util.hashset底层的实现其实是一个java.util.hashmap支持。

hashset是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashcodeequals方法。

我们先来使用一下set集合存储,看下现象,再进行原理的讲解:

public class hashsetdemo {
    public static void main(string[] args) {
        //创建 set集合
        hashset<string>  set = new hashset<string>();

        //添加元素
        set.add(new string("安琪拉屎"));
        set.add("刘备胎");
        set.add("猪八戒烟"); 
        set.add("安琪拉屎");  
        //遍历
        for (string name : set) {
            system.out.println(name);
        }
    }
}

输出结果如下,说明集合中不能存储重复元素:

安琪拉屎
刘备胎
猪八戒烟

根据结果我们发现字符串 "安琪拉屎" 只存储了一个,也就是说重复的元素set集合不存储。

4.2 hashset集合存储数据的结构(哈希表)

什么是哈希表呢?

jdk1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而jdk1.8中哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

简单的来说,哈希表是由数组+链表+红黑树(jdk1.8增加了红黑树部分)实现的,如下图所示。

看到这张图就有童鞋要问了,这个是怎么存储的呢?看下图就明白了

总而言之,jdk1.8引入红黑树大程度优化了hashmap的性能,那么对于我们来讲保证hashset集合元素的唯一,其实就是根据对象的hashcode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashcode和equals方法建立属于当前对象的比较方式。

至于数据结构关于数组以及链表我之前写过,为了方便各位阅读,我就贴在下面了

啥?还要看红黑树?额...暂时还没写,如果不是特别急着看博主就往后推一点写,~毕竟忙嘛~ 实在急着看博主尽量抽空写一篇出来...

4.3源码分析

qnq

public class hashset<e>  
    extends abstractset<e>  
    implements set<e>, cloneable, java.io.serializable  
{  
    static final long serialversionuid = -5024744406713321676l;  
 
    // 底层使用hashmap来保存hashset中所有元素。  
    private transient hashmap<e,object> map;  
 
    // 定义一个虚拟的object对象作为hashmap的value,将此对象定义为static final。  
    private static final object present = new object();  
 
    /** 
     * 默认的无参构造器,构造一个空的hashset。 
     *  
     * 实际底层会初始化一个空的hashmap,并使用默认初始容量为16和加载因子0.75。 
     */  
    public hashset() {  
    map = new hashmap<e,object>();  
    }  
 
    /** 
     * 构造一个包含指定collection中的元素的新set。 
     * 
     * 实际底层使用默认的加载因子0.75和足以包含指定 
     * collection中所有元素的初始容量来创建一个hashmap。 
     * @param c 其中的元素将存放在此set中的collection。 
     */  
    public hashset(collection<? extends e> c) {  
    map = new hashmap<e,object>(math.max((int) (c.size()/.75f) + 1, 16));  
    addall(c);  
    }  
 
    /** 
     * 以指定的initialcapacity和loadfactor构造一个空的hashset。 
     * 
     * 实际底层以相应的参数构造一个空的hashmap。 
     * @param initialcapacity 初始容量。 
     * @param loadfactor 加载因子。 
     */  
    public hashset(int initialcapacity, float loadfactor) {  
    map = new hashmap<e,object>(initialcapacity, loadfactor);  
    }  
 
    /** 
     * 以指定的initialcapacity构造一个空的hashset。 
     * 
     * 实际底层以相应的参数及加载因子loadfactor为0.75构造一个空的hashmap。 
     * @param initialcapacity 初始容量。 
     */  
    public hashset(int initialcapacity) {  
    map = new hashmap<e,object>(initialcapacity);  
    }  
 
    /** 
     * 以指定的initialcapacity和loadfactor构造一个新的空链接哈希集合。 
     * 此构造函数为包访问权限,不对外公开,实际只是是对linkedhashset的支持。 
     * 
     * 实际底层会以指定的参数构造一个空linkedhashmap实例来实现。 
     * @param initialcapacity 初始容量。 
     * @param loadfactor 加载因子。 
     * @param dummy 标记。 
     */  
    hashset(int initialcapacity, float loadfactor, boolean dummy) {  
    map = new linkedhashmap<e,object>(initialcapacity, loadfactor);  
    }  
 
    /** 
     * 返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的。 
     *  
     * 底层实际调用底层hashmap的keyset来返回所有的key。 
     * 可见hashset中的元素,只是存放在了底层hashmap的key上, 
     * value使用一个static final的object对象标识。 
     * @return 对此set中元素进行迭代的iterator。 
     */  
    public iterator<e> iterator() {  
    return map.keyset().iterator();  
    }  
 
    /** 
     * 返回此set中的元素的数量(set的容量)。 
     * 
     * 底层实际调用hashmap的size()方法返回entry的数量,就得到该set中元素的个数。 
     * @return 此set中的元素的数量(set的容量)。 
     */  
    public int size() {  
    return map.size();  
    }  
 
    /** 
     * 如果此set不包含任何元素,则返回true。 
     * 
     * 底层实际调用hashmap的isempty()判断该hashset是否为空。 
     * @return 如果此set不包含任何元素,则返回true。 
     */  
    public boolean isempty() {  
    return map.isempty();  
    }  
 
    /** 
     * 如果此set包含指定元素,则返回true。 
     * 更确切地讲,当且仅当此set包含一个满足(o==null ? e==null : o.equals(e)) 
     * 的e元素时,返回true。 
     * 
     * 底层实际调用hashmap的containskey判断是否包含指定key。 
     * @param o 在此set中的存在已得到测试的元素。 
     * @return 如果此set包含指定元素,则返回true。 
     */  
    public boolean contains(object o) {  
    return map.containskey(o);  
    }  
 
    /** 
     * 如果此set中尚未包含指定元素,则添加指定元素。 
     * 更确切地讲,如果此 set 没有包含满足(e==null ? e2==null : e.equals(e2)) 
     * 的元素e2,则向此set 添加指定的元素e。 
     * 如果此set已包含该元素,则该调用不更改set并返回false。 
     * 
     * 底层实际将将该元素作为key放入hashmap。 
     * 由于hashmap的put()方法添加key-value对时,当新放入hashmap的entry中key 
     * 与集合中原有entry的key相同(hashcode()返回值相等,通过equals比较也返回true), 
     * 新添加的entry的value会将覆盖原来entry的value,但key不会有任何改变, 
     * 因此如果向hashset中添加一个已经存在的元素时,新添加的集合元素将不会被放入hashmap中, 
     * 原来的元素也不会有任何改变,这也就满足了set中元素不重复的特性。 
     * @param e 将添加到此set中的元素。 
     * @return 如果此set尚未包含指定元素,则返回true。 
     */  
    public boolean add(e e) {  
    return map.put(e, present)==null;  
    }  
 
    /** 
     * 如果指定元素存在于此set中,则将其移除。 
     * 更确切地讲,如果此set包含一个满足(o==null ? e==null : o.equals(e))的元素e, 
     * 则将其移除。如果此set已包含该元素,则返回true 
     * (或者:如果此set因调用而发生更改,则返回true)。(一旦调用返回,则此set不再包含该元素)。 
     * 
     * 底层实际调用hashmap的remove方法删除指定entry。 
     * @param o 如果存在于此set中则需要将其移除的对象。 
     * @return 如果set包含指定元素,则返回true。 
     */  
    public boolean remove(object o) {  
    return map.remove(o)==present;  
    }  
 
    /** 
     * 从此set中移除所有元素。此调用返回后,该set将为空。 
     * 
     * 底层实际调用hashmap的clear方法清空entry中所有元素。 
     */  
    public void clear() {  
    map.clear();  
    }  
 
    /** 
     * 返回此hashset实例的浅表副本:并没有复制这些元素本身。 
     * 
     * 底层实际调用hashmap的clone()方法,获取hashmap的浅表副本,并设置到hashset中。 
     */  
    public object clone() {  
        try {  
            hashset<e> newset = (hashset<e>) super.clone();  
            newset.map = (hashmap<e, object>) map.clone();  
            return newset;  
        } catch (clonenotsupportedexception e) {  
            throw new internalerror();  
        }  
    }  
}  

说白了,hashset就是限制了功能的hashmap,所以了解hashmap的实现原理,这个hashset自然就通,对于hashset中保存的对象,主要要正确重写equals方法和hashcode方法,以保证放入set对象的唯一性,虽说是set是对于重复的元素不放入,倒不如直接说是底层的map直接把原值替代了(这个set的put方法的返回值真有意思)。hashset没有提供get()方法,愿意是同hashmap一样,set内部是无序的,只能通过迭代的方式获得。

4.4 hashset存储自定义类型元素

给hashset中存放自定义类型元素时,需要重写对象中的hashcodeequals方法,建立自己的比较方式,才能保证hashset集合中的对象唯一

创建自定义student类

public class student {
    private string name;
    private int age;

    public student() {
    }

    public student(string name, int age) {
        this.name = name;
        this.age = age;
    }

    public string getname() {
        return name;
    }

    public void setname(string name) {
        this.name = name;
    }

    public int getage() {
        return age;
    }

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

    @override
    public boolean equals(object o) {
        if (this == o)
            return true;
        if (o == null || getclass() != o.getclass())
            return false;
        student student = (student) o;
        return age == student.age &&
               objects.equals(name, student.name);
    }

    @override
    public int hashcode() {
        return objects.hash(name, age);
    }
}
public class hashsetdemo2 {
    public static void main(string[] args) {
        //创建集合对象   该集合中存储 student类型对象
        hashset<student> stuset = new hashset<student>();
        //存储 
        student stu = new student("程序员老王", 43);
        stuset.add(stu);
        stuset.add(new student("程序员小王", 44));
        stuset.add(new student("程序员老王", 43));
        stuset.add(new student("程序员秃头哥", 23));
        stuset.add(stu);

        for (student stud : stuset) {
            system.out.println(stud);
        }
    }
}
执行结果:
student [name=程序员小王, age=44]
student [name=程序员老王, age=43]
student [name=程序员秃头哥, age=23]

4.5 linkedhashset

我们知道hashset保证元素唯一,可是元素存放进去是没有顺序的,那么我们要保证有序,怎么办呢?在hashset下面有一个子类java.util.linkedhashset,它是链表和哈希表组合的一个数据存储结构。

代码如下:

public class linkedhashsetdemo {
    public static void main(string[] args) {
        set<string> set = new linkedhashset<string>();
        set.add("秃头哥");
        set.add("地中海哥");
        set.add("平头哥");
        set.add("假发哥");
        iterator<string> it = set.iterator();
        while (it.hasnext()) {
            system.out.println(it.next());
        }
    }
}
结果:
 秃头哥
 地中海哥
 平头哥
 假发哥

最后,欢迎各位关注我的公众号,一起探讨技术,向往技术,追求技术...

《List、Set集合系列之剖析HashSet存储原理(HashMap底层).doc》

下载本文的Word格式文档,以方便收藏与打印。