JavaSE基础知识

JavaSE基础知识

文章目录

Java中jdk和jre的区别

  • jdk: Java Development Kit 的缩写,顾名思义是Java开发工具包,是程序员使用Java语言编写Java程序所需的开发工具包jdk包含了jre,同时还包含了编译Java远吗的编译器javac,还包含了很多Java程序调试和分析的工具:jconsole,juisualvm等工具软件,还包含了Java程序编写所需的文档和demo例子程序。
  • jre: Java Runtime Environment 的缩写,顾名思义是Java运行时环境,包含了Java虚拟机,Java基础类库。是使用Java语言编写的程序运行所需要的软件环境,是提供给想运行Java程序的用户使用的,还有所有的Java类库的class文件,都在lib目录下,并且都打包成了jar。
    如果你需要运行Java程序,只需要安装jre就可以了。
    如果你需要编写Java程序,需要安装jdk

Object类自带哪些方法

  • clone

克隆方法,实现对象的浅复制(深复制和浅复制的区别),只有实现了Cloneable接口才可以调用该方法,否则抛出异常ClonenNotSupportedException异常

  • getClass

final方法,获得运行时类型

  • toString

该方法用的较多,一般都有子类覆盖

  • finalize

该方法用于释放资源。

  • equals

一般equals和==是不一样的,但在Object中两者是一样的。子类一般都要重写这个方法

  • hashCode

该方法用于哈希查找,重写equals方法一般都要重写hashCode方法

  • wait

wait方法就是使当前线程进入阻塞状态,wait()方法会一直等待,并且会释放锁。除非发生一下事件:
(1)其他线程调用了该对象的notify()方法或者notifyAll()方法。
(2)其他线程调用了interrupt()方法中断该线程

  • notify

该方法可用于唤醒该对象上其他进入阻塞状态的线程

  • notifyAll

该方法可唤醒所有该对象上进入阻塞状态的线程

对String类了解多少

String 、StringBuffer 、StringBuilder之间的区别
  • 速度比较 String < StringBuffer < StringBuilder

(1)String本身就是一个对象,因为String是不可变对象(由final修饰),所以每次遍历字符串做拼接操作都会重新创建一个对象,循环100万次就会创建100万个对象,非常消耗内存空间,而且创建对象本身就是一个耗时的操作。
(2) StringBuffer和StringBuilder只需要创建一个对象,然后用append方法进行字符串拼接,就算拼接一亿次,也只有一个对象

  • 线程安全

StringBuffer是线程安全的
StringBuilder是线程不安全的,这也是速度比StringBuffer快的原因
(1)如果要操作少量的数据用String
(2)多线程操作字符串缓冲区下操作大量数据 StringBuffer
(3)单线程操作字符串缓冲区下操作大量数据 StringBuilder

java中的重写和重载的区别

重写(Override)
从字面上看,重写就是重新写一遍的意思,其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但是有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回值类型除了子类中方法的返回值是父类中方法的返回值的子类时)都相同的情况下,对方法体进行重写或修改,这就是重写。但注意:(1)子类的访问修饰符权限不能小于父类(2)重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常
重载(Overload)
在同一个类中,方法名相同而参数列表不同(参数类型不同、参数个数甚至参数顺序不同)则视为重载。同时,不能通过返回值类型是否相同来判断是否重载,重载与返回值类型无关。

注意方法的重载和重写都是实现多态的方式,区别在于重载实现的是编译时的多态性,重写是实现运行时的多态性。

java的四大基本特性

  • 封装
  • 继承
  • 多态
  • 抽象

面向对象的三大特性

  • 封装
  • 继承
  • 多态

接口和抽象类的区别

接口是对动作的抽象,抽象类是对根源的抽象

抽象类表示这个对象是什么。例如:男人和女人,他们都是人,所以他们的抽象类是人。
接口表示这个对象能做什么。例如:猫可以吃东西,狗也可以吃东西,你可以把“吃东西”定义成一个接口,然后让这些类去实现它。

