第一次 面试经验总结

创作于9月23日提前面试xx集团中的感受(第一次面试)

首先自然是对技术官做一个个人介绍,这里要分两种面试场景,所以务必准备好两种自我介绍模板!!

1、对技术面试官:

  • 其中主要讲解自己大学在编程方面干了什么,通过学习掌握了那些技术栈,做了什么项目,在那个技术栈比较有深入的理解。突出自己的亮点,核心竞争力在哪里

2、对HR来讲

  • 主要讲一讲自己在大学的丰富校园生活,有没用学生管理经验(学生会,社联等等),技术栈笼统带过就好。

发现了一些问题

  • 对于应届生而言,最重要的不是框架,而是Java的基础,应届生面试,面试官特别喜欢在一面问一些基础。
  • 所以一定要把Java基础打牢。其次面试官很少会问你框架怎么用之类的问题,只会问为什么用到这个技术,这个方案有什么优点,让你聊一聊
  • 还有一些技术性框架比较有标志性的问题会常问,比如说:Spring的IOC和AOP,SpringBoot启动类注解干了什么等等
  • 注意主次,一定要畅谈自己熟悉的领域,对自己不熟悉的一笔带过。

接下来就是技术面试官发问环节(分知识点整理如下):

1、JVM

1.1、双亲(parent)委派机制是什么?谈谈你对他的理解?

原文链接:https://blog.csdn.net/Dream_ling/article/details/109450588

  • 双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。

具体流程是这样的:

​ 1.当Application(应用) ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。

2.当Extension(扩展) ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。

3.如果Bootstrap(引导/启动) ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。

4.如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。

5.如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。

6.如果均加载失败,就会抛出ClassNotFoundException异常。
打个比方:

当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理会先检查自己是否已经加载过,如果没有再往上。注意这个过程,直到到达Bootstrap classLoader之前,都是没有哪个加载器自己选择加载的。如果父加载器无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。

1.2、既然你提到了类加载器,谈谈你对类加载器知道多少?

请添加图片描述

4:除此之外还有第四种类加载器:自定义类加载器,通过继承java.lang.ClassLoader类的方式实现自己的类加载器。

这又不得不提到JVM的执行Java程序的过程:我们写的Java程序(.java)先会被编程成二进制字节码文件(.class),然后通过jvm中的类加载器加载到Java虚拟机中,然后才会被Java虚拟机解析。

一个类如何被加载到jvm中

2、Mysql

MySQL优化有哪些常见方法?

这一块需要恶补!

这是一些sql语句的优化方法:

  • 查询语句中不要使用select *
  • 尽量减少子查询,使用关联查询(left join,right join,inner join)替代
  • 减少使用IN或者NOT IN ,使用exists,not exists或者关联查询语句替代
  • or 的查询尽量用 union或者union all 代替(在确认没有重复数据或者不用剔除重复数据时, union all会更好)
  • 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。(这一块不是很了解)
  • 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表 扫描,如:

select id from t where num is null 可以在num上设置默认值0,确保表中num列没有 null值,然后这样查询: select id from t where num=0

3、SpringBoot

原文链接:SpringBoot启动类及其原理

SpringBoot启动类在启动时会干些什么事情?你对启动类的注解知道多少?

SpringBoot的启动类上使用@SpringBootApplication注解标识,该注解试一个组合注解,包含多个其他注解。

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan是Spring中的注解,用来与配置类上,定义要扫描的组件。 上面两个注解则是SpringBoot自定义的注解。
  1. @SpringBootConfiguration:@SpringBootConfiguration注解中没有定义任何属性信息,而该注解上有一个注解@Configuration,用于标识配置类。所以@SpringBootConfiguration注解的功能和@Configuration注解的功能相同,用于标识配置类
  2. @EnableAutoConfiguration:这个注解就是SpringBoot最强大的一个功能(自动装配)!

这个注解也是一个组合注解,除了元注解外主要是由下面两个注解组成:

@AutoConfigurationPackage

