Java 中 Map 接口详解:知识点与注意事项

一、Map 接口概述

Map 接口是 Java 集合框架中非常重要的一员,位于 java.util 包下。它提供了一种键值对(Key-Value)的存储方式,用于存储具有映射关系的数据,允许通过键(key)来高效地查找对应的值(value)。这种数据结构在编程中非常常见,特别适合需要快速查找和检索的场景。

Map 接口的主要特点

1. 键唯一性

Map 中的每个键(key)都必须是唯一的,不允许出现重复的键。如果向 Map 中放入一个已经存在的键,新的值会覆盖原来的值。

示例代码

Map<String, String> map = new HashMap<>();
map.put("name", "Alice");  // 第一次放入键"name"
map.put("name", "Bob");    // 覆盖键"name"的值
System.out.println(map.get("name")); // 输出: Bob

2. 值可重复

与键不同,值(value)可以重复,多个不同的键可以对应相同的值。

示例代码

Map<String, String> map = new HashMap<>();
map.put("firstName", "John");  // 键"firstName"对应值"John"
map.put("lastName", "John");   // 键"lastName"也对应值"John"
System.out.println(map);       // 输出: {firstName=John, lastName=John}

3. 键值对映射

每个键都精确地映射到一个具体的值,通过键可以快速获取对应的值(时间复杂度通常为O(1))。

示例代码

Map<String, Integer> ageMap = new HashMap<>();
ageMap.put("Alice", 25);
ageMap.put("Bob", 30);
int aliceAge = ageMap.get("Alice");  // 25
int bobAge = ageMap.get("Bob");      // 30

Map 接口的层次结构

Map 接口
├── HashMap(无序,基于哈希表实现,性能最好)
├── TreeMap(有序,基于红黑树实现,按key排序)
├── LinkedHashMap(有序,保持插入顺序)
└── Hashtable(线程安全,但已基本被ConcurrentHashMap取代)

应用场景建议

  1. 快速查找场景:首选HashMap,因其O(1)的平均时间复杂度
  2. 需要排序的场景:选择TreeMap,保证按键排序
  3. 需要保持插入顺序的场景:使用LinkedHashMap
  4. 多线程环境:使用ConcurrentHashMap而非Hashtable
  5. 缓存实现:常使用LinkedHashMap的访问顺序特性实现LRU缓存

LRU缓存示例

final int MAX_ENTRIES = 100;
Map<String, String> lruCache = new LinkedHashMap<String, String>(MAX_ENTRIES, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }
};

二、Map 接口的常用实现类

(一)HashMap

HashMap 是 Map 接口最常用的实现类之一,它基于哈希表实现,具有以下特点:

  1. 存储结构

    • HashMap 内部使用数组和链表(JDK 1.8 及以上版本在链表长度达到一定阈值时会转为红黑树)来存储键值对
    • 哈希表通过哈希函数将键映射到数组的索引位置,从而实现快速的查找、插入和删除操作
    • 示例:当存储键值对("name", "张三")时,会先计算"name"的hashCode(),然后通过哈希算法确定数组索引位置
  2. 性能

    • 查找、插入和删除操作的平均时间复杂度为O(1)
    • 在哈希冲突严重的情况下,性能可能会下降到O(n)
    • 扩容机制:默认初始容量为16,负载因子为0.75,当元素数量超过容量*负载因子时会进行扩容
  3. 线程安全性

    • 非线程安全
    • 多线程环境下如果同时进行修改,可能会导致数据不一致或其他异常
    • 解决方案:可以使用Collections.synchronizedMap()或ConcurrentHashMap
  4. 排序特性

    • 元素是无序的
    • 不保证键值对的存储顺序和遍历顺序一致
    • 应用场景:适合需要快速查找而不关心顺序的场景

(二)TreeMap

