List、Set集合系列之剖析HashSet存储原理(HashMap底层)
目录
前言
在之前的博客文章中已经介绍了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接口特点分析:
- 元素存取有序:例如,存元素的顺序是“我”、“是”、“佩”、“奇”,那么集合中,元素的存储就是按照“我”、“是”、“佩”、“奇”的顺序完成的。
- 带有索引的集合:与数组的索引是一个道理
- 元素重复:通过元素的
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.hashset
、java.util.linkedhashset
这两个集合。
set集合取出元素的方式可以采用:迭代器、增强for。
set接口子类
4.1 hashset集合介绍
java.util.hashset
是set
接口的一个实现类,它所存储的元素是不可重复、无序(即存取顺序不一致)。java.util.hashset
底层的实现其实是一个java.util.hashmap
支持。
hashset
是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashcode
与equals
方法。
我们先来使用一下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中存放自定义类型元素时,需要重写对象中的hashcode
和equals
方法,建立自己的比较方式,才能保证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()); } } } 结果: 秃头哥 地中海哥 平头哥 假发哥
最后,欢迎各位关注我的公众号,一起探讨技术,向往技术,追求技术...