工作笔记

Java面试

100.谈谈单体架构和微服务架构的区别?一般依据怎样的原则进行微服务的拆分?

这是一道高频面试题,下面来分析如何回答该问题.

首先,微服务架构并非就一定比单体架构好,每个架构都有其适用的场景。

第一,单体架构适用的环境

单体架构特别适合初创公司的初创项目,可以小成本快速试错,且系统模块之间的调用,是进程内的通信,整体的性能会非常好,这类型的项目,推荐采用单体架构足矣,在市场还没有打开之前,采用各类看似高大上的技术,除非是为了卖弄技术,否则来说毫无意义。

做产品,需要考虑MVP模式,架构除了考虑技术,更应该考虑成本,成本意识是很关键的。

第二,我们来看看,微服务架构适合的场景

当系统经过一段时间的运营之后,如果运气不错,用户量有了一定的增量,业务也随着市场需求有了扩展,从而慢慢的整个系统的业务变得复杂而庞大,这个时候一个系统的启动时间,重新编译的时间,都可能会非常耗时,一个功能的修改也需要做全盘的回归测试,所谓牵一发而动全身,这个时候就适合对系统进行服务拆分,拆分成多个服务子系统,每个子系统可以更灵活做升级。但是要注意!此时原先的模块之间的通信,由原先的进程内通信变为进程间的通信,所以其响应速度会有所影响。

我们再来看看,微服务拆分的原则

一般我们根据业务的边界来拆分,比如按照商品,购物车,订单等等业务边界进行服务的拆分,另外一个,系统中存在的共性基础服务,比如短信,邮件,日志等等;我们也可以作为单独的服务进行拆分,作为基础服务层供上层服务复用。

97.关于synchronized的底层原理

synchronized是由一对monitorenter和monitorexit指令来实现同步的,在JDK6之前,monitor的实现是依靠操作系统内部互斥锁来实现的,所以需要进行用户态和内核态的切换,所以此时的同步操作是一个重量级的操作,性能很低。

但是,JDK6带来了新的变化,提供了三种monitor的实现方式,分别是偏向锁、轻量级锁和重量级锁,即锁会先从偏向锁再根据情况逐步升级到轻量锁和重量级锁。
这就是锁升级
在锁对象的对象头里面有一个threadid字段,默认情况下为空,当第一次有线程访问时,则将该threadid设置为当前的线程id,我们称为让其获取偏向锁,当线程执行结束,则重新将threadid设置为空。

之后,如果线程再次进入的时候,会先判断threadid与该线程的id是否一致,如果一致,则可以获取该对象,如果不一致,则发生锁升级,从偏向锁升级为轻量级锁

轻量级锁的工作模式是通过自旋循环的方式来获取锁,看对方线程是否已经释放了锁,如果执行一定次数之后,还是没有获取到锁,则发生锁升级,从轻量级锁升级为重量级锁
使用锁升级的目的是为了减少锁带来的性能消耗。

通过反编译查看字节码,就可以看到相关的指令。

javap -verbose Test.class

源码:就是写了synchronized同步代码块控制线程安全

Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: aload_1
         9: dup
        10: astore_2
        11: monitorenter
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String 获得锁
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2
        21: monitorexit
        22: goto          30
        25: astore_3
        26: aload_2
        27: monitorexit
        28: aload_3
        29: athrow
        30: return

synchronized如何保证可见性呢

首先,我们需要知道可见性原理

两个线程保证变量信息的共享可见性需要经历以下的流程

线程A->本地内存A(共享变量副本)->主内存(共享变量)

如果有变更,需要将本地内存的变量写道==写到主内存,对方才可以获取到更新。
这个是预备知识。

那么,回头看问题,synchronized是怎样保证可见性的呢

就是当获取到锁之后,每次读取都是从主内存读取,当释放锁的时候,都会将本地内存信息写到主内存,从而实现可见性。

反射是什么,可以解决什么问题。

反射是指程序在运行状态中,
1.可以对任意一个类,都能获取到这个类的所有属性和方法。
2.对于任意一个对象,都可以调用它的任意一个方法和属性。

反射是一种能力

一种在程序运行时,动态获取当前类对象的所有属性和方法的能力,可以动态执行方法,给属性赋值等操作的能力。
Class代表的就是所有字节码对象的抽象,类
反射,让java程序具备动态性
这种动态获取类信息及调用对象方法的功能称为反射。

