备注2022/6/3
底层由哈希表实现,对集合的迭代顺序不作任何保证,即哈希表存入内容顺序与输出内容顺序不一致;
案例:
// 216-test2
public class SetDemo {
public static void main(String[] args){
//创建集合对象
Set<String> sArray = new HashSet<String>();
sArray.add("hello");
sArray.add("java");
sArray.add("world");
sArray.add("world");
//输出结果没有 两个world,说明Set集合不能有重复元素
for (String s : sArray){
System.out.println(s);
}
//输出内容是:java
//world
//hello
// 表示Set集合不保证迭代顺序
}
}
public int hashCode(): 返回对象的哈希码值
1 同一个对象多次调用hashCode()方法返回的哈希值是相同的
2 默认情况下,不同对象的哈希值是不相同的
3 通过方法重写,可以实现不同对象的哈希值是相同的
// 216-test2
public class HashDemo {
public static void main(String[] args){
Student s1 = new Student("汪苏泷",33);
//相同对象的哈希值是相同的
System.out.println(s1.hashCode());
System.out.println(s1.hashCode());//460141958
System.out.println("-------------");
Student s2 = new Student("许嵩",36);
//默认情况下 不同对象的哈希值是不同的
System.out.println(s2.hashCode());//1163157884
// Student类重写了hashCode方式后,输出的哈希值会相同
//测试常用字符串的哈希值
System.out.println("hello".hashCode());//99162322
System.out.println("java".hashCode());//3254818
System.out.println("world".hashCode());//113318802
System.out.println("--------------");
System.out.println("阳泉".hashCode());//1219830
System.out.println("西安".hashCode());//1114602
System.out.println("-------------");
System.out.println("重地".hashCode());//1179395
System.out.println("通话".hashCode());//1179395
//视频中讲解这个是因为String重写了hashCode方法导致字符串哈希值一样,但是我认为不是,而是字符串重地和通话内部计算哈希值正好一样。
}
}
需求:
创建一个存储学生对象的集合,存储多个学生对象,使用程序实现在控制台遍历该集合
要求:
学生对象的成员变量值相同,我们就认为是同一个对象
关于要求涉及到重写hashcode方法和equals方法
// 216-test2
public class HashSetDemo {
public static void main(String[] args){
HashSet<Student> hs = new HashSet<Student>();
Student s1 = new Student("汪苏泷",33);
Student s2 = new Student("许嵩", 35);
Student s3 = new Student("胡夏", 36);
Student s4 = new Student("胡夏", 36);
hs.add(s1);
hs.add(s2);
hs.add(s3);
hs.add(s4);
// 在Student类中不重写hashcode方法和equals方法 则成员变量相同的对象也会被存储进集合中
// 在Student类中重写hashcode和equals方法,快捷键是 小键盘numlock ==》 alt + insert==》选择hashcode和equals==》接下来全是next
for (Student st : hs){
System.out.println(st.getName() + ", " + st.getAge());
}
}
}
源码分析:
HashSet<String> hs = new HashSet<String>();
hs.add("hello");
hs.add("java");
hs.add("world");
hs.add("world");
HashSet 跟进:
public boolean add(E e) { // e就是传入的字符串元素
return map.put(e, PRESENT)==null;
// 这里表示:
// HashSet在底层是以键值对的形式存储,值存储在HashMap的key的位置,
// HashMap值的位置存储的是persent,仅表示占位符。
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public V put(K key, V value) { // key就是传入的元素,也就是add方法中的e
return putVal(hash(key), key, value, false, true);
// putVal方法的第一个元素:key调用hashCode方法的值
}
//hash值和hashCode方法有关
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// Node[] 表示是一个数组,Node说明是一个节点 哈希表结构的一种实现,元素为节点Node的数组。
// 如果哈希表没有被初始化,则对哈希表进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根据对象的哈希值计算对象的存储位置,如果该位置没有元素,就存储元素
if ((p = tab[i = (n - 1) & hash]) == null)
// 根据hash计算对象的在table中存储的索引值。hash & (length-1) 等价于 hash % length
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
/*
存入的哈希值和以前的哈希值进行比较
xhj理解:p.hash是以前的哈希值;hash是存入的哈希值
由于比较采用的&& 短路与,所以当前一个式子计算为false则后一个式子就不用计算了。
如果哈希值不同,会继续向下执行,把元素添加到集合(也就是else中的内容)
如果哈希值相同,会调用对象的equals()方法比较
如果返回false,会继续向下执行,把元素添加到集合
如果返回true,说明元素重复,不存储
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 比较哈希值,是否相同,采用短路与,相同则比较key,key也相同,那么新创建的节点e = 原始节点p
e = p;
else if (p instanceof TreeNode)
// 判断p 节点是不是树节点,也就是哈希表中数据存储结构是 红黑树 还是 链表
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;
}
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;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashSet集合保证元素唯一性的源码具体执行过程:
1 调用对象的hashCode()方法获取对象的哈希值
2 根据对象的哈希值计算对象的存储位置
3 该位置是否有元素存储
没有——》将元素存储到该位置
有 ——》遍历该位置的所有元素,和新存入的元素比较哈希值是否相同
4 哈希值相同则调用equals()方法比较对象内容是否相同
相同 ——》 返回true,元素添加失败
不相同——》返回false,元素添加成功
HashSet集合要保证元素唯一性,需要重写hashCode和equals方法
扩展
^ 表示按位异或,相同为0,不同为1
// 217
public class LinkedHashSetDemo {
public static void main(String[] args){
LinkedHashSet<String> lhs = new LinkedHashSet<String>();
lhs.add("hello");
lhs.add("java");
lhs.add("dream");
lhs.add("dream");
//添加重复元素,发现没有办法添加入集合中
for(String s: lhs){
System.out.println(s);
}
//输出内容是:hello java dream
}
}
方法名 | 说明 |
---|---|
TreeSet() | 无参构造方法,自然排序接口,构造一个新的、空的数组,根据其元素的自然排序进行排序 |
TreeSet(Comparator comparator) | 有参构造方法,比较器接口,构造新的、空的数组,根据指定的比较器进行排序 |
TreeSet集合的有序性,是对集合元素进行排序。两种方式:
方法 | 说明 |
---|---|
自然排序接口 | TreeSet对象所属类需要实现Comparator接口,并重写compareTo(类名 对象名)方法 natrual ordering是一个接口 interface Comparable< T >,这个接口对实现它的每个类的对象强加一个整体排序,排序被称为自然排序接口 |
比较器排序 | 作为匿名内部传入TreeSet的构造方法,并重写compare方法(类名 对象名1,类名 对象名2) Comparator接口,具有比较功能,实现对一些对象的集合施加整体排序,也被称为比较器接口 |
TresSet是可以被克隆的。
概述clone()方法
在java.util包中,使用的时候需要导包。
用于复制或克隆此TreeSet实例。
是非静态方法,只能使用类对象访问;
在克隆对象时不会引发异常。
public Object clone();
// 不接受任何参数。
案例
package itheima217.test6;
import java.util.TreeSet;
public class TreeSetClone {
public static void main(String[] args) {
TreeSet<String> tree_set = new TreeSet<String>();
TreeSet<String> new_set = new TreeSet<String>();
tree_set.add("C");
tree_set.add("C++");
tree_set.add("C#");
tree_set.add("Java");
System.out.println("TreeSet:" + tree_set);
System.out.println("clone Set:" + new_set);
new_set = (TreeSet) tree_set.clone();
System.out.println("clone Set:" + new_set);
// 输出结果:
// TreeSet:[C, C#, C++, Java]
//clone Set:[]
//clone Set:[C, C#, C++, Java]
}
}
红黑树
理解xhj:
集合对象存储元素,必须是引用数据类型。要想存储基本数据类型,比如int,就得使用基本类型包装类
// 217-TreeSetDemo
public class TreeSetDemo {
public static void main(String[] args) {
// 集合对象存储元素,必须是引用数据类型
// 要想存储基本数据类型,比如int,就得使用基本类型包装类
TreeSet<Integer> ts = new TreeSet<Integer>();
//需要的是integer类型,这里填写10也是可以的,因为jdk5之后可以自动装箱
ts.add(10);
ts.add(40);
ts.add(20);
ts.add(0);
ts.add(30);
ts.add(30);
//添加两个相同的元素,发现并没有添加进去,说明TreeSet集合不能存放重复元素
for (Integer i : ts){
System.out.println(i);
}
//输出顺序是:0 10 20 30 40
}
}
重点:
1 用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序
TresSet<T> treeset = new TreeSet<T>();
2 自然排序,就是让元素所属的类实现Comparable接口,重写compareTo(T t)方法
public class 类名 extends Comparable<T>{
@Overridd
public int compareTo(T t){}
}
3 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
其中 this表示要添加的元素对象,s表示集合中存在的元素对象。
需求
存储学生对象并遍历,创建TreeSet集合使用无参构造方法
要求
按照年龄从大到小排序,年龄相同时,按照姓名的字母顺序排序
理解分析:
分析Student类 中重写的Comparable接口的方法
compareTo方法中返回值:0 、正数、负数 具体的含义;
返回值 | 具体含义 |
---|---|
负数 | 表示后续存储元素 比 之前元素的值 小 |
0 | 表示后续存储元素 和 之前元素的值 一样 |
正数 | 表示后续存储元素 比 之前元素的值 大 |
要实现升序排列 this.属性 - s.属性 解释:结果是正数(this》s),则this放在s之后;结果是负数(this《s),this放在结果之前。这样正好实现从小到大排序。
// 217-test1
// 第一点:
TreeSet存储元素所属的类必须继承Comparable接口
public class Student implements Comparable<Student>{
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student() {
}
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;
}
//由于是接口,所以需要重写里面的方法,重写的是compareTo方法
@Override
public int compareTo(Student s) {
return 1;
}
第二点:return 0 的情况:认为后续存储元素和之前的元素一样,故不存储
// wangzhaojun ,diaochan,yangyuhuan ,xishi 输入顺序
// 修改成功后,原本存储四个元素,但是只存储了一个。wangzhaojun, 23
// 原因:存储第一个元素之后,返回值是0,再存储第二个元素时,返回值还是0,程序会认为第二个元素和第一个元素同一个元素,就不存储了,后续的元素3、元素4一样
第三点:return 1 (正数)的情况:认为后续存储元素比之前存储的元素大,所以添加在之后
// wangzhaojun ,diaochan,yangyuhuan ,xishi 输入顺序
// 四个元素全部存储在集合中,输出按输入的顺序输出
// 输出顺序:wangzhaojun ,diaochan,yangyuhuan ,xishi
// 解释:存储第一个元素,不需要和别的元素进行比较直接存储;存储第二个元素,和第一个元素比较后返回值是1,表示第二个比第一个大,所以存储在第一个元素之后
第四点:return -1 (负数)的情况:认为后续存储的元素比之前存储的元素小,所以添加在之前
// wangzhaojun ,diaochan,yangyuhuan ,xishi 输入顺序
// 输出顺序:xishi,yangyuhuan,diaochan,wangzhaojun
}
测试:
// 217-test1
public class SudentDemo {
public static void main(String[] args) {
TreeSet<Student> ts = new TreeSet<Student>();
Student s1 = new Student("wangzhaojun",23);
Student s2 = new Student("diaochan",21);
Student s3 = new Student("yangyuhuan", 25);
Student s4 = new Student("xishi",24);
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
for(Student s : ts){
System.out.println(s.getName() + ", " + s.getAge());
}
// 第一点:
//报错:Student cannot be cast to java.lang.Comparable
// 原因是:comparable接口,该接口对每一个实现它的类强加一个整体排序,由于定义的Student类并没有实现该接口,所以会报错
}
}
案例:
// 第一点:
// 217-test1
public class Student implements Comparable<Student>{
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student() {
}
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;
}
//由于是接口,所以需要重写里面的方法,重写的是compareTo方法
@Override
public int compareTo(Student s) {
//年龄从小到大排序 (要求1)
// 方法有this,使用this.成员变量 可以获取
// this 表示要添加的元素,s表示已经存在的元素
int num = this.age - s.age; // 此时按照age的升序排列 num 正数
// num 正数,this.age 大于 s.age this存储在s之后
// num 负数,this.age 小于 s.age this存储在s之前
// int num = s.age - this.age; //按照age的降序排列
// num 正数,this.age 小于 s.age this存储在s之后 小数存储在大数之后
// num 负数,this.age 大于 s.age this存储在s之前 大数存储在小数之后
// s2 添加的时候:this表示s2,s表示s1
// 年龄相同时,按照姓名的字母顺序排序(要求2)
int num2 = num==0 ? this.name.compareTo(s.name):num;
// 字符串this.name 为什么可以直接调用compareTo
// 因为字符串String实现了comparable接口,所以可以直接调用compareTo方法
return num2;
}
}
测试:
public class SudentDemo {
public static void main(String[] args) {
TreeSet<Student> ts = new TreeSet<Student>();
Student s1 = new Student("wangzhaojun",23);
Student s2 = new Student("diaochan",21);
Student s3 = new Student("yangyuhuan", 25);
Student s4 = new Student("xishi",24);
Student s5 = new Student("wangsulong",24);
Student s6 = new Student("wangsulong",24);
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
ts.add(s6);
//出现年龄相同,名字不相同情况,只比较年龄的comparaTo方法就得再加内容了
// 由于 wangsulong 和 xishi 比较 w在x之前
// 添加两个wangsulong 24 集合中只有一个
for(Student s : ts){
System.out.println(s.getName() + ", " + s.getAge());
}
}
}
总结:
1 用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排列对元素进行排序的
TreeSet<T> treeset = new TreeSet<T>(Comparator comparator);
参数是Comparator接口,实际上是需要传递Comparator接口的实现类对象
2 比较器排序,就是让集合构造方法接收Comparator的实现类对象,重写compare(To1,To2)方法
TreeSet<T> treeset = new TreeSet<T>(new Comparator(){
@Override
public int compare(T t1,T t2){}
//其中t1等同于compareTo方法中的this,t2等同于s
});
3 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
返回值是1 表示t1放在t2之后;返回值是-1,表示t1放在t2之前。
理解xhj:
涉及到匿名内部类和匿名内部类中调用某类中的成员变量,采用 getXXX方法 而不是 对象.成员变量名
需求:
存储学生对象并遍历,创建TreeSet集合使用带参数构造方法
要求:
按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
// 217-test2
public class TreeSetDemo {
public static void main(String[] args){
// 创建TreeSet集合对象
TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
// 参数传递两个的原因: 因为compare方法在测试类里面,所以它其中的this表示的是TreeSetDemo,而不是Student
// 之前的this.age s.age
// 匹配 s1 s2
int num = s1.getAge() - s2.getAge();
int num2 = num == 0 ? s1.getName().compareTo(s2.getName()):num;
return num2;
}
});
// 这个带参数的方法需要 传递的是comparator接口,实际上需要的是comparator接口实现类的对象
// 一般方法:写一个类继承自Comparator接口的类,再创建对象
// 本次采用匿名内部类的方式
Student s1 = new Student("wangzhaojun",23);
Student s2 = new Student("diaochan",21);
Student s3 = new Student("yangyuhuan", 25);
Student s4 = new Student("xishi",24);
Student s5 = new Student("wangsulong",24);
Student s6 = new Student("wangsulong",24);
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
ts.add(s6);
for(Student stu : ts){
System.out.println(stu.getName() + ", " + stu.getAge());
}
}
}
需求:
用TreeSet集合存储多个学生信息(姓名、语文成绩、数学成绩),并遍历该集合
要求:
按照总分从高到低出现
主要条件是:总分从高到低,次要条件是总分相同按照语文成绩从高到低排序,语文成绩相同按照学生姓名排序
重点:
次要条件需要去分析。
// 217-test3
public class StudentDemo {
public static void main(String[] args) {
// 创建TreeSet集合对象
TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
// 通过比较器排序
int sumS1 = s1.getMath() + s1.getChinese();
int sumS2 = s2.getMath() + s2.getChinese();
int num = sumS2 - sumS1;
// 总分相同,按照语文成绩排序;
int num2 = num == 0 ? s2.getChinese() - s1.getChinese() : num;
// 语文成绩相同,按照名字顺序排列
int num3 = num2 == 0 ? s1.getName().compareTo(s2.getName()) : num2;
return num3;
}
});
Student s1 = new Student("wangsulong",45,12);
Student s2 = new Student("xusong",100,100);
Student s3 = new Student("weichen",42,100);
// 问题1 当成绩总分相同的时候,按照语文成绩排序
// 成绩相同的时候,学生就不会被添加进去了
Student s4 = new Student("yuwenwen",43,99);
// 问题2 当成绩总分相同的时候,语文成绩也相同
// 总成绩相同,语文成绩也相同,比较学生姓名
Student s5 = new Student("huachengyu",43,99);
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
for(Student st : ts){
System.out.println(st.getName() + ", " + st.getChinese()+", "+st.getMath()+", "+ (st.getChinese() + st.getMath()));
}
}
}
需求:
编写一个程序,获取10个1-20之间的随机数,要求随机数不能重复,并在控制台输出
思路:
因为不能重复,所以使用Set集合;创建随机数,可以使用random。
Set集合创建对象,使用的是HashSet或TreeSet集合。
// 217
public class SetDEmo {
public static void main(String[] args) {
//创建set集合
// Set set = new HashSet();
// HashSet集合 遍历是无序的
Set<Integer> set = new TreeSet<Integer>();
// 创建随机数对象
Random r = new Random();
// 循环判断set集合长度是否等于10
while(set.size() < 10){
int number = r.nextInt(20) + 1;
set.add(number);
}
// 遍历:
for(Integer i : set){
System.out.println(i);
}
}
}