Set集合

Set集合

这类我们不研究set集合的特有方法,我们用的set集合的方法都是用那些从Collection中继承的方法。Set也是一个接口。Set接口继承自Collection接口。

set集合的特点

  • 元素存取无序。(就是你存进去的顺序,然后遍历出来不是按你存的顺序遍历出来的,比如下面这个例子)
  • 没有索引、只能通过迭代器或增强for循环遍历。(不能用普通的for循环来遍历,因为没有索引)
  • 不能存储重复元素。就像数学里的集合。要是集合里已经有了某个元素,后面加进来的相同元素就加不进来。set判断相同元素的依据是比较这个对象和Set中的对象的hashcode值是不是相同,要是相同用equals方法判断是不是相同,要是都判断相同,就认为是相同元素。

例子

package com.liudashuai;

import java.util.HashSet;
import java.util.Set;

public class Demo {
    public static void main(String[] args) {
        Set<String> set=new HashSet<>();
        set.add("hello");
        set.add("world");
        set.add("java");
        for (String s:set) {
            System.out.println(s);
        }
    }
}
结果:
world
java
hello

哈希值

  • 哈希值简介

    哈希值是是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。(这里讲哈希值是为下面的保证元素唯一性作铺垫的)

  • 如何获取哈希值

    ​ Object类中的public int hashCode()方法可以返回对象的哈希码值

  • 哈希值的特点

    • 同一个对象多次调用hashCode()方法返回的哈希值是相同的
    • 默认情况下(即你没有重写hashCode方法),不同对象的哈希值一定是不同的。除非你重写hashCode()方法,可以实现让不同对象的哈希值相同。
  • 获取哈希值的示例

    • 学生类
    public class Student {
        private String name;
        private int age;
    
        public Student() {
        }
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        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;
        }
    
        @Override
        public int hashCode() {
            return 0;
        }
    }
    
    • 测试类
    public class HashDemo {
        public static void main(String[] args) {
            //创建学生对象
            Student s1 = new Student("林青霞",30);
    
            //同一个对象多次调用hashCode()方法返回的哈希值是相同的
            System.out.println(s1.hashCode()); //1060830840
            System.out.println(s1.hashCode()); //1060830840
            System.out.println("--------");
    
            Student s2 = new Student("林青霞",30);
    
            //默认情况下,不同对象的哈希值是不相同的
            //通过方法重写,可以实现不同对象的哈希值是相同的
            System.out.println(s2.hashCode()); //2137211482
            System.out.println("--------");
    
            System.out.println("hello".hashCode()); //99162322
            System.out.println("world".hashCode()); //113318802
            System.out.println("java".hashCode()); //3254818
    
            System.out.println("world".hashCode()); //113318802
            System.out.println("--------");
    
            System.out.println("重地".hashCode()); //1179395
            System.out.println("通话".hashCode()); //1179395
        }
    }
    

HashSet集合的特点

HashSet是泛型类HashSet,它是Set的实现类,它的底层是由哈希表(HasMap)支持的。它不保证存取顺序(符合Set的特点)。

  1. HashSet集合的特点

    • 底层数据结构是哈希表
    • 对集合的迭代顺序不作任何保证,也就是说不保证存储和取出的元素顺序一致
    • 没有带索引的方法,所以不能使用普通for循环遍历。所以只能用增强for循环和迭代器来遍历
    • 由于是Set集合,所以是不包含重复元素的集合
  2. HashSet集合的基本使用(例子为:HashSet存储字符串并遍历)

    public class HashSetDemo01 {
        public static void main(String[] args) {
            //创建集合对象
            HashSet<String> hs = new HashSet<String>();
    
            //添加元素
            hs.add("hello");
            hs.add("world");
            hs.add("java");
    
            hs.add("world");
    
            //遍历
            for(String s : hs) {
                System.out.println(s);
            }
        }
    }
    

HashSet集合保证元素唯一性源码分析

先讲一下HashMap的过程:

HashMap在JDK1.8之前(不包括JDK1.8),是用数组+单链表实现的。JDK1.8(包括JDK1.8)之后要是一个链表的长度超过8就用红黑树来优化,要是没有超过8就和原来一样用数组+链表。比如说下面的hello—>world—>java就是链表长度为3,那么就没有用红黑树来优化。所以JDK1.8之后是链表的长度不大于8就用数组+链表来实现HashMap,要是链表长度超过8就用数组+红黑树来实现。

下面这个例子是:存储HashMap键的示意图,这里没画出整个结点。比如分别执行下面代码hashMap.put(“hello”,new Object())、hashMap.put(“world”,new Object())、hashMap.put(“java”,new Object())、hashMap.put(“world”,new Object())、hashMap.put(“通话”,new Object())、hashMap.put(“重地”,new Object())。然后他们的键就是这样添加进去的,先有一个数组,然后你计算键对象的hash值(于hashCode方法用关的一个值),计算这个hash值对应的数组位置(用一个特定的算法,不一定是%16哦,这里就这么认为是%16)(那个99162322就是"hello"这个对象对应的hash值,2是计算出来的数组下标)。然后"hello"计算出来是2判断那个数组2的位置有没有东西,没有,直接存进去,然后存下一个。计算world的位置,然后发现那个2的位置有东西,然后就比较hash值,发现不一样且hello后面没有东西,就直接存在hello的下一个地方,然后存下一个,java计算的位置也是2然后笔记与hello的hash值,发现不一样,就比较world的hash值,发现也不一样,然后因为world后面没有东西,就直接存在world后面。然后存下一个world,计算到存的位置是2,然后和hello比较hash值,发现不一样,然后和world比较hash值,发现一样,然后比较是不是同一个对象,发现是的,就不存了,然后存下一个东西"通话",通过他的hash值计算位置,得到3,就存在那个数组下标为3的位置上,然后发现那里没有东西,就直接存进去,然后存”重地“,通过hash值计算的位置也是3,然后发现那个位置有东西,比较hash值,发现是一样的,然后看是不是同一个对象,发现不是,然后看”重地“.equasl(“通话”)的结果,发现一样,就不存了"重地"这个键了。

