黑马程序员全套Java教程_Java基础教程_集合进阶之Set(二十六)
3.1 Set集合的概述和特点
- Set集合的特点:
(1)不包含重复元素的集合;
(2)没有带索引的方法,所以不能使用普通for循环遍历。 - Set集合的遍历:
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("liubei");
set.add("guanyu");
set.add("zhangfei");
set.add("zhangfei");
for (String s : set){
System.out.print(s + " ");//zhangfei guanyu liubei
}
}
3.2 哈希值
- 概述:哈希值是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。
- Object类中有一个方法可以获取对象的哈希值:public int hashCode()
- 对象的哈希值特点:
(1)同一个对象多次调用hashCode()方法返回的哈希值是相同的;
(2)默认情况下,不同对象的哈希值是不同的,而重写hashCode(),可以实现让不同对象的哈希值相同。
public static void main(String[] args) {
Student s1 = new Student("liubei",11);
//同一个对象多次调用hashCode(),返回的哈希值是相同的
System.out.println(s1.hashCode());//356573597
System.out.println(s1.hashCode());//356573597
//默认情况下(调用的是Object类的hashCode(),即子类没有重写该方法),不同对象的哈希值是不同的
Student s2 = new Student("guanyu",12);
System.out.println(s2.hashCode());//1735600054
System.out.println("zhongli".hashCode());//-310378633
System.out.println("keli".hashCode());//3288151
System.out.println("youla".hashCode());//115168724
System.out.println("youla".hashCode());//115168724
//String类重写了hashCode()
System.out.println("重地".hashCode());//1179395
System.out.println("通话".hashCode());//1179395
}
3.3 HashSet集合概述和特点
- HashSet集合特点:
(1)底层数据结构是哈希表;
(2)对集合的迭代顺序不作任何保证,也就是说不保证存储和取出的元素顺序一致;
(3)没有带索引的方法,所以不能使用普通for循环遍历;
(4)由于是Set集合,所以不能包含重复元素的集合。 - HashSet集合的遍历:
public static void main(String[] args) {
HashSet<String> hs = new HashSet<>();
hs.add("liubei");
hs.add("guanyu");
hs.add("zhangfei");
hs.add("zhangfei");
for (String s : hs){
System.out.print(s + " ");//zhangfei guanyu liubei
}
}
3.4 HashSet集合保证元素唯一源码分析
HashSet<String> hs = new HashSet<>();
hs.add("liubei");
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//1、当我们调用HashSet的add方法添加元素“liubei”时,add方法将“liubei”传给put()为形参key,
public V put(K key, V value) {
//2、put()又将hash(key)的返回值连同key本身又传给putVal()作为形参,其中hash(key)即添加元素的哈希值作为形参hash,key作为形参key。
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
//3、哈希表的底层为数组,所以tab是一个元素为结点的数组,此为哈希表结构的一种实现。
Node<K,V>[] tab; Node<K,V> p; int n, i;
//4、如果哈希表未初始化,就对其进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//5、根据对象的哈希值计算对象的存储位置,如果该位置没有元素,就存储一个元素
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
/*
首先比较哈希值,存入的元素和前面的元素比较哈希值
如果哈希值相同,会继续向下执行,把元素添加到集合
如果哈希值不同,会调用对象的equals()比较
如果返回false,会继续向下执行,把元素添加到集合
如果返回true,说明元素重复,不存储
*/
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;
}
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集合添加一个元素的过程:
HashCode集合存储元素:要保证元素唯一性,需要重写hashCode()和equals()
3.5 常见的数据结构之五——哈希表
- 哈希表:
(1)JDK8之前,底层采用数组+链表实现,可以说是一个元素为链表的数组;
(2)JDK8以后,在长度比较长的时候,底层实现了优化。
(3)哈希表在存储数据元素保证元素唯一性的方法:HashSet的默认初始容量为16,即链表数组的长度为16。所以我们要将某个哈希值的元素存储时,将此哈希值对16取余数,每个元素都得到0-15的余数,放到对应索引的链表里面。其中放进链表前,首先要将元素的哈希值与已经存储了的所有哈希值进行比较,如果哈希值不相同,就可以存储进链表内;如果哈希值相同,就进一步比较相同两个哈希值对应的具体内容,如果具体内容也相同,则不存储该元素,反之存储该元素进入链表。
案例:HashSet集合存储学生对象并遍历
- 需求:创建一个存储学生对象的集合,存储多个学生对象,使用程序实现在控制台遍历该集合。要求学生对象的成员变量值相同,我们就认为是同一个对象。
public static void main(String[] args) {
HashSet<Student> hs = new HashSet<>();
Student s1 = new Student("liubei",11);
Student s2 = new Student("guanyu",12);
Student s3 = new Student("zhangfei",13);
Student s4 = new Student("zhangfei",13);
hs.add(s1);
hs.add(s2);
hs.add(s3);
hs.add(s4);
for (Student s : hs){
System.out.println(s);
}
}
运行结果:
按理说HashSet中不能存储重复元素,而上述代码成功存储并且能在控制台输出,这不符合学生对象的成员变量值相同就认为是同一个对象的需求,而这是为什么呢?我们如何保证集合元素的唯一性呢?
我们知道HashSet底层数据结构为哈希表,而哈希表又依赖于HashCode()和equals()方法,所以我们的学生类要重写HashCode和equals方法。我们在Student类中Alt+Insert快捷生成即可。
最终结果:
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", 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;
if (age != student.age) return false;
return name != null ? name.equals(student.name) : student.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
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;
}
}
- 整体思路:
(1)定义学生类;
(2)创建HashSet集合对象;
(3)创建学生对象;
(4)把学生添加到集合;
(5)遍历集合(增强for);
(6)在学生类中重写两个方法。
3.6 LinkedHashSet集合概述和特点
- LinkedHashSet集合概述和特点:
(1)是哈希表和链表实现的Set接口,具有可预测的迭代次序;
(2)由链表保证元素有序,也就是说元素的存储和取出顺序是一一致的;
(3)由哈希表保证元素唯一,也就是说没有重复的元素。 - LInkedHashSet集合存储字符串并遍历:
public class LinkedHashSetDemo {
public static void main(String[] args) {
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("liubei");
linkedHashSet.add("guanyu");
linkedHashSet.add("zhangfei");
linkedHashSet.add("zhangfei");
for (String s : linkedHashSet){
System.out.println(s);
}
}
}
运行结果:
3.7 TreeSet集合概述和特点
- TreeSet集合特点:
(1)TreeSet间接实现Set接口(直接实现NavigableSet接口,而NavigableSet接口继承自SortedSet接口,而SortedSet接口继承自Set接口);
(2)TreeSet集合元素有序(这里的顺序不是指存储和取出的顺序,而是按照一定的规则进行排序,比如说字母按照a~z排序):可以按照自然排序进行排序(Comparable接口),也可以由Comparator(即比较器排序接口)指定。具体排序方式取决于构造方法:TreeSet()根据其元素的自然排序进行排序;TreeSet(Comparator comparator)根据指定的比较器进行排序;
(3)没有带索引的方法,所以不能使用普通for循环遍历;
(4)由于是Set集合,所以不包含重复元素的集合。 - TreeSet集合存储整数并遍历:
我们知道集合存储的是引用类型,所以若是想要存储整数,我们就要使用int类型的包装类类型Integer。
public static void main(String[] args) {
TreeSet<Integer> ts = new TreeSet<>();
ts.add(10);
ts.add(20);
ts.add(30);
ts.add(15);
ts.add(30);
for (Integer s : ts){
System.out.println(s);
}
}
输出结果为元素从小到大进行输出,与存储顺序无关,即按自然排序进行排序:
3.8 自然排序Comparable的使用
- 需求:存储学生对象并遍历,创建TreeSet集合时使用无参构造方法。要求按照年龄从小到大排序,年龄相同时,按照姓名字母顺序排序。
- 我们先创建存储Student对象的TreeSet集合并进行遍历。
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
.............................
public static void main(String[] args) {
TreeSet<Student> ts = new TreeSet<>();
Student s1 = new Student("liubei", 11);
Student s2 = new Student("guanyu", 15);
Student s3 = new Student("zhangfei", 13);
Student s4 = new Student("zhangfei", 13);
ts.add(s1);
ts.add(s2);
ts.add(s3);
for (Student s : ts){
System.out.println(s);
}
}
运行后却报错了in thread “main” java.lang.ClassCastException: itheima6.Student cannot be cast to java.lang.Comparable(Student类不能转换为Comparable类),即类转换异常。我们在API文档内再次查看Comparable自然排序接口,这里提到Comparable接口对实现它的每个类的对象强加一个整体排序,这个排序被称为类的自然排序,类的compareTo方法被称为其自然比较方法。也就是说,要实现自然排序,我们就要让Student类实现Comparable接口。
首先我们使用默认重写的自然比较方法compareTo:
public class Student implements Comparable<Student> {
private String name;
private int age;
@Override
public int compareTo(Student s) {
return 0;
}
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
发现结果只输出了一个学生对象:
我们想想,当我们存储第一个元素时,元素是不需要跟其他元素进行比较的,所以能存储进集合。但当我们存储第二个元素时,s2调用compareTo方法,与已经存在的s1进行比较,不管比较过程如何,返回值都为0,认为添加元素为重复元素,所以没有添加成功。而当我们返回正数时,将s2存储在s1后面;返回负数时,将s2存储在s1前面。
compareTo()的最终实现:
@Override
public int compareTo(Student s) {
//按照年龄从小到大排序
int num1 = this.age - s.age;
//按照年龄从大到小排序
//return s.age - this.age;
//年龄相同时,按照姓名的字母顺序排序
int num2 = num1 == 0 ? this.name.compareTo(s.name) : num1;
return num2;
}
3.9 比较器排序Comparator的使用
- 存储学生对象并遍历,创建TreeSet集合使用带参构造方法。要求按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
public class Student{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
........................
public static void main(String[] args) {
//采用匿名内部类的方式实现Comparator接口
TreeSet<Student> ts = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
int num = s1.getAge() - s2.getAge();
int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
return num2;
}
});
Student s1 = new Student("liubei", 11);
Student s2 = new Student("guanyu", 15);
Student s3 = new Student("zhangfei", 13);
Student s4 = new Student("zhangfei", 13);
Student s5 = new Student("zhugeliang", 13);
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
for (Student s : ts){
System.out.println(s);
}
}
- 结论:
(1)用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的;
(2)比较器排序,就是让集合构造方法接收Comparator的实现类对象,重写compara(T o1, T o2)方法;
(3)重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写。
案例:成绩排序
- 需求:用TreeSet集合存储多个学生信息(姓名,语文成绩,数学成),并遍历该集合。要求按照总分从高到低出现。
public class Student {
private String name;
private int ChineseScore;
private int MathScore;
public Student() {
}
public Student(String name, int chineseScore, int mathScore) {
this.name = name;
ChineseScore = chineseScore;
MathScore = mathScore;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", ChineseScore=" + ChineseScore +
", MathScore=" + MathScore +
'}';
}
public static void main(String[] args) {
TreeSet<Student> ts = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
//主要条件:题目给出
int num = s2.getChineseScore()+s2.getMathScore()-s1.getChineseScore()-s1.getMathScore();
//次要条件:分析得出
int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
return num2;
}
});
Student s1 = new Student("liubei",99,88);
Student s2 = new Student("guanyu",96,80);
Student s3 = new Student("zhangfei",0,0);
Student s4 = new Student("zhugeliang",99,98);
Student s5 = new Student("zhugeliang",99,98);
Student s6 = new Student("zhaoyun",98,99);
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
ts.add(s6);
for (Student s : ts){
System.out.println(s);
}
}
案例:不重复的随机数
- 需求:编写一个程序,获取10个1~20之间的随机数,要求随机数不能重复,并在控制台输出。
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
//Set<Integer> set = new TreeSet<>();
Random r = new Random();
while (set.size() < 10){
set.add(r.nextInt(20) + 1);
}
for (Integer i : set){
System.out.println(i);
}
}