TreeMap 基于红黑树实现,它具有以下特点:

  1. 排序性

    • 元素会按照键的自然顺序(实现Comparable接口)或自定义比较器(通过Comparator实现)进行排序
    • 遍历TreeMap时可以得到有序的结果
    • 示例:存储("b",2),("a",1),("c",3)会按照"a","b","c"的顺序遍历
  2. 性能

    • 查找、插入和删除操作的时间复杂度为O(log n)
    • 相比HashMap性能稍低
    • 适合需要排序的场景
  3. 线程安全性

    • 非线程安全
    • 多线程环境下需要额外的同步措施
    • 可以使用Collections.synchronizedSortedMap()包装
  4. 特殊方法

    • 提供firstKey(), lastKey()等方法获取边界元素
    • 提供subMap(), headMap(), tailMap()等方法获取范围视图

(三)LinkedHashMap

LinkedHashMap 是 HashMap 的子类,具有以下特点:

  1. 有序性

    • 在HashMap基础上增加了一个双向链表
    • 可以保证遍历顺序与插入顺序一致(默认情况)
    • 或者按照访问顺序排序(通过构造函数设置accessOrder为true)
    • 应用场景:适合需要保持插入顺序或实现LRU缓存的场景
  2. 性能

    • 性能与HashMap相近
    • 由于需要维护双向链表,内存开销稍大
    • 查找操作时间复杂度仍为O(1)
  3. 线程安全性

    • 非线程安全
    • 多线程环境下需要额外同步
  4. 实现细节

    • 重写了HashMap的节点类,增加了before和after指针
    • 通过重写新节点创建和访问节点的方法来维护链表

(四)Hashtable

Hashtable 是一个古老的 Map 实现类,特点如下:

  1. 线程安全性

    • 是线程安全的
    • 方法大多被synchronized关键字修饰
    • 多线程环境下可以安全使用
    • 但性能相对较低
  2. 存储限制

    • 不允许键或值为null
    • 而HashMap允许键和值为null(但键只能有一个null)
    • 示例:Hashtable.put(null, "value")会抛出NullPointerException
  3. 性能

    • 由于线程安全的开销,性能不如HashMap
    • 在JDK1.0引入,现在一般推荐使用ConcurrentHashMap
  4. 继承体系

    • 是Dictionary类的子类
    • 与HashMap不同,不继承AbstractMap
    • 方法命名风格与早期Java集合类一致

三、Map 接口的核心方法

Map 接口是 Java 集合框架中用于存储键值对的核心接口,它定义了一系列用于操作键值对的方法。Map 中的键必须是唯一的,而值可以重复。以下是对 Map 接口常用方法的详细说明和实际应用场景:

(一)添加元素

V put(K key, V value)

  • 功能:将指定的键值对放入 Map 中
  • 返回值
    • 如果该键已存在,则返回原来的值,并将新值覆盖原来的值
    • 如果键不存在,则返回 null
  • 示例
    Map<String, Integer> map = new HashMap<>();
    map.put("Apple", 10);  // 返回 null
    map.put("Banana", 20); // 返回 null
    Integer oldValue = map.put("Apple", 15); // 返回 10
    

void putAll(Map<? extends K, ? extends V> m)

  • 功能:将另一个 Map 中的所有键值对添加到当前 Map 中
  • 特点:相当于批量 put 操作
  • 示例
    Map<String, Integer> map1 = new HashMap<>();
    map1.put("Apple", 10);
    map1.put("Banana", 20);
    
    Map<String, Integer> map2 = new HashMap<>();
    map2.put("Orange", 30);
    map2.put("Grape", 40);
    
    map1.putAll(map2); // map1 现在包含4个键值对
    

(二)获取元素

V get(Object key)

  • 功能:根据指定的键获取对应的值
  • 返回值:如果键不存在,则返回 null
  • 注意:由于返回 null 可能表示键不存在或值为 null,可以使用 containsKey 方法区分
  • 示例
    Map<String, Integer> map = new HashMap<>();
    map.put("Apple", 10);
    
    Integer value = map.get("Apple"); // 返回 10
    Integer notExist = map.get("Pear"); // 返回 null
    

