Java面试专栏—Java基础

Java基础

1、List 和 Set 和 Map 的区别
2、HashSet如何检查重复&是如何保证数据不可重复
3、HashMap 是线程安全的吗,为什么不是线程安全的?
4、HashMap 的扩容过程
5、HashMap1.7与1.8的区别,说明1.8 做了哪些优化,如何优化的?
6、final finally finalize的区别
7、Java中的强引用 、软引用、 弱引用、虚引用?
8、Java泛型 、反射 、动态代理和注解?
9、Collection.sort和Arrays.sort的实现原理
10、cloneable接口和comparable接口有什么区别?
11、什么是深拷贝和浅拷贝?
12、什么是序列化和反序列化?
13、Java异常分类以及处理机制
14、wait和sleep的区别
15、String、StringBuilder、StringBuffer区别

1.List 和 Set 和 Map 的区别

List (列表)
特点:
①:有序性:List中的元素是有序的,即元素的插入顺序和取出顺序是一致的。
②:可重复性:允许存储重复的元素。可以在同一个List中多次添加相同的对象。
③:元素可通过索引访问:能给通过索引(下标)来获取,修改,删除元素,索引是从0开始,就像操作数组一样方便。
常见实现类
①:ArrayList:
ArrayList 是List接口的一个动态数组实现。它提供了对元素的快速随机访问,适合需要频繁读取元素的场景。插入和删除操作(尤其是中间位置)较慢,因为可能需要移动大量元素。
②:LinkedList:
LinkedList是基于双向链表的List实现。适合需要频繁插入和删除元素的场景。尽管LinkedList支持双向遍历,但随即访问性能较差,因为链表不支持通过索引直接访问元素。
③:Vector:
Vector是一种线程安全的List实现,类似于ArrayList,但索引操作都通过同步来保证线程安全。由于同步开销太大,在单线程环境下,Vector的性能通常不如ArrayList.
④:Stack:
Stack是Vector的子类,实现栈的行为(LIFO,后进先出)。Stack提供了一组特有的方法,如push()和pop(),用于操作栈顶元素。

Set (集合)
特点:
①: 无序性(某些实现类除外):一般来说,Set不保证元素的插入顺序。HashSet不能保证元素的顺序,而LinkedHashSet能保证插入顺序,TreeSet则保证元素的自然顺序或自定义顺序。
②:不可重复性:Set中的元素是唯一的 ,不允许重复的元素存在。如果尝试将重复的元素添加到Set中,添加操作将被忽略、
常见实现类
①:HashSet:
HashSet是Set接口的一个机遇哈希表的实现。他不保证元素的顺序,运行null元素。由于使用了哈希表,HashSet的查找,插入和删除操作很快,平均时间复杂度为O(1),但在发生哈希冲突时,性能会有所下降。
②:LinkedHashSet:
LinkedHashSet是HashSet的子类,除了具备HashSet的所有特点外,还保留了元素的插入顺序。因此LinkedHashSet适合需要保持元素顺序的场景,同样也能提高较快的查找性能。
③:TreeSet:
TreeSet是Set的接口实现类,基于红黑树(自平衡二叉搜索树)实现。它保证元素的排序(自然排序或自定义比较器),适合需要有序集合的场景。TreeSet的查找,插入和删除操作的时间复杂度为 O(log n)

Map (映射)
特点:
①: 存储键值对:Map是一种用于存储键(key)和值(value)的集合,每个键对应一个唯一的值,通过键可以快速的查找,获取对应的值。
②:键的唯一性:键在同一个Map中是不允许重复的,如果添加重复的键,后面添加的值会覆盖前面的值。
②:无序性(一般情况):大部分场景的Map实现类,如HashMap,元素的存储顺序是不固定的,不过也有一些有序的Map实现类,比如LinkedHashMap可以保证插入顺序,TreeMap可以按照键的顺序存储元素。
常见实现类
①:HashMap:
HashMap: 基于哈希表实现,提供快速的插入,删除和查找操作,但不保证键值对的顺序。
②:LinkedHashMap:
是HashMap的有序版本,维护键值对的插入顺序。
③:TreeMap:
基于红黑树实现,存储键值对时会自动按照键的自然顺序或自定义顺序进行排序。
④:HashTable:
类似于HashMap,但是线程安全,已经较少使用。

总结)
List适合需要存储和按顺序访问重复元素的场景。
Set适合需要确保元素唯一性的场景
Map适合需要键值对映射的场景

