Java部分基础面试题

1、String StringBuffer 和 StringBuilder 的区别 是什么 String 为什么是不可变的?
答:可变性 :
简单的来说:String 类中使用 final 关键字字符数组保存字符串,private  final char value[],所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中 也是使用字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以 这两种对象都是可变的。 StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。
AbstractStringBuilder.java:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
     char[] value;
     int count; 
     AbstractStringBuilder() {
     }     
     AbstractStringBuilder(int capacity) { 
	     value = new char[capacity];     
     } 
     ......
 }

线程安全性 :
String 中的对象是不可变的,也就可以理解为常量,线程安全。 AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了 一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公 共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以 是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全 的。性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将 指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升, 但却要冒多线程不安全的风险。
对于三者使用的总结:
1. 操作少量的数据 = String
2. 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
3. 多线程操作字符串缓冲区下操作大量数据 = StringBuffer

2、== 与 equals(重要)
答:== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同 一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个 对象时,等价于通过“==”比较这两个对象。
 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来 两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两 个对象相等)。
举个例子:

public class test1 { 
    public static void main(String[] args) {
        String a = new String("ab"); // a 为一个引用
        String b = new String("ab"); // b 为另一个引用,对象的内容一样
        String aa = "ab"; // 放在常量池中
        String bb = "ab"; // 从常量池中查找 
        if (aa == bb) // true
            System.out.println("aa==bb");
        if (a == b) // false,非同一对象
            System.out.println("a==b");
        if (a.equals(b)) // true
            System.out.println("aEQb");
        if (42 == 42.0) { // true
            System.out.println("true");
        }     
    } 
}

说明:
 String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是 比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存 在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没 有就在常量池中重新创建一个 String 对象。

3、hashCode 与 equals(重要)
答:面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals 时必须重写hashCode方法?”
hashCode()介绍 :
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整 数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义 在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函 数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应 的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
为什么要有 hashCode
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断 对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如 果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有 相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相 等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。 如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高 了执行速度。
hashCode()与equals()的相关规定
1. 如果两个对象相等,则hashcode一定也是相同的
2. 两个对象相等,对两个对象分别调用equals方法都返回true
3. 两个对象有相同的hashcode值,它们也不一定是相等的
4. 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个 对象指向相同的数据)

4、Java 中的异常处理
Java异常类层次结构图 :
在这里插入图片描述
在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 Throwable 类。Throwable: 有两个重要的子类:Exception(异常) 和 Error(错 误) ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大 多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟 机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError。这 些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java虚拟机运行错误(Virtual MachineError)、类定义错误 (NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设 计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它 所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的 子类 RuntimeException。RuntimeException 异常由Java虚拟机抛出。 NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、 ArithmeticException(算术运算异常,一个整数除以0时,抛出该异常)和 ArrayIndexOutOfBoundsException (下标越界异常)。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
Throwable类常用方法:
 public string getMessage():返回异常发生时的详细信息
 public string toString():返回异常发生时的简要描述
 public string getLocalizedMessage():返回异常对象的本地化信息。使 用Throwable的子类覆盖这 个方法,可以声称本地化信息。如果子类没 有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
 public void printStackTrace():在控制台上打印Throwable对象封装的 异常信息
异常处理总结:
 try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch 块,则必须跟一个finally块。
 catch 块:用于处理try捕获到的异常。
finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。 当在try块或catch块中遇到return语句时,finally语句块将在方法返回 之前被执行。
在以下4种特殊情况下,finally块不会被执行:
1. 在finally语句块中发生了异常。
2. 在前面的代码中用了System.exit()退出程序
3. 程序所在的线程死亡。
4. 关闭CPU。

5、List 和 Set 的区别
答:List , Set 都是继承自 Collection 接口 List 特点:元素有放入顺序,元素可重复 ,
Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在set中的位 置是有该元素的 HashCode 决定的,其位置其实是固定的,加入Set 的 Object 必须定义 equals ()方法 ,另外list 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想 要的值。) Set和List对比 Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。 List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。

