Java面试——Java本身基础篇(2022-02-15)

1.equals和==运算符的区别

==运算符说明

对于基本类型和引用类型,==运算符的效果不同,基本的数据类型与基本数据类型的包装体是可以使用==来直接判断值是否相等的,但是包装体与包装体之间的对比不能使用==。

基本类型:比较的是值是否相同 引用类型:比较的是引用是否相同(指向的内存是否一致)

equals说明

equals是Objact自带的方法(String与包装体都重写了这个方法),实现了引用类型的值比较

2.HashMap 的实现原理

JDK1.8之前HashMap使用的是【数组+链表】来实现的,实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

向HashMap里Put数据时,回根据key来计算hash值,通过hash值得到元素所在数组的下标,若该数组的当前下标已经存放了其他数据,则在这个下标上的数据以链表的形式存放,新加入的在链表的根节点,最先加入的数据放在链表最尾端。如果数组中的当前下标没有数据,则直接将该数据放在数组里。

在JDK1.8开始将HashMap做了优化,改为【数组+链表或红黑树】,当链表中的节点数据超过八个之后,将链表转为红黑树来提高查询效率。

3.HashMap如何扩容

向HashMap里添加元素的时候,会判断当前数组里的元素个数,如果大于等于阈值,则开始扩容。 一般情况,扩容是当前容量的两倍。

  • capacity 即容量,默认16,可以在构造函数里更改。
  • loadFactor 加载因子,默认是0.75,可以在构造函数里更改
  • threshold 阈值。阈值=容量*加载因子。默认12。当元素数量超过阈值时便会触发扩容。

4.如何决定使用HashMap或TreeMap

HashMap适合插入与删除,TreeMap适合对于有序的Key集合进行排序。

5.HashSet的实现原理

HashSet不保证排序,允许存放null,不允许由重复的元素。
HashSet由HashMap实现,HashSet将所有的添加进来的值当作一个key存放在HashMap上,而HashMap的value统一为PRESENT(就是一个固定值永远不变)。
HashSet的add方法调用的是HashMap的put方法,因此不存在重复元素。关于HashMap如何实现key的不重复,请见本文的第二条。

6.ArrayList的实现原理

ArrayList的底层使用数组来实现,当向数组添加元素时,都会检查元素个数是否超出数组长度,如果超出则进行扩容,每次扩容是旧数组的1.5倍长度。 ArrayList与Array之间可以互相转换:

  • List转换成为数组:调用ArrayList的toArray方法。
  • 数组转换成为List:调用Arrays的asList方法

7.ArrayList 和 LinkedList 的区别是什么

ArrayList底层是数组,支持随机方法,时间复杂度是O(1)
LinkedList底层使用【双向循环链表】,不支持随即方法,时间复杂度为O(n)。

8.Array与ArrayList的区别

  • Array可以容纳基本类型与对象,ArrayList只能容纳对象
  • Array相比ArrayList功能较少,没有addAll、removeAll、iterator等等

9.Iterator迭代器是啥

迭代器Iterator是一种设计模式,在java里他是一个对象,可以用来遍历并选择序列中的对象。 迭代器是轻量级的对象,创建它的成本很小。 迭代器只能单向移动。

  • hasNext()方法获取是否还有下一个元素,有返回true
  • next()方法获取下一个元素
  • remove()方法将迭代器返回的元素删除

10.Iterator与ListIterator有什么不同

Iterator是Java迭代器最简单的实现,为List设计的ListIterator有跟多的功能。

  • ListIterator可以双向遍历List,也可以从List中插入与删除元素。Iterator只能单向遍历。
  • Iterator可以用来遍历Set、List,ListIterator只用于遍历List
  • ListIterator实现了Iterator接口,并且多了一些功能:添加元素、替换元素、获取前一个和后一个元素的索引等等

11.线程安全的集合

Vector

