TreeSet、Map以及一些面试题

这一块的内容主要是有关TreeSet、Map以及Java中的一些面试题的内容。

TreeSet

有关TreeSet

  • TreeSet:能够使元素按照某种规则进行排序
  • 排序有两种:1、自然排序;2、选择器排序
  • TreeSet集合的特点:元素唯一、可以排序
  • 通过观察TreeSet的add()方法,我们发现最终要看TreeMap的put()方法

发现经过TreeSet,元素按照升序输出了

public static void main(String[] args) {
        //创建集合对象
        //现在这样的排序叫做 自然排序
        TreeSet<Integer> set = new TreeSet<Integer>();
        set.add(10);
        set.add(20);
        set.add(23);
        set.add(90);
        set.add(13);
        set.add(1);
        //遍历
        for(Integer i:set){
            System.out.println(i);
        }
    }
}
//1
//10
//13
//20
//23
//90

TreeSet中的add()方法图解

  • TreeSet的底层是二叉树(红黑树)
  • 主要是记住它是中序遍历(左根右)遍历
    在这里插入图片描述

TreeSet中的add方法源码解析

add方法保证了元素的有序性
我的理解:
TreeSet里面的add实际上调用的是TreeMap里面的put方法

1、首先我们点进去add方法,发现add方法是m来实现的
2、m又是NavigableMap接口
3、接下来要找实现NavigableMap接口的一个子类对象
4、子类对象也就是TreeMap
5、最后发现调用的是TreeMap中的put方法

public interface Collection{
    ...
}

public interface Set<E> extends Collection{
    ...
}

Interface NavigableMap{

}

class TreeMap implements NavigableMap{
    public V put(K key, V value) {
        Entry<K,V> t = root;
        //一开始添加元素的时候,一定是没有根的,所以第一个元素t一定是等于null的
        //造一个根节点
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        //由于是无参构造,是自然比较,没有比较器,所以是==null的
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
            //通过观察API发现,Integer类实现了Comparable接口,所以可以向上转型
            //又发现了一点:无参构造虽然是自然排序,但是也需要有一个Comparable自然比较器
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
}


class TreeSet implements Set{
    private transient NavigableMap<E,Object> m;

    public TreeSet() {
        this(new TreeMap<E,Object>());
    }

    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
}

TreeSet存储自定义对象并保证元素的排序和唯一

首先我们开始第一种做法
我们用TreeSet存储自定义对象,并保证元素的排序和唯一性,但是并没有说明怎么排序,元素怎么保证唯一也没说(实际上,成员变量相同,就是同一个变量)

public static void main(String[] args) {

        TreeSet<Student> treeSet = new TreeSet<Student>();

        Student s1 = new Student("zhangsan", 21);
        Student s2 = new Student("lisi", 22);
        
        treeSet.add(s1);
        treeSet.add(s2);

        for(Student s : treeSet){
            System.out.println(s.getName()+"---"+s.getAge());
        }
    }
}
//Exception in thread "main" java.lang.ClassCastException: com.bigdata.shujia20.Student cannot be cast to java.lang.Comparable

按照我们固有的思维去做,在思维逻辑上并没有什么问题,但是我们发现这种做法出错了,报错的原因是我们没有重写 Comparable

第二种正确的做法:

需求:按照姓名的长度进行排序
TreeSet可以保证元素的排序和唯一性
排序:(自然排序和比较器排序)
自然排序:
创建集合的时候调用的无参构造
让元素所属的类实现自然排序接口Comparable
比较器排序:
创建集合的时候传入比较器对象,调用有参构造
让集合的构造方法接收一个比较器接口的子类对象Comparator

我们首先实现Comparator接口
并且做比较(首先比较姓名的长度,在一致的情况下,我们比较内容,之后我们比较年龄)

public class MyComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        //比较姓名的长度
        int i = o1.getName().length() - o2.getName().length();

        //比较姓名的长度一样,但是内容不一定一样
        int i1 = i == 0 ? o1.getName().compareTo(o2.getName()) : i;

        //在内容相同的情况下,比较年龄
        int i2 = i1 == 0 ? o1.getAge() - o2.getAge() : i1;
        return i2;
    }
}

之后,继续按照上面的想法,我们存储自定义对象,并保证元素的唯一和有序

public static void main(String[] args) {

        TreeSet<Student> students = new TreeSet<Student>(new MyComparator());

        Student s1 = new Student("zhangsna", 21);
        Student s2 = new Student("lisi", 22);
        Student s3 = new Student("wangwu", 23);

        students.add(s1);
        students.add(s2);
        students.add(s3);

        for(Student s:students){
            System.out.println(s.getName()+"---"+s.getAge());
        }

    }
}
//lisi---22
//wangwu---23
//zhangsna---21

