Java后端面试基础部分

Java基础

1、对this和super的理解

调用结构:属性、对象、构造器
this调用属性、方法:表示当前对象或正在创建的对象
        调用构造器:表示调用调用当前类中其他构造器 
        this和super(默认)只能有一个,且在首行声明

2、OOP面向对象的理解

面向对象是利用语言对现实事物进行抽象,面向对象具有以下特征:
        封装:把数据和操作数据的方法绑定起来,对数据的访问只能通过已经定义的接口
        继承:从已有类得到继承信息创建新类的过程
                子类继承父类所有的属性和方法,可能受到封装性的影响无法直接使用
                不继承父类的构造器,只通过super(args)方式调用;this和super默认super
        多态:允许不同子类型对象同一消息做出不同响应
                前提:类的继承、方法的重写
                虚拟方法调用:编译看左边,运行看右边 属性不存在多态性 不能调用子类特有方法,调用需要向下转型

3、重载和重写的区别

 1、首先重载发生在本类,重写发生在父类与子类之间
 2、重写权限子类大于等于父类,重载无所谓
 3、重写的返回值类型、方法名及参数列表都要相同
 4、重载只保证方法名相同,参数列表一定不同

4、深拷贝与浅拷贝

数据分为基本数据类型和引用数据类型:

        基本数据类型:直接存储在栈(stack)中的数据

        引用数据类型:栈中存储对象的引用,真实数据存放在堆中

         浅拷贝:拷贝基本数据类型的值,以及某个对象的引用地址,而不会复制对象本身,新旧对象共享同一块内存

        深拷贝(clone):拷贝基本数据类型的值,会针对对象的引用地址所指向的对象进行复制,新对象和原对象不共享内存

        赋值与浅拷贝的区别:赋值没有产生新的对象,只是对原对象的引用

5、自动装箱与自动拆箱

自动装箱:基本类型数据——>包装类对象

自动拆箱:包装类对象——>基本类型数据

 为何自动拆装箱:List集合如果要放整形的话,只能放对象,不能是基本数据类型

6、==与equals的区别 

        equals只能比较引用数据类型的值,他的底层就是==,如果未被重写,比较的就是引用数据类型的地址,如果进行重写,往往比较的是对象的属性内容。

        ==对于基本数据类型比较值,引用数据类型比较地址。

7、Object中的方法 

clone(深拷贝)、equals、finalize(通知销毁)、getClass、hashCode、notify、notifyAll、wait、toString

8、JDK和JRE的区别

 JDK(Java Decelopment Kit) Java开发工具包

 JRE(Java Runtime Environment) Java运行环境

 JDK中包含JRE和开发工具集(比如javac编译工具),JDK中有一个名为jre的目录,里面包含两个文件夹bin和lib,bin就是JVM,lib就是JVM运行所需要的类库

Java中级

1、static关键字的理解

静态的        修饰:属性、方法、代码块、内部类        随着类的加载而加载 

2、final关键字的理解

修饰:类、方法、变量

类:不能被继承 String、System、StringBuffer

方法:不能被重写

常量:没有set方法,必须初始化(显式赋值、初始化块赋值、构造器中)

3、abstract关键字的理解

修饰:类、方法

抽象类:不能创建对象、只能被继承、子类必须重写父类抽象方法并提供方法体、有构造方法,供子类创建对象时初始化父类成员变量

4、接口

        特点:无构造器、无初始化块、如果实现类是抽象类,可以不重写接口方法

        成员列表:公共的静态常量公共抽象方法、1.8后 公共静态方法公共默认方法、1.9后 私有方法       

类优先原则

        当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法重名,子类就近选择执行父类的成员方法。

接口冲突(左右为难)

        当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,只能显式保留一个

 5、接口和抽象类的区别

        抽象类除了不能实例化外满足类的特征:单继承、有构造器、抽象方法不能是private、可以有成员变量

        接口需要被实现:多继承、无构造器、抽象方法只能是public、只有静态常量

6、注解

 四个元注解 

Retention生命周期 Source、class、Runtime

Target位置

Inherited允许子类继承父类注解

Documented是否被生成到api文档

7、实例变量的赋值顺序

 成员变量默认初始化——>显式初始化——>构造器初始化——>对象.属性

8、 包装类的缓冲对象

存储:方法区

Integer、Long、Byte、Short  -128~127

Character 0~127

Boolean true/false

Float、Double 无

相同包装类之间比较,包装类的缓冲对象

不同包装类和基本数据类型比较时先自动类型转换(自动拆箱+自动类型提升)

不同包装类之间不能比较 编译出错

9、String

 final不能被继承、Serializable接口、Comparable接口(对象能比较大小)

        jdk8声明为 private final char value[] 存储在char型的value常量数组中,一旦初始化,地址不可变 

        jdk9开始,为节省空间,存储在byte型的value常量数组

        因为字符串对象设计为不可变,用字符串常量池来保存常量对象。jdk7之前存储在方法区,jdk7及之后存储在堆空间

10、String的不可变性体现在哪里

        1、对字符串进行赋值时,需要重新指定一个字符串常量的位置进行赋值,不能在原位置进行修改

        2、对字符串进行拼接时,重新开辟一个空间保存,不能在原位置修改

11、new String("a")+new String("b")在内存中创建了几个对象 

 6个

字符串拼接会创建StringBuilder对象

常量池中对象的引用

常量+常量=常量

常量+变量=堆 

String s1="hello";常量池

String s2="world";常量池

String s3=s1+s2;变量+变量=堆

String s4="hello"+s2;常量+变量=堆

String s5="hello"+"world";常量+常量=常量池

12、字符串的可变序列 StringBuilder、StringBuffer

StringBuffer jdk1.0声明 线程安全 底层使用char[] jdk9开始使用byte[]

StringBuilder jdk5.0声明 线程不安全 底层使用char[] jdk9开始使用byte[]

StringBuilder sBuilder=new StringBuilder("abc")

创建长度为16+"abc".length长度的char型数组,添加元素时,从索引0时开始添加,容量满时,默认扩容为原容量 2倍+2,并将原有value数组中值复制过来

13、序列化 