总结:
1、抽象类和接口都不能直接被实例化,前者可继承,后者可实现
2、接口只能做方法的声明,而抽象类可以做方法的声明,也可以做方法的实现(即有方法体)
3、接口里定义的变量必须是public static final修饰的公共静态常量,而抽象类中的变量是普通变量。
4、抽象类中的抽象方法必须全部被子类所实现,如果子类不能实现全部父类的抽象方法,那么该子类只能使抽象类。同样,实现一个接口,如果不能实现它的所有方法,那么该类只能使抽象类。
5、抽象类中可以没有抽象方法,可如果一个类中有抽象方法,那么这个类一定是抽象类。
6、抽象方法要被实现,所以不能是静态的,也不能是私有的
7、接口可继承接口,并可多继承,但类只能 继承一个类,可实现多接口。

java中final关键字的作用

final关键字可以用来修饰引用、方法和类
修饰引用
1、如果引用为基本数据类型,则该引用为常量,该值无法修改
2、如果引用为引用类型,比如对象、数组、则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改
3、如果引用是类的成员变量,则必须直接赋值,否则编译会报错
修饰方法
当final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍可以被继承。
修饰类
当final修饰类时,该类成最最终类,无法被继承。简称“断子绝孙类”(比如String类)

java中的修饰符public,private,protected,default

在这里插入图片描述

collection和collections的区别

Java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。
在这里插入图片描述
Java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能被实例化,就像一个工具类,服务于Java的Collection框架。

Set里的元素是不能重复的,那么用什么方法来区分重复与否呢?

