Java基础知识总结

这部分只有最基础部分,Java常用集合下篇文章写。

1 什么是面向过程与面向对象?你对面向对象的理解

1 面向过程侧重于做事情的步骤,面向对象侧重事情的参与者与动作

以洗衣机洗衣服为例:

  • 面向过程描述:打开洗衣机 -放入衣服-放入洗衣粉-启动洗衣机

  • 面向过程描述:

    参与事情的对象:人:打开洗衣机 放衣服 放洗衣粉

​ 洗衣机:洗衣 烘干

总结:面向过程更加高效,而面向对象更加易复用、扩展和维护。

2 面向对象的三大特性

  • 封装:封装的意义在于明确标识出允许外部使用的所有成员函数和可修改的属性。对象向外暴露相应的修改属性或调用方法的接口,外部调用无需修改或关心内部如何实现。

    • 体现:比如orm框架,操作数据库,我们不需要关心如何与数据库建立连接,SQL如何执行,只需要引入Mybatis框架,使用时只需调用相应方法。

  • 继承:继承基类的方法和属性,并做出自己的扩展和改变,或者重写父类方法

    • 注意:子类拥有父类对象的所有属性和方法包括私有属性和方法,但只是拥有,子类也无法访问父类的私有属性和方法。

  • 多态:基于对象所属类不同,外部对于同一个方法的调用,执行的逻辑不同。

    • 举个简单的例子:英雄联盟里面我们按下 Q 键这个动作:

      对于亚索,就是斩钢闪

      对于提莫,就是致盲吹箭

      对于剑圣,就是阿尔法突袭

    • 分类:编译时多态(静态多态):方法重载

      运行时多态(动态多态):动态绑定,通常说的多态

    父类类型 变量名 = new 子类对象;
    变量名.方法名();
    !!!!无法通过多态调用子类特有方法
    • 多态需要满足的条件:继承、重写和向上转型,只有满足这三点才可以同一逻辑执行不同行为。


2 说说C++和Java的异同

  • 相同点:都是面向对象语言,都使用了面向对象思想,二者都具有很好的可重用性

  • 不同点:(太多了,这里只是一部分)

    • Java为解释型语言,源文件-JVM编译器编译成-字节码-JVM解释执行

      C++为编译型语言,源码经过编译链接后生成可执行的二进制代码

      所以Java执行效率一般情况下低于C++,当由于JVM的存在,Java程序可以跨平台执行

    • Java为纯面向对象语言,所有代码(包括函数,变量)必须在类中实现,除了基本数据类型外,所有类型都是类,所有类的祖类为Object。C++由于兼容C,可以定义全局变量和全局函数。

    • C++支持多继承,Java不支持多继承但Java引入了接口,可以同时实现多个接口。

    • C++需要开发人员手动管理内存,Java提供垃圾回收器和回收算法管理内存。


3 访问修饰符访问范围


4 JDK、JVM与JRE的关系

JVM时程序运行的核心,只有JVM什么也赶不了,它需要字节码文件。 有了字节码文件也无法完成编译,他需要一个基本的类库:比如怎么操作文件、怎么连接网络等。而JER就是这些基础操作类库,JVM标准加上基础类库构成了Java的运行时环境,也就是我们常说的JRE(Java Runtime Environment)

JDK主要包含三部分

第一部分就是Java运行时环境,JVM

第二部分就是Java的基础类库,这个类库的数量还是非常可观的。(jdk目录下的rt.jar,里面有String等)

第三部分就是Java的开发工具,如javac、java、jar 。


5 基础考察-类型转换问题

// 代码块1
short s1 = 1; s1 = s1 + 1;
// 代码块2
short s1 = 1; s1 += 1;
其实,s1 += 1 相当于 s1 = (short)(s1 + 1)

代码块1编译报错,错误原因是:不兼容的类型: 从int转换到short可能会有损失。

代码块2正常编译和执行


6 基础考察-包装类

public static void main(String[] args) {
    Integer a = 128, b = 128, c = 127, d = 127;
    System.out.println(a == b);//flase
    System.out.println(c == d);//true
}

答案是:false,true。

执行 Integer a = 128,相当于执行:Integer a = Integer.valueOf(128),基本类型自动转换为包装类的过程称为自动装箱(autoboxing)。

Integer源码

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

