黑马程序员全套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);
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值