在java中,Class类就是关键API

public class Reflection {

    
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
        //1.以class对象为基础
        Class<?> clazz = Class.forName("com.hgz.reflection.Student");
        System.out.println(clazz);
        //2.类中每一部分,都有对应的类与之匹配
        //表示属性的类
        Field nameField =
                clazz.getField("name");
        //表示方法的类
        Method helloMethod = clazz.getDeclaredMethod("hello", String.class);
        //表示构造方法的类
        Constructor<?>[] constructors = clazz.getConstructors();
    }
}

这种能力带来很多好处,在我们的许多框架的背后实现上,都采用了反射的机制来实现动态效果。
框架提供的是一种编程的约定。
比如@Autowire就能实现自动注入
@Autowire
private IUserService userService;
注解的解析程序,来扫描当前的包下面有哪些属性加了这个注解,一旦有了这个注解,就要去容器里获取对应的类型的实现,然后给这个属性赋值。

互联网精选面试题

###怎样选择一个企业,什么样的企业有前途?
在一个上升的线做一个努力的点
技术方面和微服务,大数据,人工智能挂钩,都是好项目
业务方面和金融,教育,健康医疗挂钩

展示优势点,引导面试官问你准备好的问题!

公司采用什么样的开发模式?前后端如何对接?

前后端分离的模式,后端负责接口开发
接口文档(1.URL,2.请求方式,3.请求参数,4.返回参数,5.示例)
一般采用wagger生成技术文档

分布式篇

服务拆分的原则

1.抽取公共的基础服务
2.以业务为边界,拆分服务
3.要管理这么一堆服务,注册中心
4.服务之间需要通信,通信的方式:1.同步调用(调用方需要等待执行方的处理结果)Dubbo、SpringCloud;2.异步通知(调用方无需等待执行方的处理结果)

SpringCloud相关

SpringCloud是一系列主流框架的集合。
Spring并没有重复造轮子,只是将目前各个成熟、经得起考验的服务框架组合起来,经过封装之后,让用户以SpringBoot的风格进行开发,屏蔽了复杂的配置和实现原理,给用户提供的一套简单易懂、易部署、易维护的分布式系统开发工具集。

关于SpringCloud解决了什么问题

采用SpringBoot的开发便利性,实现了分布式系统基础设施的开发。
比如,服务注册与发现中心(注册中心),负载均衡,熔断,配置中心,网关,消息总线,链路监控等等。做到独立部署,可方便进行水平扩展,独立访问等特点。

SpringCloud常用组件及其功能

Eureka(尤里卡):基于REST服务的分布式中间件,主要用于服务的注册和发现
Feign(美[fen]):一个REST客户端,简化WebService客户端的开发
Ribbon:负载均衡框架,在微服务集群中为各个客户端的通信提供支持,主要实现中间层应用程序的负载均衡
Hystrix:容错框架,通过添加延迟阈值及容错的逻辑,帮助我我们控制分布式系统间组件的交互
Zuul,SpringCloud Gateway:服务网关,为微服务集群提供代理,过滤,路由等功能
SpringCloud Config:管理集群中的配置文件,统一配置中心
SpringCloud Sleuth:服务跟踪框架,可以跟Zipkin,Apache HTrace和ELK等数据分析,服务跟踪系统进行整合

93.Java垃圾回收机制

通常指的垃圾回收,就是说回收堆的内存。
创建的对象被保存在堆中,java虚拟机通过垃圾自动回收机制,简称GC,简化了程序员的工作。
在java中,可以调用System.gc()来表示要进行垃圾回收,不过不建议使用,因为使用之后,虽然不会立刻触发Full GC(堆内存全扫描),而是由虚拟机来决定执行时机,但是一旦执行,还是会停止所有的活动(stop the world),对应用影响很大。
一般来说建议,在一个对象不需要再被使用时,将其设置为null,这样GC虽然不会立即回收该对象的内存,但是会再下一次GC循环中被回收。
最后,提一下finalize()方法,它是在释放对象内存前,由GC调用,该方法有且进被调用一次,一般不建议重写该方法。

76.悲观锁、乐观锁的相关问题