在 Integer 中引入了 IntegerCache 来缓存一定范围的值,IntegerCache 默认情况下范围为:-128~127

本题中的 127 命中了 IntegerCache,所以 c 和 d 是相同对象,而 128 则没有命中,所以 a 和 b 是不同对象。

但是这个缓存范围时可以修改的,可能有些人不知道。可以通过JVM启动参数:-XX:AutoBoxCacheMax=<size> 来修改上限值


7 &与&&的区别

&&:逻辑与。当运算符左右两边的表达式都为 true,才返回 true。同时具有短路性,如果第一个表达式为 false,则直接返回 false。逻辑与的结果只有0与1.

&:按位与。

按位与运算符:用于二进制的计算,只有对应的两个二进位均为1时,结果位才为1 ,否则为0。

逻辑与运算符:按位与&用于计算,&&逻辑与用于条件判断。

8 Java中的数据类型

Java 中的基本数据类型只有8个:byteshortintlongfloatdoublecharboolean

剩下的都是引用类型(reference type)

基本数据类型:数据直接存储在栈上

引用数据类型:数据存储在堆上,栈上只有引用地址


9 String,StringBuilder和StringBuffer区别

  • String不可变Final是一方面因素,还有一方面是private.同时String类也是final类

  • 每次对 String 类型进⾏改变(比如+号拼接在赋给自身)的时候,都会⽣成⼀个新的 String 对象,然后将指针指向新的 String对象。

  • StringBuffer 和StrinBbuilder都是继承自AbstractStringBuilder,默认容量是16(char数组),是可变的,每次改变时会先把长度+16,再将字符串拼接。

 

synchronized修饰保证线程安全。

  • StringBuilder是StringBuffer的非线程安全版本,因为没有锁所以性能高一些。

  • 总结:

  1. 操作少量的数据: 适⽤ String

  2. 单线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuilder

  3. 多线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuffer


10 String创建对象过程

String s = "xyz" 和 String s = new String("xyz") 区别?

  • 常量池检查是否存在“XYZ”,有则直接使用,没有则在常量池创建

    Stirng中的intern()是个Native方法,它会首先从常量池中查找是否存在该常量值的字符串,若不存在则先在常量池中创建,否则直接返回常量池已经存在的字符串的引用

  • 后者除了直接使用常量池有的,还会再再堆上创建。

  • 可以理解成后者包含了前者。


11 ==和equals()

  • ==:用于比较基础类型变量和引用类型变量

    • 基础类型:比较变量数值是否相同

    • 引用类型:比较地址是否相同

    short s1 = 1; long l1 = 1;
    // 结果:true。类型不同,但是值相同
    System.out.println(s1 == l1);
    ​
    Integer i1 = new Integer(1);
    Integer i2 = new Integer(1);
    // 结果:false。通过new创建,在内存中指向两个不同的对象
    System.out.println(i1 == i2);//false
  • equals:Object 类中定义的方法,通常用于比较两个对象的值是否相等。

    • 在Object方法中等价于==,实际运用中常被重写