在这里插入图片描述

超过8个的示意图是这样的:

在这里插入图片描述

所以要HashMap判断这两个键元素相同得要然后hashCode方法计算出来的哈希值相同,且又得让新加入的键元素调用equals方法判断集合中hashCode方法计算出来位置相同的地方的那个对象依次作为新加入的元素调用equals方法的参数,看看是不是返回true,是true就不添加。比如你新加入的元素的键是”java“然后java计算出来的位置是2,然后用“java”.equals(“hello”);看看是不是true,要是是true就不用比较了,就不添加新键了,要是不是true就比较“java”.equals(“world”);看看是不是true。所以要让HashSet认为这两个元素是同一个元素就得然后HashCode计算出来的值相同,且新加元素.equals(同位置元素)返回是true。

  • HashSet集合保证元素唯一性的图解

    在这里插入图片描述

    实际上这个HashMap的数组里的是每一个元素是一个对象,你看下面这个图,然后对象里面有四个属性,一个是键,一个是值,一个是hash值,一个是存下一个结点的地址的next变量(这个变量是用来指向下一个链表元素的结点的)然后JDK1.8之后是尾插法键新结点,JDK1.8之前是用头插法键链表的新结点的。下图这个图up是用的JDK1.7。但是我们上面讲的步骤是对的,基于JDK1.8的,要是上面我们用的是头插法我们2位置的链表就是“java"->“world”->“hello”,上面的例子是尾插法键链表结点,所以是展示“hello"->“world”->“java”。

在这里插入图片描述

下面我们讨论HashSet是怎么判断新添加的元素和原来的元素是不是重复的。

通过HashSet的add源码可以看到,HashSet的add方法其实就是调用了一个HashMap的put方法,put的key是我们HashSet新添加的元素(即add方法的参数),put的value是一个Object的引用类型,但是这个Object引用类型指向的是null,所以这个put的value相当于一个虚拟值。

public class HashSet<E> extends AbstractSet<E> implements Set<E>
{
    private static final Object PRESENT = new Object();
    public HashSet() {
        map = new HashMap<>();
    }
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
}

因为这个HashSet底层是用了一个HashMap来存储的,且放在HashMap的key的位置,所以他们是不可以重复的。

还有就是我们看到,这个HashSet创建对象时,其实就是创建了一个HashMap,相当于HashSet就是套了壳的HashMap,然后就是HashSet的add进来的元素都是放在HashMap的key的位置的,然后value位置放一个没有用的东西,所以这样HashSet可以利用HashMap的key不重复的性质达到HashSet的add进来的元素不重复。

然后我们看看HashMap的key为什么不能重复。这里就涉及到了hashCode方法和equals方法了。好,继续往下看。

我们看到HashSet的add方法就是直接调用了HashMap的put方法,我们去HashMap里看看这个put方法。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
    //可以看到这个put方法调用了putVal方法,这个putval方法里面有一个参数是调用这个hash(key)方法,因为程序是先执行这个hash(key)方法的,所以我们先看这个hash(key)方法再看putVal方法。
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);//第四个参数是说这个value值可以改变,第五个参数是说表格没有处于创建状态。
    }
    //这个方法就是为了计算一个hash值,可以看到传进来的参数是null,这个值计算的hash值是0,要是不是null,计算的结果是和传进来的参数key调用hashCode()方法返回的值有关的一个值,不是单单就等于这个hashCode值,是hash值是和那个HashMap的键的hashCode有关的一个值
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    //这个putVal方法
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;//,这个tab是整个数组,现在这个数组是空的,这个Node是一个内部类,相当于上面那个图的数组一个框框,即数组的一个元素,然后这个Node类里面有四个属性:hash值,key值,value值,下一个结点的引用值(不是下一个数组元素哦,是指下一个链表元素的对象引用,就是那个图中那个链表的下一个元素)
        Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)//这里的tab = table把HashMap里的那个数组给了tab,tab就不是空的了,且让着n记录数组的长度
            n = (tab = resize()).length;//这个resize()是那个扩容机制,这里不看了,比较复杂,就是说,要是需要扩容,就扩容,然后把扩容后的那个HashMap中数组的长度给n,且把新扩容的数据(即新的HashMap的那个数组)给tab记住。
        if ((p = tab[i = (n - 1) & hash]) == null)//i = (n - 1) & hash这个是计算你要添加进来的元素位置,你看这里的hash值是和你添加元素有关的嘛。这个&是按位与运算。如果那个数组位置没有存东西,就直接新建一个Node类(看下面这个语句),添加进去。
            tab[i] = newNode(hash, key, value, null);
        else {//要是那个数组位置有东西存着就进行下面的语句。
            Node<K,V> e; K k;
         if (p.hash == hash &&  //p是哪个原来存在数组那个位置上的元素,若他的hash值和新添加进来的元素的hash值一样。就判断下面
                ((k = p.key) == key || (key != null && key.equals(k))))//要是那个新添加进来的键元素的地址和那个位置的原来键元素的地址一样(==的运算效果),就直接进行下面语句e=p。或者新添加进来的元素的equals(原来位置元素)是true且原来位置元素不是null就执行下面语句。
                e = p;
            else if (p instanceof TreeNode)//下面这个是红黑树,这里先不研究,我不会这里,JDK1.8之后要是数组一个位置的链表的元素>8,数组同一个位置的元素就不是用链表来连接的,而是用红黑树来连接的。要是是红黑树执行下面代码。
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {//死循环
                    if ((e = p.next) == null) {//要是数组那个位置的链表的下一个结点是空就直接添加,为什么不用判断数组那个位置的元素呢?因为前面那个if是false才会执行这里的语句,所以是判断过了。
                        p.next = newNode(hash, key, value, null);//单链表建表的尾插法
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);//如果原来那个链表的长度是8了,那么就用红黑树的方式添加。
                        break;//判断p.next是null才可能执行这个break.
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))//e是会继续向下的,因为这句if ((e = p.next) == null),e表示当前比较的结点,要是判断e根据hash和equals都判断出这个要添加进来的键元素和e结点里存的键元素一样,就退出循环,所以找到链表里第一个判断是一个元素的就退出循环,或者是找到next是空的,还没有找到判断是一样的,就加入循环。
                        break;
                    p = e;
                }
            }
            if (e != null) { 
                V oldValue = e.value;//要是e是非空的,即是原来的HashMap里碰到判定键存在一样的键,那么e的那个结点里面的值会让oldValue记住。e里面有四个属性吗。e是保存了原来一样的那个结点的地址。e其实是这样的,要是e不是null,则e的表示HashMap中第一个判断是和要添加的元素的键一样的那个结点的地址;要是e是null则表示,判断这个新的要添加的元素的键是原来HashMap中没有的。
                if (!onlyIfAbsent || oldValue == null)//如果onlyIfAbsent判断value值可以改变,或者e的value是null。
                    e.value = value;//就把新的值给覆盖掉原来的值。我们用put传进来的onlyIfAbsent是告诉程序可以修改值的,所以HashMap的后面加进来的键对应的HashMap的值会覆盖之前的键对应的HashMap的值。要是判断原来HashMap中有那个键,就后面添加的键就不会添加进来。
                afterNodeAccess(e);//这句不用管,没有什么关系
                return oldValue;//返回被替代的老的值
            }
        }
        ++modCount;//这个是记录实际修改次数的,+1
        if (++size > threshold)//要是添加了新元素,判断要不要扩容
            resize();
        afterNodeInsertion(evict);
        return null;//要是添加新元素返回null
    }
}