2.HashSet如何检查重复&是如何保证数据不可重复

HashSet底层数据结构:
HashSet内部是基于HashMap实现的,HashSet的每个元素实际上是作为HashMap的键(key)来存储的。HashSet的所有操作,如添加,删除和查找元素,都是通过操作这个底层的HashMap来实现的。
哈希表(Hash Table):HashMap的底层是一个哈希表,哈希表通过哈希函数将对象的hashcode映射到表中的某个位置(称为"桶"或“bucket”),从而高效的进行元素的存储和查找。

HashSet如何检查重复?
在HashSet中,检查元素是否重复的核心在于两点:
hashCode() 方法:用于计算元素的哈希值。
equals() 方法:用于比较元素是否“真正相等”。
1. hashCode() 方法

作用:hashCode()方法返回一个整数值,该值由对象的内容或状态生成,用于将对象分布到哈希表的各个“桶”中。对于同一个对象,hashCode()方法应该始终返回相同的值。
计算哈希值:当你向HashSet添加一个元素时,首先调用该元素的hashCode()方法来计算其哈希值。HashSet使用这个哈希值来确定元素在哈希表中的位置。
定位存储位置:哈希值会经过进一步的处理(如使用哈希算法)来确定该值对应的哈希表中的哪个“桶”。
2. equals() 方法
作用:equals()方法用于判断两个对象是否在逻辑上相等。它在对象内容相同时返回true,在内容不同时返回false。即使两个对象的hashCode()值相同,也不代表它们是相等的,只有equals()方法返回true,才能说明两个对象在逻辑上是相等的。
比较过程:如果HashSet发现两个元素的hashCode()相同(即它们在同一个“桶”中),它会进一步调用equals()方法来判断这两个元素是否真的相等。

HashSet 保证数据不重复的过程?
当向HashSet添加元素时,HashSet的底层实现会按以下步骤确保元素不重复:
计算哈希值:HashSet首先调用要添加元素的hashCode()方法,计算该元素的哈希值。
查找对应的桶:根据计算出的哈希值,HashSet定位到哈希表中的某个“桶”。
检查桶中的元素
如果该“桶”为空,说明当前哈希值还没有其他元素对应,则将新元素直接放入该“桶”中。
如果该“桶”中已经有元素存在,则需要进一步检查这些元素。
使用equals()方法比较
如果equals()方法确定新元素与“桶”中的某个现有元素相等,则认为新元素是重复的,HashSet不会添加这个元素。
如果equals()方法表明新元素与现有元素不相等,即使它们的哈希值相同,也可以认为这是一个不同的对象,新元素会被添加到“桶”中的链表或树结构中(在HashMap中,Java 8及以后的版本中,如果“桶”中的元素数量较多,会将链表转换为红黑树,以提高性能)。

3.HashMap 是线程安全的吗?为什么不是线程安全的?

A: 在多线程下不是线程安全的
原因: 如果有同时有2个线程(线程A和线程B),两个线程都进行插入数据的操作,刚好这2条不同的数据经过哈希计算后得到的哈希值相同,并且经过计算后要插入的位置此时没有数据,那么这2个线程都将数据插入到这个位置。

假设线程A通过if判断要插入位置不存在哈希冲突,则进入if语句的后续操作。在还没有进行插入操作时,CPU将线程资源让给了线程B,因为线程A还没有进行插入操作,所以线程B通过if判断也不存在哈希冲突,线程B也进入了if语句,并执行了插入操作。线程B执行完毕后,CPU将资源释放给线程A,因为线程A 已经进行了哈希冲突的判断并成功进入if语句中,所以线程A会直接进行插入操作并将之前线程B插入的数据进项覆盖。所以此时发生了线程不安全的情况。

4.HashMap 的扩容过程

①: HashMap的数据结构:
数组:
HashMap底层数据结构是一个数组,称为table,数组中的每个元素称为‘桶’(bucket).每个桶可以存储一个或多个键值对。
链表和红黑树:
当多个键的哈希值计算结果相同,导致他们映射到同一个桶时,这些键值对会通过链表的形式存储在该桶中。如果链表长度超过一定阈值(默认是8),链表会转为红黑树,以提高性能。
哈希函数:
HashMap使用键的hashCode()通过哈希函数计算出键在数组或者的位置,即索引