12 euqls()与hashCode()

  • hashCode相同,equals也相同吗?

    • 不一定。当有 a.equals(b) == true 时,则 a.hashCode() == b.hashCode() 必然成立,反过来,当 a.hashCode() == b.hashCode() 时,a.equals(b) 不一定为 true。

  • 为什么重写equals一定要重写hashCode

    • 1 结论:在没有使用hashMap,Set等容器的情况下,重写equals其实也不一定要重写hashCode(阿里巴巴开发手册要求一定重写)

    • 2 重写equals是为了在业务逻辑上判断实例是否相等。重写hashCode是为了让集合快速判重。(判重先hashCode,若该位置有多个元素(拉链法),再qeuals

    只重写equals不重写hashCode的逻辑错误

    User集合中两个用户name和age相同理论上是一个对象,但是没有重写hashCode方法,加入容器时,equals相同,hashCode不同,判断二者不是同一个对象

    set中同时有两个name和age相同的对象,这显然不符合常理,所以重写equals一定要重写hashCode

反过来说:重写了hashCode,equals需要重写吗?

同样要重写。假如重已经put了一个User,再put同一个User,hashCode(已重写的)比较相同,equals未重写,比较地址不同,就把同一个元素放入容器。(和上面同理的)


13 什么是反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性

反射原理:一个类在运行期只会创建一个Class字节码文件对象,获取对象对应的字节码文件就可以拿到相应属性与调用方法

  • 获取字节码文件的方式

       //方法一 全限定类名
       Class carEntityClass2 = Class.forName("com.example.demo3.Entity.CarEntity");     
       //方法二 直接类名.class拿到
        Class<CarEntity> carEntityClass0 = CarEntity.class;
​
        //方法三 创建实例 调用getClass
        CarEntity carEntity =new CarEntity();
        Class carEntityClass1 =carEntity.getClass();
    public static void main(String[] args) throws Exception {
        //获取CarEntity的Class对象
        Class carEntityClass = Class.forName("com.example.demo3.Entity.CarEntity");
​
        System.out.println("获取所有的Public的成员变量(只能获取公有)");
        Field[] field = carEntityClass.getFields();
        for (Field field1 : field) {
            System.out.println(field1.getName());
        }
        
        System.out.println("获取所有的的成员变量,不管你是Public,Private,Protected还是Default ");
        Field[] field01 = carEntityClass.getDeclaredFields();
        for (Field field1 : field01) {
            System.out.println(field1.getName());
        }
    }
//获取所有的Public的构造方法
carEntityClass.getConstructors();
//获取所有的的构造方法,不管你是Public,Private,Protected还是Default 
carEntityClass.getDeclaredConstructors();

应用场景

1.Eclispe,IDEA等开发工具利用反射动态刨析对象的类型与结构,动态提示对象的属性和方法。2.很多框架都用到反射机制,注入属性,调用方法,如Spring。Spring注解实现事务,SpringAOP动态代理的创建也是反射。


14 深拷贝、浅拷贝

  • 1)浅拷贝

    • 基本类型:直接复制数据值

    • 引用类型:只复制了对象的应用地址,新旧对象指向同一个地址,修改其中一个对象的值,另一个对象的值随之改变。

  • 2)深拷贝

    • 基本类型:直接复制数据值

    • 引用类型:开辟新的内存空间,在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象。


15 构造器可以被重写吗

Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到⼀个类中有多个构造函数的情况。


16 Java值传递

Java中只有值传递,对于对象参数,值的内容是对象的引用。


17 重写和重载的区别

  • 1)重载:发⽣在同⼀个类中,⽅法名必须相同,参数类型、个数、顺序不同,⽅法返回值和访问修饰符可以不同。

  • 2)重写:发生在不同类

    • 1 返回值类型、⽅法名、参数列表必须相同,抛出的异常范围⼩于等于⽗类,访问修饰符范围⼤于等于⽗类

    • 2 如果⽗类⽅法访问修饰符为 private/final/static 则⼦类就不能重写该⽅法,但是被 static 修饰的⽅法能够被再次声明

    • 3 构造⽅法⽆法被重写


18 静态和非静态

  • 1) 静态变量和成员变量的区别

    存储位置: 成员变量存在堆内存中,静态变量存在方法区中。

    生命周期:成员变量与对象共存亡,随着对象的创建而存在,随着对象被回收而释放。静态变量与类共存亡,随着类的加载而存在,随着类的消失而消失。

    所属对象:成员变量所属于对象,所以也称为实例变量。静态变量所属于类,所以也称为类变量。

    调用对象:成员变量只能被对象所调用 。静态变量可以被对象调用,也可以被类名调用。

  • 2) 静态方法和成员方法同理


19 静态可以调用非静态方法吗?

  • 没有创建对象实例不可以调用,若创建了实例则可以调用。因为没创建实例不知道调用哪个类的。

    public class Demo {
        public static void staticMethod() {
            // 先创建实例对象,再调用非静态方法:成功执行
            Demo demo = new Demo();//没有这一行 编译就会报错
            demo.instanceMethod();
        }
        public void instanceMethod() {
            System.out.println("非静态方法");
        }
    }

20 类初始化过程

public class InitialTest {
    public static void main(String[] args) {
        A ab = new B();
        ab = new B();
    }
}
class A {
    static { // 父类静态代码块
        System.out.print("A");
    }
    public A() { // 父类构造器
        System.out.print("a");
    }
}
class B extends A {
    static { // 子类静态代码块
        System.out.print("B");
    }
    public B() { // 子类构造器
        System.out.print("b");
    }
}

执行结果:ABabab,两个考察点:

1)静态变量只会初始化(执行)一次。(关于为什么,因为jvm有一个<init>和<Cinit>),<Cinit>在类加载阶段只执行一次。