6、HashSet 是如何保证不重复的
向 HashSet 中 add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合 equles 方法比较。 HashSet 中的 add ()方法会使用 HashMap 的 add ()方法。以下是 HashSet 部分源码:

private static final Object PRESENT = new Object(); 
private transient HashMap<E,Object> map; 
public HashSet() {
    map = new HashMap<>(); 
} public boolean add(E e) {
    return map.put(e, PRESENT)==null; 
}

HashMap 的 key 是唯一的,由上面的代码可以看出 HashSet 添加进去的值就是作为 HashMap 的key。所以不会 重复( HashMap 比较key是否相等是先比较 hashcode 在比较 equals )。

7、HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明多线程 环境下不安全)?
不是线程安全的;
如果有两个线程A和B,都进行插入数据,刚好这两条不同的数据经过哈希计算后得到的哈希码是一样的,且该位 置还没有其他的数据。所以这两个线程都会进入我在上面标记为1的代码中。假设一种情况,线程A通过if判断,该 位置没有哈希冲突,进入了if语句,还没有进行数据插入,这时候 CPU 就把资源让给了线程B,线程A停在了if语句 里面,线程B判断该位置没有哈希冲突(线程A的数据还没插入),也进入了if语句,线程B执行完后,轮到线程A执 行,现在线程A直接在该位置插入而不用再判断。这时候,你会发现线程A把线程B插入的数据给覆盖了。发生了线 程不安全情况。本来在 HashMap 中,发生哈希冲突是可以用链表法或者红黑树来解决的,但是在多线程中,可能 就直接给覆盖了。
上面所说的是一个图来解释可能更加直观。如下面所示,两个线程在同一个位置添加数据,后面添加的数据就覆盖 住了前面添加的。
在这里插入图片描述
如果上述插入是插入到链表上,如两个线程都在遍历到后一个节点,都要在后添加一个数据,那么后面添加数 据的线程就会把前面添加的数据给覆盖住。则
在这里插入图片描述
在扩容的时候也可能会导致数据不一致,因为扩容是从一个数组拷贝到另外一个数组。

8、HashMap 的扩容过程
当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值(知道这个阈字怎么念吗?不念 fa 值, 念 yu 值四声)—即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
扩容( resize )就是重新计算容量,向 HashMap 对象里不停的添加元素,而 HashMap 对象内部的数组无法装载更 多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然 Java 里的数组是无法自动扩容的,方法 是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

HashMap hashMap=new HashMap(cap);
cap =3, hashMap 的容量为4;
cap =4, hashMap 的容量为4;
HashMap hashMap=new HashMap(cap);
cap =5, hashMap 的容量为8;
cap =9, hashMap 的容量为16;
如果 cap 是2的n次方,则容量为 cap ,否则为大于 cap 的第一个2的n次方的数。 

9、HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?
HashMap结构图:
在这里插入图片描述
在 JDK1.7 及之前的版本中, HashMap 又叫散列链表:基于一个数组以及多个链表的实现,hash值冲突的时候, 就将对应节点以链表的形式存储。
JDK1.8 中,当同一个hash值( Table 上元素)的链表节点数不小于8时,将不再以单链表的形式存储了,会被 调整成一颗红黑树。这就是 JDK7 与 JDK8 中 HashMap 实现的大区别。
其下基于 JDK1.7.0_80 与 JDK1.8.0_66 做的分析
JDK1.7中
使用一个 Entry 数组来存储数据,用key的 hashcode 取模来决定key会被放到数组里的位置,如果 hashcode 相 同,或者 hashcode 取模后的结果相同( hash collision ),那么这些 key 会被定位到 Entry 数组的同一个 格子里,这些 key 会形成一个链表。
在 hashcode 特别差的情况下,比方说所有key的 hashcode 都相同,这个链表可能会很长,那么 put/get 操作 都可能需要遍历这个链表,也就是说时间复杂度在差情况下会退化到 O(n)
JDK1.8中
使用一个 Node 数组来存储数据,但这个 Node 可能是链表结构,也可能是红黑树结构
如果插入的 key 的 hashcode 相同,那么这些key也会被定位到 Node 数组的同一个格子里。 如果同一个格子里的key不超过8个,使用链表结构存储。 如果超过了8个,那么会调用 treeifyBin 函数,将链表转换为红黑树。
那么即使 hashcode 完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销
也就是说put/get的操作的时间复杂度差只有 O(log n)
听起来挺不错,但是真正想要利用 JDK1.8 的好处,有一个限制:
key的对象,必须正确的实现了 Compare 接口
如果没有实现 Compare 接口,或者实现得不正确(比方说所有 Compare 方法都返回0)
那 JDK1.8 的 HashMap 其实还是慢于 JDK1.7 的
简单的测试数据如下:
向 HashMap 中 put/get 1w 条 hashcode 相同的对象
JDK1.7: put 0.26s , get 0.55s
JDK1.8 (未实现 Compare 接口): put 0.92s , get 2.1s
但是如果正确的实现了 Compare 接口,那么 JDK1.8 中的 HashMap 的性能有巨大提升,这次 put/get 100W条 hashcode 相同的对象
JDK1.8 (正确实现 Compare 接口,): put/get 大概开销都在320 ms 左右

