Java面试——Java基础

一、面向对象编程

1、static关键字

static修饰的变量,叫静态变量,在类加载到内存的时候系统就会为静态变量分配专门的内存空间,一直到程序运行结束;
静态变量不属于类的任何一个对象,而是属于全体对象,一般通过类名访问,虽然可以通过对象名访问但一般都建议直接用类目访问;

static修饰的方法,叫静态方法;一般工具类的方法常被定义为静态方法,这样就可以用类名直接调用方法;静态方法中用到的全局变量必须是静态变量,方法内的局部变量可以是非静态的。

2、final关键字

final修饰的字段,不能被重写赋值,在定义时的值就是最终值;
final经常和static一起连用,被它们修饰的字段称为final常量,有点像配置文件的属性,程序启动、直到停止都是不变的,而且可以用类名直接访问,final常量的名字字母一般全部大写。

final修饰的实例方法,可以被子类继承,但不能被子类重写;

final修饰的类,称为最终类,不能被继承;

3、abstract关键字

修饰类,就叫抽象类;
修饰方法,就叫抽象方法(与之相对的是:实例方法);
修饰变量,就叫抽象变量(与之相对的是:实例变量)。
抽象方法和接口方法类似,但是可以有部分子类都需要进行的操作代码;
含有抽象方法的类都必须定义为抽象类;
子类继承抽象类,需要重写抽象类的抽象方法,否则子类也要定义为抽象类。

4、类的继承

1)子类会继承父类的所有实例属性和实例方法;
2)子类不会继承父类的构造方法,需要的话可以用super关键字调用;
3)静态成员不用继承,仍是用类名直接调用即可;
4)实例方法如不满足需求,子类可重写方法;
5)子类需要重写父类的抽象方法(如果有),否则子类也要定义为抽象类;
6)一个类只能继承一个父类。

5、接口的实现

接口多态:用接口对象调用接口方法,实际执行的是接口实现类重写后的方法,这就是接口的多态。

一个类可以实现多个接口(但只能继承一个父类)。

二、数组

1、数组的内存分析

数组是内存中一块连续的存储空间.

2、数组的特点+适用场景

通过下标可以快速访问数组的元素,不管数组的长度有多大,可以通过下标快速计算出每个元素的存储地址,访问速度快。
数组的缺点是插入/删除元素效率低。在插入元素时,可能需要数组扩容,需要复制/移动大量的元素。
所以数组适合应用于以访问为主,很少进行添加/删除操作的场景。

3、数组的算法(了解最好)

1)冒泡排序
2)选择排序
3)二分查找

4、Arrays工具类

大概知道就行,用的时候,可以具体看Arrays代码;
1)把数组转换为List列表
asList(T[] a)
2)copyOf(int[] original, int newLength)
复制original数组,新的数组长度是newLength
3)copyOfRange(int[] original, int from, int to)
把original数组中[from, to)范围内的元素复制到新数组中
4)binarySearch(int[] a, int key)
在a数组中采用二分查找,返回key元素的索引值
5)fill(int[] a, int fromIndex, int toIndex, int val)
把a数组中[from,to)范围的元素使用val填充
6)sort(int[] a)
排序
7)parallelSort(int[] a)
对a数组中元素并行 排序,适合于元素非常多的情况
8)sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c)
对a数组[from,to)范围的元素进行排序,根据comparator比较器比较大小
9)toString(int[] a)
可以把数组中元素转换为字符串
10)deepToString(Object[] a)
把多维数组转换为字符串

5、二维数组(了解)

就是数组元素是数组的那种

三、常用类

1、String

