目录
33. String、StringBuffer、StringBuilder的区别和联系
概念
1. 说一下Java语言的特点?
主要有以下的特点:
跨平台性:Java的“编写一次,运行无处不在”哲学是其最大的特点之一。Java编译器将源代码编译成字节码(bytecode),该字节码可以在任何安装了Java虚拟机(JVM)的系统上运行。
面向对象:Java是一门严格的面向对象编程语言,几乎一切都是对象。面向对象编程(OOP)特性使得代码更易于维护和重用,包括类(class)、对象(object)、继承(inheritance)、多态(polymorphism)、抽象(abstraction)和封装(encapsulation)。
垃圾回收机制:Java有自己的垃圾回收机制,自动管理内存和回收不再使用的对象。这样,开发者不需要手动管理内存,从而减少内存泄漏和其他内存相关的问题。
2. JVM、JDK、JRE三者关系?
3. 什么是跨平台性呢?为什么Java程序可以跨平台呢?
所谓跨平台指的是用Java语言开发的程序可以在多种操作系统上运行,常见的操作系统有Windows、Linux、MacOS系统。
跨平台性的原理是因为在不同版本的操作系统中安装有不同版本的Java虚拟机,Java程序的运行只依赖于Java虚拟机,和操作系统并没有直接关系。从而做到一处编译,处处运行。
4. 编译型语言和解释型语言的区别?
编译型语言和解释型语言的区别在于:
编译型语言:在程序执行之前,整个源代码会被编译成机器码或者字节码,生成可执行文件。执行时直接运行编译后的代码,速度快,但跨平台性较差。
解释型语言:在程序执行时,运行解释执行源代码,不生成独立的可执行文件。通常由解释器动态解释并执行代码,跨平台性好,但执行速度相对较慢。
典型的编译型语言如C、C++,典型的解释型语言如Python、JavaScript。
5. Python、Java、C++之间的区别是什么?
Java是编译型语言,Java编译器将源代码编译为字节码,而字节码则由Java虚拟机执行
python是一种解释语言,翻译时会在执行程序的同时进行翻译。
C++ 是编译型语言,支持多范式编程(面向过程、面向对象、泛型编程等)。
垃圾回收:
Python 使用自动垃圾回收(主要是引用计数+分代回收),开发者无需手动管理内存。这种机制简化了编程但可能导致不可预测的暂停(尽管通常不明显)。
Java 同样采用自动垃圾回收(JVM的GC机制),但提供了更丰富的垃圾收集器选择(如G1、ZGC等),允许对性能进行更精细调优。与Python不同,Java没有引用计数。
C++ 要求手动内存管理,通过new/delete或智能指针(如unique_ptr、shared_ptr)来控制对象生命周期。这提供了最大控制权但也容易导致内存泄漏和悬垂指针等问题。
性能:
C++ 通常具有最高性能,因为它直接编译为机器码且允许底层操作(如指针运算、内联汇编)。编译器优化(如GCC、Clang)可以生成极其高效的代码。
Java 性能介于中间,JIT编译器(即时编译)能够在运行时优化热点代码。现代JVM性能已接近原生代码,但启动时间和内存开销通常高于C++。
Python 是三者中最慢的,因其动态特性和解释执行。但可通过C扩展(如NumPy)、JIT编译器(如PyPy)或Cython来提升关键代码性能。
数据类型
6. 八种基本的数据类型
注意一下几点:
Java八种基本数据类型的字节数:1字节(byte、boolean)、2字节(short、char)、4字节(int、float)、8字节(long、double)
浮点数的默认类型为double(如果需要声明一个常量为float型,则必须要在末尾加上f或F)
整数的默认类型为int(声明Long型在末尾加上l或者L)
八种基本数据类型的包装类:除了char的是Character、int类型的是Integer,其他都是首字母大写
char类型是无符号的,不能为负,所以是0开始的
7. 自动装箱与拆箱了解吗?原理是什么?
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
Integer i = 10; //装箱
int n = i; //拆箱
原理
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
;
注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
8. 基本类型和包装类型的区别?
- 用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。
- 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被
static
修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。 - 占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
- 默认值:成员变量包装类型不赋值就是
null
,而基本类型有默认值且不是null
。 - 比较方式:对于基本数据类型来说,
==
比较的是值。对于包装数据类型来说,==
比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用equals()
方法。
9. Integer与int有什么区别?
int是Java中的原始数据类型,而Integer是int的包装类。
Integer和 int 的区别:
基本类型和引用类型:首先,int是一种基本数据类型,而Integer是一种引用类型。基本数据类型是Java中最基本的数据类型,它们是预定义的,不需要实例化就可以使用。而引用类型则需要通过实例化对象来使用。这意味着,使用int来存储一个整数时,不需要任何额外的内存分配,而使用Integer时,必须为对象分配内存。在性能方面,基本数据类型的操作通常比相应的引用类型快。
自动装箱和拆箱:其次,Integer作为int的包装类,它可以实现自动装箱和拆箱。自动装箱是指将基本类型转化为相应的包装类类型,而自动拆箱则是将包装类类型转化为相应的基本类型。这使得Java程序员更加方便地进行数据类型转换。例如,当我们需要将int类型的值赋给Integer变量时,Java可以自动地将int类型转换为Integer类型。同样地,当我们需要将Integer类型的值赋给int变量时,Java可以自动地将Integer类型转换为int类型。
空指针异常:另外,int变量可以直接赋值为0,而Integer变量必须通过实例化对象来赋值。如果对一个未经初始化的Integer变量进行操作,就会出现空指针异常。这是因为它被赋予了null值,而null值是无法进行自动拆箱的。
面向对象
10. 怎么理解面向对象?简单说说封装继承多态
面向对象是一种编程范式,它将现实世界中的事物抽象为对象,对象具有属性(称为字段或属性)和行为(称为方法)。面向对象编程的设计思想是以对象为中心,通过对象之间的交互来完成程序的功能,具有灵活性和可扩展性,通过封装和继承可以更好地应对需求变化。
Java面向对象的三大特性包括:封装、继承、多态:
封装:封装是指将对象的属性(数据)和行为(方法)结合在一起,对外隐藏对象的内部细节,仅通过对象提供的接口与外界交互。封装的目的是增强安全性和简化编程,使得对象更加独立。
继承:继承是一种可以使子类自动共享父类数据结构和方法的机制。它是代码复用的重要手段,通过继承可以建立类与类之间的层次关系,使得结构更加清晰。
多态:多态是指允许不同类的对象对同一消息作出响应。即同一个接口,使用不同的实例而执行不同操作。多态性可以分为编译时多态(重载)和运行时多态(重写)。它使得程序具有良好的灵活性和扩展性。
11. 多态体现在哪几个方面?
多态在面向对象编程中可以体现在以下几个方面:
方法重载:
方法重载是指同一类中可以有多个同名方法,它们具有不同的参数列表(参数类型、数量或顺序不同)。虽然方法名相同,但根据传入的参数不同,编译器会在编译时确定调用哪个方法。
示例:对于一个 add 方法,可以定义为 add(int a, int b) 和 add(double a, double b)。
方法重写:
方法重写是指子类能够提供对父类中同名方法的具体实现。在运行时,JVM会根据对象的实际类型确定调用哪个版本的方法。这是实现多态的主要方式。
示例:在一个动物类中,定义一个 sound 方法,子类 Dog 可以重写该方法以实现 bark,而 Cat 可以实现 meow。
接口与实现:
多态也体现在接口的使用上,多个类可以实现同一个接口,并且用接口类型的引用来调用这些类的方法。这使得程序在面对不同具体实现时保持一贯的调用方式。
示例:多个类(如 Dog,Cat)都实现了一个 Animal 接口,当用 Animal 类型的引用来调用 makeSound 方法时,会触发对应的实现。
向上转型和向下转型:
在Java中,可以使用父类类型的引用指向子类对象,这是向上转型。通过这种方式,可以在运行时采用不同的子类实现。
向下转型是将父类引用转回其子类类型,但在执行前需要确认引用实际指向的对象类型以避免 ClassCastException。
12. 多态解决了什么问题?
多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类。
多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等
13. 重载与重写有什么区别?
重载(Overloading):指的是在同一个类中,可以有多个同名方法,它们具有不同的参数列表(参数类型、参数个数或参数顺序不同),编译器根据调用时的参数类型来决定调用哪个方法。
重写(Overriding):指的是子类可以重新定义父类中的方法,方法名、参数列表和返回类型必须与父类中的方法一致,通过@override注解来明确表示这是对父类方法的重写。
重载是指在同一个类中定义多个同名方法,而重写是指子类重新定义父类中的方法。
14. 抽象类和普通类区别?
实例化:普通类可以直接实例化对象,而抽象类不能被实例化,只能被继承。
方法实现:普通类中的方法可以有具体的实现,而抽象类中的方法可以有实现也可以没有实现(即抽象方法)。
继承:一个类可以继承一个普通类,而且可以继承多个接口;而一个类只能继承一个抽象类,但可以同时实现多个接口。
实现限制:普通类可以被其他类继承和使用,而抽象类一般用于作为基类,被其他类继承和扩展使用。
15. 抽象类能加final修饰吗?
不能,Java中的抽象类是用来被继承的,而final修饰符用于禁止类被继承或方法被重写,因此,抽象类和 final 修饰符是互斥的,不能同时使用。
16. 抽象类可以被实例化吗?
在Java中,抽象类本身不能被实例化。
这意味着不能使用 new
关键字直接创建一个抽象类的对象。抽象类的存在主要是为了被继承,它通常包含一个或多个抽象方法(由 abstract
关键字修饰且无方法体的方法),这些方法需要在子类中被实现。
抽象类可以有构造器,这些构造器在子类实例化时会被调用,以便进行必要的初始化工作。然而,这个过程并不是直接实例化抽象类,而是创建了子类的实例,间接地使用了抽象类的构造器。
17. 解释Java中的静态变量和静态方法
在 Java 中,静态变量和静态方法是与类本身关联的,而不是与类的实例(对象)关联。它们在内存中只存在一份,可以被类的所有实例共享。
静态变量
静态变量(也称为类变量)是在类中使用 static
关键字声明的变量。它们属于类而不是任何具体的对象。主要的特点:
共享性:所有该类的实例共享同一个静态变量。如果一个实例修改了静态变量的值,其他实例也会看到这个更改。
初始化:静态变量在类被加载时初始化,只会对其进行一次分配内存。
访问方式:静态变量可以直接通过类名访问,也可以通过实例访问,但推荐使用类名。
静态方法
静态方法是在类中使用 static
关键字声明的方法。类似于静态变量,静态方法也属于类,而不是任何具体的对象。主要的特点:
无实例依赖:静态方法可以在没有创建类实例的情况下调用。对于静态方法来说,不能直接访问非静态的成员变量或方法,因为静态方法没有上下文的实例。
访问静态成员:静态方法可以直接调用其他静态变量和静态方法,但不能直接访问非静态成员。
多态性:静态方法不支持重写(Override),但可以被隐藏(Hide)。
使用场景
静态变量:常用于需要在所有对象间共享的数据,如计数器、常量等。
静态方法:常用于助手方法(utility methods)、获取类级别的信息或者是没有依赖于实例的数据处理。
18. 接口和抽象类有什么共同点和区别?
接口和抽象类的共同点
- 实例化:接口和抽象类都不能直接实例化,只能被实现(接口)或继承(抽象类)后才能创建具体的对象。
- 抽象方法:接口和抽象类都可以包含抽象方法。抽象方法没有方法体,必须在子类或实现类中实现。
接口和抽象类的区别
- 设计目的:接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- 继承和实现:一个类只能继承一个类(包括抽象类),因为 Java 不支持多继承。但一个类可以实现多个接口,一个接口也可以继承多个其他接口。
- 成员变量:接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值。抽象类的成员变量可以有任何修饰符(private
,protected
,public
),可以在子类中被重新定义或赋值。 - 方法:
- Java 8 之前,接口中的方法默认是
public abstract
,也就是只能有方法声明。自 Java 8 起,可以在接口中定义default
(默认) 方法和static
(静态)方法。 自 Java 9 起,接口可以包含private
方法。 - 抽象类可以包含抽象方法和非抽象方法。抽象方法没有方法体,必须在子类中实现。非抽象方法有具体实现,可以直接在抽象类中使用或在子类中重写。
- Java 8 之前,接口中的方法默认是
19. Java 中 final 作用是什么?
final
关键字主要有以下三个方面的作用:用于修饰类、方法和变量。
修饰类:当
final
修饰一个类时,表示这个类不能被继承,是类继承体系中的最终形态。例如,Java 中的String
类就是用final
修饰的,这保证了String
类的不可变性和安全性,防止其他类通过继承来改变String
类的行为和特性。修饰方法:用
final
修饰的方法不能在子类中被重写。比如,java.lang.Object
类中的getClass
方法就是final
的,因为这个方法的行为是由 Java 虚拟机底层实现来保证的,不应该被子类修改。修饰变量:当
final
修饰基本数据类型的变量时,该变量一旦被赋值就不能再改变。例如,final int num = 10;
这里的num
就是一个常量,不能再对其进行重新赋值操作,否则会导致编译错误。对于引用数据类型,final
修饰意味着这个引用变量不能再指向其他对象,但对象本身的内容是可以改变的。例如,final StringBuilder sb = new StringBuilder("Hello");
,不能让sb
再指向其他StringBuilder
对象,但可以通过sb.append(" World");
来修改字符串的内容。
深拷贝和浅拷贝
20. 深拷贝和浅拷贝的区别?
浅拷贝:是指只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。换句话说,浅拷贝只是创建一个新的对象,然后将原对象的字段值复制到新对象中,但如果原对象内部有引用类型的字段,只是将引用复制到新对象中,两个对象指向的是同一个引用对象。
深拷贝:是指在复制对象的同时,将对象内部的所有引用类型字段的内容也复制一份,而不是共享引用。换句话说,深拷贝会递归复制对象内部所有引用类型的字段,生成一个全新的对象以及其内部的所有对象。
泛型
21. 什么是泛型?为什么需要泛型?
定义类、接口、方法时,同时声明了一个或者多个类型变量(如:<E>) ,称为泛型类、泛型接口,泛型方法、它们统称为泛型。
作用:
- 适用于多种数据类型执行相同的代码
泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力!这样可以避免强制类型转换,及其可能出现的异常。
对象
22. Java创建对象除了new还有别的什么方式?
通过反射创建对象:通过 Java 的反射机制可以在运行时动态地创建对象。可以使用
Class
类的newInstance()
方法或者通过Constructor
类来创建对象。
public class MyClass {
public MyClass() {
// Constructor
}
}
public class Main {
public static void main(String[] args) throws Exception {
Class<?> clazz = MyClass.class;
MyClass obj = (MyClass) clazz.newInstance();
}
}
通过反序列化创建对象:通过将对象序列化(保存到文件或网络传输)然后再反序列化(从文件或网络传输中读取对象)的方式来创建对象,对象能被序列化和反序列化的前提是类实现
Serializable
接口。
import java.io.*;
public class MyClass implements Serializable {
// Class definition
}
public class Main {
public static void main(String[] args) throws Exception {
// Serialize object
MyClass obj = new MyClass();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.ser"));
out.writeObject(obj);
out.close();
// Deserialize object
ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.ser"));
MyClass newObj = (MyClass) in.readObject();
in.close();
}
}
通过clone创建对象:所有 Java 对象都继承自
Object
类,Object
类中有一个clone()
方法,可以用来创建对象的副本,要使用clone
方法,我们必须先实现Cloneable
接口并实现其定义的clone
方法。
public class MyClass implements Cloneable {
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
MyClass obj1 = new MyClass();
MyClass obj2 = (MyClass) obj1.clone();
}
}
23. 如何获取私有对象?
在 Java 中,私有对象通常指的是类中被声明为 private
的成员变量或方法。由于 private
访问修饰符的限制,这些成员只能在其所在的类内部被访问。
不过,可以通过下面两种方式来间接获取私有对象。
使用公共访问器方法(getter 方法):如果类的设计者遵循良好的编程规范,通常会为私有成员变量提供公共的访问器方法(即 getter 方法),通过调用这些方法可以安全地获取私有对象。
class MyClass {
// 私有成员变量
private String privateField = "私有字段的值";
// 公共的 getter 方法
public String getPrivateField() {
return privateField;
}
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
// 通过调用 getter 方法获取私有对象
String value = obj.getPrivateField();
System.out.println(value);
}
}
反射机制:反射机制允许在运行时检查和修改类、方法、字段等信息,通过反射可以绕过
private
访问修饰符的限制来获取私有对象。
import java.lang.reflect.Field;
class MyClass {
private String privateField = "私有字段的值";
}
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
MyClass obj = new MyClass();
// 获取 Class 对象
Class<?> clazz = obj.getClass();
// 获取私有字段
Field privateField = clazz.getDeclaredField("privateField");
// 设置可访问性
privateField.setAccessible(true);
// 获取私有字段的值
String value = (String) privateField.get(obj);
System.out.println(value);
}
}
反射
24. 什么是反射?
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
反射具有以下特性:
运行时类信息访问:反射机制允许程序在运行时获取类的完整结构信息,包括类名、包名、父类、实现的接口、构造函数、方法和字段等。
动态对象创建:可以使用反射API动态地创建对象实例,即使在编译时不知道具体的类名。这是通过
Class
类的newInstance()
方法或Constructor
对象的newInstance()
方法实现的。动态方法调用:可以在运行时动态地调用对象的方法,包括私有方法。这通过
Method
类的invoke()
方法实现,允许你传入对象实例和参数值来执行方法。访问和修改字段值:反射还允许程序在运行时访问和修改对象的字段值,即使是私有的。这是通过
Field
类的get()
和set()
方法完成的。
25. 反射在你平时写代码或者框架中的应用场景有哪些?
加载数据库驱动
在我们的项目中,底层数据库有时使用MySQL,有时使用Oracle,需要动态地根据实际情况加载驱动类。这时候反射机制就非常有用。假设我们有两个数据库连接类:com.mikechen.java.mysqlConnection
和com.mikechen.java.oracleConnection
。
在使用JDBC连接数据库时,我们可以使用Class.forName()
通过反射动态加载数据库驱动程序。如果是MySQL,则传入MySQL的驱动类;如果是Oracle,则传入Oracle的驱动类。
示例代码:
// 替代直接注册驱动的方式:DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
Class.forName("com.mysql.cj.jdbc.Driver"); // 加载MySQL驱动
// 或者
Class.forName("oracle.jdbc.driver.OracleDriver"); // 加载Oracle驱动
这种方式使得我们可以在不修改代码的情况下,通过配置文件或运行时参数来切换不同的数据库驱动,提高了代码的灵活性和可维护性。
配置文件加载
Spring 框架的 IOC(控制反转)实现了动态加载和管理 Bean。在 Spring 中,你可以通过配置文件来定义各种 Bean,根据实际需求配置所需的 Bean,Spring 容器会根据这些配置动态加载,从而使程序能够更加健壮地运行。
Spring 通过 XML 配置模式装载 Bean 的过程:
加载配置文件:将程序中所有的 XML 或 properties 配置文件加载到内存中。
解析配置内容:通过 Java 类解析 XML 或 properties 文件中的内容,获取对应实体类的字节码字符串以及相关的属性信息。
反射获取 Class 实例:使用反射机制,根据解析得到的字符串(类名)获取对应类的 Class 实例。
动态配置实例属性:根据配置文件中的属性信息,动态地为实例设置属性值,完成 Bean 的创建和初始化。
这一机制使得 Spring 能够灵活地管理对象生命周期,实现松耦合和高扩展性。
注解
26. Java注解的原理?
注解本质是一个继承了Annotation
的特殊接口,其具体实现类是Java运行时生成的动态代理类。
我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler
的invoke
方法。该方法会从memberValues
这个Map中索引出对应的值。而memberValues
的来源是Java常量池。
27. 对注解解析的底层实现了解吗?
注解本质上是一种特殊的接口,它继承自 java.lang.annotation.Annotation
接口,所以注解也叫声明式接口,例如,定义一个简单的注解:
public @interface MyAnnotation {
String value();
}
编译后,Java 编译器会将其转换为一个继承自 Annotation
的接口,并生成相应的字节码文件。
根据注解的作用范围,Java 注解可以分为以下几种类型:
- 源码级别注解:仅存在于源码中,编译后不会保留(
@Retention(RetentionPolicy.SOURCE)
)。 - 类文件级别注解:保留在
.class
文件中,但运行时不可见(@Retention(RetentionPolicy.CLASS)
)。 - 运行时注解:保留在
.class
文件中,并且可以通过反射在运行时访问(@Retention(RetentionPolicy.RUNTIME)
)。
只有运行时注解可以通过反射机制进行解析。
当注解被标记为 RUNTIME
时,Java 编译器会在生成的 .class
文件中保存注解信息。这些信息存储在字节码的属性表(Attribute Table)中,具体包括以下内容:
RuntimeVisibleAnnotations
:存储运行时可见的注解信息。RuntimeInvisibleAnnotations
:存储运行时不可见的注解信息。RuntimeVisibleParameterAnnotations
和RuntimeInvisibleParameterAnnotations
:存储方法参数上的注解信息。
通过工具(如 javap -v
)可以查看 .class
文件中的注解信息。
注解的解析主要依赖于 Java 的反射机制。以下是解析注解的基本流程:
1、获取注册信息:通过反射 API 可以获取类、方法、字段等元素上的注解。例如:
Class<?> clazz = MyClass.class;
MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);
if (annotation != null) {
System.out.println(annotation.value());
}
2、底层原理:反射机制的核心类是 java.lang.reflect.AnnotatedElement
,它是所有可以被注解修饰的元素(如 Class
、 Method
、 Field
等)的父接口。该接口提供了以下方法:
getAnnotation(Class<T> annotationClass)
:获取指定类型的注解。getAnnotations()
:获取所有注解。isAnnotationPresent(Class<? extends Annotation> annotationClass)
:判断是否包含指定注解。
这些方法的底层实现依赖于 JVM 提供的本地方法(Native Method),例如:
native Annotation[] getDeclaredAnnotations0(boolean publicOnly);
native <A extends Annotation> A getAnnotation(Class<A> annotationClass);
JVM 在加载类时会解析 .class
文件中的注解信息,并将其存储在内存中,供反射机制使用。
因此,注解解析的底层实现主要依赖于 Java 的反射机制和字节码文件的存储。通过 @Retention
元注解可以控制注解的保留策略,当使用 RetentionPolicy.RUNTIME
时,可以在运行时通过反射 API 来解析注解信息。在 JVM 层面,会从字节码文件中读取注解信息,并创建注解的代理对象来获取注解的属性值。
28. Java注解的作用域呢?
注解的作用域(Scope)指的是注解可以应用在哪些程序元素上,例如类、方法、字段等。Java 注解的作用域可以分为三种:
- 类级别作用域:用于描述类的注解,通常放置在类定义的上面,可以用来指定类的一些属性,如类的访问级别、继承关系、注释等。
- 方法级别作用域:用于描述方法的注解,通常放置在方法定义的上面,可以用来指定方法的一些属性,如方法的访问级别、返回值类型、异常类型、注释等。
- 字段级别作用域:用于描述字段的注解,通常放置在字段定义的上面,可以用来指定字段的一些属性,如字段的访问级别、默认值、注释等。
除了这三种作用域,Java 还提供了其他一些注解作用域,例如构造函数作用域和局部变量作用域。这些注解作用域可以用来对构造函数和局部变量进行描述和注释。
异常
29. 介绍一下Java异常
Java异常类层次结构图:
Java 的异常体系主要基于两大类:Throwable 类及其子类。Throwable 有两个重要的子类:Error 和 Exception,它们分别代表了不同类型的异常情况。
Error(错误):表示运行时环境的错误。错误是程序无法处理的严重问题,如系统崩溃、虚拟机错误、动态链接失败等。通常,程序不应该尝试捕获这类错误。例如,OutOfMemoryError、StackOverflowError 等。
Exception(异常):表示程序本身可以处理的异常条件。异常分为两大类:
- 非运行时异常:这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。
- 运行时异常:这类异常包括运行时异常(RuntimeException)和错误(Error)。运行时异常由程序错误导致,如空指针访问(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。运行时异常是不需要在编译时强制捕获或声明的。
30. Java异常处理有哪些?
异常处理是通过使用try-catch
语句块来捕获和处理异常。以下是 Java 中常用的异常处理方式:
- try-catch 语句块:用于捕获并处理可能抛出的异常。
try
块中包含可能抛出异常的代码,catch
块用于捕获并处理特定类型的异常。可以有多个catch
块来处理不同类型的异常。
try {
// 可能抛出异常的代码
} catch (ExceptionType1 e1) {
// 处理异常类型1的逻辑
} catch (ExceptionType2 e2) {
// 处理异常类型2的逻辑
} catch (ExceptionType3 e3) {
// 处理异常类型3的逻辑
} finally {
// 可选的finally块,用于定义无论是否发生异常都会执行的代码
}
- throw 语句:用于手动抛出异常。可以根据需要在代码中使用
throw
语句主动抛出特定类型的异常。
throw new ExceptionType("Exception message");
- throws 关键字:用于在方法声明中声明可能抛出的异常类型。如果一个方法可能抛出异常,但不想在方法内部进行处理,可以使用
throws
关键字将异常传递给调用者来处理。
public void methodName() throws ExceptionType {
// 方法体
}
- finally 块:用于定义无论是否发生异常都会执行的代码块。通常用于释放资源,确保资源的正确关闭。
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 处理异常的逻辑
} finally {
// 无论是否发生异常,都会执行的代码
}
Object
31. == 与 equals 有什么区别?
对于字符串变量来说,使用"=="
和"equals"
比较字符串时,其比较方法不同。"=="
比较两个变量本身的值,即两个对象在内存中的首地址,"equals"
比较字符串包含内容是否相同。
对于非字符串变量来说,如果没有对equals()
进行重写的话,"=="
和 "equals"
方法的作用是相同的,都是用来比较对象在堆内存中的首地址,即用来比较两个引用变量是否指向同一个对象。
==
:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较;equals()
:比较的是两个字符串的内容,属于内容比较。
32. hashcode和equals方法有什么关系?
在 Java 中,对于重写 equals
方法的类,通常也需要重写 hashCode
方法,并且需要遵循以下规定:
- 一致性:如果两个对象使用
equals
方法比较结果为true
,那么它们的hashCode
值必须相同。也就是说,如果obj1.equals(obj2)
返回true
,那么obj1.hashCode()
必须等于obj2.hashCode()
。 - 非一致性:如果两个对象的
hashCode
值相同,它们使用equals
方法比较的结果不一定为true
。即obj1.hashCode() == obj2.hashCode()
时,obj1.equals(obj2)
可能为false
,这种情况称为哈希冲突。
hashCode
和 equals
方法是紧密相关的,重写 equals
方法时必须重写 hashCode
方法,以保证在使用哈希表等数据结构时,对象的相等性判断和存储查找操作能够正常工作。而重写 hashCode
方法时,需要确保相等的对象具有相同的哈希码,但相同哈希码的对象不一定相等。
33. String、StringBuffer、StringBuilder的区别和联系
1、可变性:String
是不可变的(Immutable),一旦创建,内容无法修改,每次修改都会生成一个新的对象。StringBuilder
和 StringBuffer
是可变的(Mutable),可以直接对字符串内容进行修改而不会创建新对象。
2、线程安全性:String
因为不可变,天然线程安全。StringBuilder
不是线程安全的,适用于单线程环境。StringBuffer
是线程安全的,其方法通过 synchronized
关键字实现同步,适用于多线程环境。
3、性能:String
性能最低,尤其是在频繁修改字符串时会生成大量临时对象,增加内存开销和垃圾回收压力。StringBuilder
性能最高,因为它没有线程安全的开销,适合单线程下的字符串操作。StringBuffer
性能略低于 StringBuilder
,因为它的线程安全机制引入了同步开销。
4、使用场景:如果字符串内容固定或不常变化,优先使用 String
。如果需要频繁修改字符串且在单线程环境下,使用 StringBuilder
。如果需要频繁修改字符串且在多线程环境下,使用 StringBuffer
。
对比总结如下:
特性 | String | StringBuilder | StringBuffer |
---|---|---|---|
不可变性 | 不可变 | 可变 | 可变 |
线程安全 | 是(因不可变) | 否 | 是(同步方法) |
性能 | 低(频繁修改时) | 高(单线程) | 中(多线程安全) |
适用场景 | 静态字符串 | 单线程动态字符串 | 多线程动态字符串 |
例子代码如下:
// String的不可变性
String str = "abc";
str = str + "def"; // 新建对象,str指向新对象
// StringBuilder(单线程高效)
StringBuilder sb = new StringBuilder();
sb.append("abc").append("def"); // 直接修改内部数组
// StringBuffer(多线程安全)
StringBuffer sbf = new StringBuffer();
sbf.append("abc").append("def"); // 同步方法保证线程安全
Java 新特性
34. Java 8 你知道有什么新特性?
下面是 Java 8 主要新特性的整理表格,包含关键改进和示例说明:
特性名称 | 描述 | 示例或说明 |
---|---|---|
Lambda 表达式 | 简化匿名内部类,支持函数式编程 | (a, b) -> a + b 代替匿名类实现接口 |
函数式接口 | 仅含一个抽象方法的接口,可用 @FunctionalInterface 注解标记 |
Runnable ,Comparator ,或自定义接口@FunctionalInterface interface MyFunc { void run(); } |
Stream API | 提供链式操作处理集合数据,支持并行处理 | list.stream().filter(x -> x > 0).collect(Collectors.toList()) |
Optional 类 | 封装可能为 null 的对象,减少空指针异常 |
Optional.ofNullable(value).orElse("default") |
方法引用 | 简化 Lambda 表达式,直接引用现有方法 | System.out::println 等价于 x -> System.out.println(x) |
接口的默认方法与静态方法 | 接口可定义默认实现和静态方法,增强扩展性 | interface A { default void print() { System.out.println("默认方法"); } } |
并行数组排序 | 使用多线程加速数组排序 | Arrays.parallelSort(array) |
重复注解 | 允许同一位置多次使用相同注解 | @Repeatable 注解配合容器注解使用 |
类型注解 | 注解可应用于更多位置(如泛型、异常等) | List<@NonNull String> list |
CompletableFuture | 增强异步编程能力,支持链式调用和组合操作 | CompletableFuture.supplyAsync(() -> "result").thenAccept(System.out::println) |
35. Java中stream的API介绍一下
Java 引入了 Stream API,它提供了一种高效且易于使用的数据处理方式,特别适合集合对象的操作,如过滤、映射、排序等。Stream API 不仅可以提高代码的可读性和简洁性,还能利用多核处理器的优势进行并行处理。让我们通过两个具体的例子来感受下 Java Stream API 带来的便利,对比在 Stream API 引入之前的传统做法。
案例 1:过滤并收集满足条件的元素
问题场景:从一个列表中筛选出所有长度大于 3 的字符串,并收集到一个新的列表中。
没有 Stream API 的做法:
List<String> originalList = Arrays.asList("apple", "fig", "banana", "kiwi");
List<String> filteredList = new ArrayList<>();
for (String item : originalList) {
if (item.length() > 3) {
filteredList.add(item);
}
}
这段代码需要显式地创建一个新的 ArrayList,并通过循环遍历原列表,手动检查每个元素是否满足条件,然后添加到新列表中。
使用 Stream API 的做法:
List<String> originalList = Arrays.asList("apple", "fig", "banana", "kiwi");
List<String> filteredList = originalList.stream()
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
这里,我们直接在原始列表上调用 .stream()
方法创建了一个流,使用 .filter()
中间操作筛选出长度大于 3 的字符串,最后使用 .collect(Collectors.toList())
终端操作将结果收集到一个新的列表中。代码更加简洁明了,逻辑一目了然。
案例 2:计算列表中所有数字的总和
问题场景:计算一个数字列表中所有元素的总和。
没有 Stream API 的做法:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
for (Integer number : numbers) {
sum += number;
}
这个传统的 for-each 循环遍历列表中的每一个元素,累加它们的值来计算总和。
使用 Stream API 的做法:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
通过 Stream API,我们可以先使用 .mapToInt()
将 Integer 流转换为 IntStream(这是为了高效处理基本类型),然后直接调用 .sum()
方法来计算总和,极大地简化了代码。
36. Java 21 新特性知道哪些?
新语言特性:
- Switch 语句的模式匹配:该功能在 Java 21 中也得到了增强。它允许在
switch
的case
标签中使用模式匹配,使操作更加灵活和类型安全,减少了样板代码和潜在错误。例如,对于不同类型的账户类,可以在switch
语句中直接根据账户类型的模式来获取相应的余额,如case SavingsAccount sa -> result = sa.getSavings();
- 数组模式:将模式匹配扩展到数组中,使开发者能够在条件语句中更高效地解构和检查数组内容。例如,
if (arr instanceof int[] {1, 2, 3})
,可以直接判断数组arr
是否匹配指定的模式。 - 字符串模板(预览版):提供了一种更可读、更易维护的方式来构建复杂字符串,支持在字符串字面量中直接嵌入表达式。例如,以前可能需要使用
"hello " + name + ", welcome to the geeksforgeeks!"
这样的方式来拼接字符串,在 Java 21 中可以使用hello {name}, welcome to the geeksforgeeks!
这种更简洁的写法
新并发特性方面:
- 虚拟线程:这是 Java 21 引入的一种轻量级并发的新选择。它通过共享堆栈的方式,大大降低了内存消耗,同时提高了应用程序的吞吐量和响应速度。可以使用静态构建方法、构建器或
ExecutorService
来创建和使用虚拟线程。 - Scoped Values(范围值):提供了一种在线程间共享不可变数据的新方式,避免使用传统的线程局部存储,促进了更好的封装性和线程安全,可用于在不通过方法参数传递的情况下,传递上下文信息,如用户会话或配置设置。
序列化
37. 什么是序列化?什么是反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
- 序列化:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
- 反序列化:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
下面是序列化和反序列化常见应用场景:
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
38. 怎么把一个对象从一个jvm转移到另一个jvm?
- 使用序列化和反序列化:将对象序列化为字节流,并将其发送到另一个 JVM,然后在另一个 JVM 中反序列化字节流恢复对象。这可以通过 Java 的
ObjectOutputStream
和ObjectInputStream
来实现。 - 使用消息传递机制:利用消息传递机制,比如使用消息队列(如 RabbitMQ、Kafka)或者通过网络套接字进行通信,将对象从一个 JVM 发送到另一个。这需要自定义协议来序列化对象并在另一个 JVM 中反序列化。
- 使用远程方法调用(RPC):可以使用远程方法调用框架,如 gRPC,来实现对象在不同 JVM 之间的传输。远程方法调用可以让你在分布式系统中调用远程 JVM 上的对象的方法。
- 使用共享数据库或缓存:将对象存储在共享数据库(如 MySQL、PostgreSQL)或共享缓存(如 Redis)中,让不同的 JVM 可以访问这些共享数据。这种方法适用于需要共享数据但不需要直接传输对象的场景。
39. 序列化和反序列化让你自己实现你会怎么做?
Java 默认的序列化虽然实现方便,但却存在安全漏洞、不跨语言以及性能差等缺陷。
- 无法跨语言:Java 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。
- 容易被攻击:Java 序列化是不安全的,我们知道对象是通过在
ObjectInputStream
上调用readObject()
方法进行反序列化的,这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了Serializable
接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。 - 序列化后的流太大:序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。
我会考虑用主流序列化框架,比如 FastJson
、Protobuf
来替代 Java 序列化。
如果追求性能的话,Protobuf
序列化框架会比较合适,Protobuf
的这种数据存储格式,不仅压缩存储数据的效果好,在编码和解码的性能方面也很高效。Protobuf
的编码和解码过程结合 .proto
文件格式,加上 Protocol Buffer
独特的编码格式,只需要简单的数据运算以及位移等操作就可以完成编码与解码。可以说 Protobuf
的整体性能非常优秀。
40. 将对象转为二进制字节流具体怎么实现?
其实,像序列化和反序列化,无论这些可逆操作是什么机制,都会有对应的处理和解析协议,例如加密和解密,TCP 的粘包和拆包,序列化机制是通过序列化协议来进行处理的,和 class
文件类似,它其实是定义了序列化后的字节流格式,然后对此格式进行操作,生成符合格式的字节流或者将字节流解析成对象。
在 Java 中通过序列化对象流来完成序列化和反序列化:
ObjectOutputStream
:通过writeObject()
方法做序列化操作。ObjectInputStream
:通过readObject()
方法做反序列化操作
只有实现了Serializable
或Externalizable
接口的类的对象才能被序列化,否则抛出异常!
实现对象序列化:
- 让类实现
Serializable
接口:
import java.io.Serializable;
public class MyClass implements Serializable {
// class code
}
- 创建输出流并写入对象:
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
MyClass obj = new MyClass();
try {
FileOutputStream fileOut = new FileOutputStream("object.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
} catch (IOException e) {
e.printStackTrace();
}
实现对象反序列化:
- 创建输入流并读取对象:
import java.io.FileInputStream;
import java java.io.ObjectInputStream;
MyClass newObj = null;
try {
FileInputStream fileIn = new FileInputStream("object.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
newObj = (MyClass) in.readObject();
in.close();
fileIn.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
通过以上步骤,对象obj
会被序列化并写入到文件"object.ser"
中,然后通过反序列化操作,从文件中读取字节流并恢复为对象newObj
。这种方式可以方便地将对象转换为字节流用于持久化存储、网络传输等操作。需要注意的是,要确保类实现了Serializable
接口,并且所有成员变量都是Serializable
的才能被正确序列化。
设计模式
41. 了解哪些设计模式?什么是单例设计模式?
1. 设计模式认知
设计模式是针对软件设计中常见问题的可复用解决方案。常见类型包括:
创建型模式:处理对象创建(如单例、工厂)
结构型模式:处理对象组合(如适配器、代理)
行为型模式:处理对象间交互(如观察者、策略)
2. 单例模式核心定义
单例模式确保一个类仅有一个实例,并提供全局访问点。其核心目标:
控制实例数量:禁止通过
new
创建多个对象全局唯一访问:通过静态方法(如
getInstance()
)获取唯一实例
3. 关键实现要点
(1) 私有化构造方法
private Singleton() {} // 阻止外部直接实例化
(2) 静态私有成员变量
存储唯一实例:
private static Singleton instance;
(3) 静态公共访问方法
public static Singleton getInstance() {
if (instance == null) { // 首次调用时创建
instance = new Singleton();
}
return instance;
}
volatile和sychronized如何实现单例模式?
public class SingleTon {
// volatile 关键字修饰变量 防止指令重排序
private static volatile SingleTon instance = null;
private SingleTon(){}
public static SingleTon getInstance(){
if(instance == null){
//同步代码块 只有在第一次获取对象的时候会执行到 ,第二次及以后访问时 instance变量均非nu
synchronized(SingleTon.class){
if(instance == null){
instance = new SingleTon();
}
}
}
return instance;
}
}
正确的双重检查锁定模式需要需要使用 volatile
。volatile
主要包含两个功能。
- 保证可见性。使用
volatile
定义的变量,将会保证对所有线程的可见性。 - 禁止指令重排序优化。
由于 volatile
禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
42. 代理模式和适配器模式有什么区别?
- 目的不同:代理模式主要关注控制对对象的访问,而适配器模式则用于接口转换,使不兼容的类能够一起工作。
- 结构不同:代理模式一般包含抽象主题、真实主题和代理三个角色,适配器模式包含目标接口、适配器和被适配者三个角色。
- 应用场景不同:代理模式常用于添加额外功能或控制对对象的访问,适配器模式常用于让不兼容的接口协同工作。
I/O
43. Java IO 流了解吗?
IO 即 Input/Output
,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
44. I/O 流为什么要分为字节流和字符流呢?
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
主要有两点原因:
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时;
- 如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。
45. Java怎么实现网络IO高并发编程?
可以用 Java NIO ,是一种同步非阻塞的 I/O 模型,也是 I/O 多路复用的基础。
传统的 BIO 里面 socket.read()
,如果 TCP RecvBuffer 里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据, 如果使用 BIO 要想要并发处理多个客户端的 I/O,那么会使用多线程模式,一个线程专门处理一个客户端 I/O,这种模式随着客户端越来越多,所需要创建的线程也越来越多,会急剧消耗系统的性能。
NIO 是基于I/0多路复用实现的,它可以只用一个线程处理多个客户端I0,如果你需要同时管理成千上万的连接,但是每个连接只发送少量数据,例如一个聊天服务器,用NIO实现会更好一些。
46. BIO、NIO、AIO区别是什么?
- BIO(blocking IO):就是传统的
java.io
包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。优点是代码比较简单、直观;缺点是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。 - NIO(non-blocking IO) :Java 1.4 引入的
java.nio
包,提供了Channel
、Selector
、Buffer
等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。 - AIO(Asynchronous IO) :是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。