当向集合Set中增加对象时,首先集合计算要增加对象的hashCode码,根据该值来得到一个位置用来存放当前对象。如果该位置没有任何一个对象存在的话,那么直接加进去。如果该位之上已经有一个对象存在,准备增加到集合中的对象与已存在的对象进行equals方法比较。若返回false,那么集合中不存在该对象,再进行一次hashCode码的计算,将该对象放置后计算出来的地址中去。若返回true,那么集合认为集合中已经存在该对象,就不再将该对象增加到集合中去。(2)重写equals方法的时候必须重写hashCode()方法。如果一个类的两个对象,使用equals方法比较时,返回结果为true,那么两个对象将具有相同的hashCode,原因是equals方法为true表明为同一个对象,他们的hashCode当然相同(Object类的equals方法比较的是地址 (3)Object类的hashCode方法返回的是Object对象的内存地址。HashSet允许null值存在

==和equals的区别

==判断的多为栈内存的东西,也就是声明的变量部分,所以说等号判断的是内存地址。equals判断对象的内容。Object类中的equals方法就是用 “ = = ”来比较的,所以如果没有重写equals方法,equals和 = = 是等价的。通常我们会重写equals方法,让equals比较两个对象的内容,而不是比较对象的应用(地址) Object类中的hashCode是返回对象在内存中地址转换成int值(可以就当做地址来看)。所以如果没有重写hashCode方法,任何对象的hashCode都是不相等的。通常在集合类的时候要重写hashCode方法和equals方法,也就是说比较的不再是内存地址,而是对象的内容。

总结
==:
基本类型:比较的是值是否相同
引用类型:比较的是地址是否相同
equals():
引用类型:默认情况下(Object自带的方法),比较的是地址值,可进行重写,比较的是对象的成员变量值是否相同

从底层区分下ArrayList和LinkedList,Arraylist、LinkedList、HashMap的初始大小以及如何扩容

ArrayList初始化大小为10(如果你知道你的ArrayList回答道多少容量,可以在初始化的时候就指定它的大小,能节省扩容的性能开支)
扩容规则:新增的时候发现容量大小不够用了,就去扩容
扩容大小规则:扩容后大小 = 原始大小 * 1.5
linkedList是一个双向链表,没有初始化大小,也没有扩容机制,就是一直在前面或者后面新增就好。
HashMap初始化大小为16,扩容因子为0.75(可指定初始化大小和扩容因子)
扩容机制:当前大小和当前容量的比例超过了扩容因子,就会扩容,扩容后大小为一倍。

HashMap、HashTable的区别

HashMapHashTable都实现了Serializable接口,因此都支持序列化,实现了Cloneable接口,能被克隆。
初始容量:HashMap的初始容量为16,HashTable的初始容量为11,两者扩容因子默认都为0.75.
扩容机制:HashMap扩容是当前容量翻倍:*2,HashTable的扩容是当前容量翻倍+1,*2+1.
线程安全:HashMap是线程不安全的。只适用于单线程环境下,HashTable是线程安全的,但运行速率过慢,可用concurrent并发包下的ConcurrentHashMap来代替。HashTable不允许key和value为null。但是HashTable线程安全策略实现代价太大,所有get/put的操作都是synchronized的,这相当于给整个哈希表加了一把大锁。而ConcurrentHashMap采用了“ 分段锁 ”的思想。

关于ConcurrentHashMap的底层实现原理

HashMap的底层

hashMap是由数组 + 单向链表 + 二叉树(红黑树) 组成,使用的是尾插法,线程不安全。
简单来说,HashMap由数组 + 链表组成,数组是HashMap的主体,链表则是为了解决哈希冲突而存在的,而过定位到的数组位置不含链表(当前entry的next指向null),那么对于查找、添加等操作很快,仅需一次寻址即可;如果定位到数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需要遍历链表,然后通过key对象的equals方法逐一对比查找。所以,性能考虑,HashMap中的链表出现越少性能越好。

Comparable和Comparator接口是干什么的?列出它们的区别。

Java提供了只包含一个compareTo()方法的Comparable接口。这个方法可以给两个对象排序。具体来说,它返回负数,0,正数来表明已经存在的对象小于,等于,大于输入对象。
Java提供了compare()和equals()方法的Comparator接口。compare()方法用来给两个输入参数排序,返回负数,0,正数表明第一个参数是小于,等于,大于第二个参数的。equals()方法需要一个对象作为参数,它用来决定输入参数是否和comparator相等。只有当输入参数也是一个comparator并且输入参数和当前comparator的排序结果是相同的时候,这个方法才返回true。
Comparable 和 Comparator都是用来实现集合中元素的比较、排序,知识Comparable是在集合内部的方法实现的排序,Comparator是在集合外部实现的排序。

什么是流?按照传输的单位,分成哪两种流?他们的父类叫什么?

字节输入流 InputStream
字节输出流 OutputStream
字符输入流 Reader
字符输出流 Writer
所有流都是这四个流的子类
BufferedReader 缓冲字符输入流
BufferedWirter 缓冲字符输出流
属于处理流中的缓冲流,可以将读取的内容存在内存里,有readLine()方法。

什么叫对象序列化,什么是反序列化,如何实现对象序列化

对象序列化 将对象以二进制的形式保存在硬盘上
反序列化 将二进制的文件转化为对象读读取
实现序列化必须得实现Serializable接口

开启线程的三种方式

方式一:继承Thread类
步骤:
(1)定义一个类A继承java.lang.Thread类
(2)在A类中重写Thread类中的run方法
(3)在run方法中编写需要执行的操作:run方法里的代码,线程执行体
(4)在main方法(线程)中,创建线程对象,并启动(创建线程类对象 A类 a = new A 类();调用线程对象的start方法;a.start()启动线程。

注意:若直接调用run()方法,则为普通的对象调用方法,不是开启新线程

方式二:实现Runnable接口
步骤:
(1)定义一个类A实现java.lang.Runnable接口,注意A类不是线程类
(2)在A类中重写Runnable接口中的run()方法
(3)在run方法中编写需要执行的操作:run方法里的代码,线程执行体
(4)在main方法(线程)中,创建线程对象,并启动(创建线程类对象 A类 a = new A 类();调用线程对象的start方法;a.start()启动线程。

继承方式:
1):从设计上分析,Java中类是单继承的,如果继承了Thread了,该类就不能再有其他的直接父类了.
2):从操作上分析,继承方式更简单,获取线程名字也简单.(操作上,更简单)
3):从多线程共享同一个资源上分析,继承方式不能做到.
实现方式:
1):从设计上分析,Java中类可以多实现接口,此时该类还可以继承其他类,并且还可以实现其他接口,设计更为合理.
2):从操作上分析,实现方式稍微复杂点,获取线程名字也比较复杂,得使用Thread.currentThread()来获取当前线程的引用.
3):从多线程共享同一个资源上分析,实现方式可以做到(是否共享同一个资源).
补充:实现方式获取线程的名字:
String name = Thread.currentThread().getName();