1)字符串常量池
在java中用引号引起来的字符串都会被放入常量池中,常量池中的变量采用享元模式。
a、String s1 = “hellow”;
String s2 = “hellow”;
若比较s1 == s2,结果是true,因为s1存的地址和s2一样,都指向系统分配给“hellow”的内存地址。
但一般字符串比较还是要用equls,因为==有时候不管用;java有自己的优化机制,常量池有的就尽量不重建,但是若java不确定以后会不会变化,它就会重建以确保不出错。
b、以下两行共创建了多少个String对象?
String s8 = new String(“world”);
String s9 = new String(“world”);
答案是3个;new出来的两个对象好理解,第三个是“world”,java也会给它分配一块内存。
2)StringBuilder/StringBuffer
String对象是不可变的,每次进行字符串连接都会生成新的字符串对象。
如果要频繁进行字符串连接,使用StringBuilder/StringBuffer。
StringBuffer/StringBuilder字符串对象是可变的,当创建StringBuilder对象后,通过append()、reverse()等方法可以改变这个字符串对象中的字符序列。最后可以调用StringBuilder对象的toString()方法转换为String对象。
StringBuilder/StringBuffer的区别:StringBuffer是线程安全的,StringBuilder不是线程安全的。

除了append()、reverse()、deleteCharAt()几个常要方法,还有一个setLength(0)方法,用于清空sb中已经拼上的字符,参数0就是清空所有,效果类似new StringBuilder/StringBuffer()

2、数学相关类

1)Math

java.lang.Math类,定义了一组与数学函数相关的操作,包括三角函数、对数操作、指数操作等。Math类构造方法是private私有的,不能创建Math对象,但Math类的所有方法都是静态方法,可以通过类名直接调用。
eg1:Math.random(),可以生成[0,1)范围内的随机数;
eg2:sqrt(n) n的平方根, cbrt(n)立方根, pow(a,b)返回a的b次方;
eg3:max(a,b)返回a与b的较大者, min(a,b)返回较小者;
了解即可,使用时、具体方法可看Math类中的描述。

2)Random

java.util.Random类,专门用于生成随机数的。提供两个构造方法,无参构造方法使用默认的种子(当前时间),另一个构造方法可以指定随机数的种子,相同种子的Random对象可以生成相同的随机数序列。
Random生成的随机数,比Math的种类更多、使用更方便
eg1:生成随机小数
Random random = new Random();
random.nextDouble();
eg2:生成随机整数
random.nextInt();
eg3:生成指定上限的随机整数, [0, limit)
random.nextInt(100); //生成[0,100)范围内的随机整数

3)DecimalFormat

java.text.DecimalFormat类可以对数字格式化, 常用的数字格式符有#与0,区别在于使用0格式符时,不足的位数会补0。
DecimalFormat df= new DecimalFormat(“###,###.0000”);
String text = df.format(1234.56);
System.out.println( text ); //1,234.5600

4)BigDecimal

如果进行科学计算、财务计算时,使用double可能不准确,可以使用BigInteger、BigDecimal类。
可以调用其add()、subtract()、multiply()、divide()进行加减乘除操作。

3、日期相关类

详看后端->《Java基础知识纪要》的【时间数据处理】

4、包装类

只需要知道Integer装箱、拆箱可能造成的问题即可,详看
后端->《Java基础知识纪要》的【变量的基本数据类型与包装类型】

四、集合与Map

1、collecion集合包括:List与Set

1)list

ArrayList与Vector在内存的存储方式都像数组一样,它们的特点都是易访问、不易扩容(和数组一样);只不过vector是线程安全的。
LinkList底层是双向链表,特点是:访问慢、但增/删容易。

2)set

HashSet无序、不可重复;
TreeSet有序、不可重复;
新建TreeSet的两种方式:
a、new的时候,在构造方法参数列表写一个compatator匿名内部类:

TreeSet<String> treeSet = new TreeSet<>(new Comparator<String>() {
	@Override
    public int compare(String o1, String o2) {
    	return o2.compareTo(o1);
    }
});

b、要放入set的元素,实现了comparable接口、并重写compameTo()方法;比如String就实现了comparable接口、并重写了compameTo()方法,所以可以直接放入TreeSet中

TreeSet<String> treeSet2 = new TreeSet<>();
treeSet2.addAll(treeSet1);	//treeSet1 = [dd, gg, jj, mm, xx]
System.out.println(treeSet2); 

2、Map

1)HashMap