boolean containsKey(Object key)

  • 功能:判断 Map 中是否包含指定的键
  • 应用场景:在调用 get 方法前先检查键是否存在
  • 示例
    if (map.containsKey("Apple")) {
        // 执行相关操作
    }
    

boolean containsValue(Object value)

  • 功能:判断 Map 中是否包含指定的值
  • 特点:需要遍历所有值,时间复杂度较高
  • 示例
    if (map.containsValue(10)) {
        // 执行相关操作
    }
    

int size()

  • 功能:返回 Map 中键值对的数量
  • 示例
    int count = map.size(); // 返回当前键值对数量
    

boolean isEmpty()

  • 功能:判断 Map 是否为空(即 size 是否为 0)
  • 示例
    if (map.isEmpty()) {
        // Map为空时的处理逻辑
    }
    

(三)删除元素

V remove(Object key)

  • 功能:根据指定的键删除对应的键值对
  • 返回值:返回被删除的值。如果键不存在,则返回 null
  • 示例
    Integer removed = map.remove("Apple"); // 删除键为Apple的键值对
    

void clear()

  • 功能:清空 Map 中的所有键值对
  • 特点:执行后 size 将变为 0
  • 示例
    map.clear(); // 清空整个Map
    

(四)获取集合视图

Set<K> keySet()

  • 功能:返回 Map 中所有键组成的 Set 集合
  • 特点:返回的是视图,对 Map 的修改会反映在返回的 Set 上
  • 应用场景:遍历所有键
  • 示例
    for (String key : map.keySet()) {
        System.out.println(key);
    }
    

Collection<V> values()

  • 功能:返回 Map 中所有值组成的 Collection 集合
  • 特点:返回的是视图,可能包含重复值
  • 示例
    for (Integer value : map.values()) {
        System.out.println(value);
    }
    

Set<Map.Entry<K, V>> entrySet()

  • 功能:返回 Map 中所有键值对(Entry 对象)组成的 Set 集合
  • 特点
    • Entry 是 Map 接口的内部接口
    • Entry 包含 getKey() 和 getValue() 方法用于获取键和值
    • 效率最高的遍历方式
  • 示例
    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        System.out.println(entry.getKey() + ": " + entry.getValue());
    }
    

这些方法构成了 Map 接口的核心功能,为开发人员提供了灵活操作键值对的能力。在实际应用中,根据具体需求选择合适的方法可以大大提高代码的效率和可读性。

四、Map 的遍历方式

遍历 Map 是开发中经常遇到的操作,常见的遍历方式有以下几种:

(一)通过 keySet() 遍历

先获取所有键的集合,然后通过键来获取对应的值。这种方法适用于需要单独处理键或值的场景,比如只需要检查某些键是否存在。示例代码如下:

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// 遍历键集合
for (String key : map.keySet()) {
    // 通过键获取值
    Integer value = map.get(key);
    System.out.println("key: " + key + ", value: " + value);
    
    // 可以在此处对键进行其他操作
    if (key.startsWith("a")) {
        System.out.println("Found key starting with 'a': " + key);
    }
}

这种方式的缺点是当 Map 较大时,通过get(key)获取值可能会有性能损耗(对于 HashMap 影响不大,但对于 TreeMap 等基于树结构的 Map 来说,每次获取值都需要进行一次查找)。在性能敏感的场景下,建议使用entrySet()。

(二)通过 entrySet() 遍历

获取所有键值对的集合,然后直接遍历键值对。这是最推荐的遍历方式,因为它在所有Map实现中都能保持较好的性能。特别适合需要同时处理键和值的场景。示例代码如下:

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// 直接遍历键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    String key = entry.getKey();
    Integer value = entry.getValue();
    System.out.println("key: " + key + ", value: " + value);
    
    // 可以同时访问键值对
    if (value > 1) {
        System.out.println(key + " has value greater than 1");
    }
}