@Import(AutoConfigurationImportSelector.class)

  • @AutoConfigurationPackage中使用注解@Import(@Import:的作用)导入了AutoConfigurationPackages.Registrar.class到容器中,那么来分析这个类,进入到这个内部类Regisrar:

    如果觉得不耐烦,请看原文链接

    !@AutoConfigurationPackage

    static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
        }
    
        @Override
        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new PackageImports(metadata));
        }
    }
    

    该类引入的重点在于方法registerBeanDefinitions():

    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
    }
    

    首先先分析方法体中所调用的方法register()的第二个参数

     PackageImports(metadata).getPackageNames().toArray(new String[0])
    

    进入到类PackageImports的构造方法:

    PackageImports(AnnotationMetadata metadata) {
        AnnotationAttributes attributes = AnnotationAttributes
            .fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
        List<String> packageNames = new ArrayList<>(Arrays.asList(attributes.getStringArray("basePackages")));
        for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {
            packageNames.add(basePackageClass.getPackage().getName());
        }
        if (packageNames.isEmpty()) {
            packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
        }
        this.packageNames = Collections.unmodifiableList(packageNames);
    }
    

    在这个构造方法中将元数据即启动类AnnotationMetadata metadata经过处理获取标签注解信息,注解信息里面的 basePackages 和 basePackageClasses是否有数据。basePackages、 basePackageClasses为注解@AutoConfigurationPackage中的属性。如果没有数据则获取注解所在的类的名字目录,放到List中
    获得packageNames属性也就是启动类所在的包。

    回到Registrar中的registerBeanDefinitions()方法中register()方法的第二个参数即为启动类所在的包的名称,并且使用数组来进行表示。

    在分析**register()**方法,register()源码如下:

    public static void register(BeanDefinitionRegistry registry, String... packageNames) {
       if (registry.containsBeanDefinition(BEAN)) {
          BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition) registry.getBeanDefinition(BEAN);
          beanDefinition.addBasePackages(packageNames);
       }
       else {
          registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames));
       }
    }
    

    这个方法的if语句为判断registry这个参数中是否已经注册了AutoConfigurationPackages的类路径所对应的bean(AutoConfigurationPackages)。如若已经被注册,则把上面分析的第二个参数所获取的包(启动类所在的包的名称)添加到这个bean的定义中。如若没有,则注册这个bean并且把包名设置到该bean的定义中。

小结:@AutoConfigurationPackage就是添加该注解的类所在的包作为自动配置包进行管理。他的实现就是依赖于工具类AutoConfigurationPackages中的内部类Registrar对所标注的包进行注册

  • @Import(AutoConfigurationImportSelector.class)自动配置导入选择器

小结:@EnableAutoConfiguration的实现方式是导入配置文件META-INF/spring.factories中EnableAutoConfiguration所对应的所有的类。

顺带谈谈SpringBoot自动装配的实现:

自动装配的实现主要依靠三个核心关键技术。 引入 Starter 启动依赖组件的时候,这个组件里面必须要包含@Configuration配置类,在这个配置类里面通过@Bean 注解声明需要装配到 IOC 容器的 Bean 对 象。 这个配置类是放在第三方的 jar 包里面,然后通过 SpringBoot 中的约定优于配置思想,把这个配置类的全路径放在 classpath:/META-INF/spring.factories 文件中。 这样 SpringBoot 就可以知道第三方 jar 包里面的配置类的位置,这个步骤主要是 用到了 Spring 里面的 SpringFactoriesLoader 来完成的。 SpringBoot 拿到所第三方 jar 包里面声明的配置类以后,再通过 Spring 提供的 ImportSelector 接口(类似于Spring中的import注解),实现对这些配置类的动态加载。

4、Java基础

4.1、== 和 equals 的区别:

首先从概念上入手:

==:

  • 基本类型:比较的是基本类型具体的值是否相等
  • 引用类型(eg:String):比较的是引用类型的地址是否相等,因为引用类型的值的就是对象的地址