10、对象的四种引用
强引用:只要引用存在,垃圾回收器永远不会回收

Object obj = new Object();
User user=new User();

可直接通过obj取得对应的对象 如 obj.equels(new Object()); 而这样 obj 对象对后面 new Object 的一个强引用,只有当 obj 这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。
软引用:非必须引用,内存溢出之前进行回收,可以通过以下代码实现

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null

这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象 时,则返回null; 软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的 真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
弱引用:第二次垃圾回收时回收,可以通过如下代码实现

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null; wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时, 将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的 isEnQueued 方法返回对象是否被垃圾回收器标记。
ThreadLocal 中有使用到弱引用,

public class ThreadLocal<T> {  
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            
            Entry(ThreadLocal<?> k, Object v) { 
                super(k); 
                value = v; 
            } 
       }
       //....  
    } 
    //..... 
}

***虚引用 垃圾回收时回收,无法通过引用取到对象值,***可以通过如下代码实现

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除
 

11、Arrays.sort 和 Collections.sort 实现原理 和区别
Collection和Collections区别:
java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。 java.util.Collections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、 线程安全等操作。 然后还有混排(Shuffling)、反转(Reverse)、替换所有的元素(fill)、拷贝(copy)、返 回Collections中小元素(min)、返回Collections中大元素(max)、返回指定源列表中后一次出现指定目 标列表的起始位置( lastIndexOfSubList )、返回指定源列表中第一次出现指定目标列表的起始位置 ( IndexOfSubList )、根据指定的距离循环移动指定列表中的元素(Rotate);
事实上Collections.sort方法底层就是调用的array.sort方法,

public static void sort(Object[] a) {
    if (LegacyMergeSort.userRequested) {
        legacyMergeSort(a); 
    } else {
    	ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }    
}
//void java.util.ComparableTimSort.sort()
static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
	assert a != null && lo >= 0 && lo <= hi && hi <= a.length;
	int nRemaining  = hi - lo;
	if (nRemaining < 2)
		return;		// Arrays of size 0 and 1 are always sorted
	// If array is small, do a "mini-TimSort" with no merges
	if (nRemaining < MIN_MERGE) {
		int initRunLen = countRunAndMakeAscending(a, lo, hi);
		binarySort(a, lo, hi, lo + initRunLen);
		return;
	} 
}
 

legacyMergeSort (a):归并排序 ComparableTimSort.sort() : Timsort 排序
Timsort 排序是结合了合并排序(merge sort)和插入排序(insertion sort)而得出的排序算法
Timsort的核心过程:
TimSort 算法为了减少对升序部分的回溯和对降序部分的性能倒退,将输入按其升序和降序特点进行了分 区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个run。针对这 些 run 序列,每次拿一个 run 出来按规则进行合并。每次合并会将两个 run合并成一个 run。合并的结果保 存到栈中。合并直到消耗掉所有的 run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。
综上述过程,Timsort算法的过程包括
(0)如何数组长度小于某个值,直接用二分插入排序算法
(1)找到各个run,并入栈
(2)按规则合并run