总之:在HashMap的put方法中,HashMap的键是靠HashCode方法和equals方法来判断存储的元素是不是一个元素的。所以我们要想让HashMap认为两个键对象是同一个东西,就得重写键对象的HashCode方法和equals方法,让HashCode方法和equals方法都判断出这两个键是一个东西,这样HashMap就不会重复添加后面来的键,并把新那个新添加的键对应的值(即比如hashMap.put(“abc”,“hello”)执行时,要是这个hashMap对象的键为"abc"的位置存在一个"111"的value值,那么新添加进来的"hello"会覆盖掉"111",这样"abc"对应的值就是"hello"了)然后因为这个HashSet是用HashMap实现的,比如hashSet.add(“qwe”),相当于hashMap.put(“qwe”,new Object()),所以这个后面要是继续hashSet.add(“qwe”)的话,就相当于继续执行hashMap.put(“qwe”,new Object())然后所以这个新添加的"qwe"不会去覆盖原来的"qwe"。

HashSet集合存储学生对象并遍历

  • 案例需求

    • 创建一个存储学生对象的集合,存储多个学生对象,使用程序实现在控制台遍历该集合
    • 要求:学生对象的成员变量值相同,我们就认为是同一个对象(所以就要重写equals方法和hashCode方法然HashSet认为那个两个添加进来的对象是一个东西。HashSet添加进来的相当于HashMap的键,所以重写equals方法和hashCode方法就可以让你想让他觉得是一个东西的元素就不添加了)
  • 代码实现

    • 学生类(这个equals和hashCode方法可以快捷键来重写,就按住alt+ins,选择equals and hashCode,选择Intellij Default,然后next,next,next,finish就行了)

      public class Student {
          private String name;
          private int age;
      
          public Student() {
          }
      
          public Student(String name, int age) {
              this.name = name;
              this.age = age;
          }
      
          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;
          }
      
          @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 class HashSetDemo02 {
          public static void main(String[] args) {
              //创建HashSet集合对象
              HashSet<Student> hs = new HashSet<Student>();
      
              //创建学生对象
              Student s1 = new Student("林青霞", 30);
              Student s2 = new Student("张曼玉", 35);
              Student s3 = new Student("王祖贤", 33);
      
              Student s4 = new Student("王祖贤", 33);
      
              //把学生添加到集合
              hs.add(s1);
              hs.add(s2);
              hs.add(s3);
              hs.add(s4);
      
              //遍历集合(增强for)
              for (Student s : hs) {
                  System.out.println(s.getName() + "," + s.getAge());
              }
          }
      }
      //输出:
      王祖贤,33
      张曼玉,35
      林青霞,30//后面那个“王祖贤”没有添加进来
      

LinkedHashSet集合概述和特点

这是一个具体类,这个类继承了HashSet,实现了Set接口。他的底层是由HashMap和链表实现的,MashMap保证LinkedHashSet的不可重复性,满足Set集合的特点。还有一点就是LinkedHashSet是一个存取顺序一致的集合,这个存取一致是因为底层的链表。

  1. 所以他的特点是:

    • 哈希表和链表实现的Set接口,具有可预测的迭代次序(因为存取的顺序是一致的)。
    • 由链表保证元素有序,也就是说元素的存储和取出顺序是一致的
    • 由哈希表保证元素唯一,也就是说没有重复的元素
  2. LinkedHashSet集合基本使用

    public class LinkedHashSetDemo {
        public static void main(String[] args) {
            //创建集合对象
            LinkedHashSet<String> linkedHashSet = new LinkedHashSet<String>();
    
            //添加元素
            linkedHashSet.add("hello");
            linkedHashSet.add("world");
            linkedHashSet.add("java");
    
            linkedHashSet.add("world");
    
            //遍历集合
            for(String s : linkedHashSet) {
                System.out.println(s);
            }
        }
    }
    //输出:
    hello
    world
    java
    //体现了存取顺序一致,和和预测的迭代顺序。也体现了他的不可重复性。
    
  3. LinkedHashSet比起HashSet有的有点是有序,比起ArrayList有的特点是不可重复。

  4. 我们现在不用学习他的特有方法,现在只要知道这个集合有什么特点,什么时候用他就行了(在集合又要有序,又要不重复,又要是单列存储时用),他使用时用的方法就用那些继承来的方法就行了。