Set集合的一些小练习

HashSet类的简单概述

在这里插入图片描述

使用HashSet编写一个小程序,获取10个随机数,且随机数不重复

public static void main(String[] args) {
        //创建随机数对象
        Random rd = new Random();

        //创建HashSet集合
        HashSet<Integer> integers = new HashSet<>();

        //判断集合的长度是否小于10
        while (integers.size()<10){
            int i = rd.nextInt(20)+1;
            integers.add(i);
        }

        //遍历集合
        for(Integer i:integers){
            System.out.println(i);
        }
    }
}
//16
//17
//18
//5
//7
//9
//10
//13
//14
//15

使用TreeSet键盘录入五个学生信息(姓名、语数英,按总分从高到低输出)

public static void main(String[] args) {
        //创建Scanner对象
        Scanner sc = new Scanner(System.in);

        //创建TreeSet集合
        TreeSet<Student1> treeSet = new TreeSet<Student1>(new Comparator<Student1>() {
            @Override
            public int compare(Student1 o1, Student1 o2) {
                //总分从高到低
                int i = o2.sum() - o1.sum();
                //总分相同,不一定语文成绩相同
                int i1 = i == 0 ? o1.getChinese() - o2.getChinese():i;
                //总分相同不一定数学成绩相同
                int i2 = i1 == 0?o1.getMath() - o2.getMath():i1;
                //总分相同不一定英语成绩相同
                int i3 = i2 == 0?o1.getEnglish() - o2.getEnglish():i2;
                //姓名还不一定相同
                int i4 = i3 == 0?o1.getName().compareTo(o2.getName()):i3;
                return i4;
            }
        });

        System.out.println("请输入学生信息");

        for(int i=0;i<5;i++){
            System.out.println("你正在输入第"+(i+1)+"位学生的姓名");
            String name = sc.next();
            System.out.println("你正在输入第"+(i+1)+"位学生的语文成绩");
            int chinese = sc.nextInt();
            System.out.println("你正在输入第"+(i+1)+"位学生的数学成绩");
            int math = sc.nextInt();
            System.out.println("你正在输入第"+(i+1)+"位学生的英语成绩");
            int english = sc.nextInt();

            //创建学生对象,并赋值
            Student1 s = new Student1(name, chinese, math, english);

            treeSet.add(s);

        }

        System.out.println("学生信息录入完毕.......");

        System.out.println("学生按照总分从高到低如下:");
        System.out.println("姓名\t语文成绩\t数学成绩\t英语成绩\t总分");
        for(Student1 s: treeSet){
            System.out.println(s.getName()+"\t"+s.getChinese()+"\t"+s.getMath()+
                    "\t"+s.getEnglish()+"\t"+s.sum());
        }
        
    }
}

Map集合

Map集合的引入

作为一个学生,是根据学号的不同来区分学生,假设我们以及用学生的学号来获取到了学生的姓名了,那年龄该怎么获取呢
如果采取前面的集合的方式的话,我们把学生的学号和姓名作为一个对象存入到集合,然后存储整个对象,将来在遍历的时候,通过判断获取到学生的姓名
但我们这时候都已经把姓名拿出来了,为什么还要拿出学号呢
我们的需求是:仅仅知道学号,就想知道学生的姓名或者其他信息,该怎么办?
Java中提供了一个新的方式:Map集合

通过查看API我们发现,Map集合的特点是将键映射到值的对象
Map中不能包含重复的键,每个键可以映射到最多一个值
<K,V>映射
Key Value
1001 zhangsan
1002 lisi
1003 wangwu
1004 zhaoliu
1001(重复了) tianqi

Map集合的特点

  • 将键映射到值的对象
  • 一个映射不能包含重复的键
  • 每个键最多只能映射到一个值

Map接口和Collection接口的不同:

  • Map集合的元素是成对出现的,Map集合的键是唯一的,值是可以重复的
  • Collection集合存储的元素是单独出现的,Collection的子接口Set中的元素是唯一的,List中的元素是可以重复的

Map集合的功能