12、LinkedHashMap 的应用
基于 LinkedHashMap 的访问顺序的特点,可构造一个 LRU(Least Recently Used) 近少使用简单缓存。 也有一些开源的缓存产品如 ehcache 的淘汰策略( LRU )就是在 LinkedHashMap 上扩展的。

13、Cloneable 接口实现原理
Cloneable接口是Java开发中常用的一个接口, 它的作用是使一个类的实例能够将自身拷贝到另一个新的实例中, 注意,这里所说的“拷贝”拷的是对象实例,而不是类的定义,进一步说,拷贝的是一个类的实例中各字段的值。
在开发过程中,拷贝实例是常见的一种操作,如果一个类中的字段较多,而我们又采用在客户端中逐字段复制的方 法进行拷贝操作的话,将不可避免的造成客户端代码繁杂冗长,而且也无法对类中的私有成员进行复制,而如果让需要 具备拷贝功能的类实现Cloneable接口,并重写clone()方法,就可以通过调用clone()方法的方式简洁地实现实例拷贝功能
深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在C++语言中,若不弄懂,则会在delete的时候出问 题,但是我们在这幸好用的是Java。虽然Java自动管理对象的回收,但对于深拷贝(深复制)和浅拷贝(浅复制),我们 还是要给予足够的重视,因为有时这两个概念往往会给我们带来不小的困惑。
浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。深拷 贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。举例来说更加清楚:对象 A1 中包含对 B1 的引 用, B1 中包含对 C1 的引用。浅拷贝 A1 得到 A2 , A2 中依然包含对 B1 的引用, B1 中依然包含对 C1 的引 用。深拷贝则是对浅拷贝的递归,深拷贝 A1 得到 A2 , A2 中包含对 B2 ( B1 的 copy )的引用, B2 中包含 对 C2 ( C1 的 copy )的引用。
若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝

14、wait 和 sleep 的区别
源码如下

public class Thread implements Runnable {
public static native void sleep(long millis) throws InterruptedException;
	public static void sleep(long millis, int nanos) throws InterruptedException {
		if (millis < 0) {
			throw new IllegalArgumentException("timeout value is negative");
		}
		if (nanos < 0 || nanos > 999999) {
			throw new IllegalArgumentException(
				"nanosecond timeout value out of range");
		}
		if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
			millis++;
		} 
		sleep(millis);
	}
		//...
} 

public class Object {
public final native void wait(long timeout) throws InterruptedException;
	public final void wait(long timeout, int nanos) throws InterruptedException {
		if (timeout < 0) {
			throw new IllegalArgumentException("timeout value is negative");
		}
		if (nanos < 0 || nanos > 999999) {
		throw new IllegalArgumentException(
			"nanosecond timeout value out of range");
		}
		if (nanos > 0) {
			timeout++;
		}
		wait(timeout);
	}
	//...
}

1、 sleep 来自 Thread 类,和 wait 来自 Object 类。
2、主要是sleep方法没有释放锁,而wait方法释放了 锁,使得其他线程可以使用同步控制块或者方法。 3、wait,notify和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用(使 用范围)
4、 sleep 必须捕获异常,而 wait , notify 和 notifyAll 不需要捕获异常
(1) sleep 方法属于 Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可 运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了 sleep 方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在 sleep 的过程中过 程中有可能被其他对象调用它的 interrupt() ,产生 InterruptedException 异常,如果你的程序不捕获这个异 常,线程就会异常终止,进入 TERMINATED 状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语 句块(可能还有 finally 语句块)以及以后的代码。
注意 sleep() 方法是一个静态方法,也就是说他只对当前对象有效,通过 t.sleep() 让t对象进入 sleep ,这样 的做法是错误的,它只会是使当前线程被 sleep 而不是 t 线程
(2) wait 属于 Object 的成员方法,一旦一个对象调用了wait方法,必须要采用 notify() 和 notifyAll() 方法 唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了 wait() 后,这个线程就会释放它持有的所有 同步资源,而不限于这个被调用了 wait() 方法的对象。 wait() 方法也同样会在 wait 的过程中有可能被其他对 象调用 interrupt() 方法而产生 。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值