Set简述
特点:不重复,无序(插入和存储顺序不同),只允许存在一个null
Set的实现类有:
HashSet
LinkedHashSet(HashSet的子类):他是有序的!!
TreeSet
1 HashSet
1.1不多比比,先看源码
结论:HashSet是HashMap的一个实例。set的底层就是hashMap,所有的操作都是基于map的操作。
Set接口是一种不包括重复元素的Collection,它维持它自己的内部排序,所以随机访问没有任何意义。
1.2HashSet的存储过程
简述
他的存储方式使他具无序跟不可重复的特性.
在JDK1.8之前,哈希表底层采用 数组+链表 实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。 但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过8时,将链表转换为红黑树,这样大大减少了查找时间。
哈希表(散列表)原理详解
二叉树简介笔记
扩展:加载因子
扩展:加载因子
HashMap(HashSet)默认的加载因子是0.75,最大容量是16,因此可以得出HashMap的默认容量是:0.75*16=12。所以到容量到12的时候就会扩容。
存储过程:
(1)首先,通过对象的hashCode方法算出对象的哈希码值,它内部采用哈希码对某个数字n进行取余的方式对哈希码进行分组和划分对象的存储区域。(这可以看出HashSet存储跟插入顺序不一样)
(2)如果算出来的存储位置的索引值上有元素对象,这种现象,称为哈希碰撞
或者哈希冲突
。需要存储的元素对象就与该位置已经存在的对象进行比较,通过equals
方法(所以自定义类就要重写hashcode方法和equals方法),判断是否相等,如果相等,不插入。如果不相等,需要在该位置后面插入该元素对象。
存储结构大概如下图:
图摘自:https://blog.csdn.net/qq_41294444/article/details/82106775
1.3HashSet存储的自定义类 去除重复值
HashSet就是采用哈希算法存取对象的集合,它内部用哈希值对某个数字n进行取余的方式对哈希码进行分组和划分对象的存储区域。然后根据哈希码找到相应的存储区域,最后取出该存储区域内的每个元素与该对象进行equals方法比较,这样不用遍历集合中的所有元素就可以的到结论
HashSet去重原理:先走hashCode()分组,跟组员用equals()比较,不同的话直接保存
重写hashCode的原则:
1.相同的对象,一定要有相同的hashCode值(相同hashCode一定在同一存储区域,一定会走equals方法,如果重写了equals,就肯定是相同的,就不保存了)
2.不同的对象,尽可能有不同的hashCode值(不同就尽量不在同一内存区域)
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((sNo == null) ? 0 : sNo.hashCode());
return result;
}
重写方法解释:这样的话就是判断属性值的hashcode,如果属性值一样,这两个对象一定是一样的,他们的hashcode就必须一样。所以 可以写成return name.hashCode()+age.hashCode()
,但这样有可能不一样的属性值的和是一样的呀。
要满足,不同的对象要尽量有不同的hashcode,就需要上边代码的优化了。
重写hashCode()为什么prime是31?
问题:为什么prime是31?
原因一:更少的乘积结果冲突
31是质子数中一个“不大不小”的存在,如果你使用的是一个如2的较小质数,那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。而如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),那么这几个数就被作为生成hashCode值得备选乘数了。
原因二:31可以被JVM优化
JVM里最有效的计算方式就是进行位运算了:
左移 << : 左边的最高位丢弃,右边补全0(把 << 左边的数据2的移动次幂)。
右移 >> : 把>>左边的数据/2的移动次幂。
无符号右移 >>> : 无论最高位是0还是1,左边补齐0。
所以 : 31 * i = (i << 5) - i(左边 312=62,右边 2*2^5-2=62) - 两边相等,JVM就可以高效的进行计算啦。。。
总结:hashCode方法决定存储位置;equals方法决定是追加还是覆盖。重写自定义类的hashCode方法时一定要保证散列值相同的情况下,equals方法一定要返回true
2TreeSet
2.1TreeSet简述:
① TreeSet 底层 TreeMap,存储结构是红黑树
。
②无序,不可重复,可排序
2.2问题
问题1:TreeSet为什么是无序的?
首先,他底层是基于TreeMap的红黑树,不是HashSet的底层数组+链表+红黑树。但他的无序,不可重复,可排序的特点肯定也是因为他的底层存储数据结构。
因为他是可排序的(元素的自然排序),所以存储的位置不一定跟插入顺序一样,所以就导致了他的无序。
问题2:那么他的自然排序是怎么实现的呢?
如果直接创建TreeSet集合并添加自定义类并输出,会报 class cannot be cast to java.lang.Comparable
问题3,怎么实现不可重复的?区别hashSet
跟自然排序一样。实现compareTo()或第三方比较器Comparator,只要两个元素比较完是一样的就判定为同一个元素(这就很局限性,只会保存比较结果不同的元素,添加新元素的时候会用compare比较,如果顺序一样(compare方法return 0)就不添加了)。他的去重不用重写hashCode和equals
排序方法1 使用无参构造方法TreeSet()
自定义类实现Comparable接口,并实现compareTo(T o)方法
① 在自定义类实现Comparable 接口 ,可以对该类所对应的集合进行排序,这种排序 称为元素的自然顺序排序。
class Employee implements Comparable<Employee>{.实现compareTo() ..}
② 实现该接口的方法 :compareTo(T o) (ps:实现代码在下边)
1)返回值 大于0 (对象在参数后) 小于0 (对象在参数前 ) 等于0 (相等)
(2)TreeSet集合默认无参构造方法,按照元素的自然顺序进行排序,要求存入该集合的元素自定义类的对象的所在类 必须要实现Comparable,实现接口中的方法:
compareTo方法,否则会报异常 ClassCastException 类型转换异常。
Exception in thread "main" java.lang.ClassCastException
Person cannot be cast to java.lang.Comparable
,类型转换异常,不能将自定义类转换成Comparable类型。
排序方法2:使用构造方法TreeSet(Comparator<? super E> comparator)
创建Comparator接口的实现类,并实现compare(),传该实现类对象给构造方法
class ComparatorStudents implements Comparator<Students>{...实现compare()...}
实现compare方法跟下边的compareTo是一样的。
TreeSet<Students> ts = new TreeSet<>(new ComparatorStudents());
Comparator : 强行对某个对象集合进行整体排序。
实现compare(T o1, T o2) 方法 。
适用范围 : 当集合中的对象对应的类不能实现Comparable接口的时候,此时,就需要通过第三方比较器来进行比较。
重写compareTo()
重写Comparator 的compare()是类似的。
排序规则:salary高的在前面,salary相同时age大的在前面,age也相同时按照name升序排列。代码如下:
@Override
public int compareTo(Employee o) {
// TODO Auto-generated method stub
// 如果工资相同
if (this.salary == o.salary) {
// 如果年龄也相同
if (this.age == o.age) {
return this.name.compareTo(o.name);
}
return -(this.age - o.age);
}
return (int) -(this.salary - o.salary);
}
注意:
TreeSet的排序和不可重复都是由compareTo( )/compare( )决定的,只要compareTo()返回值是0,就代表两个元素一样(但其实并不是这样的,他的意思是只要排序的位置一样就判定为相同的元素)。
举例说明:
public class TestTreeSet2 {
public static void main(String[] args) {
TreeSet<Students> ts = new TreeSet<>(new ComparatorStudents());
ts.add(new Students("aa", "001", 21));
ts.add(new Students("aa", "002", 20));
ts.add(new Students("aa", "004", 21));
for(Students s:ts) {
System.out.println(s);
}
}
}
class ComparatorStudents implements Comparator<Students>{
@Override
public int compare(Students o1, Students o2) {
return o1.getName().compareTo(o2.getName());
}
}
输出结果只有:Students [name=aa, sNo=001, age=21]
3LinkedHashSet
HashSet的子类。该集合底层是哈希表与双向链表组成,插入与存储顺序相同。
有序不重复
遍历集合
很简单了呀,增强for遍历万物;set也有Iterator迭代器。
转数组遍历就low了。