Set接口
Set集合继承自Collection集合
类似数学中的集合概念;特点:集合中元素不能重复,且元素无序排列
无序排列理解:元素所在的位置与向集合中添加元素的顺序没有关系
元素不重复原因:底层数据结构是一个哈希表,能保证元素是唯一的,元素不重复!!!
它通过它的接口子实现类HashSet集合去实例化,而HashSet集合底层是HashMap集合的实例!!!
实例1
package org.westos_01; import java.util.HashSet; import java.util.Set; public class SetDemo { /** * 说明问题: * 1)Set集合元素不重复(从打印的内容以及集合元素个数) * 2)集合元素无序(添加的顺序与打印的顺序) * 3)添加、增、删、查询方法的使用---这四个方法的返回值类型都是boolean类型 * 4) Set集合存储字符串元素并遍历---for增强循环(jdk5后的版本) */ public static void main(String[] args) { //创建Set集合对象 Set<String> set = new HashSet<String>() ; //添加元素(增) set.add("hello"); set.add("java") ; set.add("java") ; set.add("world") ; set.add("world") ; set.add("world") ; System.out.println("集合中添加的元素:"+set);//打印的是内容(原因:传入参数类型是String,底层重写了toString()方法) System.out.println("集合中元素的个数:"+set.size()); //增强for遍历 for(String s :set){ System.out.println(s); } //删除元素(删) System.out.println("是否成功删除:"+set.remove("hello"));//删除是否成功 System.out.println("删除后集合中的元素:"+set); //查询 System.out.println("是否包含此元素:"+set.contains("hello")); } }HashSet类
特点:按照哈希算法(了解这个算法)来存取集合的元素
此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 Set 的迭代顺序;特别是它不保证该顺序恒久不变。
注:HashSet有一个子类:LinkedHashSet类
存取和查找的机理
机理:当向集合中添加一个元素时,HashSet会调用对象的hashCode()方法获取哈希码,然后根据这个哈希码进一步计算出对象在集合中存放的位置。
问题1 为什么能保证HashSet集合元素唯一?
当创建一个HashSet集合的对象时,会先调用其构造方法(蓝框标记)看源码1
即:
源码说明:可以看到实际上是创建了一个HashMap类对象的实例,map是HashMap类对象的实例
此时add()添加元素,看对应的源码2
源码说明:其实是调用了HashMap的put方法
看HashMap的put()方法的源码3
源码说明:调用了HashMap中的hash()方法和putVal()方法
看hash方法(重要)的源码4
源码说明:携带hashSet集合元素key,调用对象(传入参数类型)的hashCode()方法,进行位运算;涉及到Object类hashCode()方法重写
看putVal()方法(具体用处不知,后续会提到)
源码5
package 测试集; import java.util.HashMap.Node; import java.util.HashMap.TreeNode; public class Demo { //对putVal方法说明 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) /* 当new HashMap实例时, * 并没有初始化其成员变量transient Node<K,V>[] table;, * 也就是说并没有为table分配内存, * 只有当put元素时才通过resize()方法对table进行初始化。 * */ n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K, V> e; K k; 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; } //如果hash码相同,则调用相应的集合元素(元素参数类型)的equals()方法判断,涉及到equals()方法的重写 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;//如果没有找到该key的结点,则执行插入操作,需要对modCount增1。 if (++size > threshold) resize();//在执行插入操作之后,如果size超过了threshold(阈值容量),这要扩容。 afterNodeInsertion(evict); return null; } } /** (1)通过hash值得到所在bucket的下标,如果为null,表示没有发生碰撞,则直接put;散列表中的slot通常称为bucket (2)如果发生了碰撞,则解决发生碰撞的实现方式:链表还是树。 (3)如果没有找到该key的结点,则执行插入操作,需要对modCount增1。 (5)在执行插入操作时,如果bucket中bin的数量超过TREEIFY_THRESHOLD,则要树化。 (6)在执行插入操作之后,如果size超过了threshold,这要扩容。 */
源码说明:根据获取的hashCode进一步计算对象是否重复
源码6----table 哈希表
源码说明:当new HashMap实例时,并没有初始化其成员变量table(也即并没有为table分配内存)。只有当put元素时才通过resize方法对table进行初始化
源码7----resize()方法(随后补充)
相关链接:1 点击打开链接(比较好),2 点击打开链接,3 点击打开链接,4 点击打开链接,5 点击打开链接,6 点击打开链接
HashSet集合的add()方法,底层是依赖于双列集合HashMap<K,V>的put(K key,V value)来实现的
put(K key,V value):
底层又依赖于HashCode()和equals()方法,传递添加元素的时候,首先判断的是
每一个元素对应的HashCode值是否一样,如果HashCode值一样,还比较他们的equals()方法,由于现在集合存储的是String类型,String类型本身重写了equals()方法,所以,默认比较的是内容是否相同,如果内容相同,这里最终返回的就是第一次存储的那个元素,由这两个方法保证元素唯一性!