数组+链表;
通过键对应的哈希值->(哈希函数)->哈希码->数组下标,
节点连到数组、或其他链表后边;
在这里插入图片描述
查询的时候,根据键对应的哈希值->(哈希函数)->哈希码->数组下标,找到对应链表,然后根据键的哈希值,找到对应的键值对。
map的键,如同set、是不可重复的,所以其哈希值注定不同(哈希码可能相同)。

2)TreeMap

TreeMap的创建与TreeSet类似,有两种方式:
a、new的时候,在构造方法参数列表写一个compatator匿名内部类:

TreeMap<String , Integer> treeMap = new TreeMap<>(new Comparator<String>() {
	@Override
	public int compare(String o1, String o2) {
		return o2.compareTo(o1);
	}
});

b、map的键值元素,实现了comparable接口、并重写compameTo()方法;比如String就实现了comparable接口、并重写了compameTo()方法,所以,以String为键值的、可以直接放入TreeMap中

3、Collections工具类

java.util.Collections工具类定义了一组对集合的操作
1)addAll( Collection, 元素), 可以向Collection中添加若干元素
Collections.addAll(list, “xx”, “oo”, “dd”, “jj”, “mm”);
2)sort()排序, 在JDK8前,对List排序使用Collections.sort(),在JDK8中List集合增加了自己的sort()

 Collections.sort(list);	//没有指定Comparator比较器要求List集合中 元素实现Comparable接口
 System.out.println(list);	//[dd, jj, mm, oo, xx]

五、异常

1、异常概述

在这里插入图片描述
运行时异常,不需要预先处理,解决靠规范合理的代码;
受检异常,需要预处理,包括抛出、捕获两种方式;
异常抛出后,就不断向上、交给调用者处理,最终一般都是上抛到JVM处理,JVM默认的异常处理方式是:中断程序+打印异常信息。
异常捕获语法:
try{
把有需要预处理的受检异常的代码入在try代码块中
}catch(异常类型 e){
捕获异常,进行预处理,
程序运行后,万一产生了异常,程序员提供的一预处理方案
}finally{
finally子句不是必需的,
不管是否有异常产生总是会执行
}

2、自定义异常及示例

很多时候,系统是否抛出异常,是根据业务需求来定的。比如程序中的数据或者执行与既定的业务需求不符,这就可以视为一种异常,与业务需求不符的异常需要由程序员来决定抛出。
抛出异常时要选择合适的异常类,以便更为明确地描述该异常情况,如果觉得java的异常类没有合适的,程序员可以自定义异常类。
自定义异常类都要继承Exception类;如果希望自定义运行时异常,则该异常类需要继承RuntimeException类(Exception的子类)。
定义异常类时一般提供两个构造方法,一个是无参构造方法,一个是带有String字符串参数的构造方法,这个字符串就作为异常的描述信息。

自定义异常应用示例:
在设置Person类属性时,如果设置的年龄超出范围,就抛出一个年龄越界的异常;如果设置的性别不合理,就抛出一个性别非法异常。

//person类
public class Person {
    private String name;
    private int age;
    private String gender;

    public String getName() {
        return name;
    }

    public Person setName(String name) {
        this.name = name;
        return this;
    }

    public int getAge() {
        return age;
    }

    public Person setAge(int age) {
        if (age >= 0 && age < 130 ) {
            this.age = age;
        }else {     //抛出一个年龄越界的异常
           //通过throw抛出一个异常对象;运行时异常,不需要额外处理
            throw new AgeOutOfBoundsException(age + "超出人的年龄范围");
        }
        return this;
    }

    public Person setGender(String gender) throws IllegalGenderException {
        //性别合理就赋值,不合理就抛出异常
        if ("男".equals(gender) || "女".equals(gender) ) {
            this.gender = gender;
        }else {     //抛出性别非法异常
            //throw抛出的受检异常对象,当前方法需要通过throws声明它,对于 setGender 方法来说,它就是受检异常
            throw new IllegalGenderException(gender + "不是合法的性别");
        }
        return this;
    }