1、添加功能:
V put(K key, V value) 将指定的值与该映射中的指定键相关联(可选操作)。
2、删除功能:
void clear() 从Map中删除所有的映射(可选操作)。
V remove(Object key) 如果存在(从可选的操作),从该地图中删除一个键的映射。
3、判断功能:
boolean containsKey(Object key) 如果此映射包含指定键的映射,则返回 true 。
boolean containsValue(Object value) 如果此地图将一个或多个键映射到指定的值,则返回 true 。
boolean isEmpty() 如果此地图不包含键值映射,则返回 true 。
4、获取功能:
Set<Map.Entry<K,V>> entrySet() 返回Map中包含的映射的Set视图。
V get(Object key) 返回到指定键所映射的值,或 null如果此映射包含该键的映射。
Set keySet() 返回此地图中包含的键的Set视图。
Collection values() 返回Map中包含的值的Collection视图。
5、长度功能:
int size() 返回Map中键值映射的数量。

Map集合中的方法测试

public static void main(String[] args) {

        HashMap<String, String> hashMap = new HashMap<String, String>();

        hashMap.put("zhangsan","21");
        hashMap.put("lisi","22");
        hashMap.put("wangwu","23");
        hashMap.put("zhaoliu","24");

        System.out.println(hashMap);

//        //void clear()
        hashMap.clear();
        System.out.println(hashMap);
//
//        //V remove(Object key)删除一个键的映射
        hashMap.remove("zhangsan");
        System.out.println(hashMap);

//        //containsKey(Object key)如果此映射包含指定键的映射,返回true
        System.out.println(hashMap.containsKey("lisi"));
//
//        //boolean isEmpty() 如果不包含键值的映射,返回true
        System.out.println(hashMap.isEmpty());
//
//        //int size() 返回Map集合中键值映射的数量
        System.out.println(hashMap.size());

    }
}

Map集合的获取功能

  • Set<Map.Entry<K,V>> entrySet() 返回Map中包含的映射的Set视图。
  • V get(Object key) 返回到指定键所映射的值,或 null如果此映射包含该键的映射。
  • Set keySet() 返回此地图中包含的键的Set视图。
  • Collection values() 返回Map中包含的值的Collection视图。
public static void main(String[] args) {
        //创建集合对象
        HashMap<String, String> hashMap = new HashMap<String,String>();

        //创建元素并添加
        hashMap.put("zhangsan","21");
        hashMap.put("lisi","22");
        hashMap.put("wangwu","23");
        hashMap.put("zhaoliu","24");

        //V get(Object key)返回到指定键所映射的值,或null,如果此映射包含该键的映射
        System.out.println(hashMap.get("zhangsan"));//21
        System.out.println(hashMap.get("lisi"));//22

        //Set<K> KeySet() 返回Map中包含所有键的Set集合
        Set<String> strings = hashMap.keySet();
        for(String s : strings){
            System.out.println(s);//返回了所有的Key
        }

        //Collection<V> values() 返回Map中包含的值的Collection集合
        Collection<String> values = hashMap.values();
        for(String s:values){
            System.out.println(s);//返回了所有的Value
        }
    }

Map集合的两种遍历

第一种

先把所有的Key集中起来
遍历每个Key,通过Key来获取Value

   public static void main(String[] args) {

        HashMap<String, String> hashMap = new HashMap<String, String>();

        hashMap.put("zhangsan","21");
        hashMap.put("lisi","22");
        hashMap.put("wangwu","23");

        //遍历
        //首先遍历Map集合中的Key
        Set<String> keys = hashMap.keySet();
        for(String key:keys){
            String value = hashMap.get(key);
            System.out.println(key+"---"+value);
        }

    }
}
//lisi---22
//zhangsan---21
//wangwu---23

第二种

先把K和V组成一个集合
遍历集合,得到每一个键值对对象
根据键值对获取元素的键和值

如何获取键值对对象的集合:
Set<Map.Entry<K,V>> entrySet() 返回Map中包含的映射的Set集合

public static void main(String[] args) {

        HashMap<String, String> hashMap = new HashMap<String,String>();

        hashMap.put("zhangsan","21");
        hashMap.put("lisi","22");
        hashMap.put("wangwu","23");
        hashMap.put("zhaoliu","24");

        Set<Map.Entry<String, String>> entries = hashMap.entrySet();

        //遍历集合,得到每一个键值对对象
        for (Map.Entry<String, String> s : entries) {
            String key = s.getKey();
            String value = s.getValue();
            System.out.println(key + "---" + value);
        }
        
    }
}
//lisi---22
//zhaoliu---24
//zhangsan---21
//wangwu---23

HashMap

  • 基于哈希表的Map接口的实现
  • 哈希表:用来保证键的唯一性
  • HashMap<String,String>
    键:String
    值:String