Vector是对ArrayList线程安全的实现,在实现方式上没有啥区别,ArrayList实现方式见本文第六条,Vector主要就是加了synchronized关键字。 Vector存在的问题:

  • 他的add()、get()方法都会加锁,因此会发生读读互斥
  • 线程A在1下标添加元素,线程B同时在2下标添加元素,也会发成互斥。 因此Vector锁的的效率比较低。
HashTable

HashTable是对HashMap线程安全的实现,实现方式上没有区别,HashMap的实现方式见本文第二条,HashTable主要就是加了synchronized关键字。
HashTable与Vector存在的问题的问题一致,都是效率比较低

为了解决Vector集合和HashTable集合效率低下的问题,我们在选取线程安全的集合时一般会选择CopyOnWriteArrayList集合和ConcurrentHashMap集合,它的锁的粒度相较于Vector和HashTable更小,因此能够高效率的解决Vector和HashTable所存在的问题。

ConcurrentHashMap(线程安全的)的实现原理

ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。
ConcurrentHashMap与HashMap的底层数据结构一样,都是【数组+链表或红黑树】实现的。 通过CAS + synchronized关键字保证线程安全,CAS可以理解为一个乐观锁

CopyOnWriteArrayList(线程安全的)的实现原理

CopyOnWriteArrayList是ArrayList的线程安全版本,与ArrayList的底层数据结构一致。是一种读写分离的并发策略,这种容器称为“写时复制器”。
CopyOnWriteArrayList允许并发读(读不上锁)。写操作时会加锁,首先将当前容器复制一份副本,再副本中执行写操作,结束后将原容器的引用指向副本。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wQBIWZWC-1653378963970)(img.png)]
因此CopyOnWriteArrayList适合读多写少的操作,因为每次写入都需要复制一个副本,频繁写操作会频繁的GC回收。

12.并行与并发的区别

  • 并行是指多个事件在同一时间发生,并发是多个事件在同一时间【间隔】发生
  • 并行是在不同实体的多个事件,并发是在同一实体上的多个事件

13.线程与进程

进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程最少有一个线程。进程拥有独立的内存,不和其他的进程共享内存,而进程下的多个线程,共享内存资源。
线程是进程的一个实体,是CPU调度和分派的基本单位,是比程序更小的能独立运行的基本单位。 同一个进程中的多个线程之间可以【并发】执行

14.java创建进程的三种方式

  • 继承Thread类,重写run()方法,调用start()方法就可以启动一个线程。
  • 实现Runnable接口,实现run()方法(无返回值),创建Runnable接口实现类的实例对象,将其放入Thread类的构建方法的入参获取Thread实例,调用Thread实例的start()方法启动线程。
  • 实现Callable接口,实现call()方法(存在返回值),创建Callable接口的实现类对象,将其放入FutureTask类的构建方法的入参获取FutureTask实例,调用FutureTask实例的run()方法启动线程。
实现Runnable接口案例
class MyThread implements Runnable {
    private String name;

    public MyThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println(this.name + "==>" + i);
        }
    }
}

public class TestDemo {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread("ThreadA");
        MyThread thread2 = new MyThread("ThreadB");
        MyThread thread3 = new MyThread("ThreadC");

        new Thread(thread1).start();
        new Thread(thread2).start();
        new Thread(thread3).start();
    }
}
实现Callable接口
public class MyCallable implements Callable<String> {

    private int age;

    public MyCallable(int age) {
        super();
        this.age = age;
    }

    public String call() throws Exception {
        Thread.sleep(8000);
        return "返回值 年龄是:" + age;
    }

}

public class Run {