    public String getGender() {
        return gender;
    }
}
//自定义年龄越界异常类,该异常类继承了RuntimeException类,是运行时异常
public class AgeOutOfBoundsException extends RuntimeException {
    //一般提供两个构造方法
    public AgeOutOfBoundsException() {
    }
    //有String参数的构造方法,字符串是异常 的信息
    public AgeOutOfBoundsException(String message) {
        super(message);
    }
}
//自定义非法性别异常类,  该异常类继承了Exception,而不是RuntimeException类,是受检异常
public class IllegalGenderException extends Exception {
    public IllegalGenderException() {
    }
    public IllegalGenderException(String message) {
        super(message);
    }
}
//测试
public class Test {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.setName("lisi");
        //setAge()方法体中抛出的运行时异常,不需要预处理
        p1.setAge(23);
        //setGender方法声明抛出了受检异常IllegalGenderException,在调用时需要预处理
        try {
            p1.setGender("男");
        } catch (IllegalGenderException e) {
            e.printStackTrace();
        }
    }
}

六、注解

1、java.lang包中定义了一些基本的注解:

@Override注解告诉编译器检查方法覆盖(重写)是否符合规则.
@Deprecate注解表示已过时,当其他程序使用已过时的类或方法时,编译器会给出警告.
@FunctionalInterface注解表示是函数式接口. 如果接口中只有一个抽象方法可以定义为函数式接口. 函数式接口是为Lambda表达式准备的,即可以使用Lambda创建函数式接口实例.

2、自定义注解

语法:
[修饰符] @interface 注解名{
数据类型 属性名() default 默认值;
}

应用示例:

@Target(ElementType.TYPE)           //当前注解可以修饰类
@Retention(RetentionPolicy.RUNTIME) //保留到运行时
public @interface MyAnnotation {
    String name() default "hellow";  //定义name属性,默认值是hellow
}
@MyAnnotation(name = "hehe")
public class Test {
    public static void main(String[] args) {
        Class<Test> claxx = Test.class;
        //通过反射读取注解的属性值
        MyAnnotation annotation = claxx.getAnnotation(MyAnnotation.class);
        if ( annotation != null ){
            String name = annotation.name();
            System.out.println(name);
        }
    }
}

七、反射

反射就是Java中一种,根据类对象获取类、类方法、类属性等信息的,一种技术。
1)可获取的类信息
class.getModifiers() 返回类的修饰符
class.getSimpleName() 返回类的简易类名
class.getSuperclass() 返回父类
class.getInterfaces() 返回接口
2)可获取的方法信息
clss.getMethod(方法名, 形参列表) 返回指定方法签名的方法
method.invoke(对象名, 方法实参) 调用方法
3)可获取的字段信息
class.getFied(字段名) 返回指定名称的公共字段
class.getDeclaredField(字段名) 返回指定名称的任意权限的字段
field.set(对象名, 字段值) 设置对象的字段值
field.get(对象名) 返回对象的字段值
4)获取注解信息

八、线程

1、基本概念

1)进程 (Process):是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。可以把进程简单的理解为正在操作系统中运行的一个程序。
2)线程 (Thread):是进程的一个执行单元。一个线程就是进程中一个单一顺序的控制流,是进程的一个执行分支.进程是线程的容器,一个进程至少有一个线程。一个进程中也可以有多个线程。
在操作系统中是以进程为单位分配资源,如CPU、虚拟存储空间等。多个线程共享同一个进程的资源。每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储.
3)主线程:JVM启动时会创建一个主线程,该主线程负责执行main方法。主线程就是运行main方法的线程。
4)Java中的线程不是孤立的,线程之间存在一些联系。如果在A线程中创建了B线程,称B线程为A线程的子线程,相应的A线程就是B线程的父线程
串行。
5)并发与并行:串行是指各个任务依次执行,并发是指在某段时间内交替完成多个任务;并行是同时完成多个任务。
并发可以提高事物的处理效率,即一段时间内可以处理或者完成更多的事情。并行是一种更为严格、理想的并发。
从硬件角度来说,如果是单核CPU,一个处理器一次只能执行一个线程的情况下,处理器可以使用时间片轮转技术,可以让CPU快速的在各个线程之间进行切换,对于用户来说,感觉是三个线程在同时执行。如果是多核心CPU,可以为不同的线程分配不同的CPU内核。