(三)通过 values() 遍历

如果只需要遍历值而不需要键,可以使用这种方式。常见于统计、求和等场景。示例代码如下:

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// 仅遍历值
int sum = 0;
for (Integer value : map.values()) {
    System.out.println("value: " + value);
    sum += value;
}
System.out.println("Total sum: " + sum);

(四)使用迭代器遍历

迭代器遍历可以在遍历过程中安全地进行删除操作,避免了ConcurrentModificationException异常。特别适合需要条件删除的场景。示例代码如下:

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// 使用迭代器遍历
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, Integer> entry = iterator.next();
    String key = entry.getKey();
    Integer value = entry.getValue();
    System.out.println("key: " + key + ", value: " + value);
    
    // 安全删除符合条件的元素
    if (value < 2) {
        iterator.remove();
        System.out.println("Removed: " + key);
    }
}

// 验证删除结果
System.out.println("Remaining items: " + map);

(五)Java 8+ 的 forEach 方法

在Java 8及更高版本中,可以使用更简洁的lambda表达式遍历:

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// 使用lambda表达式遍历
map.forEach((key, value) -> {
    System.out.println("key: " + key + ", value: " + value);
    // 可以在此处添加其他处理逻辑
});

五、Map 的排序

(一)TreeMap 的自然排序与自定义排序

TreeMap 是基于红黑树(Red-Black tree)实现的有序映射集合,它默认会按照键的自然顺序(natural ordering)进行排序。这种排序机制要求键必须实现 Comparable 接口。

自然排序示例

  1. String 类型键:按照字典顺序(lexicographical order)排序

    TreeMap<String, Integer> stringMap = new TreeMap<>();
    stringMap.put("zebra", 1);
    stringMap.put("apple", 2);
    stringMap.put("banana", 3);
    // 遍历顺序:apple → banana → zebra
    

  2. Integer 类型键:按照数值大小排序

    TreeMap<Integer, String> intMap = new TreeMap<>();
    intMap.put(100, "century");
    intMap.put(5, "five");
    intMap.put(30, "thirty");
    // 遍历顺序:5 → 30 → 100
    

自定义排序

当需要按照非自然顺序排序时,可以在构造 TreeMap 时传入 Comparator 对象:

// 自定义比较器:按字符串长度降序排序
Comparator<String> lengthComparator = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        // 先按长度降序,长度相同则按字典序
        int lengthCompare = Integer.compare(s2.length(), s1.length());
        return lengthCompare != 0 ? lengthCompare : s1.compareTo(s2);
    }
};

TreeMap<String, Integer> customSortedMap = new TreeMap<>(lengthComparator);
customSortedMap.put("apple", 1);
customSortedMap.put("banana", 2);
customSortedMap.put("pear", 3);
customSortedMap.put("kiwi", 4);
/*
 * 遍历顺序:
 * banana(6) 
 * apple(5)
 * kiwi(4)
 * pear(4)
 */

(二)HashMap 和 LinkedHashMap 的排序处理

HashMap 的无序性

HashMap 是基于哈希表实现的,不保证任何顺序(既不是插入顺序,也不是自然顺序)。它的迭代顺序可能随着时间变化而变化。

LinkedHashMap 的有序性

LinkedHashMap 默认保持键值对的插入顺序(insertion-order),但这不是排序顺序。构造时可通过参数设置为访问顺序(access-order)。

// 保持插入顺序的LinkedHashMap(默认)
LinkedHashMap<String, Integer> insertionOrderMap = new LinkedHashMap<>();
insertionOrderMap.put("apple", 1);
insertionOrderMap.put("banana", 2);
insertionOrderMap.put("pear", 3);
// 遍历顺序:apple → banana → pear

// 访问顺序的LinkedHashMap(最近最少使用)
LinkedHashMap<String, Integer> accessOrderMap = new LinkedHashMap<>(16, 0.75f, true);
accessOrderMap.put("apple", 1);
accessOrderMap.put("banana", 2);
accessOrderMap.put("pear", 3);
accessOrderMap.get("apple"); // 访问后,apple会移到末尾
// 遍历顺序:banana → pear → apple