    public static void main(String[] args) throws InterruptedException {
        try {
            MyCallable callable = new MyCallable(100);

            ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 5L,
                    TimeUnit.SECONDS, new LinkedBlockingDeque());
            //这里使用线程池来运行call()方法里的内容
            Future<String> future = executor.submit(callable);
            System.out.println("main A " + System.currentTimeMillis());
            System.out.println(future.get());
            System.out.println("main B " + System.currentTimeMillis());
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

14.Runnable与Callable的区别

  • Runnable的返回值是void,并且重写的是run()方法
  • Callable允许有返回值,通过FutureTask或Future获取返回值,并且重写的是Call()方法

15.线程的物种状态

  • 创建状态:创建了线程对象,没有调用start()方法
  • 就绪状态:调用start()方法后,线程调度程序没有将本线程设置为当前运行线程,或在等待、睡眠回来后,均为就绪状态
  • 运行状态:线程调度程序将本线程设置为当前运行线程,开始运行run()方法里的内容
  • 阻塞状态:线程在运行时被暂停,比如运行了seep()、wait()方法
  • 死亡状态:run()方法执行结束或者调用了stop()方法,线程就死亡了,死亡的线程无法再次通过start()方法进入就绪状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-phkoKK38-1653378963971)(img_1.png)]

16. sleep()和wait()的区别

  • sleep()方法是Thread类的静态方法,让线程进入睡眠状态,让出执行机会给其他线程,但是如果在synchronized块中调用sleep()方法,是不会释放资源的,其他线程仍然无法访问。
  • wait()方法是Object类的方法,线程运行wait()方法时,该线程进入到一个【和该对象相关的等待池】,并且释放锁,使其他线程能够访问。通过notify()或notifyAll()方法唤醒等待的线程。

17.notify()和notifyAll()的区别

  • 如果线程调用了某个对象的wait()方法,该线程会进入到【该对象的等待池】里,等待池里的对象不会竞争该对象的锁
  • 有线程调用了对象的notify()方法,则会随机唤醒一个wait线程,调用notifyAll()方法则会唤醒所有wait线程。
  • 被唤醒的线程会进入该对象的【锁池】,【锁池】里的线程会取竞争该对象的锁。
  • 优先级高的线程竞争到【对象锁】的改率高,如果线程没有竞争到【对象锁】,则会继续留在【锁池】。如果线程调用wait()方法,则回到【该对象的等待池】里
  • 竞争到【对象锁】的线程会继续执行,运行完synchronized代码块,会释放掉【对象锁】,【锁池】里的线程会继续竞争

18.run()和start()的区别

  • run()方法是线程体,包含了该线程所有需要执行的事情。
  • start()方法是用来启动一个线程的,启动后不一定会立即执行run()方法,需要等待【线程调度程序】将本线程设置为【当前运行线程】时,会执行run()方法

19.创建线程池的四种方式

Executors.newFixedThreadPool(int nThreads)

  • 创建一个可以重用固定长度的线程池,提交一个任务创建一个线程,到达最大数量时规模不再变化。当发生未预期的错误导致线程结束,线程池会补充一个新的线程。
  • 使用共享的无界队列方式来运行这些线程,当任务数量大于线程池容量,会存放在一个无边界队列中,等待线程执行。任务不要一口气塞入太多,小心内存溢出。

Executors.newCachedThreadPool()

  • 创建一个可缓存的线程池,若线程池的规模超过了处理需求,则回收空闲线程,当需求增加时,自动添加新的线程
  • 对于线程池的规模不做限制

Executors.newSingleThreadExecutor()

  • 创建一个单线程的线程池,可以保证任务在队列中按照顺序串行执行
  • 如果这个线程异常结束,会创建一个新线程来代替他

Executors.newScheduledThreadPool(int nThreads)

  • 创建一个固定长度的线程池,延迟或定时的方式来执行任务,类似于Timer