序列化:Java对象从内存中保存到文件或通过网络传输出去(出内存

反序列化:文件数据或网络数据还原为内存中的Java对象 

14、IO流的分类 

按数据的流向分为:输入流和输出流

        输入流:数据从其他设备读入到内存的流,以InputStream或Reader结尾

        输出流:读出内存,以OutputStream或Writer结尾

按数据单位分为:字符流和字节流

        字符流:以字符为单位读写数据的流,一般读写文本文件Reader、Writer结尾

        字节流:以字节为单位读写数据,一般读写MP3、avi等文件,以InputStream或OutputStream结尾

按IO流的角色分为:节点流和处理流

        节点流:直接从数据源或目的地读取数据

        处理流:如BufferInputStream

15、throw和throws的区别

throw:作用在方法内,表示抛出具体异常,由方法体内的语句处理;一定抛出了异常;

throws:作用在方法的声明上,表示抛出异常,由调用者来进行异常处理;可能出现异常,不一定会发生异常;

16、try-catch-finally

输出先finally在try或者catch

从try到finally入栈执行

17、线程和进程的关系

        进程 process:程序的一次执行过程,或是正在内存中运行的应用程序。操作系统调度和分配资源的最小单位,程序是静态的,进程是动态的

        线程 thread:进程中一条执行路径 一个进程中至少有一个线程。为 CPU 调度和执行的最小单位 分时调度与抢占式调度

18、并发与并行的区别

        并行(parallel):多条指令同一时刻运行,宏观微观都是同一时刻运行

        并发(concurrency):多条指令同一时段运行,宏观上同一时刻,微观上依次执行

19、线程的创建方式

1、继承Thread类重写run方法(线程执行体)

2、实现Runable接口重写run方法

3、实现Callable接口

4、使用线程池

20、线程状态的转换

        新建new——>就绪Runable<——>运行Running——>死亡Dead

        新建——>就绪:start()启动线程

        就绪——>运行:run()获得cpu执行权        run()方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的CPU 调度决定

        运行——>就绪:失去cpu执行权或 yield()

        阻塞blocked:计时等待、锁阻塞、无限等待

                运行——>阻塞:suspend(挂起)、wait()、join(在A中通过B调用join,阻塞A直到B运行完成)、sleep()、等待同步锁

                阻塞——>就绪:resume(恢复)、wait时间到、notify()/notifyAll()、join线程结束、sleep时间到、获取同步锁

        运行——>死亡:run正常结束、出现Error或Exception、stop()

21、线程安全

同步代码块或同步方法

        synchronized(同步监视器){多个线程必须使用同一个同步监视器}

Lock同步锁ReentrantLock  lock与unlock方法

22、wait和sleep的区别

 sleep:属于Thread类、进入阻塞状态不释放锁、sleep时间结束

 wait:属于Object类、进入阻塞状态同时释放锁、wait时间到或notify

        wait必须配合synchronized使用,不然会IllegalMonitorStateException

23、死锁 

         不同的线程分别占用对方需要的同步资源不放弃 都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

诱发死锁的原因:

        互斥条件:基本无法破坏,线程需要互斥来保证安全

        占用且等待:一次性分配所有资源,就不存在等待的问题

        不可抢占:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放自身资源

        循环等待:将资源改为线性顺序,按序申请

Java高级

1、集合框架分类

单列集合Collection

        List有序的、可重复的数据:ArrayList、LinkedList、Vector

        Set无序的、不可重复的数据:HashSet-LinkedHashSet、TreeSet

双列集合Map

        HashMap-LinkedHashMap

        TreeMap

        Hashtable-Properties

        ConcurrentHashMap

List判断相等的要求:重写equals方法

向Set中添加元素的要求:重写equals和hashCode方法

2、ArrayList添加元素

 线程不安全、底层是object[]、查询 添加

* 增:
*      add(Object obj)
*      addAll(Collection coll)
*  删:
*      remove(Object obj)
*      remove(int index)
*  改:
*      set(int index, Object ele)
*  查:
*      get(int index)
*  插:
*      add(int index, Object ele)
*      addAll(int index, Collection eles)
*  长度:
*      size()
*  遍历:
*      iterator()
*      foreach
*      for

 jdk7版本时,new ArrayList<>()会在底层创建一个长度为10的object类型数组。在jdk8时,会采用动态创造数组的方式,在首次添加元素时初始化这个长度为10的数组。

 当前已使用长度size+1>数组长度,调用grow方法扩容,默认扩容为原来的1.5倍,并将原数组复制

3、new ArrayList(10)时扩容几次

指定了集合初始长度,并没有扩容 

4、数组与List之间的转换

数组——>List:jdk中Arrays工具类的asList方法

        修改数组,List受影响

List——> 数组:List的toArray方法

        修改List,数组不受影响,底层堆数组进行拷贝

5、LinkedList

双向链表 线程不安全 有序的、可重复的数据  

6、Vector

 List古老实现类 线程安全 底层Object[] 

创建对象时初始化长度为10的数组,扩容为原长度的2倍

7、HashSet添加元素

底层是HashMap(用HashMap的key来存储,value默认为object) 数组+单向链表+红黑树 用于过滤重复数据 非线程安全 集合元素可以是null

向HashSet添加元素的过程:

1、向HashSet添加元素时,调用hashCode方法得到该对象的hashCode值,根据hashCode值,通过某个函数得到该对象在底层数组中的存储位置; 

2、如果数组该位置没有元素,则直接添加

3、如果该位置有元素,比较其hashCode值,若不相等,通过链表的方式添加;

4、若两个元素的hashCode值相等,则调用equals方法比较,相等则添加失败,不相等则通过链表添加。

8、LinkedHashSet

HashSet的子类 底层:LinkedHashMap 数组+单向链表+红黑树+双向链表(记录元素的先后顺序)

9、TreeSet

添加相同类型的元素 不用考虑重写equals和hashCode方法,比较元素相等的标准是自然排序或定制排序(compareTo或compare的返回值) 

10、Set无序性的理解

 无序性不等同于随机性,不是指添加元素顺序与遍历元素顺序的不一致。无序性指set元素位置不像ArrayList中依次紧密排列,set是根据hashCode值,进行排列,此位置不是依次排列的,表现为无序性。

11、HashMap理解

线程不安全 key和value可以为null 数组(key的二次哈希+存储值)+单向链表(解决哈希冲突+存储值)+红黑树(提高性能+存储值)

HashMap.Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V>{

        final int hash;

        final K key;

        V value;

        HashMap.Node<K,V> next;

}

        key不可重复、无序        所有的key构成一个set集合(重写equals和hashCode)遍历:Set keySet() 

        value可重复、无序        所有的value构成一个Collection集合(重写equals) 遍历:Collection values()

        HashMap和key-value构成一个Entry,所有的Entry为一个Set集合 遍历:Set entrySet()

*  增:
*      put(key,value)
*      putAll(Map)
*  删:
*      Object(value) remove(key)
*  改:
*      put(key,value)
*      putAll(Map)
*  查:
*      get(key)
*  长度:
*      size()
*  遍历:
*     遍历key集:Set keySet()
*     遍历value集:Collection values()
*     遍历entry集:Set entrySet()

12、HashMap添加元素(put)的过程

在jdk7版本中,当new HashMap<>()时,底层会创建并初始化一个长度为16的Entry类型的数组,调用put方法添加元素时,会将key和value封装在一个Entry对象中:

        首先,调用key所在类的hashCode方法,得到哈希值1,此值右移16位,两个值进行异或运算得到哈希值2,哈希值经过IndexFor方法后得到数组索引i

        如果此位置i上没有元素,则直接添加

        如果此位置i上有元素,则判断两元素的hash值2:

                如果哈希值2不同,则通过单向链表添加(头插法)

                如果哈希值2相同,继续调用equals方法判断:

                        如果不同,则使用单向链表添加

                        如果相同,则认为两个key相同,用新的value替代原value

在jdk8版本中,主要有4个不同点:

        首先,在new HashMap<>()时没有初始化数组,而是首次添加元素时初始化

        第二,用Node数组代替Enty数组

        第三,链表节点插入采用尾插法

        第四,新增红黑树结构存储

寻址算法:

        调用key的hashCode方法得到哈希值1

        将哈希值1右移16位得到的值与哈希值1进行异或运算得到哈希值2

        将数组长度-1与哈希值2进行位于运算得到索引

13、为什么HashMap的数组长度一定是2的n次幂

hashMap底层计算数组索引使用的是位与运算,如果不是2的n次幂无法进行位于运算,同时位与运算比取模效率高

 扩容机制:

        当元素个数达到临界值(数组长度*0.75 默认12)时考虑扩容,默认扩容为原来2倍。

当单链表长度达到8并且数组长度大于64转变为红黑树

当红黑树节点小于6时退化为链表 

14、HashMap扩容时的死循环问题

线程一:读取当前hashMap数据,比如A、B,当准备扩容时,线程二介入,由于是头插法,线程二执行结束后变成了B的next是A,线程一再将A移入新的链表,再将B插到链头,由于线程二的介入,B的next是A,就造成了死循环

jdk8进行调整,采用尾插法,避免了死循环

15、LinkedHashMap

HashMap的子类、数组+单向链表+红黑树+双向链表(记录添加元素的顺序) 

16、TreeMap

红黑树 可以按照key-value中key的属性大小顺序进行遍历(compareTo) 

17、Hashtable和Properties

Hashtable:古老实现类、线程安全、不可以添加null和key或value、数组+单向链表 

Properties:Hashtable的子类、key和value都是String类型、常用于处理属性文件

18、Hashtable和HashMap的区别

        Hashtable:数组+链表 、线程安全、一次hash、扩容当前容量2倍+1、key和value不能是null

        HashMap:数组+单向链表+红黑树、线程不安全、两次hash、扩容为2倍、key和value可以是null

19、Iterable接口

 Collection 接口继承了 java.lang.Iterable 接口,该接口有一个 iterator()方法,那么所有实现了 Collection 接口的集合类都有一个 iterator()方法,用以返回一个实现了 Iterator接口的对象。

20、对Class类的理解

        针对编写的.java用javac.exe进行编辑,会生成一个或多个.class字节码文件,接着用java.exe命令进行解释运行。这个运行过程中,.class文件会被类的加载器加载到内存中,并存放到方法区,这些内存中一个个.class文件对应的结构为一个个Class实例。 

21、获取Class的四种方式

1、运行时类的静态属性class

2、运行时类的对象的getClass方法

3、Class的静态方法forName(类的全类名)

4、 类的加载器

22、类的生命周期

        类在内存中完整的生命周期为:加载——>使用——>卸载。其中加载,分为装载、链接,初始化三个阶段。

        装载:将.class文件读入内存,使用类的加载器为之创建一个Class对象

        链接分为三个阶段:

                验证:确保加载的类的信息符合JVM规范(以cafebabe开头)

                准备:为类变量(static)分配内存并初始化默认值

                初始化:类中的符号引用(常量名)替换为直接引用(地址)

        初始化:执行类的构造器<clinit>方法的过程,同时对静态变量,静态代码块初始化

        

        使用:JVM开始从入口方法执行用户代码

        卸载:Class对象的销毁,最后JVM退出内存        

23、类的加载器

        启动类加载器(BootStrap ClassLoader):由C/C++实现,加载Java核心库

        扩展类加载器(ExtClassLoader):由Java语言编写,是ClassLoader的子类

        应用类加载器(AppClassLoader):ClassLoader的子类,加载classpath下的类,也就是开发者编写的类

        自定义类加载器:需要开发者继承ClassLoader类,如tomcat、spring等框架的内部实现

24、类的加载器的作用

 将.class字节码文件加载到内存中,在堆中生成java.lang.Class文件,作为方法区中类数据访问入口。 

25、双亲委派机制

        类的加载器体系并不是继承关系,而是委派体系。

        当类的加载器接收到加载类的请求时,首先不会自己尝试加载这个类,而是委托自己的父类加载器完成,直到启动类加载器,然后向下加载。

原因:

        1、避免某个类被重复加载(父类已经加载的不许重复加载)

         2、保证类库API不会被修改

26、Java内存解析 

堆heap:存放对象实例

栈stack:存储局部变量、基本数据类型、对象引用

方法区Method Area:被虚拟机加载的类信息、即时编译器编译后的代码、常量、静态变量

MySQL

1、SQL

select 字段列表 from tableName where 条件 group by 分组字段 having 分组后条件 order by排序 limit分页参数

        聚合函数:
            count、max、min、avg、sum
            聚合函数不对null进行计算
        where和having的区别:
            执行时机不同:where是分组之前的过滤,不满足where条件,不参与分组,而having、是分组之后对结果进行过滤
            判断条件不同:where不能对聚合函数进行判断,而having可以

select语句执行的顺序

 from子句、where子句、group by分组子句、聚集函数、having子句、select的字段、order by排序

2、MySQL事务的特性(ACID)

        原子性:事务是不可分割的最小单元,要么全部成功,要么全部失败

        一致性:事务完成时,所有数据都保持一致(A向B转账,A减款B赠款)

        隔离性:事务在不受外部并发操作影响的独立环境下运行(A向B转账,不能受其他事务影响)

        持久性:一旦提交或回滚,对数据的影响是永久的

3、事务隔离级别

解决并发事务带来的问题:

        读未提交read-uncommitted                         脏读 不可重复读 幻读

        读已提交read-committed                                      不可重复读 幻读

        可重复读repeatable-read(MySQL默认)                               幻读

        串行化serializable(一个事务提交后才能进行另一个事务)

并发事务带来的问题:        

        脏读:事务A正在访问数据并对数据进行修改,而修改并未提交,此时事务B访问了这个数据,此数据为脏数据(事务读到了另一个事务没有提交的数据

        不可重复读:事务A多次读同一数据,事务B在A读取过程中对数据进行更新并提交,导致事务A前后读取的数据不一致(一个事务先后读取同一条记录,但两次读取的数据不同

        幻读:一个事务按条件查询数据时,没有对应数据行,但在插入数据时,又发现这条数据已经存在

4、如何定位慢查询

慢查询指的是sql响应速度慢

        在调试阶段, 可以开启MySQL自带的慢日志查询功能,只要sql执行时间超过设置的阈值,就会记录到日志文件中,我们就可以找到慢sql了

        也可以使用一些监控工具,比如阿尔萨斯、Skywalking,可以看到哪个接口比较慢,能够看到sql执行时间,从而定位sql

开启mysql慢日志查询开关

slow_query_log=1

设置慢日志时间2s,超过2s就会记录

long_query_time=2 

5、SQL执行很慢,如何优化

找到SQL慢的原因:使用explain执行计划

passible key当前sql可能会用到的索引
key当前sql实际命中的索引
key_len索引占用大小
Extra

额外的优化建议

Using where;Using Index查找使用了索引,需要的数据都在索引列中找到,不需要回表查询

Using index condition查找使用了索引,但需要回表查询

type

sql连接的类型,性能由好到坏:

null sql执行没有使用到表

system 查询系统中的表

const 根据主键或唯一索引查询

eq_ref 多表联查使用主键或唯一索引

ref 索引查询

range 范围查询

index 索引树扫描

all 全盘扫描

可以使用MySQL自带的explain分析工具:

1、通过key和key_len检查命中索引的情况

2、通过type字段查看是否有进一步优化空间(const、ref、range、index)

3、通过extra字段查看是否存在回表查询

6、索引

索引(index)是帮助MySQL高效获取数据的有序的数据结构。

B+树的特点

        1、所有元素存储在叶子节点

        2、非叶子节点存储索引

        3、叶子节点形成一个单向链表,同时MySQL对B+树进行优化,将叶子节点添加一对指针,升级为双向链表。

B+树的优势 

        1、相较于二叉树,层级更少,搜索效率高

        2、相较于B树,由于B树的叶子节点和非叶子节点都会存储数据,这样导致一叶中存储的key和指针减少,只能增加树的高度,性能降低

        3、相较于hash索引,B+树支持范围匹配及排序操作

7、聚集索引和二级索引

索引分类:

        一般分为主键索引、唯一索引、常规索引、全文索引。

        在InnoDB存储引擎中,根据索引的存储形式,分为:聚集索引和二级索引

        根据关联的字段数量分为:单列索引和联合索引

聚集索引:

        将数据和索引存储在一起,B+树的叶子节点存储了行数据,有且只有一个。

二级索引:

        将数据和索引分别存储,B+树的叶子节点存储对应的主键,可以有多个,除了聚集索引,其他的都是二级索引

聚集索引选取规则:

        如果存在主键,主键索引就是聚集索引

        如果不存在主键,第一个唯一索引就是聚集索引

        如果都不会存在,InnoDB会自动生成一个rowid作为隐藏的聚集索引

什么是回表查询:

        通过二级索引找到对应的主键值,通过主键值在聚集索引中找到整行数据

 8、覆盖索引

覆盖索引是指查询使用了索引,并且需要返回的列在该索引中能全部找到(无需回表查询的)

比如:id是主键 name是普通索引
select * from user where id=1;
select id,name from user where name='lili';

9、Limit优化

当limit 1000000,10时,MySQL会先排序1000010行,扔掉前面的1000000行,返回后面的10行,这个过程会有大量数据进出内存,导致排序时间长。使用覆盖索引+子查询,只让id进出内存,减少时间。

select * from user u1,(select id from user order by id limit 1000000,10)u2 where u1.id=u2.id;

10、创建索引的原则

        1、针对于数据量较大,且查询比较频繁的表建立索引。单表超过10万数据(增加用户体验)

        2、针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引

        3、尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引效率越高。

        4、如果是字符串类型的索引,字段长度较长,可以建立前缀索引

        5、尽量使用联合索引,联合索引很多时候可以覆盖索引

        6、控制索引数量,会影响增删改效率

11、索引失效的情况

1、联合索引违反了最左前缀法则

        联合索引做左边的列必须存在,如果跳过了某一列,后面的索引失效

2、联合索引中出现范围查询(> <),范围查询右侧索引失效

3、在索引上运算

4、字符串字段不加单引号,发生了类型转换

5、头部模糊匹配,索引失效,尾部模糊匹配不失效

6、or连接的条件,一侧有索引,一侧没有索引

7、查询条件是两个单列索引用and连接,后面索引失效

8、数据分布影响,如果MySQL评估使用索引比全表查询慢,则不使用索引

12、SQL优化的经验

表的设计优化(阿里开发手册《嵩山版》)

        1、设计比较合适的数值(tinyint int bigint)

        2、char定长效率高,varchar可变长度,效率低

SQL语句优化

        1、select语句尽量使用覆盖索引,避免使用select *

        2、避免索引失效

        3、用union all代替union union会多一次过滤,效率低

        4、能用内连接(join)尽量不使用左外连接(left join)或右外连接,如必须使用小表驱动大表

        内连接会对两个表进行优化,优先把小表放到外面,大表放到里面。left join和right join不会 

13、Redo Log和Undo Log 

redo log

        重做日志,记录的是事务提交时数据页的物理修改,用于实现事务的持久性

        当客户端发起了事务操作(update等),首先在缓冲池中查找,有没有需要操作(更新)的数据,没有的话有磁盘文件加载到缓冲池,接下来直接操作缓冲区中的数据,并将数据页的变化记录到redo log buffer中,当事务提交时,redo log buffer中的记录直接刷新到磁盘中。在择机将buffer pool中的脏数据刷新到磁盘中,如果此过程中出错,就用redo log file进行数据的恢复。

为什么每次提交时不直接将buffer pool中变更的数据刷新到磁盘,而是需要redo log?

答:性能问题 。redo log是日志文件,日志是追加的,是顺序磁盘io。而直接刷新脏数据是随机磁盘io,性能不好。

 undo log 

        回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚 和 MVCC(多版本并发控制)

redo log和undo log的区别:

1、redo log记录的是数据页的物理变化,服务宕机可用来同步数据

2、undo log记录的是逻辑日志,当事务回滚时,通过逆操作恢复原理的数据

3、redo log保证了事务的持久性,undo log保证了事务的原子性,两个共同保证了数据的一致性 

14、事务的隔离性如何保证

锁和MVCC

        MVCC解决多个事务并发的时候应该访问哪个版本 

        多版本并发控制,指维护一个数据的多个版本,使得读写操作没有冲突。MVCC的实现依赖于三个部分:表的隐藏字段、undo log日志、readView

        表的隐藏字段

                trx_id:事务id,记录每一次操作的事务id,是自增的

                roll_pointer:回滚指针,用于配合undo log,指向上一个版本

                roll_id:隐藏主键

        undo log回滚日志

                ①用于存储老版本数据

                ②当多个事务操作某一行记录,会导致该记录的undo log生成一条记录版本链(链表的头部是最新的旧纪录,尾部最早的旧纪录)

         readView:快照读sql查询时选择版本的问题

                根据readView的匹配规则判断该访问哪个版本的数据

                不同隔离级别快照读生成时机不同,访问结果不同:

                        rc:每次执行快照读生成新的ReadView

                        rr:仅在事务第一次select时生成readView

快照读:简单的select就是快照读,读取的是记录数据的可见版本,有可能是历史数据 

15、不同存储引擎之间的区别 

        存储引擎是存储数据、建立索引、更新和查询数据等技术的实现方式。存储引擎是基于表的,而不是基于库的 

InnoDB存储引擎

        支持事务、外键

        支持行级锁

        支持B+Tree索引结构

         逻辑存储结构:段页式存储(表空间、段、区、页、行)对应磁盘文件.ibd

MyISAM存储引擎

        不支持事务、外键

        支持表锁,不支持行锁

        支持B+Tree索引

Memory存储引擎

        存储在内存中,只能将这些表作为临时表或缓存

        默认hash索引,支持B+Tree

16、锁

协调多个进程或线程并发访问某一资源的机制

全局锁:锁定数据库中所有表

        应用场景:全库数据备份

表级锁:每次操作锁定整张表

        表锁:读锁和写锁

                读锁:客户端1加了读锁,客户端1及其他客户端都能读取,都不能写

                写锁:客户端1加了写锁,客户端1能够读写,其他客户端不能读写

        元数据锁:(自动加锁)为了避免DML(表数据增删改)与DDL(表结构的创建、修改、删除)冲突。(简单的说,crud时不能修改表结构)

        意向锁:为了避免DML(insert、delete、update)执行时,加的行锁与表锁的冲突,在InnoDB中引入意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查.

                分类:

                        意向共享锁(IS):由select...lock in share mode添加

                        意向排他锁(IX):由insert、delete、update添加

                兼容性与互斥性:

                        意向共享锁:与表锁读锁兼容,与表锁写锁互斥

                        意向排他锁:与表锁读锁、写锁都互斥。意向锁之间不会互斥。     

行级锁:每次操作锁定对应行数据,行锁是通过对索引上的索引项加锁实现的,而不是对记录加锁。分为行锁、间隙锁、临键锁

        行锁(Record Lock):锁定单行记录,防止其他事务进行update和delete。在rc、rr隔离级别下支持。解决了脏读、不可重复读。分为共享锁和排他锁

        间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在rr隔离级别下都支持。 

        临键锁(Next-Key Lock):行锁+间隙锁的结合(该行记录及左边间隙)。在rr隔离级别下支持

17、数据表设计的三大范式

        第一范式:列不可再分。例如:省市区在一列存储,就不满足

        第二范式:一张表只表达一层含义

        第三范式:表中每一列直接依赖于主键,而不是间接依赖。例如,如果一个订单表包含商品和商品价格,则商品价格是应该是和商品ID关联的,而不是和订单ID关联的。

数据库的设计范式和查询性能有时候是相悖的,我们需要根据实际情况做一些取舍:

        当查询频次相对不高的时候,可以提高设计范式

        当查询频次高时,更倾向于牺牲数据库的规范度,允许特定的冗余而提高性能

 Mybatis

1、Mybatis获取参数值的两种方式${}和#{}的区别

${} 本质是字符串的拼接,不会加 ''

#{} 本质是占位符赋值,会自动添加 '' ,可防止SQL注入 

2、 Mapper和映射文件的配置

映射文件的namespace和mapper接口全类名保持一致

映射文件sql语句的id和mapper接口方法名保持一致

3、Mybatis的一级缓存与二级缓存

一级缓存是sqlSession级别缓存,默认开启(sqlSession消失时,一级缓存会消失)

        存储作用域为Session

        通过同一个sqlSession查询相同数据会从缓存中取

        同一个sqlSession两次查询期间有增删改操作,会清空缓存

        手动清除sqlSession(clearCache方法)

二级缓存是sqlSessionFactory级别缓存,需要手动开启

        二级缓存在一级缓存关闭后有效(关闭后数据写入二级缓存)

        两次查询期间执行了任意增删改操作,一二级缓存失效

缓存查找顺序:二级缓存 ——> 一级缓存 ——> 数据库

4、Mybatis如何获取自动生成的主键值

insert标签中设置useGeneratedKeys=true,keyProperty=主键名

5、Mybatis动态SQL的常用标签

<if>

<where>

<trim>用于去除和添加标签中内容

<choose><when><otherwise>

<foreach>批量插入

6、PageHelper

        1、当调用PageHelper的startPage(PageNo页码,PageLimit每页条数)方法时,底层会将分页参数保存到ThreadLocal中(ThreadLocal<Page>)

        2、当执行mapper中的查询方法时,被PageInterceptor拦截,并执行interceptor方法,该方法做了两件事:

        一是拿到我们写的sql语句,并将他改造成select count(0)的形式,得到总数保存到Page的tocal属性中

        二是将sql语句改造成分页语句,得到的结果保存到Page的list属性中

7、Mybatis如何防止sql注入

1、sql执行前对数据类型进行检查(比如如果是日期,就必须是日期类型)

2、使用预编译语句

3、对特殊字符进行过滤

8、Mybatis的延迟加载 

延迟加载只存在于级联查询中,单表查询无

需要用到数据时才加载,不需要用到就不加载

lazyLoadingEnabled=true,默认关闭

延迟加载在底层是通过cglib动态代理完成的:

1、首先使用动态代理创建目标方法(开启延迟加载的mapper)的代理对象

2、当调用目标方法,进入invoke方法时,发现目标方法是null,此时执行sql查询

3、获取数据后,赋值给属性,再继续查询目标方法就有值了

9、Mybatis执行流程 

1、读取Mybatis配置文件:mybatis-config.xml加载运行环境和映射文件

2、构建会话工厂SqlSessionFactory:单例的

3、通过会话工厂创建SqlSession对象

4、通过Executor执行器操作数据库(封装jdbc操作,维护一二级缓存)

5、Executor接口的执行方法中有一个MapperStatement类型的参数,封装了映射信息

6、输入参数映射(java数据类型映射为mysql数据类型)

7、输出参数映射

Spring

1、BeanFactory与ApplicationContext

        BeanFactory是Spring的早期接口,称为Spring Bean工厂,ApplicationContext是后期接口,称为Spring容器

        Bean创建的主要逻辑和功能封装在BeanFactory中,ApplicationContext不仅继承BeanFactory,内部维护BeanFactory的引用

        BeanFactorty的API更偏向底层,ApplicationContext在BeanFactory的基础上对功能进行扩展

        Bean初始化时机不同,BeanFactory是首次调用getBean时开始Bean的创建,而ApplicationContext则是配置文件加载,容器一创建就将Bean都实例化且初始化。

scope:Bean的作用范围,常取值singleton和prototype

        singleton:单例 spring容器初始化时会创建Bean实例,放在单例池

        prototype: 原型 spring容器初始化时不会创建Bean实例,调用getBean才会实例化,每次创建一个新的Bean实例

lazy-init 是否延迟加载(对BeanFactory无效)

2、Bean的实例化

        1、Spring容器在进行初始化时,会将xml配置的bean信息封装成一个个BeanDefinition对象

        2、所有的BeanDefinition存储在一个BeanDefinitionMap中

        3、ApplicationContext对BeanDefinitionMap进行遍历,通过反射创建Bean实例对象,存储在singletonObject的Map中,也就是单例池

        4、当调用getBean方法时,直接从单例池中取 

3、Spring的后处理器

        Spring的后处理器是Spring对外开发的重要扩展点,允许我们介入到Bean的实例化中来,以达到动态注册BeanDefinition,动态修改Bean。有两种后处理器:

        BeanFactoryPostProcessor:Bean工厂后处理器,在BeanDefinitionMap填充完毕,Bean实例化之前

        BeanPostProcessor:Bean后处理器,Bean实例化之后,填充到单例池之前

        BeanFactoryPostProcessor接口:主要是对Bean定义的操作、注册BeanDefinition进Map中、修改BeanDefinitionMap中的数据 、注解开发原理

        BeanPostProcessor接口:对Bean的操作、Bean的初始化、AOP思想

4、Bean的生命周期

主要有4个阶段,Bean的实例化阶段,Bean的初始化阶段,Bean的使用,Bean的销毁

1、Bean的实例化

        Spring容器将xml文件定义的bean信息封装成一个个BeanDefinition,所有的BeanDefinition存储到一个BeanDefinitionMap中,Spring容器将该Map进行遍历,经过各种if判断,比如是否单例,是否延迟加载等,最终创建一个半成品实例。

2、Bean的初始化阶段

        ①Bean实例的属性填充,也就是依赖注入

        ②Aware接口属性注入

        ③BeanPostProcessor的before方法调用

        ④InitializingBean接口的初始化方法调用(afterPropertiesSet方法)

        ⑤自定义初始化方法回调

        ⑥BeanPostProcessor的after方法回调(AOP)

3、Bean的使用阶段:将成品Bean放入单例池中

4、Bean的销毁    

5、循环依赖问题

Spring在进行双向属性注入时产生的问题(比如A依赖B,B依赖A)

一级缓存:singletonObjects单例池,存储Bean成品

二级缓存:earlySingletonObjects早期bean单例池,当前对象已被引用

三级缓存: singletonFactories,对象未被引用,被ObjectFactories包了一下

A实例化,但尚未初始化,将A存入三级缓存

A注入B,从缓存中取,没有

B实例化,尚未初始化,放入三级缓存

B注入A,从三级缓存中取,并将A移入二级缓存

B执行其他生命周期,最终成为完整的Bean,移入单例池

A注入B

A完成其他生命周期,最终成为完整Bean,移入单例池

6、AOP产生Proxy的两种方式

jdk基于接口的动态代理:生成接口的实现类

cglib基于子类的动态代理:动态生成父类的子类

7、对AOP的理解

        AOP是面向切面编程,在Spring中,将那些与业务无关,但却对对象产生影响的公共行为和逻辑,抽取出来复用。比如记录操作日志、缓存处理、事务管理

 AOP相关概念:

        目标对象Target:被增强的方法所在的对象

        代理对象Proxy:对目标对象增强后的对象(实际调用的对象)

        连接点JoinPoint:目标对象中可以被增强的方法

        切入点PointCut:目标对象中实际被增强的方法

        通知(增强)Advice:增强部分的代码逻辑

        切面Aspect:通知+切入点

        织入Weaving:将通知和切入点动态结合的过程

 配置:

        切点表达式:execution(返回值 包.类.方法(参数))        #任意一级 ..任意

        通知类型:

                around环绕通知

                before前置通知

                after最终通知(无论有无异常都执行)

                after-returning后置通知(返回后通知)有异常不执行

                after-throwing异常通知(异常后通知)异常后执行,无异常不执行

 配置切面的两种方式:

        advisor:通过接口来确定通知类型(通知类型确定)

        aspect:通过配置确定通知类型

8、基于AOP的声明式事务管理

Spring事务相关的类:

        平台事务管理器PlatformTransactionManager:接口标准,实现类都具有事务提交、回滚和获得事务对象的功能。不同持久层有不同实现方案DataSourceTransactionManager

        事务定义TransactionDefinition:封装事务隔离级别,传播行为,过期时间

        事务状态TransactionStatus:存储事务状态信息,是否提交,是否回滚,是否有回滚点等。 

事务定义TransactionDefinition:

        name:方法名 addXxx=>add*

        isolation:隔离级别 解决事务并发问题(脏读、不可重复读、幻读)

        read-only:是否只读,默认false

        timeout:超时时间 超过x秒就取消当前事务操作

        propagation:事务传播行为 事务嵌套问题

                required默认:A调B B需要事务,A有就加入,没有B自己创建

                supports: A调B B有没有事务无所谓 A有就加入,没有就没有

9、Spring框架中单例Bean是线程安全的吗

        如果单例Bean是无状态的(不能保存数据的实例或者不能被修改的实例),那就是线程安全的,Dao类中实例大多都是无状态的

        如果是有状态的(比如ViewAndModel对象)那就是非线程安全的

        总的来说,Spring底层对bean没有关于线程安全的封装,所以是不安全的

        将bean的作用域由单例改为多例(prototype),这样每次都创建新的对象,线程之间不存在对象共享,就是安全的。

10、Spring中事务失效的场景 

        spring事务本质由AOP完成,对方法前后进行拦截,在执行方法前开启事务,执行完之后提交或回滚

1、如果在方法里自己处理了(try-catch)异常,没有抛出,就会导致事务失效

        原因:事务通知只有捉到了目标抛出的异常,才能进行后续回滚处理

        解决:在catch中添加throw new RuntimeException(e)

2、抛出检查异常(throws FileNotFoundException)

        原因:Spring默认只会回滚非检查异常(RunTimeException)

        解决:配置rollbackFor属性 @Transactional(rollbackFor=Exception.class)

3、方法未使用public修饰

        原因:Spring为方法创建代理、添加事务通知,前提条件就是该方法是public的

         解决:public

11、Spring常用注解

 声明bean相关的:

        @Component 以及三个衍生注解 @Controller @Service @Repository

 依赖注入相关:

        @Value(注入普通数据,String)

        @Autowired(按照类型,名称)

        @Resourse(按照名称,类型)

        @Qualifier(配合@Autowired按照名称)        

 设置作用域

        @Scope singleton/prototype

 延迟加载

        @Lazy

 spring配置相关:

        @ComponentScan

        @Configuration

        @Bean(“beanName)将该类交给ioc,标注方法会将方法返回值存储到spring容器

        @Import(clazz)

        @PropertySource("classpath:jdbc.properties")

AOP:

        @Aspect配置切面

        切点表达式:@PointCut

        配置通知@Around等

        开启注解配置@EnableAspectJAutoProxy

AOP声明式事务控制:

        @Transactional需要事务的方法或类上

        @EnableTransactionManagement

 其他:

        @Primary提交bean的优先级

        @Profile("dev")标注环境,只有dev环境激活才能被注册

        applicationContext.getEnvironment().setActiveProfiles("dev");  

12、Spring中常用的设计模式

        代理模式:spring中两种代理方式,jdk基于接口的动态代理,cglib动态代理,AOP面向切面编程

        单例模式:spring中bean默认为单例模式

        模板方式模式:用来解决代码重复问题

        工厂模式:spring中使用beanFactory来创建bean实例

13、Aware接口

 用于辅助依赖注入       

        ServletContextAware        web环境有效

        BeanFactoryAware                

        BeanNameAware        假设我们的类实现了BeanNameAware接口,对应这个接口有一个setBeanName方法,spring在依赖注入的初始化阶段把beanName作为参数传入进来。一般我们在自己写的类里面定义属性进行接收,就可以使用了

        ApplicationContextAware

SpringMVC

1、SpringMVC常见注解

1、请求映射:

        @RequestMapping用于映射请求路径

                派生注解:@GetMapping等

2、报文信息转换器:

        @RequestBody实现接收http请求的json数据,将json转换为Java对象(获得请求体,在控制器方法设置一个形参,用于json数据接收)

        @ResponseBody类和方法 ,加上注解返回的不再是视图名称,而是响应体(@RestController=@ResponseBody+@Controller)

3、获取请求参数

        @RequestParam形参内,指定请求参数名称

        @RequestHeader获取请求头数据

        @CookieValue

        @DataTimeFormat日期类型接收

4、请求路径获取参数

        @PathVariable     

2、SpringMVC执行流程

 

        1、用户发送请求到前端控制器DispatcherServlet

        2、DispatdcherServlet将收到的请求交给处理器映射器HandlerMapping(DispatcherServlet对请求URL进行解析,得到请求资源标识符URI,如果找不到请求资源标识符对应的映射,看是否配置了mvc:default-servlet-handler,没有就404,有一般访问的是静态资源,找不到也显示404)

        3、HandlerMapping找到具体的处理器,返回给DispatcherServlet处理器执行链对象HandlerExecutionChain,包含处理器方法和拦截器

        4、DispatcherServlet调用HandlerAdapter(处理器适配器)

        5、HandlerAdapter经过适配找到具体处理器,此时开始执行拦截器的preHandler方法【正向】

        6、Controller方法执行完成返回ModelAndView对象

        7、处理器适配器将Controller返回的ModelAndView对象返回给DispatcherServlet

        8、执行拦截器postHandle方法【逆向】

        9、DispacherServler对返回的ViewAndView进行判断,如果存在异常,会执行处理器异常解析器进行处理

        10、DispatcherServlet将ModelAndView传给视图解析器

        11、视图解析器解析后返回具体View

        12、DispacherServlet根据View进行渲染视图

        13、渲染视图完毕执行拦截器afterCompletion方法【逆向】

        14、响应给用户

DispatcherServlet是一个调度中心,接收所有请求

HandlerMapping:通过路径找到对应的控制器方法

HandlerAdaptor:1、执行Handler 2、处理Handler里的参数和返回值

ViewResolver:将逻辑视图转换为真正的视图

前后端分离开发中,在处理器适配器调用Controller方法执行结束后,通过HttpMessageConverter来返回结果转换为json并响应(方法上添加@ResponseBody)

3、SpringMVC如何发送put请求 

1、使用ajax,仅部分浏览器支持

2、使用form表单+HiddenHttpMethodFilter

        首先form表单请求方式设置为post,设置为隐藏域,name属性设置为_method,value设置为put

        设置过滤器HiddenHttpMethodFilter,过滤器会获取_method的值,并在请求方式为post的情况下换为put

4、域对象共享数据 

page request session application

1、向request域共享数据:

        ①使用原生ServletAPI向域对象共享数据     

        ②new ModelAndView的addObject方法

        ③形参Model的addAttribute

        ④形参Map的put

        ⑤形参ModelMap的addAttribute

2、向session域共享数据:

        ①使用ServletAPI

        ②HttpSession

3、application        

        ①session.getServletContext

 5、文件上传

文件上传MultipartFile

 6、拦截器和过滤器

        拦截器(Interceptor)属于SpringMVC规范,Filter属于Servlet规范

        拦截器只对进入SpringMVC管辖范围内的进行拦截,主要是Controller请求,Filter可以对所有请求进行拦截

        拦截器在DispatcherServlet执行后执行,Filter早于任何Servlet执行

拦截器用来拦截控制器方法的执行
三个抽象方法:
        preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法
        postHandle:控制器方法执行之后执行postHandle()
        afterComplation:处理完视图和模型数据,渲染视图完毕之后执行afterComplation() 

Springboot

1、Springboot自动配置原理

        1、在Spring Boot项目的引导类上有一个注解@SpringBootApplication,这个注解底层有三个注解,

        @SpringBootConfiguration这个注解底层就是@Configuration,表示配置类

        @EnableAutoConfiguration,这是实现自动化配置的核心注解

        @ComponentScan包扫描注解

        2、@EnableAutoConfiguration底层通过@Import注解导入对应的配置选择器。内部读取了该项目和引用的jar包的classpath路径下MATA-INF下的spring.factories文件中所配置类的全类名。springboot会根据配置类中所指定的条件来决定是否加载到spring容器中。 

@Conditional条件装配 满足条件,进行组件注入
        加在方法或类上,条件成立才会向下执行
        衍生注解:
            @ConditionalOnClass(name="全类名") 环境中存在指定的这个类,才会将该bean加入到IOC容器中
            @ConditionalOnMissingBean 不存在该类型的bean才会将该bean加入到IOC容器中--指定类型(value属性)或名称(name属性)应用场景:默认bean对象
            @ConditionalOnProperty(name="",havingValue="") 当前环境配置文件是否有对应属性和值 

2、加载第三方依赖的方式

1、@ComponentScan

2、@Import

        ①普通类或配置类

        ②实现ImportSelector接口/ImportBeanDefinitionRegistrar接口的类(作用:注册普通类到ioc)

3、EnableXxx封装@Bean

3、SpringBoot常见注解

@SpringBootApplication spring引导类

@SpringBootConfiguration

@EnableAutoConfiuration

@Conditional

数据结构

1、Java String和数组

boolean isEmpty():字符串是否为空

int length():返回字符串长度

String concat(str):拼接

boolean equals(Object obj):比较相等

toLowerCase/toUpperCase():大小写转换

trim():去掉前后空白字符

boolean contains(str):是否包含str

int indexOf(String str,[int fromIndex])返回指定字符串在此字符串第一次出现的位置

String subString(int beginIndex,[int endIndex]):截取

char charAt(index)返回index位置的字符

字符串转数组:

        1、char[] toCharArray()

        2、String[] split(String regex) 

数组转字符串:byte或char型数组

        1、构造器new String(char[] value)

        2、String valueof(char[] data)/copyValueof(char[] data)

数组本身是引用数据类型,而数组元素可以是任何数据类型

 静态初始化:

        ①int[] arr=new int[]{1,2};

        ②int[] arr;

           arr=new int[]{1,2};

        ③int[] arr={1,2};

动态初始化

        ①int[] arr=new int[length];

        ②int[] arr;

            arr=new int[length];

        ③int[][] arr=new int[5][];        5行的二维数组

2、素数个数统计

import java.util.*;

public class Main{

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int num = scanner.nextInt();
        System.out.println(prime(num));
    }

    public static int prime(int n) {
        boolean[] isPrime = new boolean[n];//false代表素数,默认
        int count = 0;
        for (int i = 2; i < n; i++) {
            if (!isPrime[i]) {
                count++;
                for (int j = i * i; j < n; j += i) {
                    isPrime[j] = true;
                }

            }
        }
        return count;
    }
}

3、排序算法

排序算法平均时间复杂度最好情况最坏情况空间复杂度
冒泡n1
选择1
插入n1
希尔nlognnlog²nnlog²n1
归并nlognnlognnlognn
快速nlognnlognlogn
nlognnlognnlogn1
计数
基数

结论:最好情况O(n):插入排序, 冒泡排序        最差情况O(nlogn):堆排序

4、二叉树 

等差数列求和

        Sn=n(a1+an)/2

等比数列求和

        Sn=a1(1-q^n)/(1-q)

        Sn=(a1-anq)/(1-q)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值