排序处理方法

对于需要排序的场景,可以先将Map转换为List再进行排序:

1.按键排序

Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("zebra", 1);
hashMap.put("apple", 2);
hashMap.put("banana", 3);

// 转换为Entry列表
List<Map.Entry<String, Integer>> entryList = new ArrayList<>(hashMap.entrySet());

// 按键的自然顺序排序
Collections.sort(entryList, (e1, e2) -> e1.getKey().compareTo(e2.getKey()));
/*
 * 排序结果:
 * apple=2
 * banana=3
 * zebra=1
 */

2.按值排序

// 按数值降序排序
Collections.sort(entryList, (e1, e2) -> e2.getValue().compareTo(e1.getValue()));
/*
 * 排序结果:
 * banana=3
 * zebra=1
 * apple=2
 */

3.Java 8+ 的流式处理

// 按键排序
hashMap.entrySet().stream()
    .sorted(Map.Entry.comparingByKey())
    .forEach(System.out::println);

// 按值排序
hashMap.entrySet().stream()
    .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
    .forEach(System.out::println);

注意:这种排序方式会生成新的排序结果,但不会改变原始Map的结构。如果需要保持排序结果,可以考虑将排序后的结果存入新的LinkedHashMap中。

六、注意事项

(一)键的选择

重写 hashCode() 和 equals() 方法

在使用自定义对象作为 Map 的键时,必须正确重写 hashCode() 和 equals() 方法,原因如下:

  1. 哈希定位机制:HashMap 等基于哈希表的 Map 实现首先调用键的 hashCode() 方法计算哈希值,然后通过哈希值确定存储位置(桶)
  2. 相等性判断:当发生哈希冲突时,Map 会调用 equals() 方法判断键是否真正相等
  3. 不重写的后果
    • 默认的 hashCode() 方法(Object 类提供)基于对象地址计算
    • 默认的 equals() 方法使用 == 比较对象引用
    • 可能导致相同的逻辑键被当作不同键处理,无法正确查找或删除键值对

示例

class Person {
    private String name;
    private int age;
    
    // 必须重写 hashCode 和 equals
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person other = (Person) obj;
        return age == other.age && Objects.equals(name, other.name);
    }
}

键的不可变性

推荐使用不可变对象作为 Map 的键,原因包括:

  1. 哈希值稳定性:如果键对象被修改,其 hashCode() 值可能改变,导致:

    • 无法通过 get() 方法找到原本存储的值(因为计算出的新哈希值指向不同的桶)
    • 可能造成内存泄漏(旧值无法被访问但仍存在于 Map 中)
  2. 推荐键类型

    • String
    • Integer 等包装类
    • 其他不可变类
    • 如果要使用自定义可变对象,必须确保不会在作为键期间修改其关键字段

不良实践示例

Map<List<String>, String> map = new HashMap<>();
List<String> key = new ArrayList<>();
key.add("a");
map.put(key, "value");
key.add("b"); // 修改了键对象,导致哈希值改变
System.out.println(map.get(key)); // 可能返回null,无法找到原本存储的值

(二)线程安全问题

线程安全概况

  1. 非线程安全实现

    • HashMap
    • TreeMap
    • LinkedHashMap
    • 多线程并发修改可能导致:
      • 数据不一致
      • 无限循环(在JDK7及之前版本的HashMap中可能发生)
      • ConcurrentModificationException
  2. 线程安全解决方案

    • 使用并发容器

      Map<String, String> concurrentMap = new ConcurrentHashMap<>();
      

      • ConcurrentHashMap 特点:
        • 分段锁设计(JDK7)或 CAS + synchronized(JDK8+)
        • 高并发性能优于 Hashtable
        • 不会锁定整个表
    • 使用同步包装器

      Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
      

      • 特点:
        • 所有方法都使用同步块保护
        • 遍历时仍需要手动同步
        • 性能低于 ConcurrentHashMap