TreeSet集合概述和特点

这是一个具体类,,他没有直接实现Set接口,看下面的继承图就知道了,TreeSet实现了NavigableSet接口,这个NavigableSet又继承了SortedSet接口,然后这个SortedSet接口继承了Set接口,然后TreeSet又实现了这个AbStractSet接口,这个接口也继承了Set接口,所以TreeSet间接继承了Set。TreeSet存取的顺序不同,但是TreeSet存元素是排序存储的,他排序的顺序是怎样的就看他的构造方法用什么了,要是用无参构造就用自然排序进行排序的,要是用TreeSet(Comparator<? super E> comparator)构造器就是用比较器进行排序的。具体怎样看下面笔记。

在这里插入图片描述

  1. TreeSet集合的特点

    • TreeSet的特点是元素有序。(这里的有序是指集合中的元素是按一定顺序排序的,不是指存取顺序是有序的。集合中的排序方式取决于使用哪个构造方法去构造这个集合对象。)
      • TreeSet():使用这个构造方法,集合会根据其元素的自然排序进行排序
      • TreeSet(Comparator comparator) :使用这个构造方法,集合会根据指定的比较器进行排序,根据传进来的比较器来排序。
    • TreeSet没有带索引的方法,所以不能使用普通for循环遍历
    • 由于是Set集合,所以不包含重复元素的集合
  2. TreeSet集合基本使用

    public class TreeSetDemo01 {
        public static void main(String[] args) {
            //创建集合对象
            TreeSet<Integer> ts = new TreeSet<Integer>();
    
            //添加元素
            ts.add(10);
            ts.add(40);
            ts.add(30);
            ts.add(50);
            ts.add(20);
    
            ts.add(30);
    
            //遍历集合
            for(Integer i : ts) {
                System.out.println(i);
            }
        }
    }
    //输出:
    10
    20
    30
    40
    50
    //体现没有重复,也体现排序存储。因为这个构造TreeSet用的是无参的构造方法,所以这里是自然排序的结果。自然排序为什么是这样的结果,看下面的笔记。
    

Comparable接口

我们先看一个案例

  • 案例需求

    • 存储学生对象并遍历,创建TreeSet集合使用无参构造方法
    • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
  • 实现步骤

    • 用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的
    • 自然排序,就是让元素所属的类实现Comparable接口,重写compareTo(T o)方法(要是用无参构造方法,那么添加进来的元素一定得实现Comparable接口,不然用TreeSet集合的add方法把元素添加进去的时候就会抛出ClassCastException异常,说你添加的元素不能被Comparable接口接收,所以你要避免这个异常就必须把要添加到TreeSet集合中的元素的类声明上添加一个实现Comparable接口。)
    • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
  • 错误示例:

    package com.liudashuai;
    
    import java.util.TreeSet;
    
    public class Demo {
        public static void main(String[] args) {
            //创建集合对象
            TreeSet<Student> ts = new TreeSet<Student>();
            //创建学生对象
            Student s1 = new Student("xishi", 29);
            //把学生添加到集合
            ts.add(s1);
        }
    }
    
    package com.liudashuai;
    public class Student{
        private String name;
        private int age;
    
        public Student() {
        }
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        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;
        }
    
    }
    

    执行效果

    在这里插入图片描述

  • 正确示例(用无参构造方法创建的TreeSet对象的集合,那么这个集合里面的元素的类必须是实现Comparable接口):

    package com.liudashuai;
    
    import java.util.TreeSet;
    
    public class Demo {
        public static void main(String[] args) {
            //创建集合对象
            TreeSet<Student> ts = new TreeSet<Student>();
    
            //创建学生对象
            Student s1 = new Student("xishi", 29);
            Student s2 = new Student("wangzhaojun", 28);
            Student s3 = new Student("diaochan", 30);
            Student s4 = new Student("yangyuhuan", 33);
    
            Student s5 = new Student("linqingxia",33);
            Student s6 = new Student("linqingxia",33);
    
            //把学生添加到集合
            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.getName() + "," + s.getAge());
            }
        }
    }
    
    package com.liudashuai;
    public class Student implements Comparable<Student> {
        private String name;
        private int age;
    
        public Student() {
        }
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        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;
        }
    
        @Override
        public int compareTo(Student s) {
    //        return 0;
    //        return 1;
    //        return -1;
            //按照年龄从小到大排序
            int num = this.age - s.age;
    //        int num = s.age - this.age;
            //年龄相同时,按照姓名的字母顺序排序
            int num2 = num==0?this.name.compareTo(s.name):num;
            return num2;
        }
    }
    

    执行效果

    在这里插入图片描述

  • 下面讲一下要添加元素重写那个compareTo方法的作用,每次要add进来的元素都是会被执行,这个添加进来的对象.CompareTo(集合中的每一个元素),因为是比较集合中每一个元素,所以Student这个类里面的CompareTo(Student s)的参数写的是Student。要是结果返回是0就判断是一样的,不添加,即后面添加的元素不会被添加到集合;要是返回结果是>0,就判断这个新添加的元素比集合中正在比较的元素大,就添加到那个元素的后面。要是返回结果是<0,就判断这个新添加的元素比集合中正比较的元素小,就放在那个元素的前面。

  • 这里需要注意的是:像那个String里面是本来就已经实现了Comparable接口。所以可以直接用int num2 = num==0?this.name.compareTo(s.name):num;String添加到TreeSet集合中他的比较规则是:要是新添加的对象和集合里的元素一个个比较,比较的时候是把新添加到集合的字符串对象的每一个字符和集合中正在比较的元素的每一个字符的asc码进行比较,要是新添加字符串的第一个字符比集合中正比较的字符串的第一个字符的asc码大,那么这个新添加字符串就放在TreeSet集合中正比较的那个字符串后面,要是第一个字符两个字符串都一样,那么比下面一个字符,要是比较的两个字符比到一个字符串都比完了,都是一样的,那么长的字符串排在TreeSet的那个所比较元素的后面;要是新添加的元素比正比较的元素大就继续比集合中的下一个存储的字符串,直到比到这个新添加元素比集合中正比较元素小为止,或者比较到整个集合比完为止。

  • 还有就是Integer内部也是实现了这个Comparable接口的,且是要是新添加元素对应的int值比集合中元素正比较元素对应的int值大就返回1,比集合中正比较元素对应的int值小就返回-1,所以上面那个“TreeSet集合基本使用”那个例子存储时是“小的在前面,大的排在后面”,然后遍历出来就是那样的“小的在前面,大的排在后面”。

  • 上面的那个

    int num = this.age - s.age;
    int num2 = num==0?this.name.compareTo(s.name):num;
    return num2;

    是有细节的,我们把判断是否相同的主要判断条件写在前面,然后后面用三元运算符判断前面主要条件判断出来的结果是不是为0,要是是0怎么样,要是不是0怎么样(一般不是0就返回前面主要条件的放回)

    实现Comparable接口的类都得被强加一个整体排序方法(因为实现,所以这里说的是”强加“,整体排序的方法是指compareTo方法),借助compareTo进行排序就叫作自然排序。那个集合的排序是怎样的,得看集合里元素的所属类的compareTo方法。这个compareTo方法也被叫作自然比较方法。

