本篇是我看JavaGuide和《Java核心技术》总结的高频面试题。如有不对的,请在评论区指正。
1、Java 语言的特点(如果你简历上有提到 C++ 可能还会问你 Java 和 C++ 的区别)。【⭐⭐】
-
简单易学;
-
面向对象(封装,继承,多态);
-
平台无关性( Java 虚拟机实现平台无关性);
-
支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);
-
可靠性(具备异常处理和自动内存管理机制);
-
安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源);
-
高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的);
-
支持网络编程并且很方便;
-
编译与解释并存;
2、JavaSE & JavaEE
Java SE 是 Java 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序。
3、JVM & JRE & JDK【⭐⭐⭐】
JVM
Java虚拟机,是运行Java字节码文件的虚拟机。不同系统有不同的Java虚拟机。由于代码编译之后的字节码文件(.class)都一样,所以JVM是实现Java语言跨平台的核心。
JRE
Java运行时环境。用来运行Java程序的软件,不带开发工具。组成:JVM+Java基本类库
JDK
Java开发工具包。它是功能齐全的Java SDK。组成:JRE+工具(Javac、javadoc、jdb......)
4、什么是字节码文件?采用字节码文件的好处?
程序被编译之后产生的.class
文件。在不同的系统,字节码文件是一样的。
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C、 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
5、为什么说 Java 语言“解释与编译并存”。【⭐⭐】
编译型:编译型语言会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
解释型:解释型语言会通过解释器一句一句的将代码解释为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
Java程序先经过编译器编译成.class文件,然后通过解释器一句一句解释成机器可以看懂的代码
6、注释形式
单行注释//
多行注释/*和*/
文档注释/**和*/
7、标识符和关键字
标识符就是我们自己起的名字(类名、方法名)。关键字就是特殊的标识符
标识符书写规范:由字母、数字、货币符号以及“标点连接符”组成。第一个字符不能是数字。+和@之类的符号不能出现在标识符里,空格也不可以。标识符区分大小写。
8、移位运算符有哪些?
<<
:左移,高位丢弃,低位补零 相当于乘
>>
:带符号右移,高位补符号位,低位丢弃 相当于除
>>>
:无符号右移,忽略符号位,用0填充高位
由于float和double用二进制表示有点特殊,因此不能用来进行移位
如果移位的位数超过数值所占有的位数会怎样?
当 int 类型左移or右移位数大于等于 32 位操作时,会先求余(%)后再进行左移or右移操作。也就是说左移or右移 32 位相当于不进行移位操作(32%32=0),左移or右移 42 位相当于左移or右移 10 位(42%32=10)。当 long 类型进行左移or右移操作时,由于 long 对应的二进制是 64 位,因此求余操作的基数也变成了 64。
也就是说:x<<42
等同于x<<10
,x>>42
等同于x>>10
,x >>>42
等同于x >>> 10
。
9、Java 基本类型有哪几种,各占多少位?【⭐⭐】
Java 中有 8 种基本数据类型,分别为:
-
6 种数字类型:
-
4 种整数型:
byte
、short
、int
、long
-
2 种浮点型:
float
、double
-
-
1 种字符类型:
char
-
1 种布尔型:
boolean
。
这 8 种基本数据类型的默认值以及所占空间的大小如下:
基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
---|---|---|---|---|
byte | 8 | 1 | 0 | -128 ~ 127 |
short | 16 | 2 | 0 | -32768(-2^15) ~ 32767(2^15 - 1) |
int | 32 | 4 | 0 | -2147483648 ~ 2147483647 |
long | 64 | 8 | 0L | -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1) |
char | 16 | 2 | 'u0000' | 0 ~ 65535(2^16 - 1) |
float | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
double | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
boolean | 1 | false | true、false |
float类型的数值有一个后缀F或f。不加F的浮点数默认是double类型
double类型后缀可加可不加
long类型的数值有一个后缀L或l
10、基本数据类型和包装类的区别
用途:包装类型可用于泛型,基本数据类型不可以
存储方式:基本数据类型局部变量在栈中存放,成员变量在堆中存放。引用类型大多在堆中存放
占用空间:包装类>基本类型
默认值:基本类型默认值不是null。包装类型默认值是null
补充:
1
基本数据类型和它们的包装类有一点有很大的不同:同一性。大家知道,==运算符可以应用于包装器对象,不过检测的是对象是否有相同的内存地址,因此,下面的比较可能会失败:
Integer a = 1000;
Integer b = 1000;
if(a == b)...
不过,Java实现可以(如果选择这么做)将经常出现的值包装到相同的对象中,这样一来,以上比较就可能正确。但这种不确定性不是我们想要的。解决这个问题的办法就是比较两个包装对象时调用equals方法。
2
提示:绝对不要依赖包装器对象的同一性。不要用==比较包装器对象,也不要将包装器对象作为锁。也不要使用包装器类构造器,他们已经被弃用,并将完全删除。例如,可以使用Integer.valueOf(1000),而绝对不要使用new Integer(1000)。或者,可以依赖自动装箱:Integer a = 1000;
3
在编写代码时,我们想要改变方法中参数的值
public static void triple(int x){
x ++;
}
将int替换为Integer能解决这个问题吗?
public static void triple(Integer x){}
问题在于Integer对象是不可变的:包含在包装器内的信息不会改变。所以,不能使用包装类来创建修改数值参数的方法
11、包装类型的缓存机制
装箱:基本数据类型--->包装类,本质调用包装类的 valueOf()
拆箱:包装类--->基本数据类型,本质调用包装类的 xxxValue()
包装类型的缓存机制就是在研究装箱
机制(Integer为例):当通过自动装箱机制创建包装类对象时,首先会判断数值是否在-128—+127的范围内,如果满足条件,则会从缓存(常量池)中寻找指定数值,若找到缓存,则不会新建对象,只是指向指定数值对应的包装类对象,否则,新建对象。
Integer i = 100;//相当于Integer i = Integer.valueOf(100);
Integer j = 100;//相当于Integer i = Integer.valueOf(100);
System.out.println(i==j);//true
look一下Integer.valueOf()源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
// high value may be configured by property
int h = 127;
}
}
总结:
-
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据 -
Character
创建了数值在 [0,127] 范围的缓存数据 -
Boolean
直接返回True
orFalse
-
两种浮点数类型的包装类
Float
,Double
并没有实现缓存机制。
12、自动拆箱和自动装箱
装箱:基本数据类型--->包装类,本质调用包装类的 valueOf()
拆箱:包装类--->基本数据类型,本质调用包装类的 xxxValue()
用法1:向ArrayList<Integer>添加int类型的元素
list.add(3);
//将自动转换成 list.add(Integer.valueOf(3))
用法2:将一个Integer对象赋给一个int值
int n = list.get(index);
//转换成 int n = list.get(index).intValue()
用法3:自动拆装箱同样适用于算术表达式
Integer n = 3;
n++;
//编译器将自动地插入指令对对象拆箱,然后将结果值增1,最后再将其装箱
注意:装箱和拆箱是编译器的工作,而不是JVM的
13、BigInteger和BigDecimal
BigInteger类实现任意精度的整数运算,BigDecimal实现任意精度的浮点数运算
BigInteger
使用静态的valueOf()方法可以将一个普通的数转换为一个大数
BigInteger a = BigInteger.valueOf(100);
对于更长的数,可以使用一个带字符串参数的构造器:
BigInteger a = new BigInteger("1111111111111111111111111111111111");
不能使用人们熟悉的算术运算符(* +)来组合大数,而需要使用大数类中add和multiply方法
BigInteger c = a.add(b);//c = a+b
BigInteger d = c.multiply(b.add(BigInteger.valueOf(2)));//d = c * (b+2)
14、成员变量和局部变量的区别【⭐⭐⭐】
-
Java10中,方法中的局部变量可以用var关键字,成员变量的类型必须声明
-
语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被
public
,private
,static
,final
等修饰符所修饰,而局部变量只能被final
所修饰。 -
存储方式:从变量在内存中的存储方式来看,如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 -
生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
-
默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
补充:关于成员变量和局部变量的存储方式(这是我借鉴别人的)
第一种【成员变量】:在类中声明的变量是成员变量(全局变量),放在堆中
-
声明的是基本类型的变量,其变量名及其值放在堆内存中
-
声明的是引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中
第二种【局部变量】:在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因
-
基本类型的局部变量,其变量名及值(变量名及值是两个概念)是放在方法栈中
-
引用类型的局部变量,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆类存中的。
15、static
static的5种用法(难度:★★ 频率:★)
1.static 成员变量
public class Student {
// 静态成员变量
private static String SchoolName;
private static int nums;
// 非静态成员变量
private String name;
private int age;
}
静态变量属于类,不属于某一个对象。
所有对象共享该变量(他是一个公共字段)
通过类名直接访问
注意:在程序中,最好不要有公共字段,因为谁都可以修改该公共字段
补充:关于静态常量
public static final double PI = 3.1415;
静态变量使用的少,静态常量使用的多。在程序中,最好不要有公共字段,因为谁都可以修改该公共字段。不过,公共常量(即final字段)却没问题。因为被final修饰,所以,不允许将他第二次赋值
2.static 成员方法
public class Student {
private static String SchoolName;
private static int nums;
// 静态成员方法
public static String getSchoolName() {
return Student.SchoolName;
}
}
直接使用类名调用
静态方法中只能出现静态的属性和方法
静态方法中不可以出现this、super
注意:可以使用对象调用静态方法,不过,非常容易混淆。不推荐使用
3.static 代码块
public class Student {
private static String SchoolName;
private static int nums;
// 静态代码块
static {
Student.SchoolName = "清风小学";
Student.nums = 0;
}
}
静态代码块不需要程序主动调用,在JVM加载类时系统会执行 static 代码块。所有的static 代码块只能在JVM加载类时被执行一次。
静态代码块通常用来初始化变量
4.static 内部类
public class Student {
private static String SchoolName;
private static int nums;
// 静态内部类
static class test{
public test() {
System.out.println("Hello,student!" );
}
}
}
5.静态导包
import static java.lang.Math.*;
在代码中我们可以从原来的Math.PI
改变为PI
16、静态方法为什么不能调用非静态成员?
因为静态方法和静态数据成员会随着类的定义而被分配和装载入内存中,而非静态方法和非静态数据成员只有在类的对象创建时在对象的内存中才有这个方法的代码段。
17、静态方法a和实例方法b有何不同?
-
调用:a是类名调用,b是实例化调用
-
访问成员限制:a只能访问静态的字段和方法,b没有限制
18、静态变量a和实例变量b的区别
-
调用方式:a是类名调用,b是实例化调用
-
加载方式:a只会加载一次,b加载次数与创建对象的次数一致
-
生命周期:a生命周期最长,随着“类”的加载而加载,随着类的消失而消失,b随着“对象”的消失而消失
注意:在java8之前把静态变量存放于方法区,在java8时存放在堆中
19、重载和重写的区别。 【⭐⭐⭐⭐】
-
重载是发生在一个类中,重写是发生在父子类
-
重载要求方法名一样,其他的随意(返回值类型、参数列表)
-
重写要求方法签名必须相同,返回值类型可以兼容
-
父类Employee有方法
public Employee getBuddy(){...}
-
子类Manager覆盖父类的方法
public Manager getBuddy(){...}
-
-
子类重写的方法的访问权限大于等于父类被重写的方法
-
子类重写的方法不可以抛出新的异常,或者抛出和父类被重写的方法相同的异常
注意:方法签名=方法名+参数列表
20、什么是可变长参数?
public class PrintStream{
public PrintStream printf(String fmt,Object... args){
return format(fmt,args);
}
}
其中Object... args
就是可变长参数,它表明这个方法可以接收任意数量的对象(除了fmt参数以外)
System.out.printf("%d",n);
System.out.printf("%d %s",n,"widgets");
实际上printf方法接收两个参数,一个是格式字符串,另一个是Object[]数组。换句话说,对于printf实现者来说,Object...
参数类型与Object[]
完全一样
底层实现
编译器需要转换每个printf调用,将参数打包到一个数组中,并根据需要自动装箱
System.out.printf("%d %s",new Object[]{ Integer.valueOf(n), "widgets" });
现在我们自定义一个方法
public static double max(double... values){
//返回最大的浮点数
}
double m = max(3.1,40.0,-7);
//编译器会将 new double[]{3.1,40.0,-7}传递给max函数
21、Object 类的常见方法有哪些?
equals方法
底层实现:
public boolean equals(Object obj) {
return (this == obj);
}
equals重写实例:
public class Employee {
private String name;
private double salary;
private LocalDate hireDay;
@Override
public boolean equals(Object otherObject){
if(this == otherObject)return true;
if(otherObject == null)return false;
if(getClass() != otherObject.getClass())return false;
Employee other = (Employee) otherObject;
return name.equals(other.name)
&& salary == other.salary
&& hireDay.equals(other.hireDay);
}
}
getClass返回一个对象所属的类,在代码中,只有当两个对象都属于同一类时,才有可能相等
注意:为了防备name或hireDay可能为null的情况,需要使用Objects.equals方法。如果两个参数都为null,Objects.equals(a,b)调用将返回true;如果其中一个为null,则返回false;否则,如果两个参数都不为null,则调用a.equals(b)。利用这个方法,我们将上面最后一条语句修改为:
return Objects.equals(name,other.name)
&& salary == other.salary
&& Objects.equals(hireDay,other.hireDay);
hashCode方法
作用:获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
Object类的hashCode码由对象的存储地址导出,String类的hashCode码由内容导出
补充:判断两个对象是否相等
-
如果两个对象的
hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。 -
如果两个对象的
hashCode
值相等并且equals()
方法也返回true
,我们才认为这两个对象相等。 -
如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等。
补充:有关hashCode方法和equals方法(why看下面)
-
如果重新定义了equals方法,还必须为用户可能插入散列表的对象重新定义hashCode方法
-
equals与hashCode的定义必须相容:如果x.equals(y)返回true,x.hashCode()就必须和y.hashCode()的返回值相等
hashCode()
和 equals()
【⭐⭐⭐⭐】:这个问题经常问,面试官经常问为什么重写 equals()
时要重写 hashCode()
方法?另外,这个问题经常结合着 HashSet
问。
引入:
我们重写了Person的equals()。但是,很奇怪的发现:HashSet中仍然有重复元素:p1 和 p2。为什么会出现这种情况呢?
这是因为虽然p1 和 p2的内容相等,但是它们的hashCode()不等;所以,HashSet在添加p1和p2的时候,认为它们不相等。
所以这个问题的答案:如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也可能不相等。为了保证“两个相等的对象的 hashCode
值必须是相等”,我们也需要重写hashCode方法
toString方法
源码:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
重写toString方法实例(通过getClass().getName()获得类名的字符串):
@Override
public String toString () {
return getClass().getName() +
"{name='" + name + '\'' +
", salary=" + salary +
", hireDay=" + hireDay +
'}';
}
toString在代码中经常被隐晦的调用:
var p = new Point(10,20);
String a = "hello"+p;
//这里编译器自动地调用toString方法
//x是一个任意对象
System.out.println(x);
//这里会隐晦的调用x.toString方法
22、==
和 equals()
的区别。【⭐⭐⭐】
==
如果比较的是基本数据类型,那么比较的是变量的值
如果比较的是引用数据类型,那么比较的是地址值(两个对象是否指向同一块内存)
equals
如果没重写equals方法比较的是两个对象的地址值
如果重写了equals方法后我们往往比较的是对象中的属性的内容
equals
方法是从Object类中继承的,默认的实现就是使用==
23、为什么要有 hashCode?
我们以“HashSet
如何检查重复”为例子来说明为什么要有 hashCode
?
下面这段内容摘自我的 Java 启蒙书《Head First Java》:
当你把对象加入
HashSet
时,HashSet
会先计算对象的hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode
值作比较,如果没有相符的hashCode
,HashSet
会假设对象没有重复出现。但是如果发现有相同hashCode
值的对象,这时会调用equals()
方法来检查hashCode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals
的次数,相应就大大提高了执行速度。
其实, hashCode()
和 equals()
都是用于比较两个对象是否相等。
24、String
、StringBuffer
和 StringBuilder
的区别。 【⭐⭐⭐⭐】
-
StringBuilder
和StringBuffer
类似,均代表可变字符序列,而且方法也一样 -
String
:不可变字符序列,效率低,但复用率高
-
运行速度
-
运行速度快慢顺序为:StringBuilder > StringBuffer > String
-
String最慢的原因:
-
String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可以更改的,但后两者的对象是变量,是可以更改的。
-
-
-
线程安全
-
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的(很多方法带有synchronized关键字)。
-
-
使用场景
-
String:适用于少量的字符串操作的情况。
-
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况。
-
StringBuffer:适用于多线程下在字符缓冲区进行大量操作的情况。
-
25、String 为什么是不可变的?
Java文档中将String类对象称为是不可变的。如同数字3永远是数字3一样,字符串"Hello"永远是包含字符H、e、l、l和o的代码单元序列。你不能修改这些值,不过,我们已经看到,可以修改字符串变量的内容(即将它的地址值修改为另一个地址)。
原因
String
类中使用 final
关键字修饰字符数组来保存字符串【所以String
对象是不可变的。❎】
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
}
🐛 修正:我们知道被
final
关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final
关键字修饰的数组保存字符串并不是String
不可变的根本原因,因为这个数组保存的字符串是可变的(final
修饰引用类型变量的情况)。
String
真正不可变有下面几点原因:
保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。
String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变
26、字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
27、面向对象三大特性是什么。并解释这三大特性。【⭐⭐⭐⭐】
继承、封装、多态
1、继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能。
关于继承如下 3 点请记住:
-
子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
-
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
-
子类可以用自己的方式实现父类的方法。(以后介绍)。
2、封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
3、多态
表现:一个Employee类型的变量既可以引用一个Employee类型的对象,也可以引用Employee类的任何一个子类的对象(例如:Manager类)
例子:
-
员工类
public class Employee { private String name; private double salary; private LocalDate hireDay; public Employee (String name, double salary, LocalDate hireDay) { this.name = name; this.salary = salary; this.hireDay = hireDay; } //get set方法省略 public void print(){ System.out.print("Employee"); } }
-
经理类
public class Manager extends Employee{ private double bonus; public Manager (String name, double salary, LocalDate hireDay, double bonus) { super(name, salary, hireDay); this.bonus = bonus; } public double getBonus () { return bonus; } public void setBonus (double bonus) { this.bonus = bonus; } public void print(){ System.out.print("Manager"); } }
现在有段代码:
Manager boss = new Manager(....);
Employee[] staff = new Employee[3];
staff[0] = boss;
在这个例子,变量staff[0]与boss引用同一个对象。但编译器只将staff[0]看成是一个Employee对象
这意味着,可以这样调用
boss.setBonus(5000);
但不能这样调用
staff[0].setBonus(5000);
这是因为staff[0]声明的类型是Employee,而setBonus不是Employee类的方法。
不过,不能将超类的引用赋给子类变量。例如,下面的赋值是非法的:
Manager m = staff[i];
原因很清楚:不是所有的员工都是经理。
上面的例子已经将多态的知识点讲的差不多了。
下面假设要调用x.f(args),隐式参数x声明为类C的一个对象。下面是方法调用过程的详细描述:
-
编译器首先查看对象的声明类型和方法名。编译器会一一列出C类中方法名f的方法和其父类中所有方法名是f的方法。将他们放在候选名单中
-
接下来,编译器要确定方法调用中提供的参数的类型。通过重载解析,明确具体要调用的方法是哪一个
-
如果是private方法、static方法、 final 方法 或者构造器,那么编译器可以准确地知道应该调用哪个方法。这称为静态绑定(staticbinding)。与此对应的是,如果要调用的方法依赖于隐式参数的实际类型,那么必须在运行时使用动态绑定。在我们的示例中,编译器会利用动态绑定生成一个调用f (Strig)的指令。
-
程序运行并且采用动态绑定调用方法时,虚拟机必须调用与x所引用对象的实际类型对应的那个方法。假设x的实际类型是D,它是C类的子类。如果D类定义了方法f(String),就会调用这个方法;否则,将在D类的超类中寻找f(String),依此类推。
总结:
-
对象类型和引用类型之间具有继承(类)/实现(接口)的关系;(没看懂)
-
引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定(虚拟机来确定);
-
多态不能调用“只在子类存在但在父类不存在”的方法;
-
比如:
Employee a = new Manager(...) ; a.setBonus(5000) ;
-
结果报错
-
-
如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
-
比如:
Employee a = new Manager(...) ; a.print() ;
-
输出的是:Manager
-
28、面向对象和面向过程的区别。【⭐⭐⭐】
两者的主要区别在于解决问题的方式不同:
-
面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
-
面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
面向对象
public class Circle {
// 定义圆的半径
private double radius;
// 构造函数
public Circle(double radius) {
this.radius = radius;
}
// 计算圆的面积
public double getArea() {
return Math.PI * radius * radius;
}
// 计算圆的周长
public double getPerimeter() {
return 2 * Math.PI * radius;
}
public static void main(String[] args) {
// 创建一个半径为3的圆
Circle circle = new Circle(3.0);
// 输出圆的面积和周长
System.out.println("圆的面积为:" + circle.getArea());
System.out.println("圆的周长为:" + circle.getPerimeter());
}
}
面向过程
public class Main {
public static void main(String[] args) {
// 定义圆的半径
double radius = 3.0;
// 计算圆的面积和周长
double area = Math.PI * radius * radius;
double perimeter = 2 * Math.PI * radius;
// 输出圆的面积和周长
System.out.println("圆的面积为:" + area);
System.out.println("圆的周长为:" + perimeter);
}
}
29、深拷贝和浅拷贝。【⭐】
关于深拷贝和浅拷贝区别,我这里先给结论:
-
浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
-
深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
30、获取字节码文件对象的三种方式
-
Class.forName("全类名")
-
类名.class
-
对象.getClass()
31、何谓反射?【⭐⭐】
反射可以获取到字段(成员方法or构造方法),并对其解剖,得到该变量的修饰符、变量名、类型等
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
32、反射的优缺点?
优点:
-
无视修饰符访问类中所有的内容。
-
反射可以跟配置文件结合起来使用,动态的创建对象,动态的调用方法。
缺点:
增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点
33、反射的应用场景?
正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
另外,像 Java 中的一大利器 注解 的实现也用到了反射。
为什么你使用 Spring 的时候 ,一个@Component
注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value
注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。
33、何谓注解?
Annotation
(注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解本质是一个继承了Annotation
的特殊接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
34、注解的解析方法有哪几种?
注解只有被解析之后才会生效,常见的解析方法有两种:
-
编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 -
运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的
@Value
、@Component
)都是通过反射来进行处理的。
35、异常可以分为哪几种【⭐⭐⭐】
-
异常有两种类型:非检查型(unchecked)异常和检查型(checked)异常
-
非检查型:Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。但是,编译器并不期望你为这些异常提供处理器。毕竟,你应该集中精神避免这些错误的发生,而不是编写处理器。
RuntimeException 及其子类都统称为非受检查异常
-
NullPointerException
(空指针错误) -
ArithmeticException
(数学运算异常) -
ArrayIndexOutOfBoundsException
(数组下标越界异常) -
ClassCastException
(类型转换错误) -
NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类) -
IllegalArgumentException
(参数错误比如方法入参类型错误)
-
-
检查型:Java 代码在编译过程中,如果受检查异常没有被
catch
或者throws
关键字处理的话,就没办法通过编译。常见的编译时异常:
-
SQLException
:操作数据库时,查询表可能发生异常 -
IOException
:操作文件时,发生的异常 -
FileNotFoundException
:操作一个不存在的文件时,发生的异常 -
ClassNotFoundException
:加载类,而该类不存在时,发生的异常 -
EOFException
:操作文件,到文档末尾,发生的异常 -
IllegalArguementException
:参数异常
-
36、Throwable 类常用方法有哪些?
-
String getMessage()
: 返回异常发生时的简要描述 -
String toString()
: 返回异常发生时的详细信息 -
String getLocalizedMessage()
: 返回异常对象的本地化信息。使用Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同 -
void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
37、try-catch-finally
finally 语句块是在 try 或者 catch 中的 return 语句之前执行的。更加一般的说法是,finally 语句块应该是在控制转移语句之前执行,控制转移语句除了 return 外,还有 break ,continue和throw。
38、throw 和 throws 的区别?
1、 throw代表动作,表示抛出一个异常的动作; throws代表一种状态,代表方法可能有异常抛出
throw用在方法实现中,而throws用在方法声明中; throw只能用于抛出一种异常,而throws可以抛出多个异常
2、 throw关键字用来在程序中明确的抛出异常,相反 throws语句用来表明方法不能处理的异常。 每一个方法都必须要指定哪些异常不能处理,所以方法的调用者才能够确保处理可能发生的异常,多个异常是用逗号分隔的。
3、 throw是在代码块内的,即在捕获方法内的异常并抛出时用的 throws是针对方法的,即将方法的异常信息抛出去 可以理解为throw是主动(在方法内容里我们是主动捕获并throw的),而throws是被动(在方法上是没有捕获异常进行处理,直接throws的) 例子:
public void str2int(String str) throws Exception { //这里将得到的异常向外抛出
try {
System.out.println(Integer.parseInt(str));
} catch(NumberFormatException e) {
//TODO 这里可以做一些处理,处理完成后将异常报出,让外层可以得到异常信息
throw new Exception("格式化异常");
}
}
4、 throws用于方法头部,声bai明该方法会抛出du什么类型的异常; throw用于方法体中,zhi抛出某种类型的异常。
5、 作用不同:throw用于在程序中抛出异常,throws用于声明在该方法内抛出了异常。 使用位置不同:throw位于方法体内部,可以作为单独语句是用,throws必须跟在方法参数列表后面,不能单独使用。 内容不同:throw抛出一个异常对象,而且只能是一个,throws后面跟异常类,可以有多个。