1. 什么是字节码?采用字节码的好处是什么?
所谓的字节码,就是 Java 程序经过编译之类产生的.class 文件,字节码能够被虚拟机识别,从而实现 Java 程序的跨平台性。
Java 程序从源代码到运行主要有三步:
- 编译:将我们的代码(.java)编译成虚拟机可以识别理解的字节码(.class)
- 解释:虚拟机执行Java字节码,将字节码翻译成机器能识别的机器码
- 执行:对应的机器执行二进制机器码
只需要把Java程序编译成Java虚拟机能识别的Java字节码,不同的平台安装对应的Java虚拟机,这样就可以可以实现Java 语言的平台无关性。
2. 为什么说Java 语言"编译与解释并存"?
高级编程语言按照程序的执行方式分为编译型和解释型两种。
简单来说:
- 编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;
- 解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。
比如,你想读一本外国的小说,你可以找一个翻译人员帮助你翻译,有两种选择方式,你可以先等翻译人员将全本的小说(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。
Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为Java程序要经过先编译,后解释两个步骤,由Java编写的程序需要先经过编译步骤,生成字节码(*.class文件),这种字节码必须再经过JVM,解释成操作系统能识别的机器码,在由操作系统执行。因此,我们可以认为Java 语言编译与解释并存。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
基础语法
3. Java 有哪些数据类型?
定义:Java 语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。
Java 语言数据类型分为两种:基本数据类型和引用数据类型。
基本数据类型:
- 数值型
- 整数类型(byte、short、int、long)
- 浮点类型(float、double)
- 字符型(char)
- 布尔型(boolean)
Java 基本数据类型范围和默认值:
引用数据类型:
- 类(class)
- 接口(interface)
- 数组([])
4.自动类型转换、强制类型转换?看看这几行代码?
Java所有的数值型变量可以相互转换,当把一个表数范围小的数值或变量直接赋给另一个表数范围大的变量时,可以进行自动类型转换;反之,需要强制转换。
这就好像,小杯里的水倒进大杯没问题,但大杯的水倒进小杯就不行了,可能会溢出。
float f=3.4,对吗?
不正确。3.4 是单精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换
float f =(float)3.4;或者写成float f =3.4F short s1 = 1;s1 = s1 + 1;对吗?short s1 = 1;s1 += 1;对吗?
对于short s1 = 1;s1 = s1 + 1;编译出错,由于1是 int类型,因此s1+1运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。
而short s1 = 1;s1 += 1;可以正确编译,因为 s1+= 1;相当于s1 =(short(s1 + 1);其中有隐含的强制类型转换。
5.什么是自动拆箱/封箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
Java 可以自动对基本数据类型和它们的包装类进行装箱和拆箱。
举例:
Integer i = 1; //装箱
int n = i; //拆箱
6. &和&&有什么区别?
&
运算符有两种用法:短路与
、逻辑与
。
&&
运算符是短路与运算
。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。
&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&。
例如在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为 username!= null && !username.equals("")
,二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
7. switch是否能作用在byte/long/String上?
Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。
从 Java 5 开始,Java中引入了枚举类型,expr 也可以是 enum 类型。
从Java 7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
8.break,continue,return的区别及作用?
- break 跳出整个循环,不再执行循环(结束当前的循环体)
- continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
- return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)
9. 用最有效率的方法计算 2乘以8?
2<<3。位运算,数字的二进制位左移三位相当于乘以2的三次方。
10.说说自增自减运算?看下这几个代码运行结果?
在写代码的过程中,常见的一种情况是需要某个整数类型变量增加1或减少1,Java提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(–)。
++和–运算符可以放在变量之前,也可以放在变量之后。
当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。
例如,当 b = ++a 时,先自增(自己增加 1),再赋值(赋值给 b);当 b = a++ 时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。
用一句口诀就是:“符号在前就先加/减,符号在后就后加/减"。
看一下这段代码运行结果?
int i = 1;
i = i++;
System. out. println(i);
答案是 1。有点离谱对不对。
对于JVM 而言,它对自增运算的处理,是会先定义一个临时变量来接收i的值,然后进行自增运算,最后又将临时变量赋给了值为 2 的 i,所以最后的结果为 1。
相当于这样的代码:
int i = 1;
int temp = i;
i++;
i = temp;
System.out.println(i);
这段代码会输出什么?
int count = 0;
for(int i = 0;i < 100;i++)
{
count = count++;
}
System.out.println("count = "+count);
答案是 0。
和上面的题目一样的道理,同样是用了临时变量,count实际是等于临时变量的值。
类似于:
int autoAdd(int count)
{
int temp = count;
count = coutn + 1;
return temp;
}
面向对象
15. 面向对象和面向过程的区别?
- 面向过程:面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的一次调用就可以。
- 面向对象:面向对象,把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事件在解决整个问题的过程所发生的行为。目的是为了写出通用的代码,加强代码的重用,屏蔽差异性。
用一个比喻:面向过程是编年体;面向对象是纪传体。
16. 面向对象有哪些特性
-
封装
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法。
-
继承
继承是使用已存在的类的定义作为基础创建新的类,新类的定义可以增加新的属性或新的方法,也可以继承父类的属性和方法。通过继承可以很方便地进行代码复用。
关于继承有以下三个要点:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
-
多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。在Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
17. 重载(overload)和重写(override)的区别?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
-
重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;
-
重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。
方法重载的规则:
- 方法名一致,参数列表中参数的顺序,类型,个数不同。
- 重载与方法的返回值无关,存在于父类和子类,同类中。
- 可以抛出不同的异常,可以有不同修饰符。
18. 访问修饰符public、private、protected、以及不写(默认)时的区别?
Java 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持4种不同的访问权限。
- default(即默认,什么也不写):在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。
- private:在同一类内可见。可以修饰变量、方法。注意:不能修饰类(外部类)。
- public:对所有类可见。可以修饰类、接口、变量、方法。
- protected:对同一包内的类和所有子类可见。可以修饰变量、方法。注意:不能修饰类(外部类)。
19. this 关键字有什么作用?
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this 的用法在 Java 中大体可以分为 3种:
- 普通的直接引用,this 相当于是指向当前对象本身
- 形参与成员变量名字重名,用this来区分:
public Person(String name,int age){
this.name=name;
this.age=age;
}
-
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
- 引用本类的构造函数
20. 抽象类(abstract class)和接口(interface)有什么区别?
- 接口的方法默认是public,所有方法在接口中不能有实现(Java 8开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
- 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。
- 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。
- 接口方法默认修饰符是public,抽象方法可以有public,protected和default这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰!)。
- 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
- 在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现方法是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。
- jdk9的接口被允许定义私有方法。
总结一下 jdk7-jdk9 Java 中接口的变化:
- 在 jdk 7 或更早版本中,接口里面只能有常量变量和抽象方法。这些接口方法必须由选择实现接口的类实现。
- jdk 8 的时候接口可以有默认方法和静态方法功能。
- jdk 9 在接口中引入了私有方法和私有静态方法。
21. 成员变量与局部变量的区别有哪些?
- 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被public,private,static等修饰符所修饰,而局部变量不能被访问控制修饰符及static所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 从变量在内存中的存储方式来看:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引用数据类型,那存放的是指向堆内存对象的引用或者是指向常量池中的地址。
- 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
- 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被final修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
22.静态变量和实例变量的区别?静态方法、实例方法呢?
静态变量和实例变量的区别?
- 静态变量:是被static修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何多少个对象,静态变量在内存中有且仅有一个副本。
- 实例变量:必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。
静态方法和实例方法有何不同?
类似地。
- 静态方法:static修饰的方法,也被称为类方法。在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名·方法名"的方式。静态方法里不能访问类的非静态成员变量和方法。
- 实例方法:依存于类的实例,需要使用"对象名.方法名"的方式调用;可以访问类的所有成员变量和方法。
24. final 关键字有什么作用?
final 表示不可变的意思,可用于修饰类、属性和方法:
- 被 final 修饰的类不可以被继承
- 被 final 修饰的方法不可以被重写
- 被final修饰的变量不可变,被final修饰的变量必须被显式地指定初始值,还得注意的是,这里的不可变指的是变量的引用不可变,不是引用指向的内容的不可变。
例如:
final StringBuilder sb = new StringBuilder("abc");
sb.append ("d");
System.out.println(sb); //abcd
25. final、finally、finalize的区别?
-
**final **用于修饰变量、方法和类:final 修饰的类不可被继承;修饰的方法不可被重写;修饰的变量不可变。
-
finally作为异常处理的一部分,它只能在try/catch语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下,system.exit(0)可以阻断finally执行。
-
finalize是在java.lang.Object里定义的方法,也就是说每一个对象都有这么个方法,这个方法在gc启动,该对象被回收的时候被调用。
一个对象的 finalize方法只会被调用一次,finalize被调用不一定会立即回收该对象,所以有可能调用finalize后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize了,进而产生问题,因此不推荐使用finalize方法。
26. == 和 equals 的区别?
==:它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型**==比较的是值,引用数据类型==**比较的是内存地址)。
equals():它的作用也是判断两个对象是否相等。但是这个"相等"一般也分两种情况:
- 默认情况:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过"=="比较这两个对象,还是相当于比较内存地址。
- 自定义情况:类覆盖了equals()方法。我们平时覆盖的 equals()方法一般是比较两个对象的内容是否相同,自定义了一个相等的标准,也就是两个对象的值是否相等。
举个例子,Person,我们认为两个人的编号和姓名相同,就是一个人:
public class Person {
private String no;
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return Objects.equals(no, person.no) &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(no, name);
}
}
27. hashCode 与 equals?
这个也是面试常问——“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?”
什么是 HashCode?
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int 整数,定义在 Object 类中,是一个本地方法,这个方法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();
- 1
哈希码主要在哈希表这类集合映射的时候用到,哈希表存储的是键值对(key-value),它的特点是:能根据"键"快速的映射到对应的“值”。这其中就利用到了哈希码!
为什么要有 hashCode?
上面已经讲了,主要是在哈希表这种结构中用的到。
例如HashMap怎么把key映射到对应的value上呢?用的就是哈希取余法,也就是拿哈希码和存储元素的数组的长度取余,获取key对应的value所在的下标位置。
为什么重写 equals 时必须重写 hashCode 方法?
如果两个对象相等,则hashcode一定也是相同的。两个对象相等,对两个对象分别调用equals方法都返回true。反之,两个对象有相同的 hashcode值,它们也不一定是相等的。因此,equals方法被覆盖过,则hashCode 方法也必须被覆盖。
hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?
因为可能会碰撞,hashCode()所使用的散列算法也许刚好会让多个对象传回相同的散列值。越糟糕的散列算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode()。
28. Java是值传递,还是引用传递?
Java 语言是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。
JVM 的内存分为堆和栈,其中栈中存储了基本数据类型和引用数据类型实例的地址,也就是对象地址。
而对象所占的空间是在堆中开辟的,所以传递的时候可以理解为把变量存储的对象地址给传递过去,因此引用类型也是值传递。
29. 深拷贝和浅拷贝?
- 浅拷贝:仅拷贝被拷贝对象的成员变量的值,也就是基本数据类型变量的值,和引用数据类型变量的地址值,而对于引用类型变量指向的堆中的对象不会拷贝。
- 深拷贝:完全拷贝一个对象,拷贝被拷贝对象的成员变量的值,堆中的对象也会拷贝一份。
例如现在有一个 order 对象,里面有一个 products 列表,它的浅拷贝和深拷贝的示意图:
因此深拷贝是安全的,浅拷贝的话如果有引用类型,那么拷贝后对象,引用类型变量修改,会影响原对象。
浅拷贝如何实现呢?
Object类提供的clone()方法可以非常简单地实现对象的浅拷贝。
深拷贝如何实现呢?
-
重写克隆方法:重写克隆方法,引用类型变量单独克隆,这里可能会涉及多层递归。
-
序列化:可以先将原对象序列化,再反序列化成拷贝对象。
30. Java创建对象有哪几种方式?
Java 中有以下四种创建对象的方式:
- new创建新对象
- 通过反射机制
- 采用 clone 机制
- 通过序列化机制
前两者都需要显式地调用构造方法。
对于clone机制,需要注意浅拷贝和深拷贝的区别,对于序列化机制需要明确其实现原理,在 Java 中序列化可以通过实现 Externalizable 或者 Serializable 来实现。
String
31. String 是 Java 基本数据类型吗?可以被继承吗?
String 是Java 基本数据类型吗?
不是。Java中的基本数据类型只有8个:byte、short,int,long,float,double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type)。
String 是一个比较特殊的引用数据类型。
String 类可以继承吗?
不行。String 类使用 final 修饰,是所谓的不可变类,无法被继承。
32. String 和 StringBuilder、StringBuffer 的区别?
- String:String的值被创建后不能修改,任何对String的修改都会引发新的String对象的生成。
- StringBuffer:跟String类似,但是值可以被修改,使用 synchronized 来保证线程安全。
- StringBuilder:StringBuffer 的非线程安全版本,性能上更高一些。
33. String str1 = new String(“abc”)和String str2 ="abc"的 区别?
两个语句都会去字符串常量池中检查是否已经存在"abc",如果有则直接使用,如果没有则会在常量池中创建“abc”对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hSpOr4cc-1692104323159)(C:%5CUsers%5CA%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20230814201830290.png)]
但是不同的是,String str1 = new String(“abc”)还会通过 new String()在堆里创建一个"abc"字符串对象实例。所以后者可以理解为被前者包含。
String s = new String(“abc”)创建了几个对象?
很明显,一个或两个。如果字符串常量池已经有“abc”,则是一个;否则,两个。
当字符创常量池没有"abc",此时会创建如下两个对象:
- 一个是字符串字面量"abc"所对应的、字符串常量池中的实例
- 另一个是通过 new String()创建并初始化的,内容与"abc"相同的实例,在堆中。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
34. String 不是不可变类吗?字符串拼接是如何实现的?
String 的确是不可变的,“+”的拼接操作,其实是会生成新的对象。
例如:
String a = "hello ";
String b = "world!";
String ab = a + b;
在jdk1.8之前,a和b初始化时位于字符串常量池,ab拼接后的对象位于堆中。经过拼接新生成了String对象。如果拼接多次,那么会生成多个中间对象。
内存如下:
在Java8 时JDK 对"+"号拼接进行了优化,上面所写的拼接方式会被优化为基于StringBuilder 的 append 方法进行处理。Java 会在编译期对“+”号进行处理。
下面是通过javap -verbose命令反编译字节码的结果,很显然可以看到StringBuilder的创建和append方法的调用。
stack=2, locals=4, args_size=1
0: ldc #2 // String hello
2: astore_1
3: ldc #3 // String world!
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":
()V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:
(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6 // Method java/lang/StringBuilder.append:
(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:
()Ljava/lang/String;
24: astore_3
25: return
也就是说其实上面的代码其实相当于:
String a = "hello ";
String b = "world!";
StringBuilder sb = new StringBuilder();
sb.append(a);
sb.append(b);
String ab = sb.toString();
此时,如果再笼统的回答:通过加号拼接字符串会创建多个 String 对象,因此性能比 StringBuilder 差,就是错误的了。因为本质上加号拼接的效果最终经过编译器处理之后和 StringBuilder 是一致的。
当然,循环里拼接还是建议用 StringBuilder,为什么,因为循环一次就会创建一个新的 StringBuilder 对象,大家可以自行实验。
35. intern 方法有什么作用?
JDK 源码⾥里里已经对这个⽅方法进⾏行行了了说明:
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
意思也很好懂:
- 如果当前字符串内容存在于字符串常量池(即 equals()方法为 true,也就是内容一样),直接返回字符串常量池中的字符串
- 否则,将此String对象添加到池中,并返回String对象的引用
Integer
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取