比较器排序Comparator的使用

Comparator接口被叫作比较器排序接口。

先看一个例子

  • 案例需求

    • 存储学生对象并遍历,创建TreeSet集合使用带参构造方法
    • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
  • 实现步骤

    • 用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的
    • 比较器排序,就是让集合构造方法接收Comparator的实现类对象,重写compare(T o1,T o2)方法
    • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
  • 代码实现

    • 学生类

      public class Student {
          private String name;
          private int age;
      
          public Student() {
          }
      
          public Student(String name, int age) {
              this.name = name;
              this.age = age;
          }
      
          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;
          }
      }
      
    • 测试类

      public class TreeSetDemo {
          public static void main(String[] args) {
              //创建集合对象
              TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
                  @Override
                  public int compare(Student s1, Student s2) {
                      //this.age - s.age
                      //s1,s2
                      int num = s1.getAge() - s2.getAge();
                      int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
                      return num2;
                  }
              });
      
              //创建学生对象
              Student s1 = new Student("xishi", 29);
              Student s2 = new Student("wangzhaojun", 28);
              Student s3 = new Student("diaochan", 30);
              Student s4 = new Student("yangyuhuan", 33);
      
              Student s5 = new Student("linqingxia",33);
              Student s6 = new Student("linqingxia",33);
      
              //把学生添加到集合
              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.getName() + "," + s.getAge());
              }
          }
      }
      //结果
      wangzhaojun,28
      xishi,29
      diaochan,30
      linqingxia,33
      yangyuhuan,33
      
    • 这样的话学生类就不用写实现Comparable接口了。到时候比较存学生类的集合对象TreeSet对象就不会内部调用student类里面的比较器(Comparable其实可以叫内比较器接口,那个Comparator可以叫外比较器接口),而是直接调用构造方法传进来的比较器对象的compare方法去比较新添加进来的元素和集合中已经存的元素的大小。就是先和TreeSet集合中的第一个比然后和第二个比……(其实也不是一个个比的,这里涉及到了TreeMap的数据结构,她是树的结构,所以她可以和树的某个孩子比较就行了,另一个就不用比较了,所以不是一个个比较的,这里是TreeMap底层数据结构的效果,这样功能还是到达了和一个个一样的效果,但是比较次数少了)来判断新元素应该存在哪里。新添加的元素是compare(Student s1, Student s2)方法的第一个参数s1,集合中正在比较的元素是第二个参数s2。

    • 因为那个Comparator是一个接口,但是TreeSet构造器需要的参数是一个对象,所以这里用了匿名内部类。

TreeSet的使用案例

成绩排序案例:

  • 案例需求

    • 用TreeSet集合存储多个学生信息(姓名,语文成绩,数学成绩),并遍历该集合
    • 要求:按照总分从高到低出现
  • 代码实现

    • 学生类

      public class Student {
          private String name;
          private int chinese;
          private int math;
      
          public Student() {
          }
      
          public Student(String name, int chinese, int math) {
              this.name = name;
              this.chinese = chinese;
              this.math = math;
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public int getChinese() {
              return chinese;
          }
      
          public void setChinese(int chinese) {
              this.chinese = chinese;
          }
      
          public int getMath() {
              return math;
          }
      
          public void setMath(int math) {
              this.math = math;
          }
      
          public int getSum() {
              return this.chinese + this.math;
          }
      }
      
    • 测试类

      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) {
      //                int num = (s2.getChinese()+s2.getMath())-(s1.getChinese()+s1.getMath());
                      //主要条件
                      int num = s2.getSum() - s1.getSum();
                      //次要条件
                      int num2 = num == 0 ? s1.getChinese() - s2.getChinese() : num;
                      int num3 = num2 == 0 ? s1.getName().compareTo(s2.getName()) : num2;
                      return num3;
                  }
              });
      
              //创建学生对象
              Student s1 = new Student("林青霞", 98, 100);
              Student s2 = new Student("张曼玉", 95, 95);
              Student s3 = new Student("王祖贤", 100, 93);
              Student s4 = new Student("柳岩", 100, 97);
              Student s5 = new Student("风清扬", 98, 98);
      
              Student s6 = new Student("左冷禅", 97, 99);
      //        Student s7 = new Student("左冷禅", 97, 99);
              Student s7 = new Student("赵云", 97, 99);
      
              //把学生对象添加到集合
              ts.add(s1);
              ts.add(s2);
              ts.add(s3);
              ts.add(s4);
              ts.add(s5);
              ts.add(s6);
              ts.add(s7);
      
              //遍历集合
              for (Student s : ts) {
                  System.out.println(s.getName() + "," + s.getChinese() + "," + s.getMath() + "," + s.getSum());
              }
          }
      }
      //结果:
      林青霞,98,100,198
      柳岩,100,97,197
      左冷禅,97,99,196
      赵云,97,99,196
      风清扬,98,98,196
      王祖贤,100,93,193
      张曼玉,95,95,190
      