equals:

  • equals是Java所有类的祖先Object类中的方法,这个方法在Object中的作用和==其实是一样的,比较的是地址是否相等,但是话说回来了,咱们用到的Java所有的包装类比如说String都是重新了Object类的equals()方法的,所以在包装类中,equals方法比较的是值是否相等而不是地址是否相等

看个代码思考思考:

String str1 = new String("123456");
String str2 = new String("123456");
System.out.println(str1==str2); 
//因为new出来的对象都会存放在虚拟机的堆内存区域,所以每次new都会分配地址,==对于引用类型来说比较的是地址,所以为false
//因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
System.out.println(str1.equals(str2));//equals在包装类中比较的是值

相信学习过jvm的都知道有个东西叫Stringtable(串池)吧,串池存在的意义就是避免字符串重复。

提到了stringtable就不得不说一说运行时常量池和方法区了:

4.1.1、方法区
4.1.1.1、定义:

Java 虚拟机具有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于常规语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,如运行时常量池字段和方法数据,以及方法和构造函数的代码,包括实例初始化以及接口初始化中使用的特殊方法§2.9)。

方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾回收或压缩它。此规范不规定方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小的,也可以根据计算的要求进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的。

Java 虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,以及在大小不同的方法区域的情况下,控制最大和最小方法区域的大小。

以下异常情况与方法区域相关联:

  • 如果方法区域中的内存无法用于满足分配请求,则 Java 虚拟机将引发 .OutOfMemoryError
4.1.1.2、组成:
  • 1.8以前 永久代(堆内存)
  • 1.8之后 元空间(本地内存)
    在这里插入图片描述
4.1.1.3、常量池

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

也叫 class文件常量池,主要存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)

  • 字面量:例如文本字符串、fina修饰的常量。
int i = 2; 
String str = "abcdefg";
  • 符号引用:例如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
4.1.1.4、运行时常量池

*运行时常量池,常量池是 .class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

4.1.1.5、StringTable(串池)

字符串常量池,也可以理解成运行时常量池分出来的一部分。类加载到内存的时候,字符串会存到字符串常量池里面。利用池的概念,避免大量频繁创建字符串。

从上面两个图,我们可以知道

  • 在jdk1.8中,串池存储在常量池中
  • 在jdk1.7之后,串池存储在堆内存中

实际上在java中,存在很多这样的常量池。其目的只有一个,就是为了复用,节约内存。

这里先给出两个概念:

  • 字符串常量拼接(编译期间优化)
  • 字符串变量拼接

看一下这段代码可能你就懂了:

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
   
    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的,为什么是懒惰的,因为他在未javac之前只是个字面量还不是字符串对象,只要编译后才能确定他是一个字符串对象
        String s2 = "b";
        String s3 = "ab";
        //String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")  
        //String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
        //System.out.println(s3 == s5);
    }
}

接下来咱们一起看几道笔试题,顺便分析分析:

String s1 = "a"; 
String s2 = "b"; 
String s3 = "a" + "b"; 
String s4 = s1 + s2;
String s5 = "ab"; 
String s6 = s4.intern(); 
// 问 
System.out.println(s3 == s4);  //false
System.out.println(s3 == s5);  //true
System.out.println(s3 == s6);  //true

String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern(); 
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢 
System.out.println(x1 == x2);//false

从头开始分析:

记住一句话,如果是new 出来的对象一定在堆中,如果是字面量,才会加入到常量池中