方式三:实现Callable接口
步骤:
(1)定义一个类A实现Callable接口,注意A类不是线程类
(2)在A类中重写Callable接口中的call()方法,并且有返回值。
(3)在call方法中编写需要执行的操作:call方法里的代码,线程执行体
(4)创建该类的实例
(5)创建FutureTask的实例,使用FutureTask类来包装Callable对象
(6)将FutureTask的实例作为Thread的target创建并启动新线程
(7)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

注意 不能对同一线程对象两次调用start()方法

进程,线程,协程之间的区别

进程是计算机资源分配的最小单位,主要用于数据隔离,不共享任何状态,调度有操作系统完成,有独立的内存空间
线程是计算机工作的最小单位,计算机真正工作的就是线程,那么在一个进程中可以有多个线程,每个线程会共享父进程的资源,而一个应用程序可以有多个进程。可共享变量,调度由操作系统完成。
协程调度完全由用户控制,一个线程可以有多个协程,

线程之间是如何通信的

方法一:等待(wait) / 通知机制(notify)
wait()方法是Object类的方法,该方法使当前线程处进入阻塞状态。notify()方法可以随机唤醒一个处于阻塞状态中的线程,并使该线程进入就绪状态(可运行状态),也就是notify()方法仅通知“ 一个 ”线程。notifyAll()方法可以使所有阻塞状态的线程重新进入就绪状态(可运行状态)。
方法二:比如在线程B中调用了线程A的join方法,知道线程A执行完毕后,才会继续执行线程B
方法三:Thread.yield()方法,暂停当前正在执行的线程对象,并执行其他线程。yield()是使当前运行线程回到可运行状态,让出时间片给其他线程。(因为cpu发时间片给线程是随机的,所以无法保证CPU不把时间片发给当前线程,所以该方法有时会无效)。

在Java中wait和seelp方法的不同

  • sleep()方式是属于Thread类中的方法,而wait()是属于Object类中的方法。
  • sleep()方法没有释放锁,而wait()方法释放了锁,使得其他线程可以使用同步控制块或者方法
  • wait,notify和notifyAll只能在同步控制方法或者同步控制块里使用,而sleep可以在任何地方使用
  • sleep必须捕获异常,因为sleep的过程中可能会被其他对象调用它的interrupt()方法,产生InterruptedException异常,如果不捕获这个异常,线程会异常终止,如果捕获了它,程序可继续执行catch语句块以及之后的代码。而wait,notify,notifyAll则不用捕获异常。
  • 注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只能使当前线程被sleep而不是t线程。
  • wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()或者notifyAll()方法唤醒该线程,如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法同样会在wait的过程中有可能被其他对象调用interrupt()方法而唤醒。

如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程正在wait/sleep/join,则线程B会立刻抛出InterruptException,在catch(){}中直接return即可安全的结束线程。

谈谈ThreadLocal关键字

ThreadLocal一般称为线程本地变量,并非一个线程。它的作用就是为使用该变量的线程都提供一个变量值的副本,每个线程都可以独立的改变自己的副本,而不会和其他副本造成冲突。

ThreadLocal详情解析

run()和start()方法区别

  • start()方法是用来启动线程,真正实现了多线程运行。
  • run()方法当做普通方法的方式调用,程序还是要顺序执行,

把需要执行的代码放在run()方法中,start()方法启动线程将自动调用run()方法。

什么是线程池,为什么要用线程池,说出几种常见的线程池

  • 线程池是为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销分摊到了多个任务上。其好处是,因为在请求到达时,线程已经存在,所有无意中也消除了线程创建带来的延迟。这样就可以立即请求服务,是应用程序相应更快。而且适当的调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其他任何新到的请求一直等待,知道获得一个线程来处理为止,从而防止资源不足。
  • 使用线程池的风险
  • 死锁
  • 资源不足
  • 并发错误
  • 线程泄露
  • 请求过载
  • 常用的几种线程池
    newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空线程,若无可回收,则新建线程
    newFixedThreadPool 创建一个指定工作线程数量的线程池。如果工作线程数量达到线程池初始的最大数,则将提交的任务存入池队列中。
    newSingleThreadExecutor 创建单一线程化的Executor,即创建唯一工作者线程来执行任务,保证所有任务按照指定顺序执行。
    newScheduleThreadPool 创建一个无限大小的线程池,而且支持定时的以及周期性的任务执行。
  • 为什么要使用线程池