2)当有父类时,完整的初始化顺序为:父类静态变量(静态代码块)->子类静态变量(静态代码块)->父类非静态变量(非静态代码块)->父类构造器 ->子类非静态变量(非静态代码块)->子类构造器 。


21 抽象类与接口

  • 1)抽象类只能单继承,接口可以多实现。

  • 2)抽象类可以有构造方法,接口不能有构造方法。

  • 3)抽象类可以有成员变量,接口没有成员变量,只能有常量(默认就是 public static final)

  • 4)接口类可以有非抽象方法,JDK7接口不支持非抽象方法,JDK8之后接口也支持非抽象方法(Default和静态方法)。

  • 5)抽象类方法修饰符可以任意。JDK8前接口方法只能public,JDK9支持private。

设计思想的区别:

  • 接口:自上而下的抽象过程,接口规范行为,是对行为的抽象。

  • 抽象类:自下而上的抽象过程,抽象类提供了通用实现,是对某一事物的抽象。

下面有个非常形象的说法:

普通类像亲爹 ,他有啥都是你的。

抽象类像叔伯,有一部分会给你,还能指导你做事的方法。

接口像干爹,可以给你指引方法,但是做成啥样得你自己努力实现。


22 Error与Exception

  • 1)二者都继承自Throwable

  • 2)Exception:程序本身可以处理的异常,可以通过 catch 来进⾏捕获。 Exception ⼜可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)

除了RuntimeException及其⼦类(NullPointExecrptionArithmeticException(ByZero)ClassCastException)以外,其他的 Exception 类及其⼦类都属于检查异常。常⻅的受检查异常有: IO 相关的异常(FileNotFoundExcepiton)、 ClassNotFoundExceptionSQLException ...

  • 3)Eroor:属于程序无法处理的错误,无法通过tryCatch捕获。

    常见的Error: OutOfMemoryError虚拟机内存不够错误,Virtual MachineErrorJava 虚拟机运⾏错误


23 Final关键字

修饰类:该类不能再派生出新的子类,不能作为父类被继承。因此,<font color=“red”>一个类不能同时被声明为abstract 和 final</font>。

修饰方法:该方法不能被子类重写。

修饰变量:该变量必须在声明时给定初值,而在以后只能读取,不可修改。 如果变量是对象,则指的是引用不可修改,但是对象的属性还是可以修改的。


24 try、catch、finally

  • 1)finally:无论是是否捕获到异常,finally中的语句都会被执行。如果try或catch块中有return语句时,finally语句块将回在方法返回前被执行。

public class TryDemo {
    public static void main(String[] args) {
        System.out.println(test());
    }
    public static int test() {
        try {
            return 1;
        } catch (Exception e) {
            return 2;
        } finally {
            System.out.print("3");
        }
    }
}

  • 2)Finally不会被执行的情况:

    • 在 try 或 finally 块中⽤了 System.exit(int) 退出程序。但是,如果 System.exit(int) 在异常语句之后, finally 还是会被执⾏

    1. 程序所在的线程死亡。

    2. 关闭 CPU。

  • 3)当 try 语句和 finally 语句中都有 return 语句时,在⽅法返回之前,finally 语句的内容将被执⾏,并且 finally 语句的返回值将会覆盖原始的返回值。

  public static void main(String[] args) {
        System.out.println(test1());
    }
    public static int test1() {
        try {
            return 2;
        } finally {
            return 3;
        }
    }
} //结果是3

25 JDK1.8新特性

(先小声bb,jdk18都要出了,嗯,对的你知道我想说什么)

  • 1)接口默认方法:Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字即可

  • 2)Lambda表达式和函数式接口:Lambda表达式本质上是一段匿名内部类,也可以是一段可传递的代码,把函数作为一个方法的参数(感兴趣的可以看一下Spring三级缓存原理,第三级缓存中存的的就是一个函数式接口,参数是Lambda表达式)。用 Lambda 表达式使代码更加简洁,但是也不要滥用,否则会有可读性等问题,《Effective Java》作者 Josh Bloch 建议使用 Lambda 表达式最好不要超过3行。

四大函数式接口(这部分后续在JUC部分内容再细讲):

Function:函数型接口, 有一个输入参数,有一个输出

Predicate:有一个输入参数,返回值只能是 布尔值!