1.悲观锁是利用数据库本身的锁机制来实现,会锁记录。
实现的方式:…
2.乐观锁是一种不锁记录的实现方式,采用**CAS(Compare and swap)**模式,使用version字段来作为判断依据。
每次对数据的更新操作,都会对version + 1,这样提交更新操作时,如果version的值已被更改,则更新失败。
3.乐观锁的实现选择version字段的原因:如果选择其他字段,比如业务字段store(库存),
那么就可能出现所谓的ABA问题。
在这里插入图片描述

synchronized和lock的区别

1.作用的位置不同
synchronized可以给方法,代码块加锁
lock只能给代码块加锁
2.锁的获取和释放机制不同
synchronized无需手动获取锁和释放锁,发生异常会自动解锁,不会出现死锁。
lock需要自己加锁和释放锁,如lock()和unlock(),如果忘记使用unlock(),则会出现死锁。
所以,一般我们会在finally里面使用unlock()。
补充:
//明确采用人工的方式来上锁
lock.lock();
//明确采用手工的方式来释放锁
lock.unlock();

synchronized修饰成员方法,线程获取的是当前调用该方法的对象实例的对象锁。
synchronized修饰静态方法时,默认的锁对象,当前类的class对象,比如User.class
synchronized修饰代码块时,可以自己来设置锁对象,比如
synchronized(this) {
//线程进入,就自动获取到锁
//线程执行结束,自动释放锁
}

事务的隔离级别

4个级别
READ UNCMMITTED 读未提交,脏读、不可重复读、幻读有可能发生。
READ COMMITTED 读已提交,可避免脏读的发生,但不可重复读、幻读有可能发生。
REPEATABLE READ 可重复读,可避免脏读、不可重复读的发生,但幻读有可能发生。
SERIALIZABLE 串行化,可避免脏读、不可重复读、幻读的发生,但性能会影响比较大。
特别说明:
幻读,是指在本地事务查询数据时只能看到3条,但是当执行更新时,却会更新4条,所以称为幻读。
在这里插入图片描述

数据库设计的三大范式及反范式

数据库的三大范式