HashMap<String,String>:

   public static void main(String[] args) {

        HashMap<String, String> hashMap = new HashMap<String,String>();

//存储数据的时候,尽量不使用001、002这样的数据,因为以0开头的是八进制
//在008之后,就会开始报错
        hashMap.put("zhangsan","21");
        hashMap.put("lisi","22");
        hashMap.put("wangwu","23");
        hashMap.put("zhaoliu","24");

        Set<Map.Entry<String, String>> entries = hashMap.entrySet();
        for(Map.Entry<String, String> s:entries){
            String key = s.getKey();
            String value = s.getValue();
            System.out.println(key+"---"+value);
        }
    }
}
//lisi---22
//zhaoliu---24
//zhangsan---21
//wangwu---23

HashMap<String,Student>:

public static void main(String[] args) {
        HashMap<String, Student> hashMap = new HashMap<String,Student>();

        Student s1 = new Student("zhangsan", 21);
        Student s2 = new Student("lisi", 22);
        Student s3 = new Student("wangwu", 23);
        Student s4 = new Student("zhaoliu", 24);

        hashMap.put("s1",s1);
        hashMap.put("s2",s2);
        hashMap.put("s3",s3);
        hashMap.put("s4",s4);

        Set<Map.Entry<String, Student>> entries = hashMap.entrySet();

        for (Map.Entry<String, Student> s : entries){
            String key = s.getKey();
            Student value = s.getValue();
            System.out.println(key+"---"+value.getName()+"---"+value.getAge());
        }
    }
}
//s3---wangwu---23
//s4---zhaoliu---24
//s1---zhangsan---21
//s2---lisi---22

HashMap<Student,String>:
要求:如果两个对象的成员变量的值都相同,就判定同一个对象
关键点就是重写hashCode和equals方法

class Student3{
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student3 student3 = (Student3) o;
        return age == student3.age &&
                Objects.equals(name, student3.name);
    }

    @Override
    public int hashCode() {

        return Objects.hash(name, age);
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    Student3(){}

    Student3(String name,int age){
        this.name = name;
        this.age = age;
    }
}

public class MapDemo8 {
    public static void main(String[] args) {

        HashMap<Student3, String> hashMap = new HashMap<Student3, String>();

        Student3 s1 = new Student3("zhangsan", 21);
        Student3 s2 = new Student3("lisi", 22);
        Student3 s3 = new Student3("wangwu", 23);
        Student3 s4 = new Student3("zhangsan", 21);

        hashMap.put(s1,"s1");
        hashMap.put(s2,"s2");
        hashMap.put(s3,"s3");
        hashMap.put(s4,"s1");

        Set<Map.Entry<Student3, String>> entries = hashMap.entrySet();

        for(Map.Entry<Student3, String> s:entries){
            Student3 key = s.getKey();
            String value = s.getValue();
            System.out.println(key.getName()+"---"+key.getAge()+"---"+value);
        }
    }
}
//lisi---22---s2
//zhangsan---21---s1
//wangwu---23---s3

LinkedHashMap

  • 是哈希表和链表实现的Map接口,具有可预知的迭代顺序
  • 由哈希表保证唯一性
  • 由链表保证顺序,有序(存储和取出的顺序一致)
  • 底层也是红黑树(结合红黑树、哈希表,查询速度不慢)
   public static void main(String[] args) {
        LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();

        linkedHashMap.put("zhangsan","21");
        linkedHashMap.put("lisi","22");
        linkedHashMap.put("wangwu","23");
        linkedHashMap.put("zhaoliu","24");

        Set<Map.Entry<String, String>> entries = linkedHashMap.entrySet();
        for(Map.Entry<String, String> s:entries){
            String key = s.getKey();
            String value = s.getValue();
            System.out.println(key+"---"+value);
        }
    }
}
//zhangsan---21
//lisi---22
//wangwu---23
//zhaoliu---24

TreeMap

  • 是基于红黑树的Map接口的实现
    TreeMap<String,String>
public static void main(String[] args) {
        TreeMap<String, String> treeMap = new TreeMap<String,String>();

        treeMap.put("zhangsan","21");
        treeMap.put("lisi","22");
        treeMap.put("wangwu","23");
        treeMap.put("zhaoliu","24");

        Set<Map.Entry<String, String>> entries = treeMap.entrySet();

        for (Map.Entry<String, String> s : entries){
            String key = s.getKey();
            String value = s.getValue();
            System.out.println(key+"---"+value);
        }
    }
}
//lisi---22
//wangwu---23
//zhangsan---21
//zhaoliu---24

TreeMap<Student,String>