public class ThreadPoolTest {
    public static void main(String[] args) {
        //创建一个可重用,固定线程数的线程池 容量为 2
        ExecutorService pool = Executors.newFixedThreadPool(2);

        //创建一个可缓存的线程池,可根据需要创建新线程的线程池.旧的线程可用时将重用他们.对短期异步的程序,可提高程序性能
        pool = Executors.newCachedThreadPool();

        //创建一个单线程池,只有一个线程,线程任务保证串行运行。可以在旧的线程挂掉之后,重新启动一个新的线程来替代它。
        pool = Executors.newSingleThreadExecutor();

        //创建一个固定长度的线程池,容量为4,给定一个延迟后,可以运行命令或者定期执行,类似于Timer
        pool = Executors.newScheduledThreadPool(4);

        //线程池添加任务
        FixedDemo demo = new FixedDemo();
        FixedDemo demo2 = new FixedDemo();
        FixedDemo demo3 = new FixedDemo();
        pool.execute(demo);
        pool.execute(demo2);
        pool.execute(demo3);

        //手动关闭线程池
        pool.shutdown();
    }
}

class FixedDemo extends Thread {
    @Override
    public void run() {
        System.out.println("我是一个线程");
        System.out.println("[currentThread = ]" + Thread.currentThread().getName());
    }
}

20.线程池中submit()execute()方法有啥区别

submit()方法有返回值,可以获取Callable接口实现类返回的值。execute()方法无返回值。

21.java程序中如何保证多线程运行安全

  • 原子性:提供互斥访问,保证当前只有一个线程可以对数据进行操作(atomic,synchronized)
  • 可见性:一个线程对内存的修改可以及时的被其他线程看到(synchronized,volatile)
  • 有序性:一个线程观察其他线程中的指令顺序,由于指令重排序,观察结果一般是无序的,使用【Happens-Before】解决有序性问题

22.什么是死锁,如何防止

死锁是多个进程在运行时,竞争资源或彼此通信,导致的一种阻塞现象,没有外力干涉,则一直阻塞下去。

死锁的必要条件
  • 互斥条件:进程对分配到的资源不允许其他进程访问,其他进程要访问该资源,只能等待占用资源的线程释放后。
  • 请求和保持条件:进程获得一定资源后,又对其他资源发起请求,但是请求的资源被其他线程占用,此时请求阻塞,但又不会释放已占用的资源。
  • 不可剥夺条件:进程获得资源并未使用完毕,不可被剥夺,只能等待该进程使用完毕后释放。
  • 环路等待条件:进程发生死锁后,多个进程之间形成头尾相接的循环等待资源关系。

这四个条件是死锁的必要条件,有一条不满足就不会发生死锁。 有一个特殊情况,使用【Lock】上锁,发生了异常,并且没有在finally里进行unlock()释放锁,也会造成死锁。

23.ThreadLocal是什么,有哪些使用场景

ThreadLocal用于支持线程局部变量,归属线程自身所有,不在多个线程之间共享。
在管理环境下(比如web服务器),使用线程局部变量要谨慎,在这种环境下,工作线程的生命周期比任何应用变量的生命周期都要长,一旦线程局部变量在工作完成后没有释放,会存在内存泄露的问题。

24.synchronized 底层实现原理

synchronized可以保证方法或者代码块在运行时,同时只能有一个线程进入临界区,还保证了共享变量的【可见性】(本文章的21条有提到) Java里的每一个对象都可以作为锁,这是synchronized实现同步的基础

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前的Class对象
  • 同步方法块(就是一个代码块),锁是代码块里的对象

25.synchronized 和 Lock 有什么区别

  • synchronized是java的保留关键字,Lock是一个java类
  • synchronized无法判断是否获取到锁,Lock可以判断是否获取到锁
  • synchronized可以自动释放锁,正常执行完成会释放,发生异常导致未正常结束也会释放。Lock一定是需要在finally里调用unlock()进行手动释放。
  • synchronized上锁后,其他线程想要获取锁,就必须等待,这个等待是没有时间上限的,Lock上锁后,其他线程要获得锁,不一定会一直等待,尝试获取不到锁,线程可以不用一直等待就结束了。
  • synchronized的锁可重入、不可中断、非公平锁,Lock锁可重入、可判断、可公平
  • Lock适合大量同步的代码时使用,synchronized适合少量同步代码时使用

26.反射是什么

