![cover](https://my-blog-pwd.oss-cn-shenzhen.aliyuncs.com/Java.png)
Java基础面试题
1:final, finally, finalize 的区别是什么?
final 用于修饰属性, 方法和类, 分别表示属性不能被重新赋值, 方法不可被覆盖, 类不可被继承。
finally 是异常处理语句结构的一部分,一般以 try-catch-finally 出现, finally 代码块表示总是被执行。
finalize 是 Object 类的一个方法, 该方法一般由垃圾回收器来调用, 当我们调用 System.gc() 方法的时候, 由垃圾回收器调用 finalize() 方法,回收垃圾,JVM 并不保证此方法总被调用。
2:equals 与 == 的有什么区别?
==
== 符号在八种基本类型的作用是比较对应基本类型的数值是否相等
== 符号在对象类型的作用是比较两个对象是否相等
需要注意几种特例:
在包装类型比较的时候,如果初始化的时候使用字面量进行初始化需要注意包装类对象是否是从缓存中获取,如果是缓存中获取则需注意 == 的结果
在 String 中使用 == 的时候也需要注意,字符串是否在常量池中已经创建,是否有拼接的情况。
详细细节可以参考这篇文章
equals
在 Object 中 equals 用来比较对象是否相等
在其他对象中需要查看是否重写 equals 方法,以此判断 equals 方法的具体逻辑,比如 String 类重写了 equals 方法用来判断字符串的值是否相等
3:说一说 Object 中有哪些方法以及这些方法的功能?
registerNatives()
:用户注册 Object 中 native 相关的方法(native 方法)getClass()
: 返回对象的运行时类的引用,可以获取对象所属的具体类信息(native 方法)hashCode()
: 返回对象的哈希码值,用于在哈希表等数据结构中快速定位对象。需要与 equals() 方法一起重写,以保证相等的对象具有相等的哈希码(native 方法)equals(Object obj)
: 用于比较两个对象是否相等。默认实现比较对象的内存地址,可以被子类重写以定义自定义的相等逻辑clone()
: 创建并返回对象的副本。需要实现 Cloneable 接口才能调用此方法,否则会抛出 CloneNotSupportedException 异常(native 方法)toString()
: 返回对象的字符串表示形式,常用于调试和日志记录。默认实现返回类名和对象的哈希码notify()
: 唤醒等待在该对象上的一个线程。用于线程间的通信(native 方法)notifyAll()
: 唤醒等待在该对象上的所有线程。用于线程间的通信(native 方法)wait(long timeout)
: 在对象上等待一段指定的时间,直到其他线程调用 notify() 或 notifyAll() 方法唤醒它,或者超时时间到达(native 方法)finalize()
: 在对象被垃圾回收之前调用,用于执行对象的清理操作。已废弃,不推荐使用,应使用 try-finally 或 try-with-resources 来代替
4:两个对象的 hashCode() 相同,则 equals() 是否也一定为 true 吗?
如果两个对象的 hashCode() 相同,则 equals() 不一定为 true,两个对象 equals 相等,则它们的 hashcode 必须相等。
hashCode 的常规协定:
1: 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时, 必须 一 致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。 从某一应用程序的一次执行到同一应用程序的另一次执行, 该整数无需保持一致。
2:两个对象的 equals() 相等, 那么对这两个对象中的每个对象调用 hashCode 方法 都必须生成相同的整数结果。
3:两个对象的 equals() 不相等, 那么对这两个对象中的任一对象上调用 hashCode 方法不要求一定生成不同的整数结果。但是,为不相等的对象生成不同整数结果可以提高哈希表的性能
5:String,Stringbuffer,StringBuilder 有什么区别?
String:是一个 final 修饰的不可变的类,一旦创建就不可修改,并且不能被继承,String 实现了 equals() 方法和 hashCode() 方法。
Stringbuffer:继承自 AbstractStringBuilder ,是可变类。StringBuffer 内部的方法都是同步方法,是线程安全的。
StringBuilder:继承自 AbstractStringBuilder ,是可变类。StringBuilder 是非线程安全的,执行效率比 StringBuffer 高。
6:为什么 String 不是基本类型中的一种?
不可变性:String 对象是不可变的,一旦创建就不能修改它的值。这与基本数据类型的可变性不同,基本数据类型的值可以随时被改变
方法和属性:String 类提供了许多方法和属性,用于操作和处理字符串,如 length()、charAt()、substring() 等。这些方法和属性是对象特有的,基本数据类型没有这些操作和处理能力
继承关系:基本数据类型之间没有继承关系,而 String 类是 Object 类的子类,继承了 Object 类的方法和属性。这也是 String 类可以使用通用的方法,如 equals()、hashCode()、toString() 等的原
存储方式:基本数据类型的值直接存储在栈(Stack)中,而 String 对象存储在堆(Heap)中,并通过引用来访问
7:String 类为什么不能被继承?
根本原因:是 String 类被 final 类型修饰,导致无法被子类继承。这么做的原因如下:
不可变性:String 类的设计目的是表示不可变的字符序列,即一旦创建了一个 String 对象,它的值就不能被修改。这种不可变性确保了字符串对象在多线程环境下的安全性和线程安全性。如果允许继承 String 类并修改其行为,就可能破坏字符串的不可变性特性
安全性:String 类在 Java 中被广泛使用,如果它可以被继承并修改其行为,可能会引入一些安全隐患。例如,String 类的某些方法用于处理敏感信息(如密码),如果允许继承并重写这些方法,就有可能破坏安全性
性能优化:由于 String 类的不可变性,它可以被缓存和重用,从而提高性能。如果允许继承 String 类并修改其值,那么缓存和重用的优化策略将会受到影响,可能会导致性能下降
8:为什么说 String 是不可变的?
根本原因:是 String 类中存储字符串的对象被修饰为 private final 类型,这么做的原因如下:
String str = "明天会更好";
String strIso = new String(str.getBytes("GB2312"), "ISO-8859-1");
System.out.println(strIso);
字符串常量池:在 Java 中,字符串常量池是一块特殊的内存区域,用于存储字符串常量。当创建一个字符串常量时,如果字符串常量池中已经存在相同内容的字符串,则返回该字符串的引用,而不会创建新的对象。这样可以节省内存空间并提高性能
线程安全性:由于字符串是不可变的,多个线程可以同时访问和共享同一个字符串对象,而无需担心并发修改导致的数据不一致性或线程安全问题。这使得 String 对象在多线程环境下是安全的
不可修改性:一旦创建了 String 对象,其值就不能被修改。如果对字符串进行修改操作,比如连接、替换字符等,实际上是创建一个新的字符串对象,原始的字符串对象保持不变。这是因为 String 类中的方法都是基于不可变性设计的,它们返回的是一个新的字符串对象,而不会改变原始对象的值
9:如何将 GB2312 编码的字符串转换为 ISO-8859-1 编 码的字符串呢?
String str = "Hello, World!";
StringBuilder reversed = new StringBuilder(str).reverse();
String reversedStr = reversed.toString();
System.out.println(reversedStr);
10:String str = "abc" 和 String newStr = new String ("abc") 共同创建了几个对象?
String str = "abc" 情况:
如果在常量池中没有 "abc",则会在常量池中创建一个字符串常量对象 "abc",如果常量池中已经存在,则不会创建新的对象
String newStr = new String ("abc") 情况:
如果在常量池中没有 "abc",则会在常量池中创建一个字符串常量对象 "abc",如果常量池中已经存在,则不会创建新的字符串常量对象。但不管常量池中是否存在 "abc"。都会创建一个 newStr 的字符串对象。所以一般会问该表达式创建了几个对象,如果常量池中没有,则会创建两个对象。如果常量池已经存在,则只会创建一个对象。
11:String 类的常用方法都有那些?
indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
12:如何将字符串进行反转?
第一种方式:使用 StringBuilder 或 StringBuffer 的 reverse() 方法
String str = "Hello, World!";
StringBuilder reversed = new StringBuilder(str).reverse();
String reversedStr = reversed.toString();
System.out.println(reversedStr);
第二种方式:使用递归函数
public static String reverseString(String str) {
if (str.isEmpty()) {
return str;
}
return reverseString(str.substring(1)) + str.charAt(0);
}
String str = "Hello, World!";
String reversedStr = reverseString(str);
System.out.println(reversedStr);
第三中方式:使用字符数组
public static String reverseString(String str) {
char[] charArray = str.toCharArray();
int start = 0;
int end = charArray.length - 1;
while (start < end) {
char temp = charArray[start];
charArray[start] = charArray[end];
charArray[end] = temp;
start++;
end--;
}
return new String(charArray);
}
String str = "Hello, World!";
String reversedStr = reverseString(str);
System.out.println(reversedStr);
13:String 对象中的 replace 和 replaceAll 有什么区别?
replace 方法:支持字符和字符串的替换 replace(char oldChar, char newChar)
replaceAll 方法:基于正则表达式的字符串替换 replaceAll(String regex, String replacement)
14:Java 中基本数据类型有哪些?
byte:1 个字节,用于表示整数值
short:2 个字节,用于表示较小范围的整数值
int:4 个字节,用于表示整数值
long:8 个字节,用于表示较大范围的整数值
float:4 个字节,用于表示单精度浮点数值
double:8 个字节,用于表示双精度浮点数值
boolean:用于表示布尔值具体占用字节依据 JVM 厂商,只有两个取值:true 和 false
char:2 个字节,用于表示单个字符
15:char 型变量中能不能存贮一个中文汉字?
在 Java 中,char 类型占 2 个字节,而且 Java 默认采用 Unicode 编码,一个 Unicode 码是 16 位,所以一个 Unicode 码占两个字节,Java 中无论汉子还是英文字母都是用 Unicode 编码来表示的。所以在 Java 中 char 类型变量可以存储一个中文汉字。
16:重载和重写有什么区别?
重载:指的是在同一个类中定义多个方法,它们具有相同的名称但具有不同的参数列表。重载是编译时多态,方法在编译时根据传入的参数类型和数量来确定调用哪个方法。重载方法可以有不同的返回类型,但不能仅仅依靠返回类型的不同来进行重载。重载方法主要用于提供多个具有相似功能但参数不同的方法,以增加代码的灵活性和可读性。
重写:指的是子类重新定义了父类继承的方法,使得子类可以提供自己的实现。重写方法具有相同的名称、参数列表和返回类型。重写是运行时多态,在运行时根据对象的实际类型来确定调用哪个方法。重写方法必须具有相同或更宽松的访问权限,不能具有比父类方法更严格的访问权限。
17:抽象类和接口有什么区别?
抽象类:通过使用关键字 abstract 来定义,可以包含抽象方法和具体方法,可以有实例变量和构造方法用于创建对象和初始化实例变量,一个类只能继承一个抽象类,使用关键字 extends,抽象类用于表示一种类型的基本概念,可以包含共享的状态和行为
接口:通过使用关键字 interface 来定义,只能包含抽象方法和常量,不能包含实例变量和构造方法,一个类可以实现多个接口,使用关键字 implements,接口不能有构造方法,因为接口不能被实例化,接口可以包含默认方法( jdk1.8 之后 default 方法),提供具体的实现,实现接口的类可以直接继承这些默认方法,也可以选择性地覆盖它们,接口用于定义一组行为,表示一种能力或者契约
18:说说 Java 中多态的实现原理是什么?
Java 多态机制包括静态多态 (编译时多态) 和动态多态 ( 运行时多态 ),静态多态比如说重载, 动态多态比如说重写,一般指在运行时才能确定调用哪个方法。通常说的多态是指动态多态。
多态的实现原理主要依赖于以下两个核心概念:
继承:子类可以继承父类的属性和方法。当一个类继承另一个类时,子类将拥有父类的所有公共属性和方法,包括方法的签名(方法名称、参数列表和返回类型)。这使得子类可以覆盖(重写)父类的方法,并给出自己的实现。
方法重写:子类可以在继承的基础上对父类的方法进行重写。方法重写是指子类提供了与父类方法签名相同的方法,并且可以在子类中重新实现该方法。重写的方法可以提供新的实现,覆盖父类中的方法。在运行时,通过基类或接口类型引用子类对象时,将根据实际对象的类型来调用方法,而不是根据引用类型
19:Java 中什么是泛型?
是 JDK 1.5 中引入的一个新特性,其本质是参数化类型,解决不确定具体对象类型的问题。其所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
使用泛型的好处包括:
类型安全:泛型允许我们在编译时指定操作的数据类型,并进行类型检查。这减少了在运行时进行类型转换时出错的可能性,并提供了更强的类型安全性。
代码重用:泛型可以增加代码的可重用性。我们可以编写通用的泛型类或泛型方法,用于处理不同类型的数据,而无需为每种类型编写重复的代码。
避免强制类型转换:使用泛型可以避免在代码中频繁进行强制类型转换。泛型可以自动进行类型推断和类型转换,使代码更简洁和易读。
提高性能:由于泛型在编译时进行类型检查,而不是在运行时进行类型转换,可以提高程序的性能。
20:Java 中什么是反射?
是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。可以查看这篇文章
21:Java 中如何创建 Class 对象?
1:使用 Class.forName 静态方法
Class class1 = Class.forName("reflection.Student");
Student student = (Student) class1.newInstance();
2:使用类的. class 方法
Constructor constructor = class1.getConstructor();
Student student1 = (Student) constructor.newInstance();
3:实例对象的 getClass() 方法
Class<?> clazz = MyClass.class;
Constructor<?> constructor = clazz.getConstructor();
MyClass obj = (MyClass) constructor.newInstance();
22:创建反射对象的方式有哪些?
1:通过 Class
类的 forName
方法:
使用类的全限定名(包括包名)获取类的 Class
对象,然后通过 Class
对象创建实例。
try {
Class<?> myClass = Class.forName("com.example.MyClass");
Object myObject = myClass.newInstance(); // 使用默认构造函数创建实例
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
2:通过对象的 getClass
方法:
对象的 getClass
方法返回其运行时类的 Class
对象,然后可以使用该对象创建实例。
MyClass myObject = new MyClass();
Class<?> myClass = myObject.getClass();
try {
Object newInstance = myClass.newInstance(); // 使用默认构造函数创建实例
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
3:通过类字面常量:
使用类字面常量直接获取类的 Class
对象,然后通过该对象创建实例。
Class<MyClass> myClass = MyClass.class;
try {
MyClass myObject = myClass.newInstance(); // 使用默认构造函数创建实例
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
4:通过构造函数的 newInstance
方法:
使用类的构造函数的 newInstance
方法创建实例,这样可以选择调用特定的构造函数。
try {
Constructor<MyClass> constructor = MyClass.class.getConstructor();
MyClass myObject = constructor.newInstance(); // 使用默认构造函数创建实例
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
23:反射可以获取对象的哪些信息?
获取构造器:
获取成员变量:
获取方法:
24:反射有哪些缺陷?
性能问题:java 反射的性能并不好,原因主要是编译器没法对反射相关的代码做优化
安全问题:我们知道单例模式的设计过程中,会强调将构造器设计为私有,因为这样可以防止从外部构造对象。但是反射可以获取类中的域、方法、构造器,修改访问权限。所以这样并不一定是安全的
25:面向对象的三大特征是哪些?
封装(Encapsulation):封装是指将数据和对数据的操作封装在一个单元内,通过对外提供公共接口来控制对数据的访问。封装将数据和行为捆绑在一起,隐藏了对象的内部细节,只暴露必要的接口供外部使用。通过封装,可以实现数据的安全性、保护数据的完整性,并且简化了对数据的操作
继承(Inheritance):继承是指通过一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。子类可以直接访问父类中的非私有成员,并且可以扩展和修改父类的行为。继承实现了代码的重用性和层次化的组织结构,可以减少代码的冗余,提高代码的可维护性和可扩展性
多态(Polymorphism):多态是指同一个方法名可以在不同的对象上产生不同的行为。多态通过继承和方法重写实现,允许一个对象在不同的上下文中以不同的方式被使用。多态性可以提高代码的灵活性和可扩展性,允许以统一的方式操作不同类型的对象。多态包括编译时多态(静态多态)和运行时多态(动态多态)两种形式
26:守护线程是什么,用什么方法实现守护线程?
守护线程(Daemon Thread)是在后台运行的线程,它的存在并不会阻止程序的终止。当所有的非守护线程结束时,守护线程会自动终止。守护线程通常用于执行一些后台任务或提供一些支持性的服务。例如,垃圾回收器(Garbage Collector)就是一个守护线程,它在程序运行时负责回收无用的对象
可以通过设置线程对象的 setDaemon(true)
将线程设置为守护线程。
27:JDK 和 JRE 有什么区别?
JDK:Java Development Kit 的简称,Java 开发工具包,提供了 Java 的开发环境和运行环境
JRE:Java Runtime Environment 的简称,Java 运行环境,为 Java 的运行提供了所需环境
28:深拷贝和浅拷贝有什么区别?
浅拷贝:复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化
深拷贝:将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变
总结:Java 中的 Cloneable 接口提供了一种浅拷贝的方式,但如果需要实现深拷贝,我们需要在对象中重写 clone() 方法,并在其中对引用类型的属性进行深拷贝。需要注意的是,使用 clone() 方法进行深拷贝要求被拷贝的对象及其所有引用类型的属性都实现 Cloneable 接口。
29:Java 中是值传递还是引用传递?
在 Java 中,参数传递是按值传递(Pass-by-Value)进行的。
无论是基本数据类型还是对象类型,实际参数的值都会被复制一份传递给方法中的形式参数。对于基本数据类型,传递的是值本身;对于对象类型,传递的是对象的引用(地址)。
当将一个基本数据类型作为参数传递给方法时,方法中对形式参数的修改不会影响到原始值,因为基本数据类型的值存储在栈内存中,每次传递都是将值复制一份。
当将一个对象作为参数传递给方法时,方法中对形式参数的修改会影响到原始对象,因为传递的是对象的引用(地址),指向的是同一个对象。
虽然在对象类型中传递的是对象的引用,看起来类似引用传递,但实际上仍然是值传递。这是因为对于对象的引用,传递的是引用的副本,而不是引用本身。对形式参数的修改只会影响到引用副本所指向的对象,但不会改变原始引用的指向。
总结:Java 中的参数传递是值传递的,无论是基本数据类型还是对象类型。这种值传递的方式使得方法中对参数的修改不会影响到原始值(对于基本数据类型)或原始对象的引用(对于对象类型)。
30:构造器是否可被重写?
构造器不能被重写,因为构造器是不能被继承的,因为每个类的类名都不相同,而构造器名称与类名相同,所以谈不上继承。 又由于构造器不能被继承,所以相应的就不能被重写了。但是构造器可以被重载。
31:Java 中什么是序列化 / 反序列?
序列化(Serialization)是指将对象转换为字节序列的过程,以便将其存储到文件、传输到网络或在内存中保存。
反序列化(Deserialization)则是将字节序列重新转换为对象的过程,恢复对象的状态和数据。
Java 提供了内置的序列化机制,可以通过实现 java.io.Serializable
接口来标记一个类可序列化。要进行序列化,可以使用 ObjectOutputStream
类将对象写入输出流;要进行反序列化,可以使用 ObjectInputStream
类从输入流中读取字节序列并还原为对象。
32:java8 的新特性有哪些?
Lambda 表达式:Lambda 允许把函数作为一个方法的参数
Stream API :新添加的 Stream API(java.util.stream) 把真正的函数式编程 风格引入到 Java 中
方法引用:方法引用提供了非常有用的语法,可以直接引用已有 Java 类或对象 (实例)的方法或构造器
默认方法:默认方法就是一个在接口里面有了一个实现的方法
Optional 类 :Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常
Date Time API :加强对日期与时间的处理
33:break 和 continue 有什么区别?
break:可以使流程跳出 switch 语句体,也可以在循环结构终止本层循环体,从而提前结束本层循环。
continue:的作用是跳过本次循环体中余下尚未执行的语句,立即进行下一次的循环条件判定,可以理解为仅结束本次循环。
34:简述一下面向对象的六大原则(SOLID)?
单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。换句话说,一个类应该只有一个职责,这样可以使类更加聚焦,降低耦合性,并提高代码的可维护性
开放封闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。意思是当需求发生变化时,应该通过扩展现有代码来适应新的需求,而不是修改已有的代码。这样可以保持代码的稳定性和可维护性
里氏替换原则(Liskov Substitution Principle,LSP):子类对象可以替换父类对象,而程序的行为不受影响。子类应该能够完全替代父类,并且在任何使用父类的地方都不会引起错误或异常
接口隔离原则(Interface Segregation Principle,ISP):应该为客户端提供尽可能小、精确的接口,而不应该提供大而全的接口。避免客户端依赖它们不需要的接口,这样可以降低耦合性,并提高系统的灵活性和可维护性
依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于具体实现,而具体实现应该依赖于抽象。这样可以降低模块之间的直接依赖关系,提高代码的可扩展性和可维护性
迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象有尽可能少的了解,不要直接调用其他对象的内部细节。一个对象应该只与其直接的朋友进行通信,而不应该暴露太多的内部细节给外部。这样可以降低对象之间的耦合性,提高系统的可维护性和灵活性
35:switch 支持的数据类型有哪些?
byte、short、char 、int、String(String 需要 Java 7 及更高版本)
36:Java 访问修饰符有哪些?
public:最高级别的访问修饰符,表示对所有类可见。可以在任何地方访问该成员
protected:表示该成员对同一包内的类和该类的子类可见。在不同包内的子类中,可以通过继承关系来访问该成员
default(默认修饰符):如果没有明确指定访问修饰符,则使用默认修饰符。在同一包内的类可以访问该成员,但在不同包内的类无法访问
private:最低级别的访问修饰符,表示该成员仅对所在类可见。无法在其他类中直接访问
37:Comparator 和 Comparable 有什么不同?
Comparable:是 java.lang 包下的一个类,是在对象自身类中实现的,通过实现 Comparable 接口,重写 compareTo(),对象定义了自己的比较规则。该接口比较规则是 "自然排序",即对象在集合中的排序顺序由对象的内在属性决定。适用于需要对对象进行自然排序的情况,例如使用 Collections.sort()
进行排序
Comparator:是在 java.util 包下的一个类,是一个独立的比较器,可以在对象外部单独定义比较规则,通过实现 Comparator 类,重写 compare() 方法,定义的比较规则是 "定制排序",即可以根据自定义的比较规则对对象进行排序,不依赖于对象的内在属性。适用于需要根据不同的比较规则进行排序的情况,可以在不修改对象类的情况下定义多个不同的比较器,并灵活地选择使用不同的比较器进行排序
总结:
Comparable 接口是在对象自身类中实现的,用于定义对象的自然排序规则,适用于对象自身的比较。Comparator 接口是独立的比较器,用于定义不同的比较规则,适用于对不同对象的比较和排序。使用 Comparable 接口可以使对象具备自然排序的能力,而使用 Comparator 接口可以根据不同的比较规则灵活地进行对象排序
38:Unsafe 类有哪些功能?
内存操作:Unsafe 类提供了直接操作内存的方法,如 allocateMemory() 用于分配内存、copyMemory() 用于内存复制、putXXX() 和 getXXX() 方法用于读写原始数据类型等
并发操作:Unsafe 类提供了一些方法用于执行并发操作,如 park() 和 unpark() 用于线程的阻塞和解除阻塞、compareAndSwapXXX() 用于原子性地更新变量值等
对象操作:Unsafe 类提供了一些方法来直接操作对象,如 objectFieldOffset() 用于获取对象中某个字段的偏移量、getInt() 和 putInt() 用于读写对象的字段值等
数组操作:Unsafe 类提供了一些方法来直接操作数组,如 arrayBaseOffset() 用于获取数组第一个元素的偏移量、arrayIndexScale() 用于获取数组中元素的大小、getInt() 和 putInt() 用于读写数组元素的值等
类操作:Unsafe 类提供了一些方法来操作类的加载和实例化,如 allocateInstance() 用于实例化一个类的对象、defineClass() 用于定义类等
39:类实例化顺序是什么样的?
父类静态代码块 / 静态域 -> 子类静态代码块 / 静态域 -> 父类非静态代码块 -> 父类构造器 -> 子类非静态代码块 -> 子类构造器
40:Java 中创建对象的方式有哪些?
关键字 new:通过使用关键字 new 来创建对象,调用类的构造函数初始化对象
MyClass obj = new MyClass();
反射(Reflection):通过反射机制创建对象,可以在运行时动态地创建对象实例。需要使用 Class 类和 Constructor 类来实现
Class<?> clazz = MyClass.class;
Constructor<?> constructor = clazz.getConstructor();
MyClass obj = (MyClass) constructor.newInstance();
Unsafe 类:提供了一种使用底层操作直接创建对象的机制,但需要注意的是,Unsafe 类属于 JDK 内部类,它的使用并不推荐,因为直接使用它可能导致不安全和不稳定的代码
// 获取Unsafe实例
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
// 获取对象的Class对象
Class<?> clazz = MyClass.class;
// 创建对象实例
MyClass obj = (MyClass) unsafe.allocateInstance(clazz);
// 调用对象的方法
obj.sayHello();
克隆(Clone):如果一个类实现了 Cloneable 接口,就可以通过调用 clone() 方法创建对象的副本
MyClass obj = new MyClass();
MyClass cloneObj = (MyClass) obj.clone();
对象反序列化(Object Deserialization):如果一个对象被序列化到文件或网络流中,可以通过反序列化操作将其还原成对象
FileInputStream fileIn = new FileInputStream("object.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
MyClass obj = (MyClass) in.readObject();
41:Java 中什么是内部类?
在 Java 中,可以将一个类的定义放在另外一个类的内部,这就是内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。
42:内部类有哪些分类?
静态内部类:定义在类内部的静态类,就是静态内部类。静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式:new 外部类. 静态内部类 ()
public class Outer {
private static int radius = 1;
static class StaticInner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
}
}
}
成员内部类:定义在类内部,成员位置上的非静态类,就是成员内部类。成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式:外部类实例. new 内部类 ()
public class Outer {
private static int radius = 1;
private int count =2;
class Inner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
System.out.println("visit outer variable:" + count);
}
}
}
局部内部类:定义在方法中的内部类,就是局部内部类。定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内:new 内部类 ()
public class Outer {
private int out_a = 1;
private static int STATIC_b = 2;
public void testFunctionClass(){
int inner_c =3;
class Inner {
private void fun(){
System.out.println(out_a);
System.out.println(STATIC_b);
System.out.println(inner_c);
}
}
Inner inner = new Inner();
inner.fun();
}
public static void testStaticFunctionClass(){
int d =3;
class Inner {
private void fun(){
// System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量
System.out.println(STATIC_b);
System.out.println(d);
}
}
Inner inner = new Inner();
inner.fun();
}
}
匿名内部类:匿名内部类就是没有名字的内部类,日常开发中使用的比较多。除了没有名字,匿名内部类还有以下特点: 匿名内部类必须继承一个抽象类或者实现一个接口。 匿名内部类不能定义任何静态成员和静态方法。 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法
public class Outer {
private void test(final int i) {
new Service() {
public void method() {
for (int j = 0; j < i; j++) {
System.out.println("匿名内部类" );
}
}
}.method();
}
}
//匿名内部类必须继承或实现一个已有的接口
interface Service{
void method();
}
43:内部类的优点有哪些?
一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据
内部类不为同一包的其他类所见,具有很好的封装性。当内部类使用 private 修饰时,这个类就对外隐藏了
内部类有效实现了 “多重继承”,优化 java 单继承的缺陷
匿名内部类可以很方便的定义回调
44:匿名内部类如何访问在其外面定义的变量呢?
外部变量必须是 final 或者实质上是 final 的。这意味着变量要么被声明为 final,要么在匿名内部类中没有被重新赋值。原因是匿名内部类可以访问外部变量的值,但是不能修改外部变量的值。因为匿名内部类实例的生命周期可能超过外部方法的生命周期,所以在匿名内部类中修改外部变量的值可能会导致不可预测的结果
45:什么是类加载机制?
类的加载(Class Loading)是指将类的字节码文件(.class 文件)加载到内存中,并在内存中创建对应的类对象的过程。类的加载是 Java 虚拟机(JVM)执行的重要步骤之一,它是 Java 的核心特性之一,实现了 Java 的动态性和灵活性
46:类加载的步骤有哪些?
类加载的第一阶段:加载
查找并加载类的字节码文件。字节码文件可以从本地文件系统、网络或其他来源获取。
加载的过程包括查找类文件、读取类文件的二进制数据,并将其转换为内部数据结构。在加载阶段,虚拟机需要完成以下三件事情:
1:通过一个类的全限定名来获取其定义的二进制字节流。
2:将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3:在 Java 堆中生成一个代表这个类的 java.lang.Class
对象,作为对方法区中这些数据的访问入口。
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
类加载的第二阶段:连接
其中连接分为三步:
验证(连接阶段的第一步)
这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不 会危害虚拟机自身的安全。验证阶段大致会完成 4 个阶段的检验动作:
1:文件格式验证:* 验证字节流是否符合 Class 文件格式的规范;* 例如:是否以 0xCAFEBABE 开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
2:元数据验证:对字节码描述的信息进行语义分析(注意:对比 javac 编译阶段的语义分析),以保证其描述的信息符合 Java 语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object 之外。
3:字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
4:符号引用验证:确保解析动作能正确执行
准备(连接阶段的第二步)
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1:这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
2:这里所设置的初始值通常情况下是数据类型默认的零值(如 0、0L、null、false 等),而不是被在 Java 代码中被显式地赋予的值。
3:如果类字段的字段属性表中存在 ConstantValue 属性,即同时被 final 和 static 修饰,那么在准备阶段变量 value 就会被初始化为 ConstValue 属性所指定的值。
解析(连接阶段的第三步)
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
类加载的第三阶段:初始化
为类的静态变量赋予正确的初始值,主要对类变量进行初始化。在 Java 中对类变量进行初始值设定有两种方式:
1:声明类变量是指定初始值
2:使用静态代码块为类变量指定初始值
JVM 初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
1:创建类的实例,也就是 new
的方式
2:访问某个类或接口的静态变量,或者对该静态变量赋值(注:访问类的静态常量final不会导致类初始化)
3:调用类的静态方法
4:反射(如 Class.forName(“com.shengsiyuan.Test”)
)
5:初始化某个类的子类,则其父类也会被初始化
6:Java 虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe 命令来运行某个主类
47:什么是类加载器?
类加载器是负责将可能是网络上、也可能是磁盘上的 class 文件加载到内存中。并为其生成对应的 java.lang.class
对象。
一旦一个类被载入 JVM 了,同一个类就不会被再次加载。那么怎样才算是同一个类?在 JAVA 中一个类用其全限定类名(包名和类名)作为其唯一标识,但是在 JVM 中,一个类用其全限定类名和其类加载器作为其唯一标识。也就是说,在 JAVA 中的同一个类,如果用不同的类加载器加载,则生成的 class 对象认为是不同的。
48:类加载器的种类有哪些?
启动类加载器(BootstrapClassLoader):是嵌在 JVM 内核中的加载器,该加载器是用 C++ 语言写的,主要负载加载 JAVA_HOME/lib
下的类库,启动类加载器无法被应用程序直接使用
扩展类加载器(Extension ClassLoader):该加载器器是用 JAVA 编写,且它的父类加载器是 Bootstrap,是由 sun.misc.Launcher$ExtClassLoader 实现的,主要加载 JAVA_HOME/lib/ext
目录中的类库。开发者可以这几使用扩展类加载器
系统类加载器(App ClassLoader):系统类加载器,也称为应用程序类加载器,负责加载应用程序 classpath
目录下的所有 jar 和 class 文件。它的父加载器为 Ext ClassLoader
总结:类加载器的体系并不是 “继承” 体系,而是委派体系,大多数类加载器首先会到自己的 parent 中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次。
49:什么是双亲委派模型?
如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求转交给父类加载器去完成。每一个层次的类加载器都是如此。因此所有的类加载请求都应该传递到最顶层的启动类加载器中,只有到父类加载器反馈自己无法完成这个加载请求(在它的搜索范围没有找到这个类)时,子类加载器才会尝试自己去加载。委派的好处就是避免有些类被重复加载。
优点:
避免类的重复加载:Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次
安全因素:java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class,这样便可以防止核心 API 库被随意篡改。
50:如何打破双亲委派机制?
自定义类加载器:可以通过自定义类加载器来加载类。自定义类加载器可以继承自 ClassLoader 类,并重写其中的加载类的方法,如 findClass()
方法。在自定义类加载器中,可以根据自己的需求来定义类加载逻辑,不受双亲委派机制的限制
破坏父类加载器委派机制:在默认的双亲委派机制中,父类加载器会先尝试加载类,只有在找不到类的情况下才会由子类加载器尝试加载。我们可以通过破坏父类加载器的加载顺序来打破双亲委派机制。具体做法是在自定义类加载器中重写 loadClass() 方法,在加载类之前先判断是否由父类加载器加载,如果不是,再由当前类加载器加载
使用线程上下文类加载器:Java 中有一个特殊的类加载器称为线程上下文类加载器(Thread Context Class Loader),它可以破坏双亲委派机制。线程上下文类加载器可以通过 Thread.currentThread().setContextClassLoader() 方法设置,它会在类加载时优先使用线程上下文类加载器加载类,而不是采用双亲委派机制
51:Java 中对象创建过程是什么样?
第一步:类加载检查
当遇到 new 指令之后,首先会去到静态常量池中看看能否找到这个指令所对应的符号引用,然后会检查符号引用所对应的类是否被加载——连接——初始化,如果有的话就进行第二步,如果没有就要先进行类的加载。
第二步:分配内存
当类加载检查通过后,对象的大小在类加载完成之后就可以确定,所以首先会为准备新创建的对象根据对象大小分配内存,这块内存就是在堆中划分。那么如何进行内存的分配呢,一般情况下会有两种情况:“指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
指针碰撞:假设 Java 堆的内存是绝对规整的,所有用过的内存都放一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
空闲列表:如果 Java 堆中的内存并不是规整的,已使用的内存和空间的内存是相互交错的,虚拟机必须维护一个空闲列表,记录上哪些内存块是可用的,在分配时候从列表中找到一块足够大的空间划分给对象使用。
注意:在分配内存的时候存在并发问题, 在并发情况下划分不一定是线程安全的,有可能出现正在给 A 对象分配内存,指针还没有来得及修改,对象 B 又同时使用了原来的指针分配内存的情况,所以解决方法:
1:CAS + 失败重试:通过 CAS 乐观锁去尝试更新此次操作,如果 CAS 失败就去重试,直至成功为止。
2:TLAB:预先在堆内存的 Eden 区为每一个线程分配一块名为 TLAB 的预存地址空间,当创建对象的时候就可以使用这块内存,当 TLAB 空间被占满时,再去采用 CAS + 失败重试的方法区分配内存。
第三步:初始化零值
之前的类加载过程我们了解到:在准备过程中会将 final 修饰的静态变量直接赋初值,对 static 修饰的静态变量赋零值。但是对于普通的成员变量我们不清楚是何时初始化的,那么这个阶段就是给成员变量进行初始化。
虚拟机需要将分配到的内存空间中的数据类型都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
第四步:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式
第五步:调用 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来
52:什么是动态代理?
动态代理是一种设计模式,它允许在运行时创建一个代理对象,该代理对象可以拦截并重定向对目标对象的方法调用。Java 中的动态代理是通过反射机制实现的。动态代理的主要作用是在不修改目标对象的前提下,为其提供额外的功能或行为。它通过在运行时创建代理对象,并将目标对象的方法调用转发给代理对象来实现
53:动态代理的方式有哪些?
基于接口的动态代理:代理对象实现了一个或多个接口,通过 java.lang.reflect.Proxy
类和 java.lang.reflect.InvocationHandler
接口实现。代理对象的方法调用会被转发给 InvocationHandler 对象处理
基于类的动态代理:代理对象是目标对象的子类,通过字节码生成库(如 CGLIB、Byte Buddy 等)动态生成代理类。代理对象继承了目标对象的行为,并可以重写或增加目标对象的方法
54:Java 为什么能跨平台运行?
Java 虚拟机:Java 程序在运行之前,会首先被编译成一种特殊的字节码(bytecode),而不是直接编译成特定平台的机器码。然后,这些字节码会被 Java 虚拟机执行。不同的平台上都有各自的 Java 虚拟机,它们负责将字节码转化为特定平台的机器码进行执行。这种中间层的存在使得 Java 程序能够在不同的平台上运行,只要有对应平台的 Java 虚拟机即可
平台无关的标准库:Java 提供了一个平台无关的标准库(Java API),其中包含了大量的类和方法,可以用于开发各种应用程序。这些标准库中的类都是与具体平台无关的,因此可以在不同的操作系统和硬件上使用相同的代码。这样一来,开发人员可以编写一次 Java 代码,然后在不同平台上进行编译和运行,而无需针对每个平台进行额外的修改
严格的语言规范:Java 语言具有严格的语法和规范,使得开发人员编写的 Java 代码在不同平台上的行为是一致的。这意味着无论在哪个平台上编写和编译 Java 代码,它们的行为和结果应该是相同的
55:Java 的安全性体现在哪里?
字节码验证:Java 程序在运行之前会先被编译成字节码,然后由 Java 虚拟机执行。在执行之前,Java 虚拟机会对字节码进行验证,确保它符合 Java 语言规范和安全要求。这种验证过程可以防止恶意代码和非法操作的执行
内存管理:Java 提供自动的内存管理机制,通过垃圾回收器来自动回收不再使用的内存。这减少了内存泄漏和悬挂指针等内存安全问题的风险
强类型检查:Java 是一种强类型语言,对变量的类型进行严格检查。这种类型检查可以防止类型转换错误和潜在的内存损坏
数组越界检查:Java 会对数组的访问进行边界检查,防止访问超出数组边界的位置。这可以避免数组越界访问导致的内存错误和安全漏洞
异常处理:Java 提供了异常处理机制,使得开发人员可以捕获和处理程序中的异常情况。通过合理地处理异常,可以防止程序崩溃和数据损坏,并增强程序的安全性
安全管理器:Java 提供了安全管理器(Security Manager)的机制,允许开发人员对程序的访问权限进行细粒度的控制。可以通过安全策略文件来配置和限制程序的访问权限,从而保护系统的安全性