随机数使用案例

不重复的随机数案例

  • 案例需求

    • 编写一个程序,获取10个1-20之间的随机数,要求随机数不能重复,并在控制台输出
  • 代码实现(TreeSet版的)

    public class SetDemo {
        public static void main(String[] args) {
            //创建Set集合对象
            Set<Integer> set = new TreeSet<Integer>();//使用TreeSet存储的时候是按排序存储的,迭代的时候是按顺序迭代的
    
            //创建随机数对象
            Random r = new Random();
    
            //判断集合的长度是不是小于10
            while (set.size()<10) {
                //产生一个随机数,添加到集合
                int number = r.nextInt(20) + 1;
                set.add(number);
            }
    
            //遍历集合
            for(Integer i : set) {
                System.out.println(i);
            }
        }
    }
    //结果
    1
    3
    5
    6
    8
    9
    14
    16
    18
    20
    
  • 代码实现(HashSet版的)

    package com.liudashuai;
    
    import java.util.HashSet;
    import java.util.Random;
    import java.util.Set;
    
    public class Demo {
        public static void main(String[] args) {
            //创建Set集合对象
            Set<Integer> set = new HashSet<Integer>();
    
            //创建随机数对象
            Random r = new Random();
    
            //判断集合的长度是不是小于10
            while (set.size()<10) {
                //产生一个随机数,添加到集合
                int number = r.nextInt(20) + 1;
                set.add(number);
            }
    
            //遍历集合
            for(Integer i : set) {
                System.out.println(i);
            }
        }
    }
    //结果
    1
    17
    3
    20
    4
    8
    10
    12
    13
    14
    //为什么呢?要是有两个Integer对象要添加到集合中,两个Integer对象的hashCode值应该不同,这样那么就不能判断是一个对象了。其实是因为,Integer的hashCode方法被重写了。Integer的hashCode的值是他对应的int值。所以对应int值一样的两个Integer对象他们的hashCode是一样的。equlas判断也是一样的,因为Integer对象equlas比较的也是他对应的int值是不是一样。所以两个对应int值一样的两个Integer对象,是不会被添加进来的。
    

泛型

  • 泛型概述

    JDK5中引入的特性,它提供了编译时类型安全检测机制,该机制允许在编译时检测到非法的类型。

    它的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,可以像参数一样随着传递的东西变化,比如你构造对象时,传一个String类型到ArrayList里,如ArrayList array=new ArrayList();那么这个ArrayList就会被当作是String。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,然后在使用/调用时传入具体的类型。这种参数类型可以用在类、方法和接口中,分别被称为泛型类、泛型方法、泛型接口。

    泛型就是指这个尖括号和里面的东西组成的这个就是泛型,它可以是<类型>或<类型1,类型2…>这样的。

  • 泛型定义格式

    • <类型>:指定一种类型的格式。这里的类型可以看成是形参
    • <类型1,类型2…>:指定多种类型的格式,多种类型之间用逗号隔开。这里的类型可以看成是形参
    • 将来具体调用时候给定的类型可以看成是实参,并且实参的类型只能是引用数据类型,不能是基本数据类型。
  • 泛型的好处

    • 把运行时期的问题提前到了编译期间
    • 避免了强制类型转换
  • 注意点:

    泛型类可以使用不带泛型的构造器来创建对象。

泛型类

  • 定义格式

    格式:
    修饰符 class 类名<类型> {  }//这里的类型可以随意的自己取名字,我们习惯常用的有T,E,V,K等
    如:
    public class Generic<T> {}
    
  • 好处:可以减少代码的冗余,即你要是定义了一个泛型类,你在构造方法的时候给泛型类一个参数化类型,这样你在下面使用的时候就可以改变把参数化类型当作你构造方法时传入的参数类型。

  • 示例代码

    • 泛型类

      public class Generic<T> {//这个T只有在创建这个Generic对象的时候才知道这一个对象中的T是什么。(强调这一个对象哦,要是下你创建另一个对象的用了别的类型,那么在这个新对象里的T就是你创建新对象传进来的类型了)
          private T t;
      
          public T getT() {
              return t;
          }
      
          public void setT(T t) {
              this.t = t;
          }
      }
      
    • 测试类

      public class GenericDemo {
          public static void main(String[] args) {
              Generic<String> g1 = new Generic<String>();
              g1.setT("林青霞");
              System.out.println(g1.getT());
      
              Generic<Integer> g2 = new Generic<Integer>();
              g2.setT(30);
              System.out.println(g2.getT());
      
              Generic<Boolean> g3 = new Generic<Boolean>();
              g3.setT(true);
              System.out.println(g3.getT());
          }
      }
      