反射是程序可以【访问、检测、修改】本身状态或者行为的能力。对任意一个类,Java的反射机制提供如下功能:

  • 运行时判断任意一个对象的所属类
  • 运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具备的成员变量和方法
  • 在运行时调用任意一个对象的方法

27.什么是java序列号,什么情况下需要序列化

为了保存内存中对象的状态(实例变量),并且可以将保存的对象状态再读取出来,Java提供了序列化作为解决方案。 以下几种情况适合使用序列化

  • 将内存中的对象保存到文件中或者数据库中
  • 需要将对象通过API接口或者RPC调用传输给其它系统时
  • 通过RMI传输对象时

28.什么是代理模式,动态代理、静态代理又是什么。

为其他对象提供一个代理以控制对某个对象的访问。代理类主要负责为委托了(真实对象)预处理消息、过滤消息、传递消息给委托类,代理类不现实具体服务,而是利用委托类来完成服务,并将执行结果封装处理。
其实就是代理类为被代理类预处理消息、过滤消息并在此之后将消息转发给被代理类,之后还能进行消息的后置处理。代理类和被代理类通常会存在关联关系(即上面提到的持有的被带离对象的引用),代理类本身不实现服务,而是通过调用被代理类中的方法来提供服务。
代理模式可以理解为将真实处理事件的对象封装起来,在外层套一个壳子,为其过滤参数、记录日志、传递参数等等。

  1. 静态代理:创建一个接口,然后创建被代理的类实现该接口并且实现该接口中的抽象方法。之后再创建一个代理类,同时使其也实现这个接口。在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。例如:
//接口
public interface HelloInterface {
    void sayHello();
}

//被代理类
public class Hello implements HelloInterface {
    @Override
    public void sayHello() {
        System.out.println("Hello zhanghao!");
    }
}

//代理类
public class HelloProxy implements HelloInterface {
    private HelloInterface helloInterface = new Hello();

    @Override
    public void sayHello() {
        System.out.println("Before invoke sayHello");
        helloInterface.sayHello();
        System.out.println("After invoke sayHello");
    }
}
  1. 动态代理:利用反射机制再运行的时候创建代理类,如果你有多个类需要被代理,并且代理的目的一致,则可以只编写一次代理规则,然后应用在所有的被代理类上,例如:
//接口、被代理类不变,构建一个handler类来实现InvocationHandler接口
public class ProxyHandler implements InvocationHandler {
    private Object object;

    public ProxyHandler(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before invoke " + method.getName());
        method.invoke(object, args);
        System.out.println("After invoke " + method.getName());
        return null;
    }
}

/**
 执行动态代理,输出:
 Before invoke sayHello
 Hello zhanghao!
 After invoke sayHello
 */
public class Test {
    public static void main(String[] args) {
        System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        //创建被代理对象
        HelloInterface hello = new Hello();
        //创建动态代理的模板
        InvocationHandler handler = new ProxyHandler(hello);
        //生成代理类的对象,并且调用方法
        HelloInterface proxyHello = (HelloInterface) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), handler);
        proxyHello.sayHello();
    }
}

29.对象拷贝的方式,为什么要使用对象拷贝

要对某一个对象进行处理,并且还想保留该对象处理之前的状态,则需要使用拷贝,有如下两种方式可以实现拷贝:

  • 实现Serializable接口,通过对象的序列化与反序列化实现深拷贝。
  • 实现Cloneable接口并重写Object类中的clone()方法。

30.深拷贝和浅拷贝

  • 深拷贝:将对象复制一份,两个对象修改其中任意值,另一个的值不会发生改变。
  • 浅拷贝:只是复制了的对象的引用地址,两个对象指向同一个内存地址,两个对象修改其中任意值,另一个的值也会一起改变。

31.throw与throws的区别

throw用于抛出一个异常,throws用于声明这个方法有可能会抛出异常

