Java经典面试题最详细版(面试必备)二
Java语言采用何种编码方案?有何特点?*
Java语言采用Unicode编码标准,它为每个字符制订了一个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。
访问修饰符 **
在Java编程语言中有四种权限访问控制符,这四种访问权限的控制符能够控制类中成员的可见性。其中类有两种public、default。而方法和变量有 4 种:public、default、protected、private。
public : 对所有类可见。使用对象:类、接口、变量、方法
protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
default : 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
image.png
运算符 *
&&和&
&&和&都可以表示逻辑与,但他们是有区别的,共同点是他们两边的条件都成立的时候最终结果才是true;不同点是&&只要是第一个条件不成立为false,就不会再去判断第二个条件,最终结果直接为false,而&判断的是所有的条件。
||和|
||和|都表示逻辑或,共同点是只要两个判断条件其中有一个成立最终的结果就是true,区别是||只要满足第一个条件,后面的条件就不再判断,而|要对所有的条件进行判断。
关键字
static关键字 ***
static关键字的主要用途就是方便在没有创建对象时调用方法和变量和优化程序性能
1.static变量(静态变量)
用static修饰的变量被称为静态变量,也被称为类变量,可以直接通过类名来访问它。静态变量被所有的对象共享,在内存中只有一个副本,仅当在类初次加载时会被初始化,而非静态变量在创建对象的时候被初始化,并且存在多个副本,各个对象拥有的副本互不影响。
2.static方法(静态方法)
static方法不依赖于任何对象就可以进行访问,在static方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用,但是在非静态成员方法中是可以访问静态成员方法/变量的。
public class Main {
public static String s1 = "s1";//静态变量
String s2 = "s2";
public void fun1(){
System.out.println(s1);
System.out.println(s2);
}
public static void fun2(){
System.out.println(s1);
System.out.println(s2);//此处报错,静态方法不能调用非静态变量
}
}
3.static代码块(静态代码块)
静态代码块的主要用途是可以用来优化程序的性能,因为它只会在类加载时加载一次,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。如果程序中有多个static块,在类初次被加载的时候,会按照static块的顺序来执行每个static块。
public class Main {
static {
System.out.println("hello,word");
}
public static void main(String[] args) {
Main m = new Main();
}
}
4.可以通过this访问静态成员变量吗?(可以)
this代表当前对象,可以访问静态变量,而静态方法中是不能访问非静态变量,也不能使用this引用。
5.初始化顺序
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。如果存在继承关系的话,初始化顺序为父类中的静态变量和静态代码块——子类中的静态变量和静态代码块——父类中的实例变量和普通代码块——父类的构造函数——子类的实例变量和普通代码块——子类的构造函数
final 关键字 ***
final关键字主要用于修饰类,变量,方法。
类:被final修饰的类不可以被继承
方法:被final修饰的方法不可以被重写
变量:被final修饰的变量是基本类型,变量的数值不能改变;被修饰的变量是引用类型,变量便不能在引用其他对象,但是变量所引用的对象本身是可以改变的。
public class Main {
int a = 1;
public static void main(String[] args) {
final int b = 1;
b = 2;//报错
final Main m = new Main();
m.a = 2;//不报错,可以改变引用类型变量所指向的对象
}
}
final finally finalize区别 ***
final主要用于修饰类,变量,方法
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块
中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
finalize是一个属于Object类的一个方法,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,但Java语言规范并不保证inalize方法会被及时地执行、而且根本不会保证它们会被执行。
this关键字 **
重点掌握前三种即可
1.this关键字可用来引用当前类的实例变量。主要用于形参与成员名字重名,用this来区分。
public Person(String name, int age) {
this.name = name;
this.age = age;
}
2.this关键字可用于调用当前类方法。
public class Main {
public void fun1(){
System.out.println("hello,word");
}
public void fun2(){
this.fun1();//this可省略
}
public static void main(String[] args) {
Main m = new Main();
m.fun2();
}
}
3.this()可以用来调用当前类的构造函数。(注意:this()一定要放在构造函数的第一行,否则编译不通过)
class Person{
private String name;
private int age;
public Person() {
}
public Person(String name) {
this.name = name;
}
public Person(String name, int age) {
this(name);
this.age = age;
}
}
4.this关键字可作为调用方法中的参数传递。
5.this关键字可作为参数在构造函数调用中传递。
6.this关键字可用于从方法返回当前类的实例。super
super关键字 **
1.super可以用来引用直接父类的实例变量。和this类似,主要用于区分父类和子类中相同的字段
2.super可以用来调用直接父类构造函数。(注意:super()一定要放在构造函数的第一行,否则编译不通过)
3.super可以用来调用直接父类方法。
public class Main {
public static void main(String[] args) {
Child child = new Child("Father","Child");
child.test();
}
}
class Father{
protected String name;
public Father(String name) {
this.name = name;
}
public void Say(){
System.out.println("hello,child");
}
}
class Child extends Father{
private String name;
public Child(String name1, String name2) {
super(name1); //调用直接父类构造函数
this.name = name2;
}
public void test(){
System.out.println(this.name);
System.out.println(super.name); //引用直接父类的实例变量
super.Say(); //调用直接父类方法
}
}
this与super的区别 **
相同点:
super()和this()都必须在构造函数的第一行进行调用,否则就是错误的
this()和super()都指的是对象,所以,均不可以在static环境中使用。
不同点:
super()主要是对父类构造函数的调用,this()是对重载构造函数的调用
super()主要是在继承了父类的子类的构造函数中使用,是在不同类中的使用;this()主要是在同一类的不同构造函数中的使用
break ,continue ,return 的区别及作用 **
break结束当前的循环体
continue结束本次循环,进入下一次循环
return结束当前方法
面向对象和面向过程的区别 **
面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源。
缺点:没有面向对象易维护、易复用、易扩展
面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点:性能比面向过程低
面向对象三大特性(封装、继承、多态) ***
封装
封装就是隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别。
继承
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
多态(重要)
多态是同一个行为具有多个不同表现形式或形态的能力。这句话不是很好理解,可以看这个解释,在Java语言中,多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在Java中实现多态的三个必要条件:继承、重写、向上转型。继承和重写很好理解,向上转型是指在多态中需要将子类的引用赋给父类对象。
public class Main {
public static void main(String[] args) {
Person person = new Student(); //向上转型
person.run();
}
}
class Person {
public void run() {
System.out.println("Person");
}
}
class Student extends Person { //继承
@Override
public void run() { //重载
System.out.println("Student");
}
}
运行结果为
Student
面向对象五大基本原则是什么 **
单一职责原则(Single-Resposibility Principle)
一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
开放封闭原则(Open-Closed principle)
软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
里氏替换原则 (Liskov-Substituion Principle)
子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。
依赖倒置原则(Dependecy-Inversion Principle)
依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
接口隔离原则(Interface-Segregation Principle)
使用多个小的专门的接口,而不要使用一个大的总接口。
抽象类和接口的对比 ***
在Java语言中,abstract class和interface是支持抽象类定义的两种机制。抽象类:用来捕捉子类的通用特性的。接口:抽象方法的集合。
相同点:
接口和抽象类都不能实例化
都包含抽象方法,其子类都必须覆写这些抽象方法
不同点:
在Java中定义一个不做事且没有参数的构造方法的作用 *
Java程序存在继承,在执行子类的构造方法时,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。如果父类只定义了有参数的构造函数,而子类的构造函数没有用super调用父类那个特定的构造函数,就会出错。
在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是 *
帮助子类做初始化工作。
一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么? *
主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。
构造方法有哪些特性? **
方法名称和类同名
不用定义返回值类型
不可以写retrun语句
构造方法可以被重载
变量 **
类变量:独立于方法之外的变量,用 static 修饰。
实例变量:独立于方法之外的变量,不过没有 static 修饰。
局部变量:类的方法中的变量。
成员变量:成员变量又称全局变量,可分为类变量和实例变量,有static修饰为类变量,没有static修饰为实例变量。
image.png
内部类 **
内部类包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类
-
成员内部类
1.成员内部类定义为位于另一个类的内部,成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括
private
成员和静态成员)。<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative;">class Outer{
private double a = 0;
public static int b =1;
public Outer(double a) {
this.a = a;
}class Inner { //内部类 public void fun() { System.out.println(a); System.out.println(b); } }
}</pre>
2.当成员内部类拥有和外部类同名的成员变量或者方法时,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:外部类.
this
.成员变量3.在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问。
4.成员内部类是依附外部类而存在的,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative;">class Outter{
private double a = 0;
public static int b =1;
public Outter(){}
public Outter(double a) {
this.a = a;
Inner inner = new Inner();
inner.fun(); //调用内部类的方法
}class Inner { //内部类 int b = 2; public void fun() { System.out.println(a); System.out.println(b); //访问内部类的b System.out.println(Outter.this.b);//访问外部类的b } }
}
public class Main{
public static void main(String[] args) {
Outter outter = new Outter();
Outter.Inner inner = outter.new Inner(); //创建内部类的对象
}
}</pre> -
局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。在此我向大家推荐一个架构学习交流圈。交流学习指导伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative;">class Outter {
private int outter_a = 1; private static int static_b = 2; public void test1(){ int inner_c =3; class Inner { private void fun(){ System.out.println(outter_a); System.out.println(static_b); System.out.println(inner_c); } } Inner inner = new Inner(); //创建局部内部类 inner.fun(); } public static void test2(){ int inner_d =3; class Inner { private void fun(){ System.out.println(outter_a); //编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量 System.out.println(static_b); System.out.println(inner_d); } } Inner inner = new Inner(); inner.fun(); }
}</pre>
-
匿名内部类
匿名内部类只没有名字的内部类,在日常开发中使用较多。使用匿名内部类的前提条件:必须继承一个父类或实现一个接口。
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative;">interface Person {
public void fun();
}
class Demo {
public static void main(String[] args) {
new Person() {
public void fun() {
System.out.println("hello,word");
}
}.fun();
}
}</pre> -
静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字
static
。静态内部类是不需要依赖于外部类的,并且它不能使用外部类的非static
成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static
成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative;">class Outter {
int a = 1;
static int b = 2;
public Outter() {} static class Inner { public Inner() { System.out.println(a);//报错,静态内部类不能访问非静态变量 System.out.println(b); } }
}
public class Main{
public static void main(String[] args) {
Outter.Inner inner = new Outter.Inner();
}
}
</pre> -
内部类的优点:
- 内部类不为同一包的其他类所见,具有很好的封装性;
- 匿名内部类可以很方便的定义回调。
- 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
- 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。
-
局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上
final
?<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative;">public class Main {
public static void main(String[] args) {} public void fun(final int b) { final int a = 10; new Thread(){ public void run() { System.out.println(a); System.out.println(b); }; }.start(); }
}</pre>
对于变量
a
可以从生命周期的角度理解,局部变量直接存储在栈中,当方法执行结束后,非final
的局部变量就被销毁,而局部内部类对局部变量的引用依然存在,如果局部内部类要调用没有final
修饰的局部变量时,就会造成生命周期不一致出错。对于变量
b
,其实是将fun
方法中的变量b
以参数的形式对匿名内部类中的拷贝(变量b
的拷贝)进行赋值初始化。在run
方法中访问的变量b
根本就不是test
方法中的局部变量b
,而是一个拷贝值,所以不存在生命周期不一致的问题,但如果在run
方法中修改变量b
的值会导致数据不一致,所以需要加final
修饰。
重写与重载 ***
重载和重写的区别
- 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。
- 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为
private
则子类中就不是重写。
构造器(constructor)是否可被重写(override)
构造器可以被重载,不能被重写
重载的方法能否根据返回类型进行区分?为什么?
不能,因为调用时不能指定类型信息,编译器不知道你要调用哪个函数。
== 和 equals 的区别 ***
-
==
对于基本数据类型,
==
比较的是值;对于引用数据类型,==
比较的是内存地址。 -
eauals
对于没有重写
equals
方法的类,equals
方法和==
作用类似;对于重写过equals
方法的类,equals
比较的是值。
hashCode 与 equals(为什么重写equals方法后,hashCode方法也必须重写) ***
-
equals
先看下
String
类中重写的equals
方法。<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative;"> public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}</pre>从源码中可以看到:
-
equals
方法首先比较的是内存地址,如果内存地址相同,直接返回true
;如果内存地址不同,再比较对象的类型,类型不同直接返回false
;类型相同,再比较值是否相同;值相同返回true
,值不同返回false
。总结一下,equals
会比较内存地址、对象类型、以及值,内存地址相同,equals
一定返回true
;对象类型和值相同,equals
方法一定返回true
。 - 如果没有重写
equals
方法,那么equals
和==
的作用相同,比较的是对象的地址值。
-
-
hashCode
hashCode
方法返回对象的散列码,返回值是int
类型的散列码。散列码的作用是确定该对象在哈希表中的索引位置。关于
hashCode
有一些约定:- 两个对象相等,则
hashCode
一定相同。 - 两个对象有相同的
hashCode
值,它们不一定相等。 -
hashCode()
方法默认是对堆上的对象产生独特值,如果没有重写hashCode()
方法,则该类的两个对象的hashCode
值肯定不同
- 两个对象相等,则
-
为什么重写
equals
方法后,hashCode
方法也必须重写- 根据规定,两个对象相等,
hashCode
值也许相同,所以重写equals
方法后,hashCode
方法也必须重写(面试官肯定不是想听这个答案) -
hashCode
在具有哈希机制的集合中起着非常关键的作用,比如HashMap
、HashSet
等。以HashSet
为例,HashSet
的特点是存储元素时无序且唯一,在向HashSet
中添加对象时,首相会计算对象的HashCode
值来确定对象的存储位置,如果该位置没有其他对象,直接将该对象添加到该位置;如果该存储位置有存储其他对象(新添加的对象和该存储位置的对象的HashCode
值相同),调用equals
方法判断两个对象是否相同,如果相同,则添加对象失败,如果不相同,则会将该对象重新散列到其他位置。所以重写equals
方法后,hashCode
方法不重写的话,会导致所有对象的HashCode
值都不相同,都能添加成功,那么HashSet
中会出现很多重复元素,HashMap
也是同理(因为HashSet
的底层就是通过HashMap
实现的),会出现大量相同的Key
(HashMap
中的key
是唯一的,但不同的key
可以对应相同的value
)。所以重写equals
方法后,hashCode
方法也必须重写。同时因为两个对象的hashCode
值不同,则它们一定不相等,所以先计算对象的hashCode
值可以在一定程度上判断两个对象是否相等,提高了集合的效率。总结一下,一共两点:第一,在HashSet
等集合中,不重写hashCode
方法会导致其功能出现问题;第二,可以提高集合效率。
- 根据规定,两个对象相等,
Java 中是值传递还是引用传递,还是两者共存 **
这是一个很容易搞混又很难解释清楚的问题,先说结论,Java中只有值传递
先看这样一段代码
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class Main{
public static void main(String[] args) {
int a = 1;
printValue(a);
System.out.println("a:" + a);
}
public static void printValue(int b){
b = 2;
System.out.println("b:"+ b);
}
}</pre>
输出
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">b:2
a:1</pre>
可以看到将a
的值传到printValue
方法中,并将其值改为2。但方法调用结束后,a
的值还是1,并未发生改变,所以这种情况下为值传递。
再看这段代码
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class Main{
public static void main(String[] args) {
Preson p = new Preson();
p.name = "zhangsan";
printValue(p);
System.out.println("p.name: " + p.name);
}
public static void printValue(Preson q){
q.name = "lisi";
System.out.println("q.name: "+ q.name);
}
}
class Preson{
public String name;
}</pre>
输出结果
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">q.name: lisi
p.name: lisi</pre>
在将p
传入printValue
方法后,方法调用结束,p
的name
属性竟然被改变了!所以得出结论,参数为基本类型为值传递,参数为引用类型为时为引用传递。这个结论是错误的,下面来看看判断是值传递还是值传递的关键是什么,先看定义
- 值传递:是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
- 引用传递:是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
从定义中可以明显看出,区分是值传递还是引用传递主要是看向方法中传递的是实际参数的副本还是实际参数的地址。上面第一个例子很明显是值传递,其实第二个例子中向printValue
方法中传递的是一个引用的副本,只是这个副本引用和原始的引用指向的同一个对象,所以副本引用修改过对象属性后,通过原始引用查看对象属性肯定也是被修改过的。换句话说,printValue
方法中修改的是副本引用指向的对象的属性,不是引用本身,如果修改的是引用本身,那么原始引用肯定不受影响。看下面这个例子
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class Main{
public static void main(String[] args) {
Preson p = new Preson();
p.name = "zhangsan";
printValue(p);
System.out.println("p.name: " + p.name);
}
public static void printValue(Preson q){
q = new Preson();
q.name = "lisi";
System.out.println("q.name: "+ q.name);
}
}
class Preson{
public String name;
}</pre>
输出结果
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">q.name: lisi
p.name: zhangsan</pre>
可以看到将p
传入printValue
方法后,printValue
方法调用结束后,p
的属性name
没有改变,这是因为在printValue
方法中并没有改变副本引用q
所指向的对象,而是改变了副本引用q
本身,将副本引用q
指向了另一个对象并对这个对象的属性进行修改,所以原始引用p
所指向的对象不受影响。所以证明Java中只存在值传递。
IO流 *
Java IO流主要可以分为输入流和输出流。按照照操作单元划分,可以划分为字节流和字符流。按照流的角色划分为节点流和处理流。
Java I0流的40多个类都是从4个抽象类基类中派生出来的。
- InputStream:字节输入流
- Reader:字符输入流
- OutputStream:字节输出流
- Writer:字符输出流
BIO,NIO,AIO 有什么区别? **
-
BIO (Blocking I/O):服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务端资源要求比较高,并发局限于应用中,在jdk1.4以前是唯一的io
-
NIO (New I/O):服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,jdk1,4开始支持
-
AIO (Asynchronous I/O):服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成了再通知服务器用其启动线程进行处理。AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,jdk1.7开始支持。
这些概念看着比较枯燥,可以从这个经典的烧开水的例子去理解
**BIO **:来到厨房,开始烧水NIO,并坐在水壶面前一直等着水烧开。
NIO:来到厨房,开AIO始烧水,但是我们不一直坐在水壶前面等,而是做些其他事,然后每隔几分钟到厨房看一下水有没有烧开。
AIO:来到厨房,开始烧水,我们不一直坐在水壶前面等,而是在水壶上面装个开关,水烧开之后它会通知我。
反射 ***
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
Java获取Class对象的三种方式
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">class Person {
public String name = "zhangsan";
public Person() {
}
}</pre>
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class Main{
public static void main(String[] args) throws ClassNotFoundException {
//方式1
Person p1 = new Person();
Class c1 = p1.getClass();
//方式2
Class c2 = Person.class;
//方式3可能会抛出ClassNotFoundException异常
Class c3 = Class.forName("com.company");
}
}</pre>
因为在一个类在 JVM 中只会有一个 Class
实例,所以对c1
、c2
、c3
进行equals
比较时返回的都是true
。
反射优缺点:
- 优点:运行期类型的判断,动态加载类,提高代码灵活度。
- 缺点:性能比直接的java代码要慢很多。
反射应用场景:
- Java的很多框架都用到了反射,例如
Spring
中的xml的配置模式等 - 动态代理设计模式也采用了反射机制
JAVA异常 ***
异常主要分为Error
和Exception
两种
- Error:
Error
类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理。 - EXception:
Exception
以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
异常框图
[图片上传失败...(image-9eb7fd-1657097733640)]
除了以上的分类,异常还能分为非检查异常和检查异常
-
非检查异常(unckecked exception):该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有
try-catch
捕获它,也没有使用throws
抛出该异常,编译也会正常通过。因为这样的异常发生的原因很可能是代码写的有问题。 -
检查异常(checked exception):除了
Error
和RuntimeException
的其它异常。这是编译器要求必须处理的异常。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,所以必须处理这些异常。
下面来看下
try{}catch(){}finally{}
和return
之间的“恩恩怨怨”,这里有些乱,面试时问的也不是很多,实在记不住就算啦。还是先看代码猜结果。
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class Main{
public static void main(String[] args) {
int a = test1();
System.out.println(a);
int b = test2();
System.out.println(b);
int c = test3();
System.out.println(c);
int d = test4();
System.out.println(d);
int e = test5();
System.out.println(e);
}
public static int test1(){
int a = 1;
try{
a = 2;
return a;
}catch(Exception e){
System.out.println("hello,test1");
a = 3;
}finally{
a = 4;
}
return a;
}
//输出 2
public static int test2(){
int a = 1;
try{
a = 2;
return a;
}catch(Exception e){
System.out.println("hello,test2");
a = 3;
return a;
}finally{
a = 4;
}
}
//输出 2
public static int test3(){
int a = 1;
try{
a = 2/0;
return a;
}catch(Exception e){
System.out.println("hello,test3");
a = 3;
}finally{
a = 4;
}
return a;
}
//输出 hello,test3
// 4
public static int test4(){
int a = 1;
try{
a = 2/0;
return a;
}catch(Exception e){
System.out.println("hello,test4");
a = 3;
return a;
}finally{
a = 4;
}
}
//输出 hello,test4
// 3
public static int test5(){
int a = 1;
try{
a = 2/0;
return a;
}catch(Exception e){
a = 3;
return a;
}finally{
a = 4;
return a;
}
}
//输出 4
}</pre>
如果没有仔细的研究过,应该好多会猜错,下面总结下规律。
- 从前三个例子可以看出如果
try{}
中的代码没有异常,catch(){}
代码块中的代码不会执行。所以如果try{}
和catch(){}
都含有return
时,无异常执行try{}
中的return
,存在异常执行catch(){}
的return
。 - 不管任何情况,就算
try{}
或catch(){}
中含有return
,finally{}
中的代码一定会执行,那么为什么test1
、test2
、test3
中的结果不是4呢,因为虽然finally
是在return
后面的表达式运算之后执行的,但此时并没有返回运算之后的值,而是把值保存起来,不管finally
对该值做任何的改变,返回的值都不会改变,依然返回保存起来的值。也就是说方法的返回值是在finally
运算之前就确定了的。 - 如果
return
的数据是引用数据类型,而在finally
中对该引用数据类型的属性值的改变起作用,try
中的return
语句返回的就是在finally
中改变后的该属性的值。这个不理解可以看看上面提到的Java的值传递的问题。在此我向大家推荐一个架构学习交流圈。交流学习指导伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多 - 如果
finally{}
中含有return
,会导致程序提前退出,不在执行try{}
或catch(){}
中的return
。所以test5
返回的值是4。
最后总计一下try{}catch(){}finally{}
的执行顺序。
- 先执行
try
中的语句,包括return
后面的表达式; - 有异常时,执行
catch
中的语句,包括return
后面的表达式,无异常跳过catch
语句; - 然后执行
finally
中的语句,如果finally
里面有return
语句,执行return
语句,程序结束; -
finally{}
中没有return
时,无异常执行try
中的return
,如果有异常时则执行catch
中的return
。前两步执行的return
只是确定返回的值,程序并未结束,finally{}
执行之后,最后将前两步确定的return
的返回值返回。
JAVA注解 **
面试问的不多,但是在使用框架开发时会经常使用,但东西太多了,这里只是简单介绍下概念。
Annotation
注解可以看成是java中的一种标记记号,用来给java中的类,成员,方法,参数等任何程序元素添加一些额外的说明信息,同时不改变程序语义。注解可以分为三类:基本注解,元注解,自定义注解
-
标准注解
- @Deprecated:该注解用来说明程序中的某个元素(类,方法,成员变量等)已经不再使用,如果使用的话的编译器会给出警告。
- @SuppressWarnings(value=“”):用来抑制各种可能出现的警告。
- @Override:用来说明子类方法覆盖了父类的方法,保护覆盖方法的正确使用
-
元注解(元注解也称为元数据注解,是对注解进行标注的注解,元注解更像是一种对注解的规范说明,用来对定义的注解进行行为的限定。例如说明注解的生存周期,注解的作用范围等)
- @Target(value=“ ”):该注解是用来限制注解的使用范围的,即该注解可以用于哪些程序元素。
- @Retention(value=“ ”):用于说明注解的生存周期
- @Documnent:用来说明指定被修饰的注解可以被javadoc.exe工具提取进入文档中,所有使用了该注解进行标注的类在生成API文档时都在包含该注解的说明。
- @Inherited:用来说明使用了该注解的父类,其子类会自动继承该注解。
- @Repeatable:java1.8新出的元注解,如果需要在给程序元素使用相同类型的注解,则需将该注解标注上。
-
自定义注解:用@Interface来声明注解。
JAVA泛型 ***
Java 泛型是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
-
泛型擦除(这是面试考察泛型时经常问到的问题)
Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。看下面代码
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative;">public class Main{
public static void main(String[] args) {
ArrayList<Integer> arrayList1 = new ArrayList<>();
ArrayList<String> arrayList2 = new ArrayList<>();System.out.println(arrayList1.getClass() == arrayList2.getClass()); }
}</pre>
输出结果
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative;">true</pre>
可以看到
ArrayList<Integer>
和ArrayList<String>
的原始类型是相同,在编译成字节码文件后都会变成List
,JVM看到的只有List
,看不到泛型信息,这就是泛型的类型擦除。在看下面这段代码<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative;">public class Main{
public static void main(String[] args) throws Exception {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, "a");
System.out.println(arrayList.get(0));
System.out.println(arrayList.get(1));
}
}</pre>输出
<pre class="java hljs language-java" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; position: relative;">1
a</pre>可以看到通过反射进行
add
操作,ArrayList<Integer>
竟然可以存储字符串,这是因为在反射就是在运行期调用的add
方法,在运行期泛型信息已经被擦除。 -
既然存在类型擦除,那么Java是如何保证在
ArrayList<Integer>
添加字符串会报错呢?Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
JAVA序列化 **
-
序列化:将对象写入到IO流中
-
反序列化:从IO流中恢复对象
-
序列化的意义:将Java对象转换成字节序列,这些字节序列更加便于通过网络传输或存储在磁盘上,在需要时可以通过反序列化恢复成原来的对象。
-
实现方式:
- 实现Serializable接口
- 实现Externalizable接口
-
序列化的注意事项:
- 对象的类名、实例变量会被序列化;方法、类变量、
transient
实例变量都不会被序列化。 - 某个变量不被序列化,可以使用
transient
修饰。 - 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
- 反序列化时必须有序列化对象的
class
文件。
- 对象的类名、实例变量会被序列化;方法、类变量、
深拷贝与浅拷贝 ***
-
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,两个引用指向两个对象,但对象内容相同。
-
浅拷贝:对基本数据类型进行值传递,对引用数据类型复制一个引用指向原始引用的对象,就是复制的引用和原始引用指向同一个对象。
具体区别看下图
image.png
深拷贝的实现方式
重载clone方法
public class Main{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, CloneNotSupportedException {
Address s = new Address("天津");
Person p = new Person("张三", 23, s);
System.out.println("克隆前的地址:" + p.getAddress().getName());
Person cloneP = (Person) p.clone();
cloneP.getAddress().setName("北京");
System.out.println("克隆后的地址:" + cloneP.getAddress().getName());
}
}
class Address implements Cloneable {
private String city;
public Address(String name){
this.city = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public String getName() {
return city;
}
public void setName(String name) {
this.city = name;
}
}
class Person implements Cloneable{
private String name;
private int age;
private Address address;
public Person(String name, int age, Address address){
this.name = name;
this.age = age;
this.address = address;
}
@Override
public Object clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
person.address = (Address)address.clone();
return person;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
输出
克隆前的地址:天津
克隆后的地址:北京
其实就是Person类和Address类都要重写clone方法,这里面需要注意的一点是super.clone()为浅克隆,所以在在Person类中重写clone方法时,address对象需要调用address.clone()重新赋值,因为address类型为引用类型。
序列化
public class Main{
public static void main(String[] args) throws IOException, ClassNotFoundException {
Address s = new Address("天津");
Person p = new Person("张三", 23, s);
System.out.println("克隆前的地址:" + p.getAddress().getName());
Person cloneP = (Person) p.deepClone();
cloneP.getAddress().setName("北京");
System.out.println("克隆后的地址:" + cloneP.getAddress().getName());
}
}
class Address implements Serializable{
private String city;
public Address(String name){
this.city = name;
}
public String getName() {
return city;
}
public void setName(String name) {
this.city = name;
}
}
class Person implements Serializable{
private String name;
private int age;
private Address address;
public Person(String name, int age, Address address){
this.name = name;
this.age = age;
this.address = address;
}
public Object deepClone() throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
输出
克隆前的地址:天津
克隆后的地址:北京
常见的Object方法 ***
这些方法都很重要,面试经常会问到,要结合其他知识将这些方法理解透彻
Object clone():创建与该对象的类相同的新对象
boolean equals(Object):比较两对象是否相等
void finalize():当垃圾回收器确定不存在对该对象的更多引用时,对象垃圾回收器调用该方法
Class getClass():返回一个对象运行时的实例类
int hashCode():返回该对象的散列码值
void notify():唤醒等待在该对象的监视器上的一个线程
void notifyAll():唤醒等待在该对象的监视器上的全部线程
String toString():返回该对象的字符串表示
void wait():在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待