2、一些面试题

1)什么是线程?线程和进程有什么区别?

答:线程是程序执行的最小执行单位,进程是资源分配的最小单位;一个进程就是一个应用程序,系统会为该进程分配资源空间,当多用户并发请求的时候,为每个用户创建一个进程资源开销太大难以实现,就开辟了线程,线程速度比较快,线程之间共享进程之间的内存资源。

1.1)多线程和高并发是两个概念。
我们做过的泰康积分商城,由于用户数量不多,所以并没有使用多线程技术,但也会有并发问题需要考虑,比如减库存这个业务;对于这种没有用多线程技术的项目,解决并发问题的主要方法就是用锁,实际上就是使关键节点串行化执行。
而多线程,要解决的其实是两个痛点,一是避免开辟进程过多造成的资源浪费(严重的情况会使系统宕机);二是解决并发问题。这两层都要看到,不要混淆。

2)如何在Java中实现线程?

有四种实现方法:

继承Thread类,重写run()方法;

实现Runnable接口,重写run()方法;

实现Callable接口,重写call()方法;可以有返回值,通过Callable<>接口的泛型指定返回值的类型。

使用ExecutorService、Callable、Future实现有返回结果的多线程。

3)Java 关键字volatile 与 synchronized 作用与区别?

答:线程有几个特性

原子性(Atomic):不被其他线程中断

可见性(Visibility):执行结果其他线程可以看到

顺序性(Ordering):保证线程操作的执行顺序

volatile 只能用来修饰变量、作用范围较小; synchronized可以修饰变量、方法、类和代码块。

volatile 修饰的变量在线程未结束的时候就可以被其他线程读取,不能保证原子性;
synchronized则保证线程执行完成后变量结果值才可见,保证了原子性、可见性和顺序性。

4)线程有哪些不同的线程生命周期?

create 新建状态

runnable 就绪状态/可运行状态

running 运行状态

blocked 阻塞状态

dead 终止状态

5)什么是死锁(Deadlock)?如何分析和避免死锁?

死锁是指多个线程因竞争资源而造成的一种相互等待的僵局,若无外力作用,这些线程任务都将无法向前推进。

避免死锁就要避免循环等待条件的产生、设置资源标识位以及执行顺序等。

6)什么是线程安全?Vector是一个线程安全类吗?

线程安全就是,多线程操作和多个线程单独、依次操作产生得结果相同。
一般认为Vector是一个线程安全的类,但实际上虽然Vector类中好多方法是用Synchronized修饰的,但是也有remove 和 contains等几个方法不是用Synchronized修饰的,这就需要在业务代码块中添加同步锁来保证线程安全了,所以严格来说不能直接说Vector是线程安全类。

7)Java中如何停止一个线程?

Thread类中的interrupt()方法可以中断线程,注意调用interrupt()方法仅仅是在当前线程打一个中断标志,并不是真正的停止线程;具体中断线程操作为:在测试、业务方法中用start()实例方法启动线程,在合适的节点用interrupt()方法给线程打中断标志,在子线程中提前设置代码——通过判断中断标志来退出子线程调用,可以用break关键字或者returen()退出。

Thread.interrupted()静态方法可以判断当前线程的中断状态,也可以调用thread.isInterrupted()实例方法判断当前线程的中断状态。这两个方法的区别在于: 静态方法interrupted()判断完中断状态后会清除中断标志, 实例方法isInterrupted()判断完后不会清除中断标志。

8)sleep()、suspend()和wait()之间有什么区别?

注:以上三个方法都是使线程进入blocked状态的

答:三个方法使用后线程的唤醒方式不同,sleep()固定时间后主动唤醒,suspend()是已经废弃的暂停方法、被resumen唤醒,wait()被notify()或者或者 notifyAll()唤醒。

9)什么是线程饿死,什么是活锁?