第一范式:列不可分(确保每列保持原子性,数据表中的所有字段值都是不可分分解的原子值。
第二范式:要有主键。(确保表中的每列都和主键相关)
第三范式:不可存在传递依赖(确保每列都和主键直接相关而不是间接相关)
比如商品表里面关联商品类别表,那么只需要一个关联字段product_type_id即可,其他字段信息可以通过表关联查询得到。
如果商品表还存在一个商品类别名称字段,如product_type_name,那就属于存在传递依赖的情况,第三范式主要是从空间的角度来考虑,避免产生冗余信息,浪费磁盘空间

2.反范式设计:(第三范式)

为什么会有反范式设计?
原因一:提高查询效率(读多写少)
比如上述的描述中,显示商品信息时,经常需要伴随商品类别信息的展示,
所以这个时候,为了提高查询效率,可以通过冗余一个商品名称字段,这个可以将原先的表关联查询转换为单表查询。
原因二:保存历史快照信息
比如订单表,里面需要包含收货人的各项信息,如姓名,电话,地址等等,这些都属于历史快照,需要冗余保存起来,
不能通过保存用户地址ID去关联查询,因为用户的收货信息可能会在后期发生变更。

关于数据库的补充:InnoDB是MySQL的默认存储引擎。

**索引的解释:**索引是对数据库表的一列或者多列的值进行排序的一种结构,使用索引可以快速访问数据库表中的特定信息。
索引的优缺点:
优点:大大加快数据检索的速度。将随机I/O变成顺序I/O(因为B+树的叶子节点是连接在一起的)。加速表与表之间的连接。
缺点:从空间角度考虑:建立索引需要占用物理空间
从时间角度考虑:创建和维护索引都需要花费时间,例如对数据库进行增删改查都需要维护索引。
索引的数据结构:InnoDB引擎的索引类型默认为B+树索引。
事务的四大特性:原子性、一致性、隔离性、持久性。
在这里插入图片描述

序列化相关

序列化是为了保持对象在内存中的状态,并且可以把保存的对象状态再读出来。
什么时候会需要用到java序列化呢?
1.需要将内存的对象状态保存到文件中
2.需要通过socket通信进行对象传输时
3.我们将系统拆分成多个服务之后,服务之间传输对象,需要序列化。

48.描述Session跟Cookie的区别(重要)

1.存储位置不同

Session:服务端
Cookie:客户端

2.存储的数据格式不同

Session:value为对象,Object类型
Cookie:value为字符串,如果我们存储一个对象,这个时候,就需要将对象转换为JSON

3.存储的数据大小

Session:受服务器内存控制
Cookie:一般来说,最大为4k

4.生命周期不同

Session:服务器端控制,默认是30分钟,注意!当用户关闭了浏览器,session并不会消失。
Cookie:客户端控制,其实是客户端的一个文件,分两种情况:
1.默认的是会话级的cookie,这种随着浏览器的关闭而消失,比如保存sessionid的cookie
2.非会话级cookie,通过设置有效期来控制,比如“7天免登录”这种功能,就需要设置有效期,setMaxAge
cookie的其他配置
httpOnly=true:防止客户端的XSS攻击
path="/":访问路径
domain=" " :设置cookie的域名

5.cookie跟session之间的联系

http协议是一种无状态协议,服务器为了记住用户的状态,我们采用的是Session的机制
而Session机制背后的原理是,服务器会自动生成会话级的cookie来保存session的标识,如下图表示:
在这里插入图片描述

36.有关线程安全的理解

一个比较专业的回答:当多个线程访问同一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。
如何做到(线程安全)呢?
实现线程安全的方式有很多种,其中在源码中常见的方式是,采用synchronized关键字给代码块或方法加锁,比如StringBuffer
查看StringBuffer的源码:
在这里插入图片描述
那么,问题来了。如果开发中需要拼接字符串,使用StringBuilder还是StringBuffer?
场景一:
如果是多个线程访问同一个资源,那么就需要上锁,才能保证数据的安全性。
这个时候如果使用的是非线程安全的对象,比如StringBuilder,那么就需要借助外力,为其加上synchronized关键字。或者呢,直接一点,使用线程安全的对象SttringBuffer
场景二:
如果每个线程访问的是各自的资源,那么就不需要考虑线程安全的问题,所以此时就不需要再去考虑线程安全的问题。放心地使用非线程安全的对象,比如StringBuilder
例如,在方法中创建对象来实现字符串的拼接。
看使用场景,如果是在方法中使用,那么建议在方法中创建StringBuilder,这时候相当于是每个线程独立占有一个StringBuilder对象,不存在多线程共享一个资源的情况所以可以安全的使用,即使StringBuilder本身并不是线程安全的。

什么时候需要考虑线程安全

1.多个线程访问同一个资源
2.资源是有状态的,比如我们上述讲的字符串拼接,这个时候数据是会有变化的

3.==和equals的区别

= = 比较的是值
比较基本的数据类型,比较的是数值
比较引用类型:比较引用指向的值(地址)
equals:
默认比较的也是地址,因为这个方法的最初定义在Object上,默认的实现就是比较地址
自定义的类,如果需要比较的是内容,那么就要学String,重写equals方法
“= =”对比两个对象基于内存引用,如果两个对象的引用完全相同(指向同一个对象)时,“= =”操作将返回true,否则返回false。“==”如果两边是基本类型,就是比较数值是否相等。

String s1 = new String("zs");
String s2 = new String("zs");
System.out.println(s1 == s2);//false
String s3 = "zs";
String s4 = "zs";
System.out.println(s3 == s4);//true
System.out.println(s3 == s1);//false
String s5 = "zszs";
String s6 = s3+s4;
System.out.println(s5 == s6);//false
final String s7 = "zs";
final String s8 = "zs";
String s9 = s7+s8;
System.out.println(s5 == s9);//true 反编译工具可验证
final String s10 = s3+s4;
System.out.println(s5 == s10);//false

Java中如何支持正则表达式操作的?

Java中的String类提供了支持正则表达式操作的方法,在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。计算机处理的信息更多的时候不是数值而是字符串,正则表达式就是在进行字符串匹配和处理的时候最为强大的工具,绝大多数语言都提供了对正则表达式的支持。

Java和JavaScript

Java是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;JavaScript是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。

Java中如何跳出当前多重循环?

在最外层循环前加一个标记如A,然后用break A;可以跳出多重循环。

&与&&的区别?

&运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。

int和Integer区别?

Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是Integer,从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。

  • 原始类型: boolean,char,byte,short,int,long,float,double
  • 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
    如:
class AutoUnboxingTest {
    public static void main(String[] args) {
        Integer a = new Integer(3);
        Integer b = 3;                  // 将3自动装箱成Integer类型
        int c = 3;
        System.out.println(a == b);     // false 两个引用没有引用同一对象
        System.out.println(a == c);     // true a自动拆箱成int类型再和c比较
    }
}

String和StringBuffer区别

JAVA 平台提供了两个类:String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。这个String类提供了数值不可改变的字符串。而这个StringBuffer类提供的字符串进行修改。当你知道字符数据要改变的时候你就可以使用StringBuffer。典型地,你可以使用StringBuffers来动态构造字符数据。

大O符号(big-O notation)

大O符号描述了当数据结构里面的元素增加的时候,算法的规模或者是性能在最坏的场景下有多么好。大O符号可以对大量数据的性能给出一个很好的说明。大O符号也可以表示一个程序运行时所需要的渐进时间复杂度上界。

数组(Array)和列表(ArrayList)的相关问题

Array和ArrayList的不同点:
Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
Array大小是固定的,ArrayList的大小是动态变化的。
ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。
对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

值传递和引用传递相关问题

值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量.
引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。 所以对引用对象进行操作会同时改变原对象.
一般认为,java内的传递都是值传递.

Java支持的数据类型,自动拆装箱相关问题

Java语言支持的8种基本数据类型是:
byte 1个字节
short 2个字节
int 4个字节
long 8个字节
float 4个字节
double 8个字节
boolean 1位
char 2个字节
自动装箱是Java编译器在基本数据类型和对应的对象包装类型之间做的一个转化。比如:把int转化成Integer,double转化成Double,等等。反之就是自动拆箱。

一个十进制的数在内存中是以补码的形式存的。

lambda表达式相关问题

Lambda 表达式 − Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

Object若不重写hashCode()的话,hashCode()如何计算?

Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法直接返回对象的内存地址。

为什么重写equals还要重写hashcode?(需要重视,薄弱点!)

HashMap中,如果要比较key是否相等,要同时使用这两个函数!因为自定义的类的hashcode()方法继承于Object类,其hashcode码为默认的内存地址,这样即便有相同含义的两个对象,比较也是不相等的。HashMap中的比较key是这样的,先求出key的hashcode(),比较其值是否相等,若相等再比较equals(),若相等则认为他们是相等的。若equals()不相等则认为他们不相等。如果只重写hashcode()不重写equals()方法,当比较equals()时只是看他们是否为同一对象(即进行内存地址的比较),所以必定要两个方法一起重写。HashMap用来判断key是否相等的方法,其实是调用了HashSet判断加入元素 是否相等。重载hashCode()是为了对同一个key,能得到相同的Hash Code,这样HashMap就可以定位到我们指定的key上。重载equals()是为了向HashMap表明当前对象和key上所保存的对象是相等的,这样我们才真正地获得了这个key所对应的这个键值对。

Map相关问题

java为数据结构中的映射定义了一个接口java.util.Map;它有四个实现类,分别是HashMap Hashtable LinkedHashMap 和TreeMap。
Map主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值重复。
Hashmap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。 HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。
Hashtable与 HashMap类似,它继承自Dictionary类,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了 Hashtable在写入时会比较慢。
LinkedHashMap 是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

关键字相关问题

final:当用final修饰一个类时,表明这个类不能被继承。对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

hashCode()和equals()方法有什么联系

Java对象的equals方法和hashCode方法是这样规定的:
➀相等(相同)的对象必须具有相等的哈希码(或者散列码)。
➁如果两个对象的hashCode相同,它们并不一定相同。

Java中的概念,什么是构造函数、什么是构造函数重载?什么是复制构造函数?

当新对象被创建的时候,构造函数会被调用。每一个类都有构造函数。在程序员没有给类提供构造函数的情况下,Java编译器会为这个类创建一个默认的构造函数。
Java中构造函数重载和方法重载很相似。可以为一个类创建多个构造函数。每一个构造函数必须有它自己唯一的参数列表。
Java不支持像C++中那样的复制构造函数,这个不同点是因为如果你不自己写构造函数的情况下,Java不会创建默认的复制构造函数。

Java中的方法覆盖(Overriding)和方法重载(Overloading)

Java中的方法重载发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况。与此相对,方法覆盖是说子类重新定义了父类的方法。方法覆盖必须有相同的方法名,参数列表和返回类型。覆盖者可能不会限制它所覆盖的方法的访问。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值