代码序号串池元素(StringTable)当前字符串所在位置当前序号代码分析
1“a”s1在串池尝试将"a"这个字符串常量加入到串池中,由于串池中没由"a"这个字符串常量,所以加入串池成功!
2“a”,“b”s2在串池尝试将"b"这个字符串常量加入到串池中,由于串池中没由"b"这个字符串常量,所以加入串池成功!
3“a”,“b”,“ab”s3在串池字符串常量在编译期间优化,相当于s3=“ab”,尝试将"ab"这个字符串常量加入到串池中,由于串池中没由"ab"这个字符串常量,所以加入串池成功!
4“a”,“b”,“ab”s4在堆中字符串变量拼接,本质:new StringBuilder().append(s1).append(s2).toString() 相当于new String(“ab”)
5“a”,“b”,“ab”s5指向串池中的"ab"由于s5=“ab”,在常量池中以及有了"ab",所以加入串池失败了,s5就直接指向串池中的"ab"字符串
6“a”,“b”,“ab”s6指向串池中的"ab"s4.intern()的意思是尝试把s4的值加入到串池中,但是串池中以及有了,就加入失败,但是会返回串池中"ab"的位置,结果就是s6指向串池中的"ab"
12“a”,“b”,“ab”x2在堆中这行代码的意思就是x2 = new String(“cd”) ,所以x2一定是在堆中的
13“a”,“b”,“ab”,“cd”x1在串池中尝试将"cd"这个字符串常量加入到串池中,由于串池中没由"cd"这个字符串常量,所以加入串池成功!
14“a”,“b”,“ab”,“cd”x2还是在堆中尝试将x2加入串池,但是堆中以及有了"cd",加入串池失败,x2仍然在堆中。特别说明:intern()方法会返回当前常量池的值

第13和14行代码调换位置后,16行的结果又是什么呢?

代码逻辑就是x2入池,x2在串池中,x1=“cd”,x1引用了串池中的"cd",也就是x1指向了"cd",x1==x2,结果为true

在jdk1.6中,最后一行的结果又会不一样,是因为:

jdk1.6中,intern的用法和1.8有区别:1.6中,x2.intern()的过程是:先将x2复制一份副本,然后将副本入池,但是副本和x2本体不是一个概念,他们所处位置不同,副本在串池中,x2本身是在堆中的。x1指向了x2的副本,所以x1 == x2的结果是false

4.2、final、finally 和 finalize都是什么?

  • final:final 修饰类、属性和方法
    • 修饰类:final 修饰的类不允许其他类继承,也就是说,final 修饰的类是独一无二的
    • 修饰方法:final 修饰的方法不允许被重写
    • 修饰属性:final 修饰的变量不能被修改
      • 修饰基本类型: final 修饰的变量不能被修改
      • 修饰引用类型:final 修饰的引用类型,只是保证对象的引用不会改变。对象内部的数据可以改变。
  • finally:finally通常配合try/catch代码块使用,由于finally中的代码最终都会被执行,所以finally中代码常见的都是解锁,流关闭以及连接 释放等操作!
  • finalize:finalize()方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。

finalize详解:

  • 由于垃圾对象的回收时机具有不确定性,所以finalize也具有不确定性。
  • finalize()方法中一般用于释放非Java 资源(如打开的文件资源、数据库连接等),或是调用非Java方法(native方法)时分配的内存。
  • finalize 现在已经不再推荐使用,在 JDK 1.9 中已经明确的被标记为 deprecated(官方提出的不建议使用,但是可以使用,只是不建议)
  • 由于finalize()方法的调用时机具有不确定性,从一个对象变得不可到达(对象可达性分析是垃圾回收是判定是否回收的依据)开始,到finalize()方法被执行,所花费的时间这段时间是任意长的。我们并不能依赖finalize()方法能及时的回收占用的资源,可能出现的情况是在我们耗尽资源之前,gc却仍未触发,因而通常的做法是提供显示的close()方法供客户端手动调用。
  • 另外,重写finalize()方法意味着延长了回收对象时需要进行更多的操作,从而延长了对象回收的时间。
  • 利用finalize()方法最多只会被调用一次的特性,我们可以实现延长对象的生命周期。(通俗来说就是让对象再活一次)

4.3、如何解决hash碰撞(hash冲突)?

原文链接:(14条消息) 通俗解释hash碰撞是什么以及如何解决_启四的博客-CSDN博客_什么是hash碰撞

首先我们要知道什么是hash?什么是哈希碰撞?

什么是hash表?

hash表的本质就是个数组,只不过这个数组中的元素是个Entry(键值对)