为了减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多次任务
可根据系统的承受能力,调整线程池中的工作线程的数目,防止赢消耗过多的内存,而把服务器累趴下。

描述一下线程的生命周期(描述5个状态)

在这里插入图片描述

为什么会发生死锁,如何避免死锁

  • 死锁的定义:多个线程因竞争同一资源而造成的一种僵局(互相等待),若无外力作用,这些进程都无法向前推进。
  • 产生死锁的原因:1)系统资源竞争。2)进程推进顺序非法。例如:P1、P2分别保持了资源R1、R2,而P1又申请R2,P2又申请R1,两者都会因为所需资源被占用而阻塞
  • 死锁产生的必要条件:必须同时满足一下四个条件:互斥条件、不可剥夺条件、请求和保持条件、循环等待条件
  • 如何避免死锁:1、加锁顺序 2、加锁时限 3、死锁检测

多线程中的锁有哪些种类,说下区别

1、可重入锁 ReentrantLock 和synchronized 都是可重入锁
2、可中断锁 Lock是可中断锁,synchronized不是
3、公平锁
4、读写锁,多个读操作不会发生冲突,读的速度为1100,写为800。
5、乐观锁
6、悲观锁

Synchronized和Lock锁的区别

来源:
lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

异常是否释放锁:
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

是否响应中断
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度。

Synchronized有什么缺陷

1、可重入性
2、不可中断性
3、可见性
缺陷:

  • 效率低:锁的释放情况少、试图获得锁时不能设定超时不能中断一个正在试图获取得锁的线程
  • 不够灵活:加锁和释放的时机单一,每个锁仅有单一的条件
  • 无法知道是否成功获取到锁

并行与并发的异同

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

Java如何实现并发

  • synchronized 关键字来保证一次只有一个线程在执行代码块
  • Volatile 关键字办证任何线程在读取Volatile修饰的变量的时候,读取的都是这个变量的最新数据
  • Thread 和 Runnable
  • Callable 和 Futures
  • Thread Pools

什么是深克隆浅克隆?描述两个的区别

如何实现克隆:
1、实现Cloneable接口
2、实现clone()方法,并调用父类Clone()

浅拷贝:仅仅克隆基本类型变量,而不克隆引用类型的变量
深克隆:既克隆基本类型变量,也克隆引用类型变量

什么是双亲委派模型?

当需要加载一个类的时候,子类加载器并不会马上去加载,而是依次去请求父类加载器加载,一直往上请求到最高类加载器:启动类加载器。当启动类加载器加载不了的时候,依次往下让子类加载器进行加载。当达到最底下的时候,如果还是加载不到该类,就会出现ClassNotFound的情况。

好处:保证了程序的安全性。例子:比如我们重新写了一个String类,加载的时候并不会去加载到我们自己写的String类,因为当请求上到最高层的时候,启动类加载器发现自己能够加载String类,因此就不会加载到我们自己写的String类了。

在这里插入图片描述

什么是类加载?类加载的过程是怎样的?

类加载:JVM将编译好的.class文件(字节码文件)以二进制流的方式加载到我们内存中,并且将二进制流中静态的数据结构转换成我们方法区中动态运行数据结构,并且在对堆内存生成一个java.lang.class对象,作为提供给外界访问我们方法区动态运行数据结构的一个入口。
类加载的过程
一般来说,我们把Java的类加载过程分为三个主要步骤:加载、链接、初始化,具体行为在Java虚拟机规范里有非常详细的定义。
首先是加载阶段(Loading),它是Java将字节码数据从不同的数据源读取到JVM中,并映射为JVM认可的数据结构(Class对象),这里的数据源可能是各种各样的形态,如jar文件、class文件,甚至是网络数据源等;如果输入数据不是ClassFile的结构,则会抛出ClassFormatError。
加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
第二阶段是链接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入JVM运行的过程中。这里可进一步细分为三个步骤:

验证(Verification),这是虚拟机安全的重要保障,JVM需要核验字节信息是符合Java虚拟机规范的,否则就被认为是VerifyError,这样就防止了恶意信息或者不合规的信息危害JVM的运行,验证阶段有可能触发更多class的加载。
准备(Preparation),创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的JVM指令。
解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。在Java虚拟机规范中,详细介绍了类、接口、方法和字段等各个方面的解析。

最后是初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值