面试题大全,已迁移到gitee开源项目,含2022年最新 20000+ 道,500多份,各类型面试题,汇总。
https://gitee.com/yoodb/ebooks/
Java面试总结(2021优化版)已发布在个人微信公众号【Java精选】,根据读者的反馈优化了部分答案存在的错误,同时根据最新面试总结和读者的建议,删除了低频问题,并添加了一些常见面试题,对文章进行了精简优化,目前约2000+道面试题,欢迎大家关注,关注后回复Java面试,即可获取最新面试资料,支持在线随时随地刷题,支持做记号,收藏等功能,中间件、分布式面试资料应有尽有!
【Java精选】专注程序员推送一些Java开发知识,包括基础知识、各大流行框架、大数据技术、数据库、面试题、面试经验、职业规划以及优质开源项目等。其中一部分由小编总结整理,另一部分来源于网络上优质资源,希望对大家的学习和工作有所帮助。
本篇面试题资料只是部分面试资料,由于内容庞大、比较多,采用持续更新方式,这样界面也比较美观、方便阅读,欢迎大家收藏,或者公众号【Java精选】查看。
一、抽象和封装
类和对象体现了抽象和封装
抽象就是解释类与对象之间关系的词。类与对象之间的关系就是抽象的关系。一句话来说明:类是对象的抽象,而对象则是类得特例,即类的具体表现形式。
封装两个方面的含义:一是将有关数据和操作代码封装在对象当中,形成一个基本单位,各个对象之间相对独立互不干扰。二是将对象中某些属性和操作私有化,已达到数据和操作信息隐蔽,有利于数据安全,防止无关人员修改。把一部分或全部属性和部分功能(函数)对外界屏蔽,就是从外界(类的大括号之外)看不到,不可知,这就是封装的意义。
二、继承
面向对象的继承是为了软件重用,简单理解就是代码复用,把重复使用的代码精简掉的一种手段。如何精简,当一个类中已经有了相应的属性和操作的代码,而另一个类当中也需要写重复的代码,那么就用继承方法,把前面的类当成父类,后面的类当成子类,子类继承父类,理所当然。就用一个关键字extends就完成了代码的复用。
三、多态
没有继承就没有多态,继承是多态的前提。虽然继承自同一父类,但是相应的操作却各不相同,这叫多态。由继承而产生的不同的派生类,其对象对同一消息会做出不同的响应。
1、JDK
JDK(Java development Toolkit),JDK是整个Java的核心,包括了Java的运行环境(Java Runtime Environment),一堆的Java工具(Javac,java,jdb等)和Java基础的类库(即Java API 包括rt.jar).
Java API 是Java的应用程序的接口,里面有很多写好的Java class,包括一些重要的结构语言以及基本图形,网络和文件I/O等等。
2、JRE
JRE(Java Runtime Environment),Java运行环境。在Java平台下,所有的Java程序都需要在JRE下才能运行。只有JVM还不能进行class的执行,因为解释class的时候,JVM需调用解释所需要的类库lib。JRE里面有两个文件夹bin和lib,这里可以认为bin就是JVM,lib就是JVM所需要的类库,而JVM和lib合起来就称JRE。
JRE包括JVM和JAVA核心类库与支持文件。与JDK不同,它不包含开发工具-----编译器,调试器,和其他工具。
3、JVM
JVM:Java Virtual Machine(Java 虚拟机)JVM是JRE的一部分,它是虚拟出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件构架,入处理器,堆栈,寄存器等,还有相应的指令系统。
JVM是Java实现跨平台最核心的部分,所有的Java程序会首先被编译为class的类文件,JVM的主要工作是解释自己的指令集(即字节码)并映射到本地的CPU的指令集或OS的系统调用。Java面对不同操作系统使用不同的虚拟机,一次实现了跨平台。JVM对上层的Java源文件是不关心的,它关心的只是由源文件生成的类文件
编译和运行Java文件,需了解两个命令:
1)javac命令:编译java文件;使用方法: javac Hello.java ,如果不出错的话,在与Hello.java 同一目录下会生成一个Hello.class文件,这个class文件是操作系统能够使用和运行的文件。
2)java命令: 作用:运行.class文件;使用方法:java Hello,如果不出错的话,会执行Hello.class文件。注意:这里的Hello后面不需要扩展名。
新建文件,编写代码如下:
public class Hello{
public static void main(String[] args){
System.out.println("Hello world,欢迎关注微信公众号“Java精选”!");
}
}
文件命名为Hello.java,注意后缀为“java”。
打开cmd,切换至当前文件所在位置,执行javac Hello.java,该文件夹下面生成了一个Hello.class文件
输入java Hello命令,cmd控制台打印出代码的内容Hello world,欢迎关注微信公众号“Java精选”!
1)48个关键字:abstract、assert、boolean、break、byte、case、catch、char、class、continue、default、do、double、else、enum、extends、final、finally、float、for、if、implements、import、int、interface、instanceof、long、native、new、package、private、protected、public、return、short、static、strictfp、super、switch、synchronized、this、throw、throws、transient、try、void、volatile、while。
2)2个保留字(目前未使用,以后可能用作为关键字):goto、const。
3)3个特殊直接量(直接量是指在程序中通过源代码直接给出的值):true、false、null。
Java的类型分成两种,一种是基本类型,一种是引用类型。其中Java基本类型共有八种。
基本类型可以分为三大类:字符类型char,布尔类型boolean以及数值类型byte、short、int、long、float、double。
数值类型可以分为整数类型byte、short、int、long和浮点数类型float、double。
JAVA中的数值类型不存在无符号的,它们的取值范围是固定的,不会随着机器硬件环境或操作系统的改变而改变。实际上《Thinking in Java》一书作者,提到Java中还存在另外一种基本类型void,它也有对应的包装类 java.lang.Void,因为Void是不能new,也就是不能在堆里面分配空间存对应的值,所以将Void归成基本类型,也有一定的道理。
8种基本类型表示范围如下:
byte:8位,最大存储数据量是255,存放的数据范围是-128~127之间。
short:16位,最大数据存储量是65536,数据范围是-32768~32767之间。
int:32位,最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。
long:64位,最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1。
float:32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。
double:64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。
boolean:只有true和false两个取值。
char:16位,存储Unicode码,用单引号赋值。
java中args即为arguments的缩写,是指字符串变量名,属于引用变量,属于命名,可以自定义名称也可以采用默认值,一般习惯性照写。
String[] args是main函数的形式参数,可以用来获取命令行用户输入进去的参数。
1)字符串变量名(args)属于引用变量,属于命名,可以自定义名称。
2)可以理解成用于存放字符串数组,若去掉无法知晓"args"声明的变量是什么类型。
3)假设public static void main方法,代表当启动程序时会启动这部分;
4)String[] args是main函数的形式参数,可以用来获取命令行用户输入进去的参数。
5)java本身不存在不带String args[]的main函数,java程序中去掉String args[]会出现错误。
在Java中final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。下面从这三个方面来了解一下final关键字的基本用法。
1、修饰类
当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。
在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。
2、修饰方法
下面这段话摘自《Java编程思想》第四版第143页:
“使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“
因此,如果只有在想明确禁止该方法在子类中被覆盖的情况下才将方法设置为final的。即父类的final方法是不能被子类所覆盖的,也就是说子类是不能够存在和父类一模一样的方法的。
final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)
3、修饰变量
final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。
final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。
当函数的参数类型声明为final时,说明该参数是只读型的。即你可以读取使用该参数,但是无法改变该参数的值。
1)类的final变量和普通变量有什么区别?
当用final作用于类的成员变量时,成员变量(注意是类的成员变量,局部变量只需要保证在使用之前被初始化赋值即可)必须在定义时或者构造器中进行初始化赋值,而且final变量一旦被初始化赋值之后,就不能再被赋值了。
2)被final修饰的引用变量指向的对象内容可变吗?
引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的
3)final参数的问题
在实际应用中,我们除了可以用final修饰成员变量、成员方法、类,还可以修饰参数、若某个参数被final修饰了,则代表了该参数是不可改变的。如果在方法中我们修改了该参数,则编译器会提示你:
The final local variable i cannot be assigned. It must be blank and not using a compound assignment。
java采用的是值传递,对于引用变量,传递的是引用的值,也就是说让实参和形参同时指向了同一个对象,因此让形参重新指向另一个对象对实参并没有任何影响。
1、为了实现字符串池
final修饰符的作用:final可以修饰类,方法和变量,并且被修饰的类或方法,被final修饰的类不能被继承,即它不能拥有自己的子类,被final修饰的方法不能被重写, final修饰的变量,无论是类属性、对象属性、形参还是局部变量,都需要进行初始化操作。
String为什么要被final修饰主要是为了”安全性“和”效率“的原因。
final修饰的String类型,代表了String不可被继承,final修饰的char[]代表了被存储的数据不可更改性。虽然final修饰的不可变,但仅仅是引用地址不可变,并不代表了数组本身不会改变。
为什么保证String不可变呢?
因为只有字符串是不可变,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现,反之变量改变它的值,那么其它指向这个值的变量值也会随之改变。
如果字符串是可变,会引起很严重的安全问题。如数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接或在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则改变字符串指向的对象值,将造成安全漏洞。
2、为了线程安全
因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
3、为了实现String可创建HashCode不可变性
因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。使得字符串很适合作为Map键值对中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
默认方法
抽象类可以有默认的方法实现;而接口类在JDK1.8之前版本,不存在方法的实现。
实现方式
抽象类子类使用extends关键字来继承抽象类,如果子类不是抽象类,子类需要提供抽象类中所声明方法的实现;而接口类子类使用implements来实现接口,需要提供接口中所有声明的实现。
构造器
抽象类中可以有构造器;而接口中不能有构造器。
和正常类区别
抽象类不能被实例化;而接口是完全不同的类型。
访问修饰符
抽象类中抽象方法可以有public、protected、default等修饰;而接口类默认是public,不能使用其他修饰符。
多继承
抽象类一个子类只能存在一个父类;而接口类一个子类可以存在多个接口。
添加新方法
抽象类中添加新方法,可以提供默认的实现,因此可以不修改子类现有的代码;而接口类中添加新方法,则子类中需要实现该方法。
面向过程
性能相比面向对象高,因其类调用时需要实例化,开销比较大,消耗资源,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
面向对象
易维护、复用以及扩展,由于面向对象有封装、继承、多态性等特征,可以设计出低耦合、高内聚的系统,使得更加灵活,易于维护。
1)简单易学;
2)面向对象(封装,继承,多态);
3)平台无关性(Java虚拟机实现平台无关性);
4)可靠性;
5)安全性;
6)支持多线程;
7)支持网络编程并方便易用;
8)编译与解释并存。
重载(Overload) 是指让类以统一的方式处理不同类型数据的一种手段,实质表现就是多个具有不同的参数个数或者不同类型的同名函数,存在于同一个类中,返回值类型不同,是一个类中多态性的一种表现。
调用方法时通过传递不同参数个数和参数类型来决定具体使用哪个方法的多态性。
重写(Override) 是指父类与子类之间的多态性,实质就是对父类的函数进行重新定义。
如果子类中定义某方法与其父类有相同的名称和参数则该方法被重写,需注意的是子类函数的访问修饰权限不能低于父类的。
如果子类中的方法与父类中的某一方法具有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法,如需父类中原有的方法则可使用super关键字。
静态方法和实例方法的区别主要体现在两个方面:
其一在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式而实例方法只能试用后面这种方式。也就是说,调用静态方法可以无需创建对象进行实例化。
其二静态方法在访问本类的成员时,只允许访问静态成员也就是静态成员变量和静态方法,而不允许访问实例成员变量和实例方法,实例方法是没有这个限制的。
使用==比较
用于对比基本数据类型的变量,是直接比较存储的 “值”是否相等;
用于对比引用类型的变量,是比较的所指向的对象地址。
使用equals比较
equals方法不能用于对比基本数据类型的变量;
如果没对Object中equals方法进行重写,则是比较的引用类型变量所指向的对象地址,反之则比较的是内容。
Integer是int的包装类,默认值是null;int是基本数据类型,默认值是0;
Integer变量必须实例化后才能使用;int变量不需要;
Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值。
分析总结
1)Integer与new Integer不相等。new出来的对象被存放在堆,而非new的Integer常量则在常量池,两者内存地址不同,因此判断是false。
2)两个值都是非new Integer,如果值在-128,127区间,则是true,反之为false。
这是因为java在编译Integer i2 = 128时,被翻译成:
Integer i2 = Integer.valueOf(128);
而valueOf()函数会对-128到127之间的数进行缓存。
3)两个都是new Integer,两者判断为false,内存地址不同。
4)int和Integer对比不管是否new对象,两者判断都是true,因为会把Integer自动拆箱为int再去比。
内部类是指把A类定义在另一个B类的内部。
例如:把类User定义在类Role中,类User就被称为内部类。
class Role {
class User {
}
}
1、内部类的访问规则
1)可以直接访问外部类的成员,包括私有
2)外部类要想访问内部类成员,必须创建对象
2、内部类的分类
1)成员内部类
2)局部内部类
3)静态内部类
4)匿名内部类
自动装箱是指将基本数据类型重新转化为对象。
public class Test {
public static void main(String[] args) {
Integer num = 9;
}
}
num = 9的值是属于基本数据类型,原则上不能直接赋值给对象Integer。但是在JDK1.5版本后就可以进行这样的声明自动将基本数据类型转化为对应的封装类型,成为对象后可以调用对象所声明的方法。
自动拆箱是指将对象重新转化为基本数据类型。
public class Test {
public static void main(String[] args) {
// 声明Integer对象
Integer num = 9;
// 隐含自动拆箱
System.out.print(num--);
}
}
由于对象不能直接进行运算,而是需要转化为基本数据类型后才能进行加减乘除。
// 装箱
Integer num = 10;
// 拆箱
int num1 = num;
首先明确一点HashMap是支持空键值对的,也就是null键和null值,而ConcurrentHashMap是不支持空键值对的。
查看一下JDK1.8源码,HashMap类部分源码,代码如下:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap在调用put()方法存储数据时会调用hash()方法来计算key的hashcode值,可以从hash()方法上得出当key==null时返回值是0,这意思就是key值是null时,hash()方法返回值是0,不会再调用key.hashcode()方法。
ConcurrentHashMap类部分源码,代码如下:
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
ConcurrentHashmap在调用put()方法时调用了putVal()方法,而在该方法中判断key为null或value为null时抛出空指针异常NullPointerException。
ConcurrentHashmap是支持并发的,当通过get()方法获取对应的value值时,如果指定的键为null,则为NullPointerException,这主要是因为获取到的是null值,无法分辨是key没找到null还是有key值为null。
父类中静态方法不能被子类重写。
重写只适用于实例方法,不能用于静态方法,而且子类当中含有和父类相同签名的静态方法,一般称之为隐藏。
public class A {
public static String a = "这是父类静态属性";
public static String getA() {
return "这是父类静态方法";
}
}
public class B extends A{
public static String a = "这是子类静态属性";
public static String getA() {
return "这是子类静态方法";
}
public static void main(String[] args) {
A a = new B();
System.out.println(a.getA());
}
}
如上述代码所示,如果能够被重写,则输出的应该是“这是子类静态方法”。与此类似的是,静态变量也不能被重写。如果想要调用父类的静态方法,应该使用类来直接调用。
Map接口和Collection接口是所有集合框架的父接口
Collection接口的子接口包括:Set接口和List接口。
Set中不能包含重复的元素。List是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。
Map接口的实现类主要有:HashMap、Hashtable、ConcurrentHashMap以及TreeMap等。Map不能包含重复的key,但是可以包含相同的value。根据键得到值,对map集合遍历时先得到键的set集合,对set集合进行遍历,得到相应的值。
Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
Iterator所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含以下三种方法:
hasNext()是否还有下一个元素
next()返回下一个元素
remove()删除当前元素
1)Map提供的是键值对映射(即Key和value的映射),而Collection提供的是一组数据并不是键值对映射。
2)若果Map继承了Collection接口,那么所实现的Map接口的类到底是用Map键值对映射数据还是用Collection的一组数据呢?比如平常所用的hashMap、hashTable、treeMap等都是键值对,所以它继承Collection是完全没意义,而且Map如果继承Collection接口的话,违反了面向对象的接口分离原则。
接口分离原则:客户端不应该依赖它不需要的接口。
另一种定义是类间的依赖关系应该建立在最小的接口上。
接口隔离原则将非常庞大、臃肿的接口拆分成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。
接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署,让客户端依赖的接口尽可能地小。
3)Map和List、Set不同,Map放的是键值对,List、Set存放的是一个个的对象。说到底是因为数据结构不同,数据结构不同,操作就不一样,所以接口是分开的,还是接口分离原则。
java.util.Collection是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java类库中有很多具体的实现。
Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。其直接继承接口有List与Set。
Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
java.util.Collections是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
1)ArrayList是Array动态数组的数据结构,LinkedList是Link链表的数据结构,此外,它们两个都是对List接口的实现。前者是数组队列,相当于动态数组;后者为双向链表结构,也可当作堆栈、队列、双端队列。
2)当随机访问List时(get和set操作),ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
3)当对数据进行增加和删除的操作时(add和remove操作),LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
4)从利用效率来看,ArrayList自由性较低,因为它需要手动设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。
5)ArrayList主要控件开销在于需要在List列表预留一定空间;而LinkList主要控件开销在于需要存储结点信息以及结点指针信息。
Hashtable是线程安全,而HashMap则非线程安全。
Hashtable所有实现方法添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。
HashMap允许使用null作为key,不过建议还是尽量避免使用null作为key。HashMap以null作为key时,总是存储在table数组的第一个节点上。而Hashtable则不允许null作为key。
HashMap继承了AbstractMap,HashTable继承Dictionary抽象类,两者均实现Map接口。
HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。
HashMap扩容时是当前容量翻倍即:capacity*2,Hashtable扩容时是容量翻倍+1即:capacity*2+1。
HashMap和Hashtable的底层实现都是数组+链表结构实现。
当HashMap中元素个数超过数组大小*loadFactor时,需进行数组扩容。
loadFactor默认值为0.75,默认情况下,数组大小为16,HashMap中元素个数超过16 * 0.75=12的时,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以能够预选知道HashMap中元素的个数,应该预设数组的大小,可以有效的提高HashMap的性能。
假设有1000个元素new HashMap(1000),理论上来讲new HashMap(1024)更合适,不过上面已经提过即使是1000个元素,HashMap也会自动设置为1024。但是new HashMap(1024),而0.75*1024 <1000, 为了可以0.75 * size >1000,必须new HashMap(2048),避免了resize的问题。
总结:
添加元素时会检查容器当前元素个数。当HashMap的容量值超过临界值(默认16 * 0.75=12)时扩容。HashMap将会重新扩容到下一个2的指数幂(16->32->64)。调用resize方法,定义长度为新长度(32)的数组,然后对原数组数据进行再Hash。注意的是这个过程比较损耗性能。
JDK1.7下ArrayList()初始化后的默认长度是10,源码如下:
//无参构造方法
public ArrayList() {
this(10);
}
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
通过上述源代码可以看出,默认的构造方法中直接指定数组长度为10,同时调用重载的构造方法,创建了长度为10的一个数组。
//无参构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
构造方法中静态类型的数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}是个空数组,数组长度为0,因此JDK1.8下的ArrayList()初始化后默认的数组长度为0。
1)Arrays.asList()方法不适用于基本数据类型
byte
short
int
long
float
double
boolean
2)Arrays.asList()方法把数组与列表链接起来,当更新其中之一时,另一个自动更新。
3)Arrays.asList()方法不支持add和remove方法。
Set系列集合添加元素无序的根本原因是底层采用哈希表存储元素。
JDK1.8以下版本:哈希表 = 数组 + 链表 + (哈希算法)
JDK1.8及以上版本:哈希表 = 数组 + 链表 + 红黑树 + (哈希算法)
当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
Comparable接口出自java.lang包,它有一个compareTo(Object obj)方法用来排序。
Comparator接口出自java.util包,它有一个compare(Object obj1, Object obj2)方
法用来排序。
一般对集合使用自定义排序时,需要重写compareTo()方法或compare()方法。
当需要对某一个集合实现两种排序方式,比如一个用户对象中的姓名和身份证分别采用一种排序方法。
方式一:重写compareTo()方法实现姓名、身份证排序
方式二:使用自定义的Comparator方法实现姓名、身份证排序
方法三:使用两个Comparator来实现姓名、身份证排序
其中方式二代表只能使用两个参数的形式Collections.sort()。
Collections是一个工具类,sort是其中的静态方法,是用来对List类型进行排序的,它有两种参数形式:
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
并发是针对服务器而言,是否并发的关键是看用户操作是否对服务器产生了影响。并发是指在同一时刻与服务器进行了交互的在线用户数量。这些用户的最大特征是和服务器产生了交互,这种交互既可以是单向的传输数据,也可以是双向的传送数据。
进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间)。
比如用户点击桌面的IE浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。当用户再次点击左边的IE浏览器,又启动了一个进程,操作系统将为新的进程分配新的独立的地址空间。目前操作系统都支持多进程。
进程是表示自愿分配的基本单位。而线程则是进程中执行运算的最小单位,即执行处理机调度的基本单位。
通俗来讲:一个程序有一个进程,而一个进程可以有多个线程。
并行(parallellism)是指两个或者多个事件在同一时刻发生,而并发(parallellism)是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件,而并发是在同一实体上的多个事件。
并行是在一台处理器上同时处理多个任务(Hadoop分布式集群),而并发在多台处理器上同时处理多个任务。
进程是系统中正在运行的一个程序,程序一旦运行就是进程。
进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。
一个进程可以拥有多个线程,每个线程使用其所属进程的栈空间。线程与进程的一个主要区别是,统一进程内的一个主要区别是,同一进程内的多个线程会共享部分状态,多个线程可以读写同一块内存(一个进程无法直接访问另一进程的内存)。同时,每个线程还拥有自己的寄存器和栈,其他线程可以读写这些栈内存。
线程是进程的一个实体,是进程的一条执行路径。
线程是进程的一个特定执行路径。当一个线程修改了进程的资源,它的兄弟线程可以立即看到这种变化。
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。当线程启动以后,它不能一直占用着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、死亡。
新建(new Thread)
当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
就绪(runnable)
线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源
运行(running)
线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。
堵塞(blocked)
由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。
正在等待:调用wait()方法。(调用motify()方法回到就绪状态)
被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复)
死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
自然终止:正常运行run()方法后终止
异常终止:调用stop()方法让一个线程终止运行
乐观锁
乐观锁的意思是乐观思想,即认为读多写少,遇到并发写的可能性低,每次获取数据时都认为不会被修改,因此不会上锁,但是在更新操作时会判断有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读比较写的操作。
Java中的乐观锁基本上都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,反之失败。
悲观锁
悲观锁的意思是悲观思想,即认为写多,遇到并发写的可能性高,每次获取数据时都认为会被修改,因此每次在读写数据时都会上锁,在读写数据时就会block直到拿到锁。
Java中的悲观锁就是Synchronized、AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
多线程是指从软件或者硬件上实现多个线程并发执行的技术,包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
Java中提供了notify()和notifyAll()两个方法来唤醒在某些条件下等待的线程。
当调用notify()方法时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。
当调用notifyAll()方法时,等待该锁的所有线程都会被唤醒,但是在执行剩余的代码之前,所有被唤醒的线程都将争夺锁。
如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的notifyAll()方法(唤醒所有 wait 线程)或notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争优先级高的线程竞争到对象锁的概率大,若某线程没有竞争到该对象锁,它将会留在锁池中,唯有线程再次调用wait()方法,才会重新回到等待池中。
而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,释放掉该对象锁,此时锁池中的线程会继续竞争该对象锁。
因此,notify()和notifyAll()之间的关键区别在于notify()只会唤醒一个线程,而notifyAll()方法将唤醒所有线程。
ThreadLocal并非是一个线程本地实现版本,它并不是一个Thread,而是thread local variable(线程局部变量)。也许把它命名为ThreadLocalVar更合适。
线程局部变量(ThreadLocal)功能非常简单,就是为每一个使用该变量的线程都提供了一个变量值副本,是Java中一种较为特殊的线程绑定机制,使得每一个线程都独立地改变所具有的副本,而不会和其他线程的副本冲突。
Java程序的跨平台特性主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将字节码文件解释成为特定的机器码进行运行。
因此在运行时,Java源程序需要通过编译器编译成为.class文件。
众所周知java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳,它会装载jvm.dll(windows下,下皆以windows平台为例,linux下和solaris下其实类似,为:libjvm.so),这个动态连接库才是java虚拟机的实际操作处理所在。
JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。
使用JVM就是为了支持与操作系统无关,实现跨平台。所以,JAVA虚拟机JVM是属于JRE的,而现在我们安装JDK时也附带安装了JRE(当然也可以单独安装JRE)。
Spring中文翻译过来是春天的意思,被称为J2EE的春天,是一个开源的轻量级的Java开发框架, 具有控制反转(IoC)和面向切面(AOP)两大核心。Java Spring框架通过声明式方式灵活地进行事务的管理,提高开发效率和质量。
Spring框架不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何 Java 应用都可以从Spring中受益。Spring框架还是一个超级粘合平台,除了自己提供功能外,还提供粘合其他技术和框架的能力。
1)IOC 控制反转
对象创建责任的反转,在Spring中BeanFacotory是IOC容器的核心接口,负责实例化,定位,配置应用程序中的对象及建立这些对象间的依赖。XmlBeanFacotory实现BeanFactory接口,通过获取xml配置文件数据,组成应用对象及对象间的依赖关系。
Spring中有3中注入方式,一种是set注入,另一种是接口注入,另一种是构造方法注入。
2)AOP面向切面编程
AOP是指纵向的编程,比如两个业务,业务1和业务2都需要一个共同的操作,与其往每个业务中都添加同样的代码,通过写一遍代码,让两个业务共同使用这段代码。
Spring中面向切面编程的实现有两种方式,一种是动态代理,一种是CGLIB,动态代理必须要提供接口,而CGLIB实现是由=有继承。
Spring MVC属于Spring FrameWork的后续产品,已经融合在Spring Web Flow中。
Spring框架提供了构建Web应用程序的全功能MVC模块。
使用Spring可插入MVC架构,从而在使用Spring进行WEB开发时,可以选择使用Spring中的Spring MVC框架或集成其他MVC开发框架,如Struts1(已基本淘汰),Struts2(老项目还在使用或已重构)等。
通过策略接口,Spring框架是高度可配置的且包含多种视图技术,如JavaServer Pages(JSP)技术、Velocity、Tiles、iText和POI等。
Spring MVC 框架并不清楚或限制使用哪种视图,所以不会强迫开发者只使用JSP技术。
Spring MVC分离了控制器、模型对象、过滤器以及处理程序对象的角色,这种分离让它们更容易进行定制。
Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。
Spring Boot框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。
2014年4月发布第一个版本的全新开源的Spring Boot轻量级框架。它基于Spring4.0设计,不仅继承了Spring框架原有的优秀特性,而且还通过简化配置来进一步简化了Spring应用的整个搭建和开发过程。
另外Spring Boot通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决。
Spring Cloud是一系列框架的有序集合,它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。
Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
Spring Cloud的子项目,大致可分成两大类:
一类是对现有成熟框架“Spring Boot化”的封装和抽象,也是数量最多的项目;
第二类是开发一部分分布式系统的基础设施的实现,如Spring Cloud Stream扮演的是kafka, ActiveMQ这样的角色。
对于快速实践微服务的开发者来说,第一类子项目已经基本足够使用,如:
1)Spring Cloud Netflix是对Netflix开发的一套分布式服务框架的封装,包括服务的发现和注册,负载均衡、断路器、REST客户端、请求路由等;
2)Spring Cloud Config将配置信息中央化保存, 配置Spring Cloud Bus可以实现动态修改配置文件;
3)Spring Cloud Bus分布式消息队列,是对Kafka, MQ的封装;
4)Spring Cloud Security对Spring Security的封装,并能配合Netflix使用;
5)Spring Cloud Zookeeper对Zookeeper的封装,使之能配置其它Spring Cloud的子项目使用;
6)Spring Cloud Eureka是Spring Cloud Netflix微服务套件中的一部分,它基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务治理功能。注意的是从2.x起,官方不会继续开源,若需要使用2.x,风险还是有的。但是我觉得问题并不大,eureka目前的功能已经非常稳定,就算不升级,服务注册/发现这些功能已经够用。consul是个不错的替代品,还有其他替代组件,后续篇幅会有详细赘述或关注微信公众号“Java精选”,有详细替代方案源码分享。
Spring Cloud优点:
1)服务拆分粒度更细,有利于资源重复利用,有利于提高开发效率,每个模块可以独立开发和部署、代码耦合度低;
2)可以更精准的制定优化服务方案,提高系统的可维护性,每个服务可以单独进行部署,升级某个模块的时候只需要单独部署对应的模块服务即可,效率更高;
3)微服务架构采用去中心化思想,服务之间采用Restful等轻量级通讯,比ESB更轻量,模块专一性提升,每个模块只需要关心自己模块所负责的功能即可,不需要关心其他模块业务,专一性更高,更便于功能模块开发和拓展;
4)技术选型不再单一,由于每个模块是单独开发并且部署,所以每个模块可以有更多的技术选型方案,如模块1数据库选择mysql,模块2选择用oracle也是可以的;
5)适于互联网时代,产品迭代周期更短。系统稳定性以及性能提升,由于微服务是几个服务共同组成的项目或者流程,因此相比传统单一项目的优点就在于某个模块提供的服务宕机过后不至于整个系统瘫痪掉,而且微服务里面的容灾和服务降级机制也能大大提高项目的稳定性;从性能而言,由于每个服务是单独部署,所以每个模块都可以有自己的一套运行环境,当某个服务性能低下的时候可以对单个服务进行配置或者代码上的升级,从而达到提升性能的目的。
Spring Cloud缺点:
1)微服务过多,治理成本高,不利于维护系统,服务之间接口调用成本增加,相比以往单项目的时候调用某个方法或者接口可以直接通过本地方法调用就能够完成,但是当切换成微服务的时候,调用方式就不能用以前的方式进行调试、目前主流采用的技术有http api接口调用、RPC、WebService等方式进行调用,调用成本比单个项目的时候有所增加;
2)分布式系统开发的成本高(容错,分布式事务等)对团队挑战大
2)独立的数据库,微服务产生事务一致性的问题,由于各个模块用的技术都各不相同、而且每个服务都会高并发进行调用,就会存在分布式事务一致性的问题;
3)分布式部署,造成运营的成本增加、相比较单个应用的时候,运营人员只需要对单个项目进行部署、负载均衡等操作,但是微服务的每个模块都需要这样的操作,增加了运行时的成本;
4)由于整个系统是通过各个模块组合而成的,因此当某个服务进行变更时需要对前后涉及的所有功能进行回归测试,测试功能不能仅限于当个模块,增加了测试难度和测试成本;
总体来说优点大过于缺点,目前看来Spring Cloud是一套非常完善的微服务框架,目前很多企业开始用微服务,Spring Cloud的优势是显而易见的。
Dubbo(读音[ˈdʌbəʊ])是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和Spring框架无缝集成。
Dubbo提供了六大核心能力:面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可扩展能力,运行期流量调度,可视化的服务治理与运维。
Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
核心组件
Remoting: 网络通信框架,实现了 sync-over-async 和request-response 消息机制;
RPC: 一个远程过程调用的抽象,支持负载均衡、容灾和集群功能;
Registry: 服务目录框架用于服务的注册和服务事件发布和订阅。
dubbo协议(推荐使用)
单一TCP长连接和NIO异步通讯,适合大并发小数据量的服务调用,以及服务消费者远大于提供者的情况。
缺点是Hessian二进制序列化,不适合传送大数据包的服务
rmi协议
采用JDK标准的rmi协议实现,传输参数和返回参数对象需要实现Serializable接口。
使用java标准序列化机制,使用阻塞式短连接,传输数据包不限,消费者和提供者个数相当。
多个短连接,TCP协议传输,同步传输,适用常规的远程服务调用和rmi互操作。
缺点是在依赖低版本的Common-Collections包,java反序列化存在安全漏洞,需升级commons-collections3 到3.2.2版本或commons-collections4到4.1版本。
webservice协议
基于WebService的远程调用协议(Apache CXF的frontend-simple和transports-http)实现,提供和原生WebService的互操作。
多个短连接,基于HTTP传输,同步传输,适用系统集成和跨语言调用。
http协议
基于Http表单提交的远程调用协议,使用Spring的HttpInvoke实现,对传输数据包不限,传入参数大小混合,提供者个数多于消费者。
缺点是不支持传文件,只适用于同时给应用程序和浏览器JS调用。
hessian协议
集成Hessian服务,基于底层Http通讯,采用Servlet暴露服务,Dubbo内嵌Jetty作为服务器实现,可与Hession服务互操作。
通讯效率高于WebService和Java自带的序列化。
适用于传输大数据包(可传文件),提供者比消费者个数多,提供者压力较大。
缺点是参数及返回值需实现Serializable接口,自定义实现List、Map、Number、Date、Calendar等接口
thrift协议
协议:对thrift原生协议的扩展添加了额外的头信息,使用较少,不支持传null值。
memcache协议
基于memcached实现的RPC协议。
redis协议
基于redis实现的RPC协议。
推荐使用Zookeeper作为注册中心,还有Redis、Multicast、Simple注册中心,但不推荐。
Dubbo是阿里巴巴内部使用的分布式业务框架,于2012年由阿里巴巴开源。
由于Dubbo在阿里巴巴内部经过广泛的业务验证,在很短时间内,Dubbo就被许多互联网公司所采用,并产生了许多衍生版本,如网易,京东,新浪,当当等等。
由于阿里巴巴内部策略的调整变化,在2014年10月Dubbo停止维护。随后部分互联网公司公开了自行维护的Dubbo版本,比较著名的如当当DubboX,新浪Motan等。
在2017年9月,阿里宣布重启Dubbo项目,并决策在未来对开源进行长期持续的投入。随后Dubbo开始了密集的更新,并将搁置三年以来大量分支上的特性及缺陷快速修正整合。
在2018年2月,阿里巴巴将Dubbo捐献给Apache基金会,Dubbo成为Apache孵化器项目。
Dubbo有六种集群容错方案,默认是Failover Cluster。
集群容错方案 | 说明 |
---|---|
Failover Cluster | 失败自动切换,自动重试其它服务器(默认) |
Failfast Cluster | 快速失败,立即报错,只发起一次调用 |
Failsafe Cluster | 失败安全,出现异常时,直接忽略 |
Failback Cluster | 失败自动恢复,记录失败请求,定时重发 |
Forking Cluster | 并行调用多个服务器,只要一个成功即返回 |
Broadcast Cluster | 广播逐个调用所有提供者,任意一个报错则报错 |
Spring Container
自动加载META-INF/spring目录下的所有Spring配置。这个在源码中已经“写死”:
DEFAULT_SPRING_CONFIG = "classpath*:META-INF/spring/*.xml"
因此服务端主配置需放到META-INF/spring/目录下。
Jetty Container
启动一个内嵌Jetty,用于汇报状态,配置在java命令-D参数或dubbo.properties中
dubbo.jetty.port=8080 ----配置jetty启动端口
dubbo.jetty.directory=/foo/bar ----配置可通过jetty直接访问的目录,用于存放静态文件
dubbo.jetty.page=log,status,system ----配置显示的页面,缺省加载所有页面
Log4j Container
自动配置log4j的配置,在多进程启动时,自动给日志文件按进程分目录。 配置在java命令-D参数或者dubbo.properties中
dubbo.log4j.file=/foo/bar.log ----配置日志文件路径
dubbo.log4j.level=WARN ----配置日志级别
dubbo.log4j.subdirectory=20880 ----配置日志子目录,用于多进程启动,避免冲突
Dubbo默认使用Netty框架,也是推荐的选择。
Dubbo还集成有Mina、Grizzly。
Dubbo允许配置多协议,在不同服务上支持不同协议或同一服务上同时支持多种协议。
Dubbo默认是同步等待结果阻塞的,支持异步调用。
Dubbo是基于NIO的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小,异步调用会返回一个Future对象。
Dubbo 2.2.0以上版本支持服务降级。
通过dubbo:reference中设置mock="return null"实现服务降级。
mock的值可以修改为true,再跟接口同一个路径下实现一个Mock类,命名规则是 “接口名称+Mock” 后缀。最后在Mock类里实现降级逻辑。
Dubbo是通过JDK的ShutdownHook来完成优雅停机的,所以如果使用kill -9 PID等强制关闭指令,是不会执行优雅停机的,只有通过kill PID时,才会执行。
/ | Dubbo | Spring Cloud |
---|---|---|
服务注册中心 | Zookeeper | Spring Cloud Netfix Eureka |
服务调用方式 | RPC | REST API |
服务监控 | Dubbo-monitor | Spring Boot Admin |
熔断器 | 不完善 | Spring Cloud Netflix Hystrix |
服务网关 | 无 | Spring Cloud Netflix Zuul |
分布式配置 | 无 | Spring Cloud Config |
服务跟踪 | 无 | Spring Cloud Sleuth |
数据流 | 无 | Spring Cloud Stream |
批量任务 | 无 | Spring Cloud Task |
信息总线 | 无 | Spring Cloud Bus |
最大区别
Dubbo底层是使用Netty这样的NIO框架,是基于TCP协议传输的,配合以Hession序列化完成RPC通信。
SpringCloud是基于Http协议+rest接口调用远程过程的通信,相对来说,Http请求会有更大的报文,占的带宽也会更多。
但是REST相比RPC更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖,这在强调快速演化的微服务环境下,显得更为合适,至于注重通信速度还是方便灵活性,具体情况具体考虑。
背景区别
Dubbo是来源于阿里团队,Spring Cloud是来源于Spring团队,Spring广泛遍布全球各种企业开发中,可以确保SpringCloud的后续更新维护,Dubbo虽然来自国内顶尖的阿里团队,但是曾经被阿里弃用停更,但是后来阿里又低调重启维护。
定位区别
Dubbo是SOA时代的产物,它的关注点主要在于服务的调用,流量分发、流量监控和熔断。
Spring Cloud诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了Spirng、Spirng Boot 的优势之上,两个框架在开始目标就不一致,Dubbo定位服务治理、Spirng Cloud是一个生态。
因此可以大胆地判断,Dubbo未来会在服务治理方面更为出色,而Spring Cloud在微服务治理上面无人能敌。
模块区别
1、Dubbo主要分为服务注册中心,服务提供者,服务消费者,还有管控中心;
2、相比起Dubbo简单的四个模块,SpringCloud则是一个完整的分布式一站式框架,它有着一样的服务注册中心,服务提供者,服务消费者,管控台,断路器,分布式配置服务,消息总线,以及服务追踪等。
性能区别
Dubbo每次测试除去网络波动之外,都表现非常稳定。
Spring Cloud在第一次最慢,之后越来越快,连续测试4次以上单次测试性能超过Dubbo。
Spring Cloud-zuul在第一次最慢,之后也表现越来越快,连续4次以上测试,单次性能与dubbo相近,相差不超过0.02ms。
事务(transaction)是指数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元)。
通俗的说就是事务可以作为一个单元的一组有序的数据库操作。如果组中的所有操作都成功,则认为事务成功,即使只有一个操作失败,事务也不成功。如果所有操作完成,事务则提交,其修改将作用于所有其他数据库进程。如果一个操作失败,则事务将回滚,该事务所有操作的影响都将取消。
redis是一个高性能的key-value数据库,它是完全开源免费的,而且redis是一个NOSQL类型数据库,是为了解决高并发、高扩展,大数据存储等一系列的问题而产生的数据库解决方案,是一个非关系型的数据库。但是,它也是不能替代关系型数据库,只能作为特定环境下的扩充。
redis是一个以key-value存储的数据库结构型服务器,它支持的数据结构类型包括:字符串(String)、链表(lists)、哈希表(hash)、集合(set)、有序集合(Zset)等。为了保证读取的效率,redis把数据对象都存储在内存当中,它可以支持周期性的把更新的数据写入磁盘文件中。而且它还提供了交集和并集,以及一些不同方式排序的操作。
MyBatis框架是一个优秀的数据持久层框架,在实体类和SQL语句之间建立映射关系,是一种半自动化的ORM实现。其封装性要低于Hibernate,性能优秀,并且小巧。
ORM即对象/关系数据映射,也可以理解为一种数据持久化技术。
MyBatis的基本要素包括核心对象、核心配置文件、SQL映射文件。
数据持久化是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称。
ZooKeeper由雅虎研究院开发,ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,后来托管到Apache,是Hadoop和Hbase的重要组件。
ZooKeeper是一个经典的分布式数据一致性解决方案,致力于为分布式应用提供一个高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务。
ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
ZooKeeper包含一个简单的原语集,提供Java和C的接口。
ZooKeeper代码版本中,提供了分布式独享锁、选举、队列的接口,代码在$zookeeper_home\src\recipes。其中分布锁和队列有Java和C两个版本,选举只有Java版本。
于2010年11月正式成为Apache的顶级项目。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
分布式应用程序可以基于ZooKeeper实现数据发布与订阅、负载均衡、命名服务、分布式协调与通知、集群管理、Leader选举、分布式锁、分布式队列等功能。
Linux全称GNU/Linux,是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX的多用户、多任务、支持多线程和多CPU的操作系统。
伴随着互联网的发展,Linux得到了来自全世界软件爱好者、组织、公司的支持。它除了在服务器方面保持着强劲的发展势头以外,在个人电脑、嵌入式系统上都有着长足的进步。使用者不仅可以直观地获取该操作系统的实现机制,而且可以根据自身的需要来修改完善Linux,使其最大化地适应用户的需要。
Linux不仅系统性能稳定,而且是开源软件。其核心防火墙组件性能高效、配置简单,保证了系统的安全。
在很多企业网络中,为了追求速度和安全,Linux不仅仅是被网络运维人员当作服务器使用,甚至当作网络防火墙,这是Linux的一大亮点。
Linux具有开放源码、没有版权、技术社区用户多等特点,开放源码使得用户可以自由裁剪,灵活性高,功能强大,成本低。尤其系统中内嵌网络协议栈,经过适当的配置就可实现路由器的功能。这些特点使得Linux成为开发路由交换设备的理想开发平台。
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。
数据结构(data structure)是带有结构特性的数据元素的集合,它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系,并对这种结构定义相适应的运算,设计出相应的算法,并确保经过这些运算以后所得到的新结构仍保持原来的结构类型。
简而言之,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,即带“结构”的数据元素的集合。“结构”就是指数据元素之间存在的关系,分为逻辑结构和存储结构。
数据的逻辑结构和物理结构是数据结构的两个密切相关的方面,同一逻辑结构可以对应不同的存储结构。算法的设计取决于数据的逻辑结构,而算法的实现依赖于指定的存储结构。
数据结构的研究内容是构造复杂软件系统的基础,它的核心技术是分解与抽象。
通过分解可以划分出数据的3个层次;再通过抽象,舍弃数据元素的具体内容,就得到逻辑结构。类似地,通过分解将处理要求划分成各种功能,再通过抽象舍弃实现细节,就得到运算的定义。
上述两个方面的结合可以将问题变换为数据结构。这是一个从具体(即具体问题)到抽象(即数据结构)的过程。
然后,通过增加对实现细节的考虑进一步得到存储结构和实现运算,从而完成设计任务。这是一个从抽象(即数据结构)到具体(即具体实现)的过程。
Maven即为项目对象模型(POM),它可以通过一小段描述信息来管理项目的构建,报告和文档的项目管理工具软件。
Maven 除了以程序构建能力为特色之外,还提供高级项目管理工具。
由于Maven的缺省构建规则有较高的可重用性,所以常常用两三行Maven构建脚本就可以构建简单的项目。
由于Maven面向项目的方法,许多Apache Jakarta项目发文时使用Maven,而且公司项目采用Maven的比例在持续增长,相比较Gradle,在之后的篇幅中会说明,欢迎大家关注微信公众号“Java精选”。
Maven这个单词来自于意第绪语(犹太语),意为知识的积累,最初在Jakata Turbine项目中用来简化构建过程。
当时有一些项目(有各自Ant build文件),仅有细微的差别,而JAR文件都由CVS来维护。于是希望有一种标准化的方式构建项目,一个清晰的方式定义项目的组成,一个容易的方式发布项目的信息,以及一种简单的方式在多个项目中共享JARs。
MQ全称为Message Queue 消息队列(MQ)是一种应用程序对应用程序的通信方法。
消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。
MQ是消费生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取队列中的消息。
消息生产者只需要把消息发布到MQ中而不用管谁来获取,消息消费者只管从MQ中获取消息而不管是谁发布的消息,这样生产者和消费者双方都不用清楚对方的存在。
目前在生产环境,使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ等。
设计模式(Design pattern) 是解决软件开发某些特定问题而提出的一些解决方案也可以理解成解决问题的一些思路,通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性好。使用设计模式最终的目的是实现代码的高内聚和低耦合。
高内聚低耦合是软件工程中的概念,是判断软件设计好坏的标准,主要用于程序的面向对象的设计,主要看类的内聚性是否高,耦合度是否低。
目的是使程序模块的可重用性、移植性大大增强。
通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低。
内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事,它描述的是模块内的功能联系;耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。
Nginx(engine x)是一个轻量级、高性能的HTTP和反向代理web服务器,同时也是一个IMAP、POP3、SMTP代理服务器。
Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点(俄文:Рамблер)开发的,第一个公开版本0.1.0发布于2004年10月4日。
Nginx相比较Apache、lighttpd具有占有内存少,稳定性高等优势,并且依靠并发能力强,丰富的模块库以及友好灵活的配置而闻名。
1)按键盘WIN+R键,打开后在运行框中输入“CMD”命令,点击确定。
2)在CMD窗口,输入“netstat -ano”命令,按回车键,即可查看所有的端口占用情况。
3)找到本地地址一览中类似“0.0.0.0:8080”信息,通过此列查看8080端口对应的程序PID。
4)打开任务管理器,详细信息找到对应的应用PID(若不存在通过设置可以调出来),右键结束任务即可。
应用层协议(application layer protocol)定义了运行在不同端系统上的应用程序进程如何相互传递报文。
应用层协议
1)DNS:一种用以将域名转换为IP地址的Internet服务,域名系统DNS是因特网使用的命名系统,用来把便于人们使用的机器名字转换为IP地址。
现在顶级域名TLD分为三大类:国家顶级域名nTLD;通用顶级域名gTLD;基础结构域名。
域名服务器分为四种类型:根域名服务器;顶级域名服务器;本地域名服务器;权限域名服务器。
2)FTP:文件传输协议FTP是因特网上使用得最广泛的文件传送协议。FTP提供交互式的访问,允许客户指明文件类型与格式,并允许文件具有存取权限。
基于客户服务器模式,FTP协议包括两个组成部分,一是FTP服务器,二是FTP客户端,提供交互式的访问面向连接,使用TCP/IP可靠的运输服务,主要功能:减少/消除不同操作系统下文件的不兼容性 。
3)telnet远程终端协议:telnet是一个简单的远程终端协议,它也是因特网的正式标准。又称为终端仿真协议。
4)HTTP:超文本传送协议,是面向事务的应用层协议,它是万维网上能够可靠地交换文件的重要基础。http使用面向连接的TCP作为运输层协议,保证了数据的可靠传输。
5)电子邮件协议SMTP:即简单邮件传送协议。SMTP规定了在两个相互通信的SMTP进程之间应如何交换信息。SMTP通信的三个阶段:建立连接、邮件传送、连接释放。
6)POP3:邮件读取协议,POP3(Post Office Protocol 3)协议通常被用来接收电子邮件。
7)远程登录协议(Telnet):用于实现远程登录功能。
8)SNMP:简单网络管理协议。由三部分组成:SNMP本身、管理信息结构SMI和管理信息MIB。SNMP定义了管理站和代理之间所交换的分组格式。SMI定义了命名对象类型的通用规则,以及把对象和对象的值进行编码。MIB在被管理的实体中创建了命名对象,并规定类型。
Servlet是用Java编写的服务器端程序, 其主要功能在于交互式地浏览和修改数据,生成动态Web内容。
狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,我们一般将Servlet理解为后者。
Docker是一个容器化平台,它以容器的形式将你的应用程序及所有的依赖项打包在一起,以确保你的应用程序在任何环境中无缝运行。
到目前为止,Docker看起来还很像一个典型的Linux虚拟化栈。实际上,Docker镜像的第二层是root文件系统rootfs,它位于引导文件系统之上。rootfs可以是一种或多种操作系统(如Debian或者 Ubuntu文件系统)。
在传统的Linux 引导过程中,root文件系统会最先以只读的方式加载,当引导结束并完成了完整性检查之后,它才会被切换为读写模式。但是在Docker中root文件系统永远只能是只读状态,并且Docker利用联合加载(union mount)技术又会在root文件系统层加载更多的只读文件系统。联合加载指的是一次同时加载多个文件系统,但是在外面看起来只能看到一个文件系统。联合加载会将各层文件系统叠加到一起,这样最终的文件系统会包含所有底层的文件和目录。
Docker将这样的文件系统称为镜像。一个镜像可以放到另一个镜像的顶部。位于下面的镜像称为父镜像(parent image),可以依次类推,直到镜像栈的最底部,最底部的镜像称为基础镜像(base image)。最后,当从一个镜像启动容器时,Docker会在该镜像的最顶层加载一个读写文件系统。如果想在Docker中运行的程序就是在这个读写层中执行的。
Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),它的并发性能得到了很大提高。难能可贵的是,在保证快速和易用性的同时,并没有丧失可维护性和性能等优势。使用它可以快速简单地开发网络应用程序。
Netty极大地简化并优化了TCP和UDP套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
支持多种协议如FTP、SMTP、HTTP以及各种二进制和基于文本的传统协议。
官方话术就是Netty成功地找到了一种在不妥协可维护性和性能的情况下实现易于开发,性能,稳定性和灵活性的方法。
1、Netty是一款基于NIO(Nonblocking IO,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。
2、Netty的传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。
3、Netty封装了NIO操作的很多细节,统一的API,支持多种传输类型,阻塞和非阻塞的,提供了易于使用调用接口。
4、Netty简单而强大的线程模型。
5、Netty自带编解码器解决TCP粘包/拆包问题。
6、Netty自带各种协议栈。
7、Netty真正的无连接数据包套接字支持。
7、Netty比直接使用Java核心API有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。
8、Netty安全性不错,有完整的SSL/TLS 以及 StartTLS支持。
9、Netty是活跃的开源项目,版本迭代周期短,bug 修复速度快。。
10、Netty成熟稳定,经历了大型项目的使用和考验,而且很多开源项目都使用到了Netty, 比如经常接触的Dubbo、RocketMQ等等。
ES是一种开源、RESTful、可扩展的基于文档的搜索引擎,它构建在Lucene库上。
用户使用Kibana就可以可视化使用数据,同时Kibana也提供交互式的数据状态呈现和数据分析。
Apache Lucene搜索引擎基于JSON文档来进行搜索管理和快速搜索。
Elasticsearch,可简称为ES(官方未给出简称名字,很多人都这么叫而已)一种开源、RESTful、可扩展的基于文档的搜索引擎,构建是在Apache Lucene库的基础上的搜索引擎,无论在开源还是专有领域,Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。 但是,Lucene只是一个库。想要发挥其强大的作用,需使用Java并要将其集成到应用中。
Elasticsearch是使用Java编写并使用Lucene来建立索引并实现搜索功能,但是它的目的是通过简单连贯的RESTful API让全文搜索变得简单并隐藏Lucene的复杂性。
用户通过JSON格式的请求,使用CRUD的REST API就可以完成存储和管理文本、数值、地理空间、结构化或者非结构化的数据。
Elasticsearch不仅是Lucene和全文搜索引擎,它还提供:
1、分布式的实时文件存储,每个字段都被索引并可被搜索
2、实时分析的分布式搜索引擎
3、可以扩展到上百台服务器,处理PB级结构化或非结构化数据,而且所有的这些功能被集成到一台服务器,应用可以通过简单的RESTful API、各种语言的客户端甚至命令行与之交互。
Elasticsearch非常简单,它提供了许多合理的缺省值,并对初学者隐藏了复杂的搜索引擎理论。开箱即用(安装即可使用),只需很少的学习既可在生产环境中使用。Elasticsearch在Apache 2 license下许可使用,可以免费下载、使用和修改。
ElasticSearch是基于lucence开发的,也就是运行时需要java jdk支持。所以要先安装JAVA环境。
注意:由于ElasticSearch 5.x往后依赖于JDK 1.8及以上版本,安装高版本ES需要考虑与JDK版本的兼容性问题。
节点是指ElasticSearch的实例。当启动Elasticsearch的实例,就会启动至少一个节点。
相同集群名的多个节点的连接就组成了一个集群,在默认情况下,集群中的每个节点都可以处理http请求和集群节点间的数据传输,集群中所有的节点都知道集群中其他所有的节点,可以将客户端请求转发到适当的节点。
节点有以下类型:
主(master)节点:在一个节点上当node.master设置为True(默认)的时候,它有资格被选作为主节点,控制整个集群。
数据(data)节点:在一个节点上node.data设置为True(默认)的时候。该节点保存数据和执行数据相关的操作,如增删改查,搜索,和聚合。
客户端节点:当一个节点的node.master和node.data都设置为false的时候,它既不能保持数据也不能成为主节点,该节点可以作为客户端节点,可以响应用户的情况,并把相关操作发送到其他节点。
部落节点:当一个节点配置tribe.*的时候,它是一个特殊的客户端,它可以连接多个集群,在所有连接的集群上执行搜索和其他操作。
1、针对节点按照分工职责进行划分;
2、生产集群建议部署3台以上单独作为master节点,只负责成为主节点,维护整个集群的状态。
3、设置一批data节点,负责存储数据、建立索引和查询索引服务。对磁盘,内存要求相对较高。
4、设置一批client节点,只负责处理用户请求,实现请求转发,负载均衡等功能。
ElasticSearch分片是指随着索引文件的增加,磁盘容量、处理能力都会变得不足,在这种情况下,将索引数据切分成很多文件块,也可以称为数据的最小单元块。整个ES集群的核心就是对所有分片的分布、索引、负载、路由等提高处理效率。
举例说明:
假设IndexA有2个分片,向IndexA中插入100条数据 (100个文档),那么这100条数据会尽可能平均的分为50条存储在第一个分片,剩下的50条会存储在另一个分片中。
有点类似主流关系型数据库的表分区的概念。
ElasticSearch支持的类型查询主要分为匹配(文本)查询和基于Term的查询。
文本查询包括基本匹配、match phrase、multi-match、match phrase prefix、common terms、query-string、simple query string等。
Term查询包括term exists、type、term set、range、prefix、ids、wildcard、regexp、and fuzzy等。
Spark是一种快速、通用、可扩展的大数据分析引擎。
2009年诞生于加州大学伯克利分校AMPLab。2010年开源,2013年6月成为Apache孵化项目。2014年2月成为Apache顶级项目。
目前,Spark生态系统已经发展成为一个包含多个子项目的集合,其中包含SparkSQL、SparkStreaming、GraphX、MLlib等子项目。
Spark是基于内存计算的大数据并行计算框架。Spark基于内存计算,提高了在大数据环境下数据处理的实时性,同时保证了高容错性和高可伸缩性,允许用户将Spark部署在大量廉价硬件之上,形成集群。
Spark得到了众多大数据公司的支持,这些公司包括Hortonworks、IBM、Intel、Cloudera、MapR、Pivotal、百度、阿里、腾讯、京东、携程、优酷土豆。
当前百度的Spark已应用于凤巢、大搜索、直达号、百度大数据等业务;阿里利用GraphX构建了大规模的图计算和图挖掘系统,实现了很多生产系统的推荐算法;腾讯Spark集群达到8000台的规模,是当前已知的世界上最大的Spark集群。
1)本地模式:适用于测试。
2)standalone 模式:使用spark自带的资源调度框架。
3)spark on yarn 模式:最流行的方式,使用yarn集群调度资源。
4)mesos模式:国外使用比较多。
1)面试官您好,我叫Java精选公众号(面试者姓名),本科毕业(211或985重点突出一下),来自于更多内容关注Java精选公众号(省份、市区)。从网上看到公司招聘的这个职位,觉得自己比较适合公司的岗位,对自己的发展也有帮助,所以来这里争取下这份工作。
2)接下来主要讲解一下自己的核心竞争力(最擅长的技能、最深入研究的知识领域、最积极的部分)和闪光点(做过最成功的事,主要的成就)。核心就是要体现自己胜任这份工作(介绍时候要自然有底气,合情合理,面试前要充分准备好)。
初级Java工程师
面试初级Java工程师岗位时,首先应该重点突出一下自身的知识结构,因为面试初级岗位的应聘者往往没有实际工作经验,因此要强调自身的知识结构体系。如果是计算机相关专业的应届生,那么一定要首先突出描述自己的学历背景;因为学历背景是自身知识结构一个有力的“敲门砖”,如果是非计算机专业的毕业生,则要重点突出说一下自身的学习经历,强调自己的学习能力,与此同时突出一下自己的动手和实践能力。最后不管是计算机专业还是非计算机专业都要重点描述一下掌握了哪些Java技术,比如开源框架、数据库、大数据、消息队列、缓存等等。
总的来说,初级Java工程师岗位的面试需要在自我介绍中突出三点内容,其一是自身的知识结构;其二是独立开发能力;其三是学习能力,这三个方面的表述往往能够决定最终的面试结果。
中高级Java工程师
面试中高级Java工程师岗位时,首先应该重点强调一下自己的项目开发经验,从技术选型、方案设计到具体编码都要有一个清晰的思路来描述,在描述过程中要把自己的开发经验突出一下,比如经历了哪些不同的开发场景,解决了哪些疑难问题等,如果具有较多的行业经验,也一定要突出一下(多行业经验有时未必是好事,建议挑重点行业或者面试企业对口行业)。
面试中高级Java工程师岗位时虽然比较看重经验,但同时也比较看重应聘者的沟通交流能力,因为不少开发问题说到底就是沟通不清楚导致的问题,沟通能力较强的程序员往往能够更高效地完成工作任务。
3)先介绍到这里,面试官看看您还有什么问题,我可以再补充。
4)在最后自我介绍结束时要说谢谢,任何面试官都喜欢有礼貌的人!
注意:切记不要自曝其短(说自己的缺点),在进行自我介绍的时候千万不要和简历有冲突!(有的会说简历未更新,那也是面试者自身问题)
内容持续更新中…欢迎大家持续关注!!!全量认准公众号Java精选,回复Java面试,获取面试资料!!!