public static void main(String[] args) {
        TreeMap<Student, String> treeMap = new TreeMap<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                int i = o1.getName().compareTo(o2.getName());
                int i1 = i == 0 ? o1.getAge() - o2.getAge() : i;
                return i1;
            }
        });

        Student s1 = new Student("zhangsan", 21);
        Student s2 = new Student("lisi", 22);
        Student s3 = new Student("wangwu", 23);
        Student s4 = new Student("zhaoliu", 24);

        treeMap.put(s1,"s1");
        treeMap.put(s2,"s2");
        treeMap.put(s3,"s3");
        treeMap.put(s4,"s4");

        Set<Map.Entry<Student, String>> entries = treeMap.entrySet();
        for(Map.Entry<Student, String> s : entries){
            Student key = s.getKey();
            String value = s.getValue();
            System.out.println(key.getName()+"---"+key.getAge()+"---"+value);
        }
    }
}
//lisi---22---s2
//wangwu---23---s3
//zhangsan---21---s1
//zhaoliu---24---s4

Map集合的一个小案例

“aababcabcdabcde”,获取字符串中每一个字母出现的次数要求结果:a(5)b(4)c(3)d(2)e(1)

public static void main(String[] args) {
        //定义一个字符串
//        String s = "aababcabcdabcde";

        Scanner sc = new Scanner(System.in);
        String s = sc.next();

        //定义一个TreeMap
        TreeMap<Character, Integer> map = new TreeMap<>();

        //将字符串转换成字符数组
        char[] chars = s.toCharArray();

        //遍历字符数组,得到每一个字符
        for(char ch:chars){
            //拿到字符作为键去集合中找值,看返回值
            Integer i = map.get(ch);

            if(i == null){
                map.put(ch,1);
            }
            else{
                //不是null,就把value的值加1,把该字符作为键,value设置为1
                i++;
                map.put(ch,i);
            }
        }

        //定义一个StringBuidler进行拼接
        StringBuilder sb = new StringBuilder();

        //遍历集合
        Set<Map.Entry<Character, Integer>> entries = map.entrySet();
        for(Map.Entry<Character, Integer> e:entries){
            Character key = e.getKey();
            Integer value = e.getValue();
            sb.append(key).append("(").append(value).append(")");
        }

        //把StringBuilder转换成String输出
        String s1 = sb.toString();
        System.out.println(s1);
    }

面试题

HashMap和Hashtable的区别

  • HashMap中的键和值允许null,而Hashtable不允许键和值存在null
  • HashMap是线程不安全的,但是效率高;Hashtable是线程安全的,效率低

List、Set、Map等接口是否都继承Map接口

Collection(接口)

  • List(接口)

    • ArrayList
      底层数据结构是数组,查询快,增删慢
      线程不安全,效率高
    • Vector
      底层数据结构是数组,查询快,增删慢
      线程安全,效率低
    • LinkedList
      底层数据结构是链表,查询慢,增删快
      线程不安全,效率高
      不知道选谁,就选ArrayList
  • Set(接口)

    • HashSet
      底层数据结构是哈希表,保证数据的唯一性
      • LinkedHashSet
        底层数据结构是哈希表和链表(双链表)
        哈希表和链表共同保证数据的有序和唯一
    • TreeSet
      底层数据结构是红黑树
      排序的两种方式:都是在创建TreeSet对象的时候,构造方法调用
      1、自然排序,通过类来实现接口
      2、比较器排序,可以重写个类实现,也可以用匿名内部类
  • Map(接口)

    • HashMap
      底层数据结构是哈希表
      • LinkedHashMap
        底层数据结构是哈希表和链表
        哈希表和链表共同保证数据的唯一和有序
    • TreeMap
      底层数据结构是红黑树(键是红黑树结构,可以保证键的排序和唯一性)
      排序的两种方式:都是在创建TreeMap对象的时候,构造方法调用
      1、自然排序,通过类来实现接口
      2、比较器排序,可以重写个类实现,也可以用匿名内部类

Hashtable中不能存null

public class InterviewDemo {
    public static void main(String[] args) {
        HashMap<String, String> map1 = new HashMap<String, String>();
        Hashtable<String, String> map2 = new Hashtable<String, String>();

        map1.put("shujia","hello");
        map1.put(null,"java");
        map1.put("world",null);
        map1.put(null,null);


        map2.put("shujia","hello");
        map2.put(null,"java");
        map2.put("world",null);
        map2.put(null,null);

        System.out.println(map1);
        System.out.println(map2);

    }
}

感谢阅读,我是啊帅和和,一位大数据专业即将大四学生,祝你快乐。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

啊帅和和。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值