这里的学号是个key,哈希表就是根据key值来通过哈希函数计算得到一个值,这个值就是数组的下标值,用来确定这个Entry要存放在哈希表中哪个位置。

什么是哈希碰撞(冲突)?

hash碰撞指的是,两个不同的值(比如小陈、小吴的学号)经过hash计算后,得到的hash值相同,后来的小吴要放到原来的小陈的位置,但是数组的位置已经被小陈占了,导致冲突

怎么解决hash碰撞(冲突)?

原文链接:(14条消息) 解决哈希冲突(四种方法)_君诀的博客-CSDN博客_解决哈希冲突

解决hash冲突的思想基本就是:让哈希表的元素存放位置唯一。

大致有几种方法:

  • 开放定址法(线性探测法):就是从发生冲突的那个位置开始,按照一定的 次序从 hash 表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲 位置中。ThreadLocal 就用到了线性探测法来解决 hash 冲突的。平方探测法(二次探测)也是相同的道理,只不过找空闲位置的规则不同。
  • 链式寻址法:这是一种非常常见的方法,简单理解就是把存在 hash 冲突的 key, 以单向链表的方式来存储,比如 HashMap 就是采用链式寻址法来实现的。 向这样一种情况,存在冲突的 key 直接以单向链表的方式进行存储
  • 再hash法:就是在冲突的hash(key)位置在进行一次哈希操作,可以自定义多个哈希方法,这个冲突了,就换一个哈希方法继续哈希,总能找到空闲的位置
  • 建立公共溢出区:就是把 hash 表分为基本表和溢出表两个部分,凡事存在冲突 的元素,一律放入到溢出表中。

5、多线程

这块需要系统学习

线程创建的方式有哪几种?

  • 继承Thread类创建线程类
    • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
    • 创建Thread子类的实例,即创建了线程对象。
    • 调用线程对象的start()方法来启动该线程。
  • 通过实现Runnable接口创建线程类
    • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    • 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    • 调用线程对象的start()方法来启动该线程。
  • 通过Callable和Future创建线程
    • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
    • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
    • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

6、Spring

原文链接:(14条消息) 聊聊Spring的两大核心技术之一AOP_我是大肥鼠的博客-CSDN博客

聊一聊Spring中的AOP?

先可以说说OOP(面向对象编程):继承,封装,多态

OOP允许我们定义从上到下的关系,但并不适合定义从左到右的关系。所以当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。比如最常用的日志功能。这时候就要引用我们的AOP了,面向切面编程。他可以对模块做横切,横向织入代码。他的底层实现思想是动态代理,比如说中介,婚介公司。

AOP可以在不改变原有代码逻辑的基础上,进行代码的功能增强,比如说AOP实现日志输出。

其中有几个概念:

  • 切面(ASPECT):横切关注点 被模块化 的特殊对象。即,它是一个类。
  • 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。(增强的具体方法,增强的内容
  • 目标(Target):被通知对象。
  • 代理(Proxy):向目标对象应用通知之后创建的对象。(如:房屋中介)
  • 切入点(PointCut):切面通知 执行的 “地点”的定义。(可以在业务方法上)
  • 连接点(JointPoint):与切入点匹配的执行点。

SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice:

  • 前置通知 :方法执行调用 ,对应的注解是 @Before
  • 后置通知 :方法执行后调用 ,对应的注解是**@After**
  • 返回通知 :方法返回后调用 ,对应注解是**@AfterReturning**
  • 异常通知 :方法出现异常调用,对应注解是**@AfterThrowing**
  • 环绕通知 :动态代理、手动推荐方法运行,对应的注解是**@Around**

AOP和OOP都是为了降低代码的耦合性,提高复用性,用来提高代码开发效率。

AOP的动态代理方式有两种:

  • cglib
  • 默认是jdk

具体的区别看这篇:

(14条消息) AOP中的动态代理的区别–JDK和CGLIB_黄泥川水猴子的博客-CSDN博客_aop cglib和jdk动态代理

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值