泛型方法

  • 好处是:相对于泛型类,你不用在构造方法的时候传一个参数类型了,要是你每次要用一个方法的时候都得重新创建一个泛型类的对象,是不是写起来很麻烦,所以这里用泛型方法就可以来简便你的操作了。你只要建一个普通对象,然后用泛型方法就行。普通类和泛型类里面都可以用泛型方法。

  • 定义格式

    修饰符 <类型> 返回值类型 方法名(类型 变量名) {  }
    
  • 示例代码

    • 带有泛型方法的类

      public class Generic {
          public <T> void show(T t) {//泛型方法的T是在调用方法的时候明确的。
              System.out.println(t);
          }
          
         //这样也行,但是泛型方法比这样的好处是:不用向下转型。就是你这样在调用方法的时候还是认为这个o是Object类的,下面的代码都认为这个o是Object类的,但是泛型方法就不一样了,上面那个你调用方法的时候,show方法里的语句就把那个t当作对应的类型了。
          
         /*public void show(Object o){
              System.out.println(t);
           }
         */
      }
      
    • 测试类

      public class GenericDemo {
          public static void main(String[] args) {
      		Generic g = new Generic();
              g.show("林青霞");
              g.show(30);
              g.show(true);
              g.show(12.34);
          }
      }
      
  • 你要用泛型类就是这样

    • 定义一个泛型类

      package com.liudashuai;
      
      public class Generic<T>{
          public void show(T t){
              System.out.println(t);
          }
      }
      
    • 测试类

      package com.liudashuai;
      
      public class Demo {
          public static void main(String[] args) {
              Generic<String> g1=new Generic<>();
              g1.show("林青霞");
              Generic<Integer> g2=new Generic<>();
              g2.show(12);
              Generic<Boolean> g3=new Generic<>();
              g3.show(true);
          }
      }
      

      你看是不是麻烦很多。每次都要新new一个对象。这是不是比泛型方法更麻烦,这就体现了泛型方法的好处了。

  • 你要是用普通类就得这样

    • 某类

      package com.liudashuai;
      
      public class Generic{
          public void show(String s){
              System.out.println(s);
          }
          public void show(Integer i){
              System.out.println(i);
          }
          public void show(Boolean b){
              System.out.println(b);
          }
      }
      
    • 测试类

      package com.liudashuai;
      
      public class Demo {
          public static void main(String[] args) {
              Generic g=new Generic();
              g.show("林青霞");
              g.show(12);
              g.show(true);
              //g.show(12.5);不行,因为那个类里面没有对应的方法,必须自己手写一个重载方法
          }
      }
      

      这个是不是比泛型类更麻烦,且没有通用性。这就体现了泛型类的好处了。

泛型接口

  • 定义格式

    修饰符 interface 接口名<类型> {  }
    
  • 示例代码

    • 泛型接口

      public interface Generic<T> {
          void show(T t);
      }
      
    • 泛型接口实现类

      public class GenericImpl<T> implements Generic<T> {
          @Override
          public void show(T t) {
              System.out.println(t);
          }
      }
      
    • 测试类

      public class GenericDemo {
          public static void main(String[] args) {
              Generic<String> g1 = new GenericImpl<String>();
              g1.show("林青霞");
      
              Generic<Integer> g2 = new GenericImpl<Integer>();
              g2.show(30);
          }
      }
      

通配符

  • 类型通配符的作用

    ​ 为了表示各种泛型List的父类,可以使用类型通配符

  • 类型通配符的分类

    • 类型通配符:<?>
      • List<?>:表示元素类型未知的List,它的元素可以匹配任何的类型
      • 这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素添加到其中
    • 指定类型通配符上限:<? extends 类型>
      • List<? extends Number>:它表示的类型是Number或者其子类型(就是这个List的泛型是未知的,但是这个未知的东西一定继承自Number或是他自己)
    • 指定类型通配符下限:<? super 类型>
      • List<? super Number>:它表示的类型是Number或者其父类型(就是这个List的泛型是未知的,但是这个未知的东西一定是Number的父类或是他自己)
  • 类型通配符的基本使用

    public class GenericDemo {
        public static void main(String[] args) {
            //类型通配符:<?>
            List<?> list1 = new ArrayList<Object>();
            List<?> list2 = new ArrayList<Number>();
            List<?> list3 = new ArrayList<Integer>();
            //list1.add("jsj");这样不行,因为你声明list1是List<?>类型的,所以他不能添加元素
            System.out.println("--------");
    
            //类型通配符上限:<? extends 类型>
    //        List<? extends Number> list4 = new ArrayList<Object>();//不行,因为你的这个通配符的上限是Number所以?最多表示Number。
            List<? extends Number> list5 = new ArrayList<Number>();
            //list5.add(12);这样也不能添加元素
            List<? extends Number> list6 = new ArrayList<Integer>();//Number是Integer的父类
            System.out.println("--------");
    
            //类型通配符下限:<? super 类型>
            List<? super Number> list7 = new ArrayList<Object>();
            List<? super Number> list8 = new ArrayList<Number>();
    //        List<? super Number> list9 = new ArrayList<Integer>();
    
        }
    }
    
  • 注意点

    • List不是List的父类,他们之间没有关系。

    • List<?> 是所有List的父类,比如List< ? >是List的父类,也是List的父类,所有List< ? > list=new ArrayList();当然这样这里List< ? >可以接受ArrayList< String >的对象,是因为ArrayList是ArrayList< ? >的子类,然后这个List< ? >是ArrayList< ? >的父类。

      不信你看程序中这样的代码会不会报错

      public class Demo{
          public static void main(String[] args) {
              ArrayList<?> list1=new ArrayList<String>();//说明ArrayList<?>是ArrayList<String>的父类
      //        List<?> list2=new ArrayList<?>();注意一点,不能new一个ArrayList<?>
              List<?> list3=list1;//说明这个List<?>是ArrayList<?>的父类
          }
      }
      
    • XX不能new一个ArrayList<?>,但是可以下面这样。就是不能实际生成一个ArrayList<? >或List<? super XX>或List<? extends XX>,只能告诉编译他表面上是这个类型的。

      ArrayList<?> list1=new ArrayList<>();//可以
      ArrayList<?> list1=new ArrayList();//可以
      
    • List<? extends Number>:它表示这个List的泛型是未知的,但是这个未知的东西一定继承自Number或是他自己。这个List<? extends Number>是所有List<Number子类>的父类,且他可以接受表面是List类型的对象

    • List<? super Number>:它表示这个List的泛型是未知的,但是这个未知的东西一定是Number的父类或是他自己。这个List<? super Number>是所有List<Number父类>的父类,且他可以接受表面是List类型的对象

    • 但是表面声明带?的那些类型,都不能使用添加方法。但是可以使用删除方法。

      public class Demo{
          public static void main(String[] args) {
              ArrayList<?> list1=new ArrayList<String>();
      //        list1.add("a");//不能通过编译器检测
              list1.remove("s");//可以通过编译器检测,也可以执行,不会抛出异常
          }
      }
      