红黑树基础:
红黑树是一种近似平衡的二叉查找树,它并非绝对平衡,但是可以保证任何一个节点的左右子树的高度不会超过二者中较低的那个的一倍。
红黑树有这样的特点
1> 每个节点要么是红色,要么是黑色。
2> 根节点必须是黑色。叶子节点必须是黑色NULL节点。
3>红色节点不能连续
4>对于每个节点,从该点至叶子节点的任何路径,都包含相同个数的黑色节点
5>能给以O(log2(N))的时间复杂度进行搜索,插入,删除操作。此外,任何不平衡都会在3次旋转之内解决。

②:HashMap中的变量:
Node<K,V>:链表节点,包含了key、value、hash、next指针四个元素
table:Node<K,V>类型的数组,里面的元素是链表,用于存放HashMap元素的实体
size:记录了放入HashMap的元素个数
loadFactor 加载因子,默认是0.75
capacity 即容量,默认16。
threshold 扩容的阈值,决定了HashMap何时扩容,以及扩容后的大小。阈值=容量*加载因子。默认12。当元素数量超过阈值时便会触发扩容。
~
扩容过程:
HashMap使用的是懒加载,构造完HashMap对象后,只要不进行put 方法插入元素之前,HashMap并不会去初始化或者扩容table。
当首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化,当添加完元素后,如果HashMap发现size(元素总数)大于threshold(阈值),则会调用resize方法进行扩容
若threshold(阈值)不为空,table的首次初始化大小为阈值,否则初始化为缺省值大小16
默认的负载因子大小为0.75,当一个map填满了75%的bucket时候,就会扩容,扩容后的table大小变为原来的两倍

 扩容因子=0.75。当使用量接近数组容量的75%的时候,数组中还有25%的剩余空间。
 碰撞的概率越大,put的元素就越多,平均到每个桶中的元素的数量也越多。
 一旦发生碰撞,需要付出更大的代价。
 所以,如果扩容因子越大,碰撞的概率也就越大,发生碰撞后的代价也更大
 ,结果导致效率大打折扣。因此扩容因子=0.75也是一个空间换时间的考虑,
 0.75这个数值应该是经过充分的考虑决定的。

~
假设扩容前的table大小为2的N次方, 元素的table索引为其hash值的后N位确定
扩容后的table大小即为2的N+1次方,则其中元素的table索引为其hash值的后N+1位确定,比原来多了一位
重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing
因此,table中的元素只有两种情况:
元素hash值第N+1位为0:不需要进行位置调整
元素hash值第N+1位为1:调整至原索引的两倍位置
扩容或初始化完成后,resize方法返回新的table
~
总结:
数据结构:
JDK1.7中,HashMap采用了“数组+链表”的组合,而JDK1.8中则采用“数组+链表+红黑树”三者结合的结构。在JDK1.8中,当链表长度超过8且数组长度大于64时,链表会转化为红黑树以优化查找性能,避免长链表造成的性能瓶颈。
hash冲突处理方式:
在JDK1.7中,链表插入新节点时采用的是头插法,这样做的好处是插入速度较快,但在并发情况下可能会产生死循环(例如在rehash期间)。而在JDK1.8中,链表插入时采用了尾插法,避免了并发扩容时死循环的问题。
扩容过程:
JDK1.8中,HashMap的扩容更为智能高效,通过高位运算决定节点位置是否发生变化。扩容时不再重新计算所有节点的哈希值,只需检查每个节点的高位,决定是否需要搬移至新数组。
性能优化:
JDK1.8的HashMap在多线程环境下性能优化明显,解决了JDK1.7在并发条件下扩容时可能导致的死循环问题。总体来看,JDK1.8的HashMap在结构上更为合理,更适用于高并发场景。

5.HashMap1.7与1.8的区别,说明1.8 做了哪些优化,如何优化的?

1.数据插入的方式不同
JDK1.7用的是头插法,而JDK1.8用的是尾插法,这是由于JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
2.组成结构不同
JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)
3.扩容的方式不同
JDK1.7的扩容方式是将旧数组的数据搬到新数据里,需要重新计算hash值。计算方式是:
newIndex=Key.hash&(table.length-1)
JDK1.8的扩容方式 如果Key.hash&oldCap==0.那么在新数组的下标newIndex=oldIndex,反之
newIndex=oldCap+oldIndex
这样做的好处是不需要改变重新计算hash值。
4.下标的计算方式不同
JDK1.7中index=Key.hashCode&(table.length-1)
JDK1.8中index=Key.hashCode^(Key.hashCode>>>16)
这样做的目的是可以在数组的长度比较小的时候,保证高低位bit都可以参与hash的运算当中去,增加了随机性,使元素位置分布的更加均匀。

