Set接口基本介绍
- 无序(添加和取出的顺序不一样),没有索引
- 不允许重复元素,最多包含一个null
- 实现类有HashSet,TreeSet,LinkedHashSet等。
Set常用方法
和List一样,都是Collection的子接口,因此常用方法和Collection一样
可以使用迭代器和增强for来遍历,不能使用索引遍历。
取出的顺序虽然和添加的不一样,但是顺序是固定的。(不论迭代多少次)
HashSet
- HashSet实现了Set接口
- HashSet底层数据结构是HashMap
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
- 可以存放null值,但是只能有一个null
- HashSet不保证元素是有序的,取决于hash后,再确定索引的结果。(即,不保证存放元素的顺序和取出顺序一致)
- 不能有重复的元素/对象
public static void main(String[] args) {
Set set = new HashSet();
set.add("1");
set.add("1");
set.add(new Employee("张三", "23"));
set.add(new Employee("张三", "23"));
System.out.println(set);
set = new HashSet();
set.add(new String("2"));
set.add(new String("2"));
System.out.println(set);
}
看上面的代码,add("1")执行了两次,但是只有一个字符串被add进了对象
下面的两个对象,虽然属性值都相同,但是却不是一个对象,所以两个都被add进去了。
而最下面的两个相同字符串,却只能被保存进一个,是因为什么呢?这就要等到我了解到HashSet的底层去重原理时再来补充吧。今天就这样,这个端午三天假期我过得很happy,做了很多平时没时间去做的事情,很充实。
端午安康~
模拟一个HashMap
继续昨天的学习:
先模拟一个HashMap,真正的HashMap底层是(数组+链表+红黑树)
@SuppressWarnings("all")
public static void main(String[] args) {
Node node = new Node("红豆", null);
Node node2 = new Node("超群", null);
Node node3 = new Node("婉嫔", null);
Node node4 = new Node("阿悦", null);
Node[] table = new Node[16];
table[5] = node;
System.out.println("table" + table);
node.next = node2;
System.out.println("table" + table);
table[6] = node3;
node2.next = node4;
System.out.println("table" + table);
}
我们可以打个断点来看一下table里的情况。
现在可以看到数组中一共有16个元素,索引为5的元素里的next下还有元素,这样就形成一个单链表。而索引为6的元素,next为null。
差不多是这个样子,真正的HashSet底层的扩容机制是下面这样的:(先说结论,之后分析源码)
- HashSet底层是HashMap
- 添加一个元素时,先得到hash值 - 会转成索引值
- 找到存储数据表table,看这个索引位置是否已经存放的有元素
- 如果没有,直接加入
- 如果有,调用equals比较(这个方法可以咱们自己重写来控制比如String类就重写了),如果相同,就放弃添加;如果不相同,则添加到最后
- 在Java8中,如果一条链表的元素个数大于或等于TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
好的各位,刚看这里大家可能还是一头雾水,接下来就去进行源码的分析:
HashSet源码分析,分析HashSet的添加元素底层是如何实现( hash() + equals() )
韩顺平的课分析HashSet底层源码时有四个视频,看来这里真的很复杂
首先创建一个类
public class HashSetDemo2_ {
@SuppressWarnings({"all"})
public static void main(String[] args) {
HashSet set = new HashSet();
set.add("java");
set.add("php");
set.add("java");
System.out.println("set=" + set);
}
}
运行这段代码后,set对象里会有[java,php],那么HashSet是如何把java字符串添加进去的呢?
首先执行这个构造器,创建了一个HashMap对象
接着执行HashMap的put方法
private static final Object PRESENT = new Object();
PRESENT我理解为是一个占位对象,每次执行add方法时都会传入一个新的
之后执行HashMap的putVal方法
稍等一下,这里会先执行hash方法,传入我们要添加的对象,也就是key,来算出对应的hash值
如果key为null,则直接返回0,这也解释了为什么HashSet中如果有null,则查询时null的索引为0。
如果key不为null,则通过后面的算法来计算出key对应的hash值,右移16位是为了防止hash碰撞
这里如果面试官问了这块得到的hash值是不是就是hashCode,我们要果断的回答不是,因为这里做了处理。
接下来执行最复杂最重要的方法putVal
由于 tab为null 或者 大小=0,所以会进入到下一步,也就是第一次扩容,到16个空间,去执行resize方法,
首先由于table为null,则oldTab也为null
这样oldCap为0,oldThr为0,newCap,newThr都为0
由于oldCap为0,不大于0,所以进入到这一步,这一步会给newCap赋值为一个默认值DEFAULT_INITIAL_CAPACITY为16,newThr为临界值,由(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)算出,DEFAULT_LOAD_FACTOR为0.75,这个是负载因子,最后得到的临界值为12。
此时threshold临界值为12
用newCap(16)创建了一个新的Node数组,并赋给了table,这样table就是一个size为16的数组,最终返回table
接下来得到n为16
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
/**
* 这段代码的作用是
* (1) i = (n - 1) & hash => 根据key得到的hash 去计算该key应该存放到table表的
* 哪个索引位置
* 并把这个位置的对象,赋给 p
* (2) 接着去判断p是否为null
* (2.1) 如果p为null,表示还没有存放元素,此时创建一个Node(key ="java",value = PRESENT)
* (2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null);
**/
接着向下执行,可以看到tab中对应的索引已经存入了这个新的Node结点
有hash值,key就是我们传入的对象,value为PRESENT,next为null。
执行++size,如果这个值 > 临界值则进行下一次扩容。(这个下一次说)
这个afterNodeInsertion(evict)方法其实没有实现,是留给比如LinkedHashMap类去实现的,可以进行比如对于元素进行排序等操作。
最后返回null,就代表add成功了,否则返回oldValue,代表该元素之前已经存在了。
走回到这里,返回true代表添加成功。
此时,java字符串已被成功add进set对象中。