最佳实践

  • 优先选择 ConcurrentHashMap
  • 如果只需要读多写少的并发访问,考虑使用 ConcurrentSkipListMap(基于跳表实现)

(三)null 值的处理

各实现类对null的处理策略

Map实现类允许null键允许null值说明
HashMap只能有一个null键
LinkedHashMap继承自HashMap
TreeMap因为需要键可比较
Hashtable设计如此
ConcurrentHashMap并发安全限制
IdentityHashMap特殊实现

注意事项

  1. 使用 TreeMap 时,如果尝试 put null 键会抛出 NullPointerException
  2. 即使允许 null 值,也要注意避免 NPE:
    String value = map.get("nonexistent");
    value.toUpperCase(); // 可能抛出NPE
    

  3. 建议使用 getOrDefault() 方法提供默认值:
    String value = map.getOrDefault("nonexistent", "");
    

(四)遍历过程中修改 Map

安全遍历和修改策略

  1. 常见错误

    Map<String, Integer> map = new HashMap<>();
    map.put("a", 1);
    map.put("b", 2);
    
    for (String key : map.keySet()) {
        if (key.equals("a")) {
            map.remove(key); // 抛出ConcurrentModificationException
        }
    }
    

  2. 正确做法

    • 使用迭代器的 remove() 方法:
      Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
      while (it.hasNext()) {
          Map.Entry<String, Integer> entry = it.next();
          if (entry.getKey().equals("a")) {
              it.remove(); // 安全删除
          }
      }
      

    • Java 8+ 可以使用 removeIf():
      map.entrySet().removeIf(entry -> entry.getKey().equals("a"));
      

    • 如果需要添加元素,可以先收集要添加的键值对,遍历结束后再添加
  3. 并发修改检测机制

    • 大多数集合类使用 modCount 字段记录结构性修改次数
    • 迭代器会检查预期的 modCount 与实际是否一致
    • 不一致则抛出 ConcurrentModificationException

(五)初始容量和加载因子(针对 HashMap)

性能优化参数

  1. 关键参数

    • 初始容量:哈希表创建时的桶数量,默认为16
    • 加载因子:哈希表扩容的阈值比例,默认为0.75
      • 当元素数量达到 capacity * loadFactor 时触发扩容
      • 扩容通常将容量翻倍(newCapacity = oldCapacity << 1)
  2. 设置建议

    • 预估元素数量为n时:
      int initialCapacity = (int) (n / 0.75f) + 1;
      Map<String, String> map = new HashMap<>(initialCapacity);
      

    • 加载因子权衡:
      • 较高值(如0.9):减少内存使用,但增加哈希冲突
      • 较低值(如0.5):减少哈希冲突,但增加内存使用
  3. 扩容开销

    • 需要重新计算所有键的哈希值
    • 重建哈希表结构
    • 在性能敏感场景应避免频繁扩容
  4. 现代JDK优化

    • JDK8+ 在哈希冲突严重时会将链表转为红黑树
    • 优化了哈希算法减少冲突
    • 但合理设置初始容量仍有重要意义

性能测试示例

// 测试不同初始容量对put性能的影响
int elements = 1_000_000;

// 不指定初始容量
long start = System.nanoTime();
Map<Integer, Integer> map1 = new HashMap<>();
for (int i = 0; i < elements; i++) {
    map1.put(i, i);
}
System.out.println("Default: " + (System.nanoTime() - start) / 1_000_000 + " ms");

// 合理设置初始容量
start = System.nanoTime();
Map<Integer, Integer> map2 = new HashMap<>((int)(elements / 0.75f) + 1);
for (int i = 0; i < elements; i++) {
    map2.put(i, i);
}
System.out.println("Optimized: " + (System.nanoTime() - start) / 1_000_000 + " ms");

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值