饿死:线程没有被合理的分配资源,都处于无限期的等待;

活锁:所有线程都认为自己的优先级别不够高,都在等待对方先执行。

10)什么是Java Timer类?如何创建一个有特定时间间隔的任务?

答:Timer可以看成一个定时器,用来安排以后在线程中执行的任务,可设置任务执行一次,或者定期重复执行。Timer可以调度TimerTask创建有特定时间间隔的任务,TimerTask是一个抽象类,实现了Runnable接口,所以具备了多线程的能力;一个Timer可以调度任意多个TimerTask,它会将TimerTask存储在一个队列中,顺序调度,如果想两个TimerTask并发执行,则需要创建两个Timer。

11)什么是线程池? 为什么要使用它?

答:线程池用来管理多线程任务中的线程。一个线程需要创建,运行和销毁,如果客户端访问量过大,那么创建线程和销毁线程将是一笔很大、且有些浪费的开销;有了线程池,我们会在程序启动的时候事先放入几个线程,当有任务调用的时候就直接去取线程池中的线程,任务结束再将链接放回线程池而不是直接销毁。当然线程池本身也占用资源,所有容量也不易过多。

12)多线程中的忙循环是什么?

答: 忙循环就是程序员用循环让一个线程等待;不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存。在多核系统中,一个等待线程醒来的时候可能就在另一个内核运行了,这样会重建缓存,为了避免重建缓存和减少等待重建的时间就可以使用忙循环。

13)Thread 类中的start() 和run() 方法有什么区别?

start 是用来启动线程的,run()则是运行线程的 ;

start 在一个线程中只能调用一次,而run则可以多次调用;

查看Thread类的源码会发现start 有synchronized修饰,可见这个方法是线程安全的。

14)在多线程中,什么是上下文切换?

即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现多线程。时间片就是CPU分配给各个线程的时间,因为时间片非常短(一般为几十毫秒),所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换。
这就像我们同时读两本书,当我们在读一本英文的技术书籍时,发现某个单词不认识,于是便打开中英文词典,但是在放下英文书籍之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。

15)(相对于线程来说)Java中堆和栈有什么不同?

答:创建的对象是在堆中,堆内存是各线程共享的区域;而栈中内存是线程私有的区域、生命周期随着线程的消亡而消亡。系统为了提高运行效率会从堆中复制一份变量到栈内存中,运行完成后再刷新到堆内存中。

16)Thread类中的yield方法有什么作用?

答:让当前正在运行的线程回到就绪状态,让出cpu,等待cpu的再次调度。

17)Java中notify 和 notifyAll有什么区别?

答:notify是唤醒等待池中的某一个线程但是不指定是那个,notifyAll是唤醒等待池中的所有线程进入锁池中竞争对象。

18)Java多线程中调用wait() 和 sleep()方法有什么不同?

答:sleep()是Thread的静态方法;线程调用此方法会让线程暂时放弃cpu资源,但是不会释放对象锁;时间到后会自动苏醒;需要捕获异常

wait()方法必须放在同步控制方法或者同步语句块中使用;会暂时放弃对象锁;需要notify()方法将其唤醒。

19)有三个线程T1,T2,T3,怎么确保它们按顺序执行?

启动某个线程的时候分别加入join()方法,例如T3调用T2、然后T2调用T1,利用单线程池法

20)什么是ThreadLocal?

ThreadLocal是局部本地变量,为单个线程私有变量、不能共享,所以不存在线程安全问题;
相对的还有共享变量,线程不安全,需要用锁机制来维护。

21)Java线程池中submit() 和 execute()方法有什么区别?

答:两个方法都可以向线程池中提交任务,区别是:
execute()是Executor中的方法,用void修饰,即没有返回值;
submit() 是ExecutorService中的方法,有返回值,返回值为Future对象,可以用get方法获取执行结果。

22)Java中Runnable和Callable有什么不同?

答:区别是:
Runnable从jdk1.0就有,Callable从jdk5.0有;
callabel的call()方法可返回Future对象,Runnable的run()方法没有返回值。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值