32.final、finally、finalize的区别

  • final用于修饰变量、方法、类:修饰变量时该变量无法被改变,是一个常量;修饰方法时该方法无法被重写;修饰类时该类无法被继承。
  • finally用于try-catch块里,处理异常的时候,将一定要必须执行的代码放在finally块,一般存放关闭资源的代码,防止内存泄露。
    +finalize是一个Object类的方法,一般由垃圾回收器来调用。当手动调用System.gc()时,有垃圾回收器调用finalize()来进行垃圾回收。

32.try-catch-finally中哪一个代码块可以被省略?

catch块可以被省略。
严格的说try只是适合处理【运行时异常】,try+catch适合处理【运行时异常、一般异常】。即:如果只用try,不加catch,处理【一般异常】是无法编译成功的。
finally在理论上也是可以不写,但是需要进行对资源的释放,防止内存泄露

33.在try-catch-finally中,如果catch中执行return了,finally还会被执行吗?

finally依然会执行,会在return前执行。如果finally块中包含return,则以finally块返回的为准。

public class Test{
    public static void main(String[] args) {
        /**
         * 最终输出如下两行
         * finally执行!
         * 30
         */
        System.out.println(getInt());

        /**
         * 最终输出如下两行
         * finally执行!
         * 40
         */
        System.out.println(getInt2());
    }

    public static int getInt() {
        int a = 10;
        try {
            System.out.println(a / 0);
            a = 20;
        } catch (ArithmeticException e) {
            a = 30;
            return a;
            /*
             * return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
             * 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
             * 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
             */
        } finally {
            System.out.println("finally执行!");
            a = 40;
        }
        return a;
    }

    public static int getInt2() {
        int a = 10;
        try {
            System.out.println(a / 0);
            a = 20;
        } catch (ArithmeticException e) {
            a = 30;
            return a;
            /*
             * return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
             * 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
             * 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
             */
        } finally {
            a = 40;
            return a; //如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40
        }
    }
}

34.常见的异常类

  • NullPointerException:当应用程序试图访问空对象时,则抛出该异常。
  • SQLException:提供关于数据库访问错误的异常。
  • IndexOutOfBoundsException:指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
  • IOException:当发生某种I/O异常时,抛出此异常。此类是失败或中断的I/O操作生成的异常的通用类。
  • FileNotFoundException:当试图打开指定路径名表示的文件失败时,抛出此异常。
  • NumberFormatException:当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。

35.运行时异常与一般异常的区别

浅的来说【一般异常】是由【非java程序本身】导致的错误,【运行时异常】是【java程序本身】导致的错误。
【IO异常、SQL处理异常】等属于典型的【一般异常】,比如【空指针异常、数组越界异常】等属于典型的【运行时异常】

下面就会说的深一点:
Java由两个错误处理的类Error和Exception,他们都是Throwable的子类。

  • Error类:标识JVM检测到的无法预期的错误,属于JVM层次的严重错误,导致JVM无法继续允许了,这种错误无法捕捉到,也无法恢复,只能是打印错误信息,出现了这种错误,只能尽力让Java程序安全结束,其他的啥也做不了。
  • Exception类:标识可以恢复的意外,是可以捕捉到的,Java提供了两个主要的异常【runtime exception 运行时异常】和【checked exception 一般异常】。

checked exception异常(一般异常)Java要求我们必须写try-catch处理,否则编译器死给你看。checked exception一般都是外部错误,并不是程序本身的错误,是应用环境中出现的外部错误,例如IO错误。

runtime exception异常(运行时异常),我们可以不处理,当出现这类异常,一般都是虚拟机接管,比如常见的空指针异常,一般是程序员的编码逻辑错误。
当出现运行时异常时,Java会一直往上抛,直到遇到处理代码,如果没有遇到则分两种情况。如果是多线程由Thread.run()抛出,如果单线程由main()抛出。
如果是主线程抛出的,那么这个Java程序也就结束了,如果不想这样,那只能是用try-catch包起来。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值