6.final finally finalize的区别

一.final关键字
final。它可以用于以下四个地方:
1).定义变量,包括静态的和非静态的。
2).定义方法的参数。
3).定义方法。
4).定义类。

final的用法
‌修饰类‌: 当一个类被final修饰时,表示这个类不能被继承。(String、StrngBuilder、StringBuffer、Math,不可变类),其中所有的方法都不能被重写,所有不能同时用abstract和final修饰(abstract修饰的是抽象类,抽象类是用于被子类继承的,和final起相反的作用)

修饰方法‌:当一个方法被final修饰时,表示这个方法不能被重写(子类不能覆盖这个方法)。但是子类可以用父类中final修饰的方法

‌修饰变量‌当一个变量被final修饰时,表示这个变量的值一旦被赋值就不能被改变(对于引用类型的变量,表示其引用不能被改变,但引用的对象内容是可以改变的)。

① 将List<对象>设计成final的好处
‌保证不可变性‌:将List<对象>设计成final,意味着这个列表的引用不可变。虽然列表中的元素仍然可以修改(如果元素本身是可变的),但列表的引用本身不会改变。这有助于在多线程环境中避免竞态条件,从而提高程序的线程安全性。

‌提升性能‌:在某些情况下,将List<对象>设计成final可以让JVM对代码进行优化,比如内联调用等,从而减少方法调用的开销,提升程序的性能。

‌明确设计意图‌:使用final可以明确表达代码的设计意图,让其他开发者更容易理解你的代码。当看到final修饰的List<对象>时,他们就知道这个列表的引用是不可变的。

② 将List<对象>设计成final的坏处
‌代码不易维护‌:如果过度使用final,可能会导致代码变得僵硬,不易维护。因为final限制了代码的扩展性,使得后续对代码的修改变得困难。

‌影响性能‌:虽然final在某些情况下可以提升性能,但如果滥用,反而可能导致性能下降。因为final会强制要求编译器对变量、方法等进行优化,而这种优化可能会使应用程序变慢。不过,这种情况比较少见,通常只有在极端情况下才会出现。

③ 最佳实践
‌在需要明确表示不可变性的情况下使用final‌:如果你的List<对象>确实需要保持不可变性,那么使用final是一个很好的选择。这可以确保其他开发者不会意外地修改这个列表的引用。

‌避免滥用final‌:不要为了使用final而使用final。只有在确实需要的情况下才使用它。过度使用final会导致代码变得僵硬,不易维护。
‌考虑使用不可变集合‌:如果你需要一个完全不可变的集合,那么可以考虑使用Java提供的不可变集合类,比如Collections.unmodifiableList()方法返回的集合。这样不仅可以保证集合的不可变性,还可以避免使用final带来的潜在问题。

二.finally
finally是在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出、捕获都会被执行。try块中的内容是在无异常时执行到结束。catch块中的内容,是在try块内容发生catch所声明的异常时,跳转到catch块中执行。finally块则是无论异常是否发生都会执行finally块的内容,所以在代码逻辑中有需要无论发生什么都必须执行的代码,可以放在finally块中。

三.finalize
finalize是方法名,java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的,它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

7.Java中的强引用,软引用,弱引用,虚引用?

通常,Java对象的引用可以分为4类:强引用、软引用、弱引用和虚引用。
强引用:
通常可以认为是通过new出来的对象,即使内存不足,GC进行垃圾收集的时候也不会主动回收。
Object obj = new Object();
软引用:
在内存不足的时候,GC进行垃圾收集的时候会被GC回收。
Object obj = new Object();
SoftReference softReference = new SoftReference<>(obj);
弱引用:
无论内存是否充足,GC进行垃圾收集的时候都会回收。
Object obj = new Object();
WeakReference weakReference = new WeakReference<>(obj);
虚引用:
和弱引用类似,主要区别在于虚引用必须和引用队列一起使用。
Object obj = new Object();
ReferenceQueue referenceQueue = new ReferenceQueue<>();
PhantomReference phantomReference = new PhantomReference<>(obj, referenceQueue);
引用队列:
如果软引用和弱引用被GC回收,JVM就会把这个引用加到引用队列里,如果是虚引用,在回收前就会被加到引用队列里。