可变参数

先看一个例子:

package com.liudashuai;

public class Demo {
    public static void main(String[] args) {
        System.out.println(sum(10, 20));
        System.out.println(sum(10, 20, 30));
        System.out.println(sum(10, 20, 30, 40));

//        System.out.println(sum(10,20,30,40,50));
//        System.out.println(sum(10,20,30,40,50,60));
//        System.out.println(sum(10,20,30,40,50,60,70));
//        System.out.println(sum(10,20,30,40,50,60,70,80,90,100));
    }
    public static int sum(int a,int b){
        return a+b;
    }
    public static int sum(int a,int b,int c){
        return a+b+c;
    }
    public static int sum(int a,int b,int c,int d){
        return a+b+c+d;
    }
    
}

要实现那些注释掉的sout语句,就得继续重载很多sum方法,就很麻烦。

所以出现了可变参数。

  • 可变参数介绍

    可变参数又称参数个数可变,要是在方法的形参处使用,那么方法参数个数就是可变的了

  • 可变参数定义格式为:(主要是注意那个…),具体使用等一下看例子就知道了。

    修饰符 返回值类型 方法名(数据类型… 变量名) {  }
    
  • 例子1

    public class ArgsDemo01 {
        public static void main(String[] args) {
            System.out.println(sum(10, 20));
            System.out.println(sum(10, 20, 30));
            System.out.println(sum(10, 20, 30, 40));
    
            System.out.println(sum(10,20,30,40,50));
            System.out.println(sum(10,20,30,40,50,60));
            System.out.println(sum(10,20,30,40,50,60,70));
            System.out.println(sum(10,20,30,40,50,60,70,80,90,100));
            //System.out.println(sum(12,12.5));错误
        }
    
        public static int sum(int... a) {//那些sum(int,int,……)的调用都是来执行这个方法的,但是要是sum(12,12.5)这样不能找到这个方法,就会报错。
            int sum = 0;
            for(int i : a) {	//实际上a是一个数组,你两个参数的如sum(10, 20),就是把第一个10放在a[0]位置,20放在a[1位置上]
                sum += i;
            }
            return sum;
        }
    }
    
  • 例子2

    package com.liudashuai;
    
    public class Demo {
        public static void main(String[] args) {
            f(10, 20, 30);
            f(10, 20);
        }
    
        public static void f(int... a) {// f(10, 20, 30);调用的时候没有问题,f(10, 20);调用的时候执行到a[2].sout时就会下标越界,所以使用的时候要注意数组下标越界问题
            System.out.println(a[0]);
            System.out.println(a[1]);
            System.out.println(a[2]);
        }
    }
    

在这里插入图片描述

  • 例子3

    package com.liudashuai;
    
    public class Demo {
        public static void main(String[] args) {
            f("3个数的和为:",10, 20, 30);
            f("两个数的和为:",10, 20);
        }
    
        public static void f(String s,int... a) {//可变参数必须放在最后
            int sum=0;
            for (int i:a) {
                sum+=i;
            }
            System.out.println(s+sum);
        }
    }
    

    在这里插入图片描述

  • 可变参数的注意事项

    • 这里的变量其实是一个数组
    • 如果一个方法有多个参数,包含可变参数,可变参数要放在最后(因为匹配到可变参数,他直接把从可变参数位置的那个数据开始到最后都放在了可变参数里面)

可变参数在java已经封装好的类中的使用

当然下面的这个方法不能修改和可变参数没有关系啊,他们不可以修改是因为他们生成的对象里面的方法修改时会抛出异常。比如那个Arrays里面的ArrayList的对象,他add方法执行的时候会直接执行throw new UnsupportedOperationException();所以他不能用add方法,一用就抛出异常。这里只是举例说了一些使用的地方,你看那个Arrays的asList方法可以这样用Arrays.asList(“a”);也可以这样用Arrays.asList(“hello”, “world”, “java”);,可以随便写多少个参数都不会报错,才是可变参数的效果,不可修改不是可变参数的效果

  • Arrays工具类中有一个静态方法:

    • public static List asList(T… a):返回由指定数组支持的固定大小的列表
    • 返回的集合不能做增删操作,可以做修改操作。即改变长度的操作都不行。
  • 示例代码(类型的位置都是可以用泛型来代替的哦,泛型就等于某种数据类型,所在下面可变参数的a的类型是T是ok的)

    public class ArgsDemo02 {
        public static void main(String[] args) {
            //在Arrays中有一个public static <T> List<T> asList(T... a)的方法,可以返回一个Arrays的一个内部类ArrayList的对象,这样Arrays内的ArrayList和我们常用的ArrayList不一样,这个是不能add和remove的。
            List<String> list = Arrays.asList("hello", "world", "java");
            list.add("javaee"); //会抛出UnsupportedOperationException异常
            list.remove("world"); //会抛出UnsupportedOperationException异常
            list.set(1,"javaee");//可以
        }
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值