Consumer:消费型接口: 只有输入,没有返回值

Supplier:供给型接口 没有参数,只有返回值

  • 3)Stream API:与ES6语法很像,就是些高效且易于使用的数据处理API。

public class Test { 
    public static void main(String[] args) { 
        User u1 = new User(1,"a",21); User u2 = new User(2,"b",22); 
        User u3 = new User(3,"c",23); User u4 = new User(4,"d",24); 
        User u5 = new User(6,"e",25); 
        List<User> list = Arrays.asList(u1, u2, u3, u4, u5); // 计算交给Stream流        // lambda表达式、链式编程、函数式接口、Stream流式计算 
        list.stream() .filter(u->{return u.getId()%2==0;})//筛选出id除于2为0的 
            .filter(u->{return u.getAge()>23;}) //筛选出age>23的
            .map(u->{return u.getName().toUpperCase();}) //遍历每一个把name变成大写
            .sorted((uu1,uu2)->{return uu2.compareTo(uu1);})//按照a>b排序
            .limit(1) //只取第一个
            .forEach(System.out::println);//循环打印 
    } 
}

26 Java中的IO流

  • 1)按照流的流向分,可以分为输⼊流和输出流;

  • 2)按照操作单元划分,可以划分为字节流和字符流;

  • 3)按照流的⻆⾊划分为节点流(FileInputerStream(文件操作),ByteArrayInputStream(数组操作))和处理流(PrintWriter(打印控制操作),BufferWriter(缓冲操作))。

Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派⽣出来的

InputStream/Reader: 所有的输⼊流的基类,前者是字节输⼊流,后者是字符输⼊流。OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

为什么有了字节流,还要字符流

字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是⾮常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼝,⽅便我们平时对字符进⾏流操作。

如果⾳频⽂件、图⽚等媒体⽂件⽤字节流⽐好,如果涉及到字符的话使⽤字符流⽐好。


27 Java创建对象的方式

(这个其实应该放前面的,但是我是想到漏了什么就补上的,改序号太麻烦了,将就看吧 ^_^)

  • 1)new创建新对象

  • 2)通过反射机制(无参 字节码 ,有参 拿到构造器再用newInstance())

  • 3)采用clone机制(实现Cloneable接口,重写clone方法;)

  • 4)通过序列化机制(实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。) 使用对象流 ObjectInputStream 的 readObject() 方法读取序列化对象;

前两者都需要显式地调用构造方法. 对于clone机制,需要注意浅拷贝和深拷贝的区别,对于序列化机制需要明确其实现原理,在java中序列化可以通过实现Externalizable或者Serializable来实现.


28 注解

四种元注解:

  • 1)@Target 修饰的对象范围:类,接口,枚举,注解,类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数)

  • 2)@Retention 定义 被保留的时间长短:SOURCE:在源文件中有效(即源文件保留),CLASS:在 class 文件中有效(即 class 保留),RUNTIME:在运行时有效(即运行时保留)

  • 3)@Documented 描述-javadoc:用于描述其它类型的annotation应该被作为被标注的程序成员的公共API

  • 4)@Inherited阐述了某个被标注的类型是被继承的:如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation 将被用于该class的子类。

注解的作用:很多框架都是使用注解+反射完成功能的,比如SpringBoot实现起步依赖自动装配,Spring注解配置。我常用到注解的场景是:通过自定义注解,或者SwaggerUI注解实现日志记录。


29 BIO、NIO、AIO 有什么区别?

(这里大家就当简单了解一下吧,这块内容展开说是很多的)

  • 1)BIO (Blocking I/O)同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。

JDK1.4之前,建立网络连接的时候采用BIO模式,先在启动服务端socket,然后启动客户端socket,对服务端通信,客户端发送请求后,先判断服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求,如果有的话会等待请求结束后才继续执行。

在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

  • 2)NIO (New I/O)

NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。

1)Non-blocking IO(非阻塞IO)

IO流是阻塞的,NIO流是不阻塞的。

Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

Java IO的各种流是阻塞的。这意味着,当一个线程调用 read()write() 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了

2)Buffer(缓冲区)

IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。.........

3)Channel(通道)

4)Selectors(选择器)

  • 3)AIO (Asynchronous I/O)

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。


30 强迫症

有强迫症喜欢凑个整数,想不起来最基础的内容还有什么了^_^,就凑个题目吧。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值