8.Java泛型 、反射 、动态代理和注解?

1 泛型

①:概述:
Java 泛型( generics) 是 JDK 5 中引⼊的⼀个新特性, 允许在定义类和接口的时候使⽤类型参数,泛型最⼤的好处是可以提⾼代码的复⽤性。

泛型可以在编译时对类型进行安全检测,使得所有的强制转换都是自动隐式实现的,保证了类型的安全性;
泛型作为”代码模板“,实现了 一套代码对各种类型的套用, 提高了代码的可重用性。
无论我们在哪个地方使用泛型,泛型都不能是基本类型
②:使用场景:
泛型类:在类的定义时使用泛型,为某些变量和方法定义通用的类型;
泛型方法:在各种方法中使用泛型,保证方法中参数的类型安全;
泛型集合:在各种集合中使用泛型,保证集合中元素的类型安全;
泛型接口:在接口定义时使用泛型,为某些常量和方法定义通用的类型;
泛型加反射:泛型也可以结合反射技术,实现在运行时获取传入的实际参数等功能。
③:使用
泛型类:
泛型类就是一个具有多种类型变量的类。ArrayList类引入的类型变量为E,使用<>括起来,放在类名后面
那么定义的泛型类就必须要传入参数吗?其实不是的,如果我们传入了参数那么这个泛型类才能真正的起到了限制类型的作用。否则的话就是可以传入任何类型的对象

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
   
   
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....
  }
}

//自己设计泛型
public class demo3 {
   
   
    public static void main(String[] args) {
   
   
        ArrayList<String> list = new ArrayList<String>(2);
        list.add("菜鸟明。");
        System.out.println(list.get(0));
    }
}

//E是指我们的数据类型,当我们在实例化的时候必须要指明他的类型
class ArrayList<E>{
   
   
    private Object[] elementDate;
    private int size = 0;
    //initialCapacity初始容量
    public ArrayList(int initialCapacity){
   
   
        this.elementDate = new Object[initialCapacity];
    }
	//这里的E就是我们要添加数据的类型,其必须与最开始定义的类型相同
    public boolean add(E e){
   
   
        elementDate[size++] = e;
        return true;
    }
     public E get(int index){
   
   
        return (E) elementDate[index];
    }
}

泛型方法
泛型方法,是在调用方法的时候指明泛型的具体类型 。
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
   
   
        T instance = tClass.newInstance();
        return instance;
}

public class demo04 {
   
   
    static class Fruit{
   
   
        @Override
        public String toString() {
   
   
            return "fruit";
        }
    }
    static class Apple extends Fruit{
   
   
        @Override
        public String toString() {
   
   
            return "apple";
        }
    }
    static class Person{
   
   
        @Override
        public String toString() {
   
   
            return "Person";
        }
    }
    static class GenerateTest<T>{
   
   
        public void show_1(T t){
   
   
            System.out.println(t.toString());
        }
        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
        //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public <E> void show_3(E t){
   
   
            System.out.println(t.toString());
        }
        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public <T> void show_2(T t){
   
   
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
   
   
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
        //apple是Fruit的子类,所以这里可以
        generateTest.show_1(apple);
        //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
        //generateTest.show_1(person);

        //使用这两个方法都可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //使用这两个方法也都可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}

泛型的限定符extends
限定符,就是字面意思,限定了他的范围,即缩小泛型的类型范围。

public class demo05 {
   
   
    public static void main(String[] args) {
   
   
        ArrayList<dog> list = new ArrayList<>(50);
        list.add(new dog());
        list.add(new smallDog());
        //这里会报错add(dog) in ArrayList cannot be applied:to (animal),意思是无法在狗这个类中找到animal
        list.add(new animal());
    }
}
class animal{
   
   
    public String toString(){
   
   
        return "动物";
    }
}
class dog extends animal{
   
   
    @Override
    public String toString() {
   
   
        return "狗";
    }
}

class smallDog extends dog{
   
   
    @Override
    public String toString() {
   
   
        return "小狗狗";
    }
}
class ArrayList<E extends dog>{
   
   
    private Object[] elementDate;
    private int size = 0;
    //initialCapacity初始容量
    public ArrayList(int initia
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

陈振wx:zchen2008

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

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

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

打赏作者

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

抵扣说明:

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

余额充值