模式切换
基础篇
Java 的主要特点?
Java 具有许多重要的特点,使其成为一种广泛应用于开发各种应用程序的编程语言,Java 的这些特点使其成为一种受欢迎的编程语言,适用于各种应用领域,包括企业应用、移动应用、嵌入式系统、Web 开发等。以下是 Java 的主要特点:
- 跨平台性(Platform Independence):Java 代码可以在不同的操作系统上运行,只需编写一次,然后在支持 Java 的任何平台上执行。这是通过 Java 虚拟机(JVM)实现的,它将 Java 字节码翻译成特定平台的本机机器码。
- 面向对象(Object-Oriented):Java 是一种面向对象的编程语言,支持封装、继承和多态等面向对象的概念。这有助于构建模块化、可维护和可扩展的代码。
- 自动内存管理(Garbage Collection):Java 具有垃圾回收机制,自动管理内存分配和释放。开发人员不需要手动释放内存,这有助于减少内存泄漏和错误。
- 丰富的标准库(Standard Library):Java 提供了大量的标准类库,包括各种数据结构、算法、I/O 操作、网络通信、图形用户界面(GUI)等,简化了开发过程。
- 强类型(Strongly Typed):Java 是一种强类型语言,要求变量在使用之前必须先声明其数据类型,这有助于减少类型错误和提高代码的可读性。
- 安全性(Security):Java 具有内置的安全性机制,包括字节码验证和安全沙箱,可以防止恶意代码对系统造成损害。
- 多线程支持(Multithreading):Java 内置支持多线程编程,使开发人员能够创建并发执行的应用程序,充分利用多核处理器。
- 简单性与可读性(Simplicity and Readability):Java 的语法设计简洁清晰,易于学习和理解。这有助于降低开发难度和错误率。
- 广泛的社区和生态系统:Java 拥有庞大的开发者社区和丰富的第三方库、框架和工具,有助于加速开发过程。
- 动态扩展性(Dynamic Extensibility):Java 支持动态加载类和资源,使应用程序可以在运行时根据需要加载新的功能模块。
Java 中的基本数据类型有哪些?区别是什么?
Java 中的基本数据类型包括以下几种:
- 整数类型:
- byte:8 位,有符号,范围为 -128 到 127。
- short:16 位,有符号,范围为 -32768 到 32767。
- int:32 位,有符号,范围为 -2^31 到 2^31 - 1。
- long:64 位,有符号,范围为 -2^63 到 2^63 - 1。
- 浮点类型:
- float:32 位,IEEE 754 标准的单精度浮点数。
- double:64 位,IEEE 754 标准的双精度浮点数。
- 字符类型:
- char:16 位,表示单个 Unicode 字符。
- 布尔类型:
- boolean:表示真(true)或假(false)值。
这些基本数据类型在 Java 中具有不同的存储大小和表示范围,这是为了在不同的应用场景中提供更高的灵活性和性能。例如,使用 int 来表示整数可以提供良好的性能,而 double 可以用于表示高精度的浮点数。
需要注意的是,基本数据类型是值类型,它们存储的是实际的数据值。与之相对,引用类型(如对象)存储的是对数据的引用。
基本数据类型在内存中占据固定大小的空间,而引用类型的大小取决于所引用的对象的大小。
在 Java 中,基本数据类型是不可变的,这意味着一旦创建了一个变量并初始化了它,就无法更改它的值。这与引用类型有所不同,因为引用类型的变量可以在运行时指向不同的对象。
Java 的基本数据类型提供了在编程中处理不同类型数据的基础,开发人员可以根据应用的需求选择适当的数据类型来实现所需的功能。
什么是自动装箱和拆箱?
自动装箱(Autoboxing)和拆箱(Unboxing)是 Java 中的两个特性,用于在基本数据类型和其对应的包装类之间进行转换,以便于编程过程中的数据处理。
- 自动装箱(Autoboxing): 自动装箱是指在需要的情况下,Java 编译器会自动将基本数据类型转换为对应的包装类对象。这种转换发生在需要将基本数据类型当作对象使用时,例如将基本数据类型存储在集合中,或者传递给方法需要包装类对象参数的情况下。示例:
java
int num = 42;
Integer integerObj = num; // 自动装箱
- 拆箱(Unboxing): 拆箱是指在需要的情况下,Java 编译器会自动将包装类对象转换为对应的基本数据类型。这种转换发生在需要将包装类对象当作基本数据类型使用时,例如从集合中获取值并进行基本数据类型的操作。示例:
java
Integer integerObj = 42;
int num = integerObj; // 自动拆箱
自动装箱和拆箱使得在基本数据类型和包装类之间进行转换变得更加方便,减少了繁琐的手动类型转换,提高了代码的可读性和简洁性。但需要注意的是,虽然这些特性在很多情况下很有用,但在大量数据操作的情况下可能会引起性能问题,因为频繁的装箱和拆箱操作可能会导致额外的内存开销和性能损失。
什么是 Java 中的变量作用域?
在 Java 中,变量作用域(Variable Scope)是指变量在程序中可访问的范围 。换句话说,它定义了变量在代码中的有效性,以及在哪些部分可以使用该变量。
变量作用域分为局部变量、成员变量和静态变量。局部变量在方法内有效,成员变量在类中有效,静态变量在类加载时初始化,全局有效。 具体分为以下几种:- 局部作用域(Local Scope): 局部变量在定义它的代码块内部可见,超出该代码块就无法访问。通常,在方法、循环或代码块内部声明的变量都属于局部作用域。一旦退出该代码块,局部变量就会被销毁。示例:
java
void exampleMethod() {
int localVar = 10; // 局部变量,只在 exampleMethod 内有效
// ...
}
- 实例作用域(Instance Scope): 实例变量属于对象,它在整个对象的生命周期内都可以访问。每个对象都有自己的一组实例变量,它们在类的成员方法中可以被访问。示例:
java
class MyClass {
int instanceVar; // 实例变量,属于对象的状态
}
- 类作用域(Class Scope): 静态变量(使用 static 修饰的变量)属于整个类,而不是对象。它们在类加载时被初始化,在整个类的生命周期内都可以被访问。示例:
java
class MyClass {
static int classVar; // 静态变量,属于类
}
- 方法参数作用域(Method Parameter Scope): 方法参数也属于方法的局部作用域,它们在方法内部可见,但不同于普通的局部变量,方法参数在方法调用时由外部传入。示例:
java
void exampleMethod(int parameter) {
// parameter 是方法参数,只在 exampleMethod 内有效
// ...
}
理解变量的作用域对于编写正确且易于维护的代码至关重要。不同的作用域可以帮助您限制变量的可见性,防止命名冲突,并确保内存资源的正确管理。
问题1:以下代码会输出什么?
java
public class Main {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
/** 结果
true
false
*/
为什么会出现这样的结果?输出结果表明 i1 和 i2 指向的是同一个对象,而 i3 和 i4 指向的是不同的对 象。此时只需一看源码便知究竟,下面这段代码是 Integer 的 valueOf 方法的具体实现:
java
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
其中 IntegerCache 类的实现为:
private static class IntegerCache {
static final int high;
static final Integer cache[];
static {
final int low = -128;
// high value may be configured by property
int h = 127;
if (integerCacheHighPropValue != null) {
// Use Long.decode here to avoid invoking methods that
// require Integer's autoboxing cache to be initialized
int i = Long.decode(integerCacheHighPropValue).intValue();
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
private IntegerCache() {}
}
从上面两段代码可以看出,在通过 valueOf 方法创建 Integer 对象的时候,如果数值在 [-128,127] 之间,便返回指向 IntegerCache.cache 中已经存在的对象的引用,否则创建一个新的 Integer 对象。 上面的代码中 i1 和 i2 的数值为 100,因此会直接从 cache 中拿到已经存在的对象,所以 i1 和 i2 指向的是同一个对象。而 i3 和 i4 指向的是不同的对象。
问题2:以下代码输出什么?
public class Main {
public static void main(String[] args) {
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
/**结果
false
false
*/
原因:在某个范围内的整型数值的个数是有限的,而浮点数是无限的(不是精确的)。
什么是静态变量和实例变量?有什么区别?
在 Java 中,静态变量(Static Variables)和实例变量(Instance Variables)是两种不同类型的变量,它们具有不同的特性和使用方式。
- 静态变量(Static Variables): 静态变量是使用 static 关键字声明的变量,它属于类而不属于任何特定的对象实例。这意味着 无论创建多少个类的实例,静态变量都只有一份拷贝。静态变量在类加载时被初始化,一直存在于内存中,直到程序结束。 它们通常用于表示类级别的数据或共享的属性。示例:
java
class MyClass {
static int staticVar; // 静态变量,属于类,共享属性
}
- 实例变量(Instance Variables): 实例变量是类的成员变量,每个对象实例都会有一份独立的实例变量。它们用于表示对象的状态和属性,每个对象的实例变量值可能不同。 实例变量在对象创建时被分配内存空间,与对象的生命周期相对应。示例:
java
class MyClass {
int instanceVar; // 实例变量,属于对象,不同对象可以有不同的值
}
区别:
- 静态变量属于类,实例变量属于对象。
- 静态变量只有一份拷贝,共享于所有对象实例,而实例变量每个对象实例都有一份。
- 静态变量在类加载时初始化,实例变量在对象创建时初始化。
- 静态变量可以通过类名访问,也可以通过对象实例访问,而实例变量只能通过对象实例访问。
- 静态变量通常用于表示共享属性,而实例变量用于表示对象的状态和属性。
选择使用静态变量还是实例变量取决于需求。如果某个属性对于所有对象都是相同的,则适合使用静态变量。如果属性的值因对象而异,应该使用实例变量。
Java 反射机制
Java 反射机制(Reflection)是一种在运行时检查、获取和操作类、对象、方法、字段等程序元素的能力。通过反射,你可以在运行时获取类的信息、创建对象、调用方法、访问字段等,而无需在编译时确定这些信息。
Java 反射机制的主要组件和概念包括:
- Class 类:在反射中,java.lang.Class 类代表一个 Java 类的元数据,你可以使用该类获取有关类的信息,如类名、字段、方法、接口等。
- 获取 Class 对象:你可以通过类名、对象实例、Class 类的静态方法来获取一个类的 Class 对象,从而获得类的元数据。
- 创建对象:反射允许你使用 Class 对象的 newInstance() 方法动态创建类的实例,相当于调用类的无参构造方法。
- 获取字段和方法:通过 Class 对象可以获取类的字段和方法,甚至私有的字段和方法,然后可以通过反射进行访问和调用。
- 调用方法:使用反射可以调用对象的方法,包括传递参数和获取返回值。
- 修改字段值:通过反射可以修改类的字段值,包括私有字段。
- 数组操作:可以使用反射创建、操作和访问数组对象。
- 动态代理:反射可以用于实现动态代理,动态生成代理类来代替实际对象的调用。
反射的优点是它提供了动态性和灵活性,使你能够在运行时处理未知类型的对象和类。然而,反射也有一些性能开销,因为它需要在运行时进行元数据查询和动态调用,可能会导致一些性能下降。另外,由于反射涉及到操作类的私有成员和方法,如果使用不当,可能会导致安全性问题。 要使用反射,你可以通过以下步骤:
- 获取 Class 对象:使用类名、对象实例或 Class.forName() 方法获取类的 Class 对象。
- 获取类的信息:使用 Class 对象获取类的信息,如字段、方法等。
- 创建对象、调用方法、访问字段:通过反射实现动态操作。
以下是一个简单的 Java 反射示例:
java
public class ReflectionExample {
public static void main(String[] args) throws Exception {
// 获取 Class 对象
Class<?> clazz = Class.forName("java.lang.String");
// 获取类的信息
System.out.println("Class name: " + clazz.getName());
System.out.println("Superclass: " + clazz.getSuperclass());
// 创建对象
Object obj = clazz.newInstance();
System.out.println("Instance: " + obj);
// 获取并调用方法
Method lengthMethod = clazz.getMethod("length");
int length = (int) lengthMethod.invoke(obj);
System.out.println("Length: " + length);
}
}
Java 反射的优缺点
Java 反射是一种强大的机制,允许在运行时检查、操作类、对象、方法和字段等元信息。然而,反射也具有一些优点和缺点,需要根据具体情况慎重使用。
- 优点
- 动态性:反射允许在运行时动态地获取和操作类的信息,这使得代码更加灵活,可以根据运行时的需求进行适应和修改。
- 泛型编程:反射允许在编写通用代码时处理不同类型的对象,这在一些通用框架和库中非常有用。
- 探索性编程:反射可以用于探索类的结构和行为,帮助开发者更好地理解和调试代码。
- 框架和库:反射在一些框架和库中广泛用于实现注解、AOP(面向切面编程)、持久化框架等高级功能。
- 缺点
- 性能开销:反射通常比直接调用方法或访问字段更慢,因为它需要在运行时进行查找和动态调用,这会导致性能开销。
- 类型安全性:反射操作不会受到编译器的类型检查,容易引发类型错误和运行时异常。编译器无法检查到错误,因此需要开发者自己负责类型安全性。
- 可读性和维护性:使用反射的代码通常更难阅读和维护,因为它抽象了类的结构和行为,使得代码的意图不够明确。
- 安全性:反射可以绕过访问控制权限,可能导致不安全的操作,因此需要谨慎使用。Java的安全管理器可以限制反射的使用,但配置复杂。
- 编译时检查失效:由于反射不会受到编译时类型检查的限制,可能会导致在运行时才发现的错误。
反射是一种非常强大的工具,但需要谨慎使用,特别是在性能和安全性方面要格外小心。在绝大多数情况下,可以通过正常的Java编程方式来完成任务,只有在必要时才考虑使用反射。要根据具体需求权衡使用反射所带来的便利性和开销。
JDK 动态代理为什么只能代理有接口的类?
JDK 动态代理只能代理实现了接口的类,主要是由于其实现原理所决定的。 JDK 动态代理是基于 Java 的反射机制实现的,它通过创建代理类来代理目标对象,这个代理类必须实现一个或多个接口。代理类会实现这些接口,并且在方法调用时将调用委托给一个 InvocationHandler 对象来处理。 原理中的关键点是代理类的创建。JDK 动态代理使用 Java 的 Proxy 类和 ProxyGenerator 来在运行时生成代理类。这些生成的代理类必须继承 Proxy 类并实现被代理接口中的方法。因此,如果目标类没有实现接口,代理类无法生成。
总结一下,JDK 动态代理之所以只能代理实现了接口的类,是因为其实现原理决定了代理类必须继承 Proxy 类并实现接口方法。如果需要代理没有实现接口的类,可以考虑使用其他代理方式,如 CGLIB 动态代理,它可以代理没有实现接口的类。
什么是类和对象?它们之间的关系是什么?
在面向对象编程(OOP)中,类(Class)和对象(Object)是两个核心概念。类是面向对象的基本构建块,对象是类的实例化。类定义了对象的属性和行为。
- 类(Class): 类是一个抽象的模板或蓝图,用于定义对象的属性(字段)和行为(方法)。它描述了对象的结构和行为,并定义了对象所具有的属性和方法。类是创建对象的基础,它定义了对象共同的特征和行为。在 Java 中,类是一种数据类型,通过实例化类,可以创建对象。示例:
java
class Car {
String brand;
String model;
void start() {
// 启动汽车的行为
}
}
- 对象(Object): 对象是类的实例,是具体的实体。它是类的一个具体实现,具有类所定义的属性和方法。每个对象都可以具有不同的状态(属性值),但遵循了类所定义的行为。对象是通过类的构造函数创建的,可以在程序中使用对象来执行操作和访问属性。示例:
Car myCar = new Car();
myCar.brand = "Toyota";
myCar.model = "Camry";
myCar.start();
关系:
- 类定义了对象的结构和行为,是对象的模板。
- 对象是类的实例,是类的具体实现。每个对象都属于特定的类,具有类所定义的属性和行为。
- 一个类可以有多个对象实例,它们共享类的属性和方法。
- 类和对象之间的关系是:类是对象的蓝图,对象是类的实体。对象是通过实例化类而创建的。
- 类和对象的概念使得面向对象编程能够更好地模拟现实世界的问题,并且有助于代码的模块化、可维护性和可扩展性。
说一下对象创建的过程?
对象的创建过程通常包括以下几个步骤:
- 类加载:在 Java 程序中,对象的创建始于类的加载。类加载是将类的字节码从
.class
文件加载到内存中并转换为类的定义的过程。这通常由类加载器(ClassLoader)来完成。 - 分配内存:一旦类被加载,内存分配就会发生。对象需要在堆内存中分配空间。堆是 Java 中用于存储对象的主要内存区域。
- 初始化实例变量:在内存分配后,会为对象的实例变量分配默认值(零值)。这是实例变量的初始状态。
- 调用构造方法:接下来,会调用对象的构造方法。构造方法是一个特殊的方法,用于初始化对象的状态。在构造方法中,可以为实例变量赋予特定的初值。
- 初始化代码块:如果类中包含了初始化代码块,这些代码块也会在构造方法之前执行。初始化代码块通常用于执行一些额外的初始化操作。
- 返回对象引用:在对象的构造和初始化完成后,会返回对象的引用。这个引用可以用来访问对象的方法和实例变量。
- 对象就绪:一旦对象被创建并初始化,它就处于就绪状态,可以被程序使用。
需要注意的是,Java 中的对象创建和销毁是由 Java 虚拟机(JVM)管理的,开发者通常只需要关注对象的使用,而不必显式地管理对象的创建和销毁。对象的销毁是自动进行的,当对象不再被引用时,垃圾收集器会将其回收。对象的创建过程是面向对象编程中的重要概念,它确保了对象在被使用之前得到正确的初始化。
new String("abc") 创建了几个对象?
new String("abc") 创建了两个对象:
- String 对象:"abc"是一个字符串字面量,当使用 new String("abc") 时,首先会创建一个新的 String 对象,表示字符串"abc" 。这个对象存储了字符串的值,即"abc"。
- String 对象的引用:除了上述的 String 对象,还会创建一个对这个对象的引用,这个引用是一个指向新创建的 String 对象的变量。这个引用允许你访问和操作这个字符串对象。
总之,new String("abc") 创建了一个 String 对象和一个引用变量,这个引用变量指向这个新创建的字符串对象。虽然有两个部分,但实际上只有一个字符串对象。
什么是封装、继承和多态?
在面向对象编程(OOP)中,封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)是三个基本的概念,用于构建模块化、可维护和可扩展的代码。
- 封装(Encapsulation): 封装是指将数据(属性)和操作(方法)封装在一个单独的单元(类)中,以隐藏内部实现的细节,只暴露必要的接口给外部。通过封装,可以实现数据的隐藏和保护,防止不合法的访问和修改。封装有助于提高代码的可维护性和安全性。示例:
java
class BankAccount {
private double balance; // 封装的属性
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public double getBalance() {
return balance;
}
}
- 继承(Inheritance): 继承是指一个类(子类或派生类)可以继承另一个类(父类或基类)的属性和方法。子类可以使用父类的成员,同时可以添加新的成员或重写父类的方法。继承有助于代码重用和层次结构的构建,使得类之间可以建立关系。示例:
java
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
}
- 多态(Polymorphism):多态是指同一个方法名可以在不同的类中具有不同的实现,这样在调用时会根据对象的实际类型来执行不同的操作。多态允许通过基类引用来操作派生类的对象,提高了代码的灵活性和扩展性。示例:
java
class Shape {
void draw() {
// 默认实现
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Drawing a circle");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("Drawing a rectangle");
}
}
封装、继承和多态是面向对象编程的重要概念,它们可以帮助开发者构建更加模块化、可维护和灵活的代码,提高代码的复用性和可读性。
什么是抽象类和接口?它们之间有什么区别?
在 Java 中,抽象类(Abstract Class)和接口(Interface)都是用于实现面向对象编程的重要概念,它们具有不同的特点和用途。
抽象类(Abstract Class):
抽象类是一个不能被实例化的类,它只能被继承。抽象类可以包含抽象方法(没有实际实现的方法)和具体方法(有实际实现的方法)。抽象类用于定义一组相关的类的通用行为和属性,而由子类来实现具体的细节。子类必须实现抽象类中的所有抽象方法,除非子类本身也是抽象类。示例:
java
abstract class Shape {
abstract void draw(); // 抽象方法
void printInfo() {
System.out.println("This is a shape");
}
}
接口(Interface):
接口是一种完全抽象的类,它只包含抽象方法和常量,没有具体的实现。接口用于定义一组方法的契约,实现该接口的类必须提供对应的具体实现。一个类可以实现多个接口,从而在一定程度上实现多继承的效果。示例:
java
interface Drawable {
void draw(); // 抽象方法
}
区别:
- 抽象类可以包含具体方法的实现,而接口只能包含抽象方法和常量。
- 一个类只能继承一个抽象类,但可以实现多个接口。
- 抽象类可以有构造方法,而接口不能有构造方法。
- 抽象类的访问修饰符可以是 public、protected,默认 private,而接口的方法默认是 public。
- 使用抽象类时,子类必须使用 extends 关键字继承,而使用接口时,子类必须使用 implements 关键字实现。
- 抽象类用于表示一组相关的类的通用行为和属性,接口用于定义一组方法的契约。
通常情况下,如果需要表示一组相关的类的通用行为并且这些行为有默认实现,可以使用抽象类。如果需要定义一组方法的契约,而实现这些契约的类可能来自不同的继承层次,可以使用接口。
谈谈对内部类的理解
内部类是 Java 中一种特殊的类,它定义在其他类或方法中,并且可以访问外部类的成员,包括私有成员。
内部类分为以下几种:
- 成员内部类:定义在一个类的内部,并且不是静态的。成员内部类可以访问外部类的所有成员,包括私有成员。在创建内部类对象时,需要先创建外部类对象,然后通过外部类对象来创建内部类对象。
- 静态内部类:定义在一个类的内部,并且是静态的。与成员内部类不同,静态内部类不能访问外部类的非静态成员,但可以访问外部类的静态成员。在创建静态内部类对象时,不需要先创建外部类对象,可以直接通过类名来创建。
- 局部内部类:定义在一个方法或作用域块中的类,它的作用域被限定在方法或作用域块中。局部内部类可以访问外部方法或作用域块中的 final 变量和参数。
- 匿名内部类:没有定义名称的内部类,通常用于创建实现某个接口或继承某个类的对象。匿名内部类会在定义时立即创建对象,因此通常用于简单的情况,而不用于复杂的类结构。
内部类的主要作用是实现更加灵活的封装设计。需要注意的是,过度使用内部类会增加代码的复杂性,降低可读性和可维护性。因此,在使用内部类时需要考虑是否有必要,并且仔细进行设计和命名。
静态内部类与非静态内部类的区别?
在 Java 中,静态内部类和非静态内部类都是嵌套在其他类中的内部类。它们之间有以下几点区别:
- 实例化方式:静态内部类可以直接通过外部类名来实例化,而非静态内部类必须要通过外部类的实例来实例化。
- 对外部类的引用:静态内部类不持有对外部类实例的引用,而非静态内部类持有对外部类实例的引用。这意味着在静态内部类中不能直接访问外部类的非静态成员(方法或字段),而非静态内部类可以。
- 生命周期:静态内部类的生命周期与外部类相互独立,及时外部实例被销毁,静态内部类仍然存在。非静态内部类的生命周期与外部类实例绑定,只有在外部类实例存在时才能创建非静态内部类的实例。
- 访问权限:静态内部类对外部类的访问权限与其他类一样,根据访问修饰符而定。非静态内部类可以访问外部类的所有成员,包括私有成员。
什么是重写和重载?
重写(override):外壳不变,核心重写。
重载(overload):方法名相同,其他可以不同。构造器使用的是重载。
说说深拷贝和浅拷贝?
深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是在对象复制过程中两种不同的拷贝方式,它们涉及到对象的引用和数据的复制。
浅拷贝(Shallow Copy):
- 浅拷贝是创建一个新对象,然后将原始对象的成员变量值复制到新对象中。但对于成员变量是引用类型的情况,只复制引用,不复制引用指向的对象本身。因此,新旧对象共享同一个引用指向的对象。
- 在浅拷贝中,如果原始对象的成员变量是引用类型,修改新对象中的成员变量可能会影响原始对象,反之亦然。
深拷贝(Deep Copy):
- 深拷贝是创建一个新对象,然后递归地复制原始对象及其所有成员变量指向的对象,直到没有更多的引用对象。这样,新对象与原始对象完全独立,不共享引用对象。
- 在深拷贝中,修改新对象中的成员变量不会影响原始对象,因为它们引用的是不同的对象。
选择深拷贝还是浅拷贝取决于需求和上下文:
- 如果对象的成员变量都是基本数据类型,或者你希望新旧对象共享引用对象,可以选择浅拷贝,因为它更高效。
- 如果对象的成员变量有引用类型,且你需要确保新对象与原始对象完全独立,不共享引用对象,那么需要使用深拷贝。
在实现深拷贝时,需要递归复制引用对象及其所有引用对象。可以通过实现自定义的拷贝方法,或使用第三方库或序列化/反序列化来实现深拷贝。需要注意的是,深拷贝可能涉及到循环引用和性能问题,需要谨慎处理。
Java 有几种文件拷贝方式,哪一种效率最高?
Java 中有多种文件拷贝方式,包括以下几种常见的:
- 基本字节流拷贝:使用 InputStream 和 OutputStream,逐字节或逐块地复制文件内容。
- 缓冲字节流拷贝:使用 BufferedInputStream 和 BufferedOutputStream,通过缓冲区来提高读写效率。
- NIO(New I/O)拷贝:使用 java.nio 包中的 FileChannel,可以使用 transferTo() 或 transferFrom() 方法来实现高效的文件拷贝。
- Files 类拷贝:使用 Java 7 引入的 Files 类,可以使用 Files.copy() 方法来进行文件拷贝。
- 第三方库:使用第三方库,如 Apache Commons IO 或 Guava,它们提供了更高级的文件拷贝方法。
效率最高的文件拷贝方式通常是基于 NIO 的拷贝,使用 FileChannel 来执行文件复制操作。这是因为 NIO 提供了更接近底层操作系统的 I/O 操作,可以利用操作系统的零拷贝(zero-copy)特性,从而提高文件拷贝的效率。在大文件拷贝时,NIO 通常会比传统的基于字节流的拷贝更快。 示例代码使用 NIO 进行文件拷贝:
java
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.StandardCopyOption;
public class FileCopyWithNIO {
public static void main(String[] args) throws IOException {
Path sourcePath = Path.of("source-file.txt");
Path targetPath = Path.of("target-file.txt");
try (FileChannel sourceChannel = FileChannel.open(sourcePath, StandardOpenOption.READ);
FileChannel targetChannel = FileChannel.open(targetPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
}
}
}
需要注意的是,文件拷贝的效率还受到硬件和操作系统的影响,因此在实际应用中,应该根据具体情况选择合适的文件拷贝方式,并进行性能测试以确定最佳的方法。
谈谈什么是零拷贝?
零拷贝(Zero-Copy)是一种计算机系统中的数据传输和处理技术,旨在最小化数据在内存之间的复制操作。传统的数据传输过程中,数据通常需要从一个缓冲区复制到另一个缓冲区,这会导致额外的 CPU 和内存开销。零拷贝技术的目标是通过减少或消除数据复制来提高数据传输的效率。
以下是零拷贝的一些关键概念和特点:
- 减少数据复制:在传统的数据传输过程中,数据通常需要从一个缓冲区复制到另一个缓冲区,这需要 CPU 和内存的资源。零拷贝技术通过不复制数据或最小化复制来减少这种开销。
- 直接内存访问:零拷贝通常使用直接内存(Direct Memory)或内核缓冲区(Kernel Buffer)来避免数据复制。这些缓冲区可以在用户空间和内核空间之间共享,从而减少数据的不必要复制。
- 文件传输优化:在文件传输中,零拷贝可以通过使用 sendfile() 或 mmap() 等系统调用来优化,将文件的内容直接传输到网络或另一个文件,而不需要中间缓冲区。
- 网络数据传输:在网络数据传输中,零拷贝可以通过使用 DMA(Direct Memory Access)或操作系统的网络栈来减少数据复制,从而提高网络数据传输的效率。
- 高性能:零拷贝技术通常用于高性能的应用程序,如网络服务器、文件传输和多媒体处理,以最大程度地减少数据复制和降低系统开销。
零拷贝技术的应用范围广泛,它可以显著提高数据传输和处理的效率,尤其是在大规模数据处理和高并发应用中。然而,实施零拷贝需要更复杂的编程和系统支持,因此需要谨慎考虑何时使用零拷贝,以确保在性能优化和代码复杂性之间取得平衡。
如何实现对象的克隆?
在 Java 中,实现对象的克隆有两种方式:浅拷贝和深拷贝。
浅拷贝:通过创建一个新对象,并将原始对象的非静态字段值复制给新对象实现。新对象与原始对象共享引用数据。在 Java 中,可以使用 clone() 方法实现浅拷贝。要实现一个类的克隆操作,需要满足以下条件:
- 类实现 Cloneable 接口,表示该类可以被克隆。
- 重写 Object 类的 clone() 方法,调用 super.clone() 方法实现浅拷贝。
javaclass Person implements Cloneable { private String name; private int age; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
深拷贝:通过创建一个新对象,并递归地复制原始对象及其所有引用对象,直到没有更多的引用对象。新对象与原始对象完全独立,不共享引用对象。实现深拷贝的方法有以下几种:
- 序列化/反序列化:通过将对象序列化为字节流,然后反序列化为新对象来实现深拷贝,要求对象及其引用类型字段实现 Serializable 接口。
- 自定义拷贝方法,递归拷贝引用类型字段。
强引用、软引用、弱引用、虚引用?
在 Java 中,引用是用来管理对象生命周期的机制。Java 提供了四种不同类型的引用,包括强引用、软引用、弱引用和虚引用,它们的特点和用途如下:
- 强引用(Strong Reference):
- 强引用是默认的引用类型,在代码中通常不显式声明。
- 当一个对象具有强引用时,垃圾回收器不会回收该对象,只有当没有强引用指向该对象时,它才会被回收。
java
Object obj = new Object(); // obj是强引用
- 软引用(Soft Reference):
- 软引用通过 java.lang.ref.SoftReference 类实现。
- 当内存不足时,垃圾回收器会回收被软引用指向的对象。这使得软引用非常适合用于实现内存敏感的缓存。
- 弱引用(Weak Reference):
- 弱引用通过 java.lang.ref.WeakReference 类实现。
- 弱引用比软引用更弱,只要没有强引用指向对象,垃圾回收器就会回收被弱引用指向的对象。
java
WeakReference<Object> weakRef = new WeakReference<>(new Object());
- 虚引用(Phantom Reference):
- 虚引用通过 java.lang.ref.PhantomReference 类实现。
- 虚引用几乎没有实际用途,它通常用于跟踪对象被垃圾回收的状态。虚引用不能通过引用获取对象,只用于观察垃圾回收器的行为。
java
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object());
这些不同类型的引用可以用于不同的场景,根据对象的生命周期需求来选择合适的引用类型。强引用最常见,而软引用和弱引用用于处理内存敏感的情况,虚引用则用于特殊的监控和清理需求。需要注意的是,使用引用类型需要小心,以避免内存泄漏或不正确的对象生命周期管理。
值传递和引用传递?
值传递和引用传递是程序中常见的两种参数传递方式,它们在方法调用时决定了参数的传递方式。
- 值传递(Pass by Value):
- 值传递是指将实际参数的副本传递给方法,而不是实际参数本身。在方法内部对参数的修改不会影响到实际参数。
- Java 中的基本数据类型(如 int、float、char 等)以及不可变对象(如 String、包装类等)是通过值传递传递的。
java
public void modifyValue(int value) {
value = 100;
}
int num = 10;
modifyValue(num);
System.out.println(num); // 输出 10
- 引用传递(Pass by Reference):
- 引用传递是指将实际参数的引用传递给方法,方法内部对参数的修改会影响到实际参数。
- Java 中的对象和数组是通过引用传递传递的,因为它们是引用类型,实际上是指向对象的引用。
java
public void modifyArray(int[] array) {
array[0] = 100;
}
int[] arr = {1, 2, 3};
modifyArray(arr);
System.out.println(arr[0]); // 输出 100
需要注意的是,Java 中的参数传递方式是值传递,但对于引用类型的参数,传递的是引用的副本,因此在方法内部对引用类型参数的修改会影响到实际参数。这种方式类似于引用传递,但实际上是值传递。
在 Java 中实现单例模式有哪些方法
在 Java 中,实现单例模式有多种方法,下面列举了一些常见的方法:
- 懒汉式(Lazy Initialization):
- 在首次使用时才创建单例对象。
- 双重检查锁定(Double-Checked Locking)是一种懒汉式的变体,可以提供线程安全的延迟初始化。
java
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
- 饿汉式(Eager Initialization):
- 在类加载时就创建单例对象,无论是否使用。
- 线程安全,但可能会浪费内存,因为对象在初始化时就创建了。
java
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return instance;
}
}
- 静态内部类:
- 利用 Java 类加载器的特性,确保在加载类时线程安全地创建单例对象。
java
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
private static class SingletonHelper {
private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return SingletonHelper.instance;
}
}
- 枚举单例:
- 使用枚举类来实现单例模式,枚举类保证了线程安全和防止反射攻击。
java
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
// Singleton behavior
}
}
- 使用容器管理单例:
- 使用容器(如 Spring 容器)来管理单例对象的生命周期。
java
@Service
public class SingletonService {
// Singleton behavior
}
以上是一些常见的单例模式实现方法,选择哪种方法取决于具体需求和设计考虑。不同方法有各自的优缺点,例如延迟初始化、线程安全性、反射安全性等方面的差异,因此在实际应用中需要根据场景选择适当的实现方式。
Java SPI 是什么?有什么用?
SPI(Service Provider Interface,服务提供商接口)是 Java 中的一种扩展机制,它允许在不修改代码的情况下向应用程序添加或替换组件。SPI 通常与 Java 的服务加载机制一起使用,用于在运行时动态加载实现类或模块。SPI 的核心思想是将接口与其实现解耦,使得应用程序可以在不修改源代码的情况下切换或扩展功能。
SPI 主要包括以下几个关键概念:
- 服务接口(Service Interface):定义了一组抽象方法,通常由应用程序或框架提供,表示一种服务或功能的契约。
- 服务提供者接口(Service Provider Interface):是一个 Java 接口,定义了服务的具体实现。它通常由服务提供者实现,并以实现类的形式存在。
- 服务提供者(Service Provider):是实现了服务提供者接口的类,它提供了服务的具体实现。
- 服务加载器(Service Loader):是 Java 提供的工具类,用于在运行时加载和查找服务提供者的实现。
使用 SPI 的主要用途包括:
- 插件系统:SPI 可以用于构建插件系统,使得应用程序可以动态加载和卸载插件,从而扩展其功能。
- 扩展性:SPI 允许应用程序在不修改代码的情况下,通过添加或替换服务提供者来扩展功能。
- 配置和定制:SPI 可以用于配置和定制应用程序的行为,例如日志系统、数据存储、国际化等。
- 框架和库:SPI 常用于框架和库中,使得框架的用户可以自定义其行为,而无需修改框架的源代码。
SPI 的典型示例是 Java 的数据库驱动程序加载机制,其中不同的数据库供应商提供了不同的驱动程序实现,应用程序可以根据需要选择合适的数据库驱动程序,而不需要修改应用程序的代码。 要使用 SPI,通常需要遵循一定的约定,例如在 META-INF/services 目录下创建一个以服务接口的全限定名命名的文件,该文件包含了服务提供者接口的实现类的名称。然后,可以使用 ServiceLoader 类来加载和获取服务提供者的实例。SPI 的使用需要谨慎设计,以确保良好的扩展性和灵活性。
== 和 equals 的区别是什么?
- == 解读
对于基本类型和引用类型,== 的作用效果是不一样的,如下所示:
- 基本类型:比较值是否相同。
- 引用类型:比较对象内存地址是否相同(即判断是否指向了同一个对象或两个对象的地址是否相同)。
java
String x = "string";
String y = "string";
String z1 = new String("string");
String z2 = new String("string");
System.out.println(x==y); // true
System.out.println(x==z1); // false
System.out.println(z1==z2); // false
System.out.println(x.equlas(y)); // true
System.out.println(x.equals(z1)); // true
因为 x 和 y 指向的是同一个引用,所以 == 也是 true,而 new String() 方法则重写开辟了内存空间,所以 == 结果为 false,而 equals 比较的一直是值,所以结果都为 true。
- equals 解读
equals 是 Object 中的一个方法,本质就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。 首先来看默认情况下 equals 比较一个有相同值的对象。
java
Class Cat{
public Cat(String name){
this.name = name;
}
private String name;
public String getName(){
return name;
}
}
Cat c1 = new Cat("HHH");
Cat c2 = new Cat("HHH");
System.out.println(c1.equals(c2)); // false
输出的结果是 false,我们来看一下 equals 源码:
java
public boolean equals(Object obj){
return (this == obj);
}
原来 equals 本质上是 ==。 那么两个相同值的 String 对象,为什么返回值是 true?
java
String s1 = new String("AAA");
String s2 = new String("AAA");
System.out.println(s1.equals(s2)); // true
同样的,当我们进入 String 的 equals 方法就可以知道:
java
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;
}
原来是 String 重写了 Object 的 equals 方法,把引用比较改成了值比较。
- 总结
== 对于基本类型来说是值比较,对于引用类型来说是比较对象内存地址是否相同。而 equals 默认情况下是引用比较,只是很多类型重写了 equals 方法,比如 String、Integer 等把它变成值比较,所以一般情况下 equals 比较的是值是否相等。
String str = "i" 与 String str = new String("i") 一样吗?
不一样,因为内存分配的方式不一样。String str = "i" ,Java 虚拟机会将其分配到常量池中。String str = new String("i") 则会被分配到堆内存中。
String
- 概述
- String 被声明为 final,因此不能被继承。
- 内部使用 char 数组存储数据,该数组被声明为 final,因此 value 数组初始化之后就不能再引用其他数组了。并且 String 内部没有改变
- value 数组的方法,因此可以保证 String 不可变。不可变不是不可修改,String 也是可以修改的。
java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
- String 的三大特性
- 不可变性:是一个 immutable 模式的对象,不可变的主要作用是当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
- 常量池优化:String 对象创建之后,会在字符串常量池进行优化,下次创建同样对象时,会直接返回缓存的引用。
- final:String 类不可被继承,提高了系统的安全性。
- String 不可变的好处
- 可以缓存 hash 值:因为 String 的 hash 值经常被使用,例如,String 用作 HashMap 的 Key。不可变的特性使得 hash 值也不可变,因此只需要进行一次计算。
- String Pool:如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才能使用 String Pool。
- 安全性:String 经常作为参数,String 的不可变性保证了参数的不可变。例如在作为网络连接参数的情况。
- 线程安全:String 不可变性保证了线程的安全性,因此可以在多个线程中使用。
- String 实例化的方式
- 直接赋值,在字符串常量池中存储:String str = "hello";
- 通过构造函数,在堆内存中存储,可以将字符串的值传入,也可以传入一个 char 数组。
- intern() 方法
当调用某个字符串对象的 intern() 方法时,会先去字符串常量池中查找,如果已经存在一个值相等的字符串对象,就返回该对象的引用,否则在字符串常量池中创建该对象,并返回。
String / StringBuffer / StringBuilder
- String 是 final 修饰的,不可变,每次操作都会产生新的 String 对象
- StringBuffer 和 StringBuilder 都是在原对象上操作
- StringBuffer 是线程安全的,方法都是 synchronized 修饰的;StringBuilder 线程不安全的
- 性能:StringBuilder > StringBuffer > String
- 场景:StringBuilder 和 StringBuffer 都继承了 AbstractStringBuilder 抽象类,底层都是可变字符数组。经常需要改变字符串内容时使用 StringBuilder 或者 StringBuffer
- 优先使用 StringBuilder,多线程使用共享变量时使用 StringBuffer
Integer 和 int 的区别?Java 为什么要设计封装类?
Integer 和 int 是 Java 中的两种不同数据类型,它们之间存在一些重要的区别:
- 基本数据类型 vs 引用数据类型:
- int 是 Java 的基本数据类型(primitive type),它表示一个 32位 的有符号整数。基本数据类型存储在栈内存中,具有较低的内存开销和更快的访问速度。
- Integer 是 Java 的引用数据类型(reference type),它是 int 的包装类,用于将基本数据类型转换为对象。引用数据类型存储在堆内存中,通常具有更高的内存开销和访问速度较慢。
- 自动装箱和拆箱:
- Java 提供了自动装箱(autoboxing)和拆箱(unboxing)功能,可以自动地在 int 和 Integer 之间进行转换。
- 自动装箱允许你将 int 赋值给 Integer,而拆箱允许你将 Integer 转换为 int,这使得基本数据类型和包装类之间的转换更加方便。
java
int primitiveInt = 42;
Integer boxedInt = primitiveInt; // 自动装箱
int unboxedInt = boxedInt; // 自动拆箱
- 空值处理:
- int 是基本数据类型,不能表示空值(null)。如果需要表示空值,必须使用 Integer 并将其设置为 null。
java
Integer nullableInt = null; // 可以表示空值
int nonNullableInt = 0; // 不能表示空值,0是默认值
为什么 Java 要设计封装类(包装类)如 Integer 呢?
这主要与 Java 的面向对象设计和通用性有关:
- 面向对象设计:Java 是一种面向对象的编程语言,所有东西都是对象。基本数据类型不是对象,但有时需要将它们作为对象来操作和传递。因此,Java 提供了包装类来实现这种转换,使基本数据类型也能够具备对象的特性。
- 通用性:在某些情况下,需要将数据传递给方法或集合等只能处理对象的情况。包装类使得可以将基本数据类型转换为对象,以满足这些需求。
- 泛型编程:Java 的泛型支持要求数据类型是引用类型,因此包装类使得泛型可以处理基本数据类型和引用类型一样。
总之,Integer 和其他包装类的设计使得 Java 能够更灵活地处理基本数据类型和对象之间的转换,同时也满足了面向对象编程的需求。但要注意,在需要高性能和低内存开销的情况下,应尽量使用基本数据类型,而在需要对象特性或泛型支持的情况下使用包装类。
Integer 使用不当导致的生产事故?
在 Java 中,Integer 类型的使用不当可能导致一些生产事故,主要与以下几个因素相关:
- 自动拆箱引发的空指针异常:
- 自动拆箱是将 Integer 对象转换为 int 基本数据类型的过程,如果 Integer 对象为 null,在拆箱时会引发 NullPointerException。
- 在生产环境中,如果未正确检查和处理可能为 null 的 Integer 对象,可能导致空指针异常,从而中断应用程序的正常运行。
java
Integer value = null;
int result = value; // 自动拆箱,可能引发 NullPointerException
- 不恰当的装箱和拆箱操作:
- 过度的自动装箱和拆箱操作可能导致性能下降。在高并发或大数据量情况下,频繁的装箱和拆箱操作可能导致性能问题。
java
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
numbers.add(i); // 自动装箱
}
- 数值溢出问题:
- Integer 类型的取值范围是有限的,如果超出了范围,可能导致数值溢出。在处理大整数或数值计算时,需要谨慎检查溢出问题。
java
Integer maxInt = Integer.MAX_VALUE;
Integer overflow = maxInt + 1; // 溢出,结果为负数
为了避免这些问题,应该在使用 Integer 类型时采取以下措施:
- 始终检查 Integer 是否为 null,以避免空指针异常。
- 在需要高性能的情况下,尽量减少自动装箱和拆箱操作,可以使用基本数据类型进行计算。
- 在进行数值计算时,谨慎处理溢出问题,可以考虑使用 Long 或其他更大范围的数据类型。
综上所述,Integer 类型在不当使用时可能导致生产事故,特别是在不注意空指针异常、性能和溢出问题的情况下。开发者应当理解 Integer 类型的特性,并在使用时谨慎处理这些问题。
Integer a1=100,Integer a2=100,a1==a2 的运行结果及原因
在 Java 中,对于整数范围在 -128 到 127 之间的 Integer 对象,当使用自动装箱(Autoboxing)创建它们时,会使用对象池(Integer Pool)来缓存这些常用的整数对象,以提高性能和减少内存消耗。因此,当创建的 Integer 对象的值在 -128 到 127 范围内时,它们会被缓存,即相同的值将引用相同的对象。
在这种情况下:
java
Integer a1 = 100;
Integer a2 = 100;
boolean result = (a1 == a2);
result 的运行结果会是 true,因为 a1 和 a2 都引用了相同的 Integer 对象,即对象池中的对象。
这是因为在范围 -128 到 127 内的整数,Integer 对象会被缓存,所以当使用 Integer a1 = 100; 和 Integer a2 = 100; 创建这两个 Integer 对象时,它们实际上引用了相同的对象。因此,使用 == 运算符比较它们时会返回 true,表示它们是同一个对象的引用。
但需要注意的是,这个对象池的范围只适用于 -128 到 127 范围内的整数。如果你创建的 Integer 对象的值超出了这个范围,那么不会使用对象池,每次都会创建新的对象。例如:
java
Integer a3 = 200;
Integer a4 = 200;
boolean result2 = (a3 == a4); // 这里的 result2 会是 false
在上面的示例中,a3 和 a4 的值超出了 -128 到 127 范围,因此它们不会引用同一个对象,result2 会返回 false。
instanceof 关键字的作用?
instanceof 概念在多态中引出。在多态下,子类只能调用父类的方法,而子类独有的方法父类无法调用,如果强制调用就要向下类型转换,但是向下类型转换具有一定的风险,有可能会转换失败,因此需要 instanceof 先判断,再进行转换。
instanceof 严格来说是一个 Java 的一个双目运算符,用来测试一个对象是否为一个类的实例。 用法如下:
java
boolean res = obj instanceof Class
作用:
判断前面的对象是否属于后面的类,或属于其子类。 如果是则返回 true,反之 false。
其中 obj 为一个对象,Class 表示一个类或接口。当 obj 位为 Class 的对象,或是其直接或间接子类,或者是其接口的实现类,结果 res 都返回 true,反之 false。
注意:
instanceof 前面的引用变量编译时的类型要么与后面的类的类型相同,要么与后面的类具有父子继承关系。 即编译器会检查 obj 是否能转换成右边的 Class 类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体的看运行时确定。
java
Object test = "Hello"; // test 实际类型是 String,但是 Object 是所有类的父类
System.out.println(test instanceof Object); // 返回 true ,因为 test 编译时为 Object 类,test 可以是 Object 类实例
System.out.println(test instanceof String); // 返回 true ,因为 Object 是 String 的父类,test 可以是 String 类的实例
System.out.println(test instanceof Math); // 返回 false ,因为 Object 是 Math 的父类,但是 test 不是 Math 类的实例
System.out.println(null instanceof Object); // 返回 false,因为在 JavaSE 规范中对 instanceof 运算符的规定就是:如果 obj 为 null,则返回 false。
// 不符合 instanceof 语法规则:
String test02 = "Hello"; // test02 是 String 类
System.out.println(test02 instanceof Math); // 编译出错,String 类和 Math 类无继承关系
实例:
java
// 抽象一个人类作为父类
class Person {
String name;
public void classes() {}
public void doWork() {}
}
// 学生类
class Students extends Person {
Students(String myName) {
this.name = myName;
}
// 子类重写父类方法,覆盖了父类方法
public void classes() {
System.out.println(this.name + "在听课");
}
// 子类重写父类方法,覆盖了父类方法
public void doWork() {
System.out.println(this.name + "在写作业");
}
// 子类独有方法
public void playing() {
System.out.println(this.name + "在玩游戏");
}
}
// 老师类
class Teachers extends Person {
Teachers(String myName) {
this.name = myName;
}
// 子类重写父类方法,覆盖了父类方法
public void classes() {
System.out.println(this.name + "在上课");
}
// 子类重写父类方法,覆盖了父类方法
public void doWork() {
System.out.println(this.name + "在改作业");
}
// 子类独有方法
public void shopping() {
System.out.println(this.name + "在逛街");
}
}
public class PolymorphismTest02 {
public static void main(String[] args) {
//此处发生多态
Person s = new Students("张三");
Person t = new Teachers("李四");
s.classes();
s.doWork();
// 无法调用 students 特有的方法,这时需要向下转换类型
//s.playing();
trans(s);
// 同理 Teachers 也需要向下转换类型
t.classes();
t.doWork();
trans(t);
}
// 这个函数能很好的体现出为什么需要用 instancof
// 因为不能确定传入函数的参数到底是 Teachers 还是 Students
public static void trans(Person p) {
if (p instanceof Students) {
Students s2 = (Students)p;
// 通过向下转型便可以调用 Students 特有方法了
s2.playing();
}
else if (p instanceof Teachers) {
Teachers t2 = (Teachers)p;
t2.shopping();
}
}
}
/** 结果
学生在听课
学生在写作业
学生在玩游戏
老师在上课
老师在改作业
老师在逛街
*/
常规 for 循环与增强 for 循环的区别?
- 执行效率:在大多数情况下,常规 for 循环的执行效率要比增强 for 循环高。这是因为增强 for 循环需要额外的步骤获取集合或数组中的元素,而常规 for 循环可以直接通过索引的方式访问元素,避免额外的开销。
- 可变性:常规 for 循环具有更大的灵活性,可以自循环过程中修改计数器,从额控制循环的行为。而增强 for 循环是只读的,不能在循环过程中修改集合或数组的元素。
- 代码简洁性:增强 for 循环通常比常规 for 循环更简洁、易读,尤其是在遍历集合或数组时。使用增强 for 循环可以减少迭代器或索引变量的声明和管理。
为什么重写 equals() 就一定要重写 hashCode() 方法?
在 Java 中,如果重写了 equals() 方法,那么必须同时重写 hashCode() 方法,以保证对象在哈希表中的一致性。
这是因为在 Java 中,hashCode() 方法的主要作用是确定对象的哈希码,而哈希码用于在哈希表等数据结构中快速定位对象的存储位置。当你将对象存储在哈希表中时,哈希表会使用对象的哈希码来确定对象应该放置在哪个槽位(bucket)中。然后,当你尝试查找一个对象时,哈希表会首先根据对象的哈希码来定位到可能包含该对象的槽位,然后再使用 equals() 方法来确保找到的对象与目标对象相等。
因此,如果重写了 equals() 方法,以更准确地定义两个对象是否相等,那么必须同时重写 hashCode() 方法,以确保以下两点:
- 一致性:如果两个对象通过 equals() 方法被认为是相等的,那么它们的哈希码必须相等。也就是说,obj1.equals(obj2) 返回 true,那么 obj1.hashCode() 和 obj2.hashCode() 应该返回相同的哈希码。
- 性能:如果两个对象通过 equals() 方法被认为是不相等的,那么它们的哈希码不一定要不相等,但最好是不同的。这可以帮助哈希表更均匀地分配对象,提高哈希表的性能。
如果不满足这两个条件,可能会导致在哈希表等数据结构中的不一致行为,例如,插入对象成功后无法查找到它,或者在查找时无法正确比较对象。 总之,重写 equals() 方法和 hashCode() 方法是一种约定,用于确保对象在哈希表等数据结构中的一致性和正确性。如果你重写了一个,就必须同时重写另一个,以避免潜在的问题。
Java 中的异常有哪些类型?它们之间有什么关系?
在 Java 中,异常(Exceptions)是表示程序运行期间可能发生的错误或异常情况的对象。Java 中的异常可以分为两种类型:检查异常(Checked Exceptions)和非检查异常(Unchecked Exceptions)或者可称为:编译时异常和运行时异常。
- 检查异常(Checked Exceptions): 检查异常也称为编译时异常,是在编译时被检查的异常,必须在代码中显式地处理或声明。它们通常表示外部因素导致的错误,如文件不存在、网络连接中断等。如果不处理或声明检查异常,编译器会报错。示例:
java
try {
FileReader file = new FileReader("file.txt");
// ...
} catch (FileNotFoundException e) {
// 处理文件不存在异常
}
- 非检查异常(Unchecked Exceptions): 非检查异常也称为运行时异常(RuntimeExceptions),它们是在运行时抛出的异常,不需要在代码中显式地处理或声明。非检查异常通常表示程序逻辑错误,如除以零、空指针引用等。示例:
java
int result = 10 / 0; // 抛出 ArithmeticException
关系:
- 所有异常类都是继承自 java.lang.Throwable 类。
- java.lang.Exception 是所有检查异常的基类,它有许多子类,如 IOException、SQLException 等。
- java.lang.RuntimeException 是所有非检查异常的基类,它有许多子类,如 NullPointerException、ArithmeticException 等。
Java 中的异常机制允许开发者识别和处理可能的错误情况,有助于提高程序的健壮性和可靠性。在处理异常时,通常使用 try-catch 语句来捕获和处理异常,或者使用 throws 关键字在方法签名中声明可能抛出的异常。
try-catch-finally 块的作用是什么?
try-catch-finally 块是 Java 异常处理机制的关键部分,用于捕获和处理可能出现的异常情况,以及在异常处理完成后执行清理操作。try 中放置可能抛出异常的代码,catch 捕获异常并处理,finally 用于执行无论是否发生异常都需要执行的代码。
它的作用如下:
- 捕获异常(Catch Exceptions):try-catch 块用于捕获可能在 try 块中抛出的异常。当异常发生时,程序会跳过 try 块的剩余部分,而是转而执行对应的 catch 块,从而可以在 catch 块中处理异常情况。示例:
java
try {
// 可能会抛出异常的代码
} catch (ExceptionType e) {
// 处理异常的代码
}
- 清理操作(Cleanup Operations):finally 块用于执行无论是否发生异常都需要进行的清理操作,例如关闭文件、释放资源等。无论是否有异常,finally 块中的代码都会被执行,以确保资源得到正确释放。示例:
try {
// 可能会抛出异常的代码
} catch (ExceptionType e) {
// 处理异常的代码
} finally {
// 清理操作,例如关闭文件或释放资源
}
- 结合使用:try-catch-finally 块可以结合使用,以实现同时捕获异常并执行清理操作。即使在 catch 块中有 return 语句,finally 块中的代码也会在方法返回之前执行。示例:
try {
// 可能会抛出异常的代码
} catch (ExceptionType e) {
// 处理异常的代码
} finally {
// 清理操作,例如关闭文件或释放资源
}
总之,try-catch-finally 块是一种强大的机制,可用于处理异常和执行必要的清理操作,从而保证程序的稳定性和资源管理。在实际编程中,合理使用异常处理可以增强代码的可靠性和可维护性。
finally 块一定会执行吗?
finally 块在 Java 中通常用于执行清理工作,例如关闭文件、释放资源或确保某些代码块一定会执行。但并不是绝对保证 finally 块一定会执行,有一些特殊情况下它可能不会被执行,例如:
- System.exit() 被调用:如果在程序中调用了 System.exit() 方法,整个 Java 虚拟机会立即退出,此时 finally 块中的代码不会执行。
java
try {
// 一些代码
System.exit(0); // finally 块不会执行
} finally {
// 清理代码
}
- 无限循环或死锁:如果 finally 块中的代码触发了无限循环或死锁,那么它可能无法正常执行完毕。
java
try {
// 一些代码
} finally {
while (true) {
// 无限循环,finally 块无法退出
}
}
- 线程被强制中断:如果 finally 块正在执行,但线程被强制中断(使用 Thread.interrupt()),finally 块中的代码可能会被中断,不会正常执行完毕。
finally 块的主要目的是确保在一些特殊情况下(如异常抛出或返回语句执行)执行清理工作,但在某些情况下,如程序终止、死锁或线程被中断等情况下,它可能无法完全执行。开发者在使用 finally 块时应特别注意处理这些情况,以确保程序的正确性和稳定性。
什么是自定义异常?
自定义异常(Custom Exception)是指在 Java 编程中,开发者可以根据特定需求和场景,创建自己的异常类来表示特定类型的错误或异常情况。通过自定义异常,可以更好地组织和处理程序中的异常情况,使代码结构更清晰,错误信息更具有可读性。 自定义异常一般是通过创建一个继承自现有的异常类(如 Exception、RuntimeException 等)的新类来实现的,以便于对异常进行分类和定制。自定义异常类通常应该提供一些构造方法,允许设置异常信息和传递其他必要的参数。 示例自定义异常类的基本结构:
java
public class CustomException extends Exception {
// 可以定义构造方法,用于初始化异常信息
public CustomException() {
super();
}
public CustomException(String message) {
super(message);
}
public CustomException(String message, Throwable cause) {
super(message, cause);
}
}
使用自定义异常的示例:
java
public class Example {
public static void main(String[] args) {
try {
// 模拟抛出自定义异常
throw new CustomException("This is a custom exception.");
} catch (CustomException e) {
System.out.println("Caught custom exception: " + e.getMessage());
}
}
}
自定义异常的优点包括:
- 更好的异常分类:可以根据不同的异常情况创建不同的自定义异常类,使异常更有意义,更易于区分。
- 更清晰的异常信息:可以自定义异常的错误信息,使得在异常处理时能够提供更具体的错误描述。
- 更好的可读性:通过自定义异常类名称,能够更准确地表达异常的含义,提高了代码的可读性和维护性。
自定义异常在大型项目中尤其有用,能够帮助开发者更好地管理和处理复杂的异常情况,使代码结构更加清晰和可维护。
运行时异常有哪些?
运行时异常是在 Java 程序运行过程中才会出现的异常,通常情况下不需要进行 try-catch 处理,并且多数情况下是由于编程错误造成的。
- 空指针异常:当程序尝试使用 null 对象时抛出。
- 数组下标越界异常:当程序访问数组的范围之外的元素时抛出。
- 类型转换异常:当程序试图将一个对象转换为不是其实例或子类的类型时抛出。
- 非法参数异常:当传递给方法的参数无效或不合法时抛出。
- 非法状态异常:当对象的状态无效或不合法时抛出。
如何避免空指针异常?
空指针异常(NullPointerException)是 Java 编程中常见的异常之一,通常是由于对 null 对象调用方法或访问属性而引起的。
如下面的例子中,定义了 User
类和 Address
类,User
类中包含一个 Address
类的属性 address
。
java
/**
* @Author: hayden
* @Date: 2024-08-28
* @Description: 用户实体类
*/
public class User {
/**
* 姓名
*/
private String name;
/**
* 地址
*/
private Address address;
public User(String name, Address address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
java
/**
* @Author: hayden
* @Date: 2024-08-28
* @Description: 地址实体类
*/
public class Address {
/**
* 街道
*/
private String street;
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}
UML 类图如下:
当我们创建一个 User
对象时,如果 Address
对象为 null,那么调用 getStreet()
方法时就会抛出空指针异常。
java
public class Main {
public static void main(String[] args) {
Address address = new Address(); // 创建一个空的 Address 对象
User user = new User("hayden", address);
String street = user.getAddress().getStreet();
System.out.println(street);
}
}
为了避免空指针异常,我们可以采取以下几种方法:
- 显式判空
在调用可能为 null 的对象的方法或属性之前,先进行 null 判断。
java
String street = user.getAddress() != null ? user.getAddress().getStreet() : null;
使用 if-else 语句进行判空。
java
public static void main(String[] args) {
Address address = new Address();
if (address != null) {
User user = new User("hayden", address);
String street = user.getAddress().getStreet();
System.out.println(street);
}
}
使用工具类优化条件判断。
java
public static void main(String[] args) {
Address address = new Address();
// ObjectUtils 用于判断对象是否为空,如果是数组,可以使用 CollectionUtils
if (ObjectUtils.isNotEmpty(address)) {
User user = new User("hayden", address);
String street = user.getAddress().getStreet();
System.out.println(street);
}
}
- 使用 Optional 类:Java 8 引入了 Optional 类,可以更方便地处理 null 值。
java
Optional<Address> optionalAddress = Optional.ofNullable(user.getAddress());
String street = optionalAddress.map(Address::getStreet).orElse(null);
- 使用 Objects.requireNonNull() 方法:Objects 类提供了 requireNonNull() 方法,可以用于检查对象是否为 null,如果为 null,则抛出 NullPointerException 异常。
java
String street = Objects.requireNonNull(user.getAddress(), "Address must not be null").getStreet();
- 使用 @NonNull 注解:可以使用 Lombok 等工具库提供的 @NonNull 注解,用于标记方法参数或返回值不允许为 null。
java
public String getStreet(@NonNull Address address) {
return address.getStreet();
}
补充
在 JDK 17 中,优化了 NullPointerException 的错误信息,使其更容易定位问题。当抛出 NullPointerException 时,错误信息将包含更多有用的信息,例如 null 引用的名称和调用链的详细信息,有助于更快地定位问题。
java
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.trim()" because the return value of "com.hyd.demo.Address.getStreet()" is null
at com.hyd.demo.Main.main(Main.java:12)
BigDecimal 常见的使用问题?
BigDecimal 类用于表示任意精度的十进制数,它提供了高精度的算术运算和舍入规则,避免浮点数运算误差。此外,还提供了丰富的算术运算方法,如加法、减法、乘法、除法等,可以满足各种计算需求。 适用于需要精确计算或防止因浮点数计算出现的精度丢失等场景。
- 使用 BigDecimal 的构造函数传入浮点数。
如下方使用 Float、Double 进行运算时,会出现精度丢失的问题。
因为浮点数在计算机中是以二进制表示的,而二进制无法精确表示某些十进制小数,在转换为二进制时产生近似值,因此在进行浮点数运算时,可能会出现精度丢失的问题。
java
public class Main {
public static void main(String[] args) {
float f1 = 0.1f;
float f2 = 0.2f;
System.out.println(f1 + f2); // 0.30000001192092896
double d1 = 0.1;
double d2 = 0.2;
System.out.println(d1 + d2); // 0.30000000000000004
}
}
这时,改用 BigDecimal 类进行计算:
java
public class Main {
public static void main(String[] args) {
BigDecimal b1 = new BigDecimal(0.1);
BigDecimal b2 = new BigDecimal(0.2);
System.out.println(b1.add(b2));
}
}
发现仍然会出现精度丢失的问题:
但如果使用 BigDecimal.valueOf() 方法传入 double 类型的值,就不会出现精度丢失的问题。
java
public class Main {
public static void main(String[] args) {
BigDecimal b1 = BigDecimal.valueOf(0.1);
BigDecimal b2 = BigDecimal.valueOf(0.2);
System.out.println(b1.add(b2));
}
}
这是因为 BigDecimal 的构造函数传入的是 double 类型,而 double 类型本身就存在精度问题。因此,应该使用 BigDecimal 的字符串构造函数,传入字符串来避免精度丢失。
下方代码展示了 BigDecimal.valueOf() 方法的实现,它内部先将 double 转为 String 调用了 BigDecimal 的字符串构造函数。
java
public static BigDecimal valueOf(double val) {
// Reminder: a zero double returns '0.0', so we cannot fastpath
// to use the constant ZERO. This might be important enough to
// justify a factory approach, a cache, or a few private
// constants, later.
return new BigDecimal(Double.toString(val));
}
- 使用 BigDecimal 的 equals() 方法比较值。
在进行比较时,常见的有两种方式,分别为使用 equals() 方法和 compareTo() 方法。
compareTo() 方法:用于比较两个 BigDecimal 对象的大小,实现了 Comparable 接口,比较的是值的大小。返回值为 -1、0、1,分别表示小于、等于、大于。
java
public class Main {
public static void main(String[] args) {
BigDecimal b1 = new BigDecimal("0.01");
BigDecimal b2 = new BigDecimal("0.010");
System.out.println(b1.equals(b2)); // false
System.out.println(b1.compareTo(b2)); // 0
}
}
从上面的结果可以看到两者返回的结果不一样。查看 BigDecimal 的 equals() 方法的源码可以发现 if (scale != xDec.scale) return false;
,即在比较时,除了值之外,还会比较精度,精度不同则返回 false。
java
/**
* 将此BigDecimal与指定的Object进行比较是否相等。与compareTo不同的是,只有当两个BigDecimal对象在值和比例上相等时,该方法才认为它们相等。
* 因此,用这种方法比较2.0并不等于2.00,因为前者的[BigInteger, scale]分量等于[20,1],而后者的分量等于[200,2]。
*/
public boolean equals(Object x) {
if (!(x instanceof BigDecimal xDec))
return false;
if (x == this)
return true;
if (scale != xDec.scale)
return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);
return this.inflated().equals(xDec.inflated());
}
- 使用不正确的舍入模式。
使用 BigDecimal 进行运算时,要正确地使用舍入模式,避免舍入误差引起的问题,如果结果是无限小数,则程序会抛出异常。
如下代码所示,在除法运算时,如果结果是无限小数,而操作结果预期是一个精确的数字,则会抛出 ArithmeticException 异常。
java
public class Main {
public static void main(String[] args) {
BigDecimal b1 = new BigDecimal("1.00");
BigDecimal b2 = new BigDecimal("3.00");
BigDecimal b3 = b1.divide(b2);
System.out.println(b3);
}
}
解决上面的问题只需要指定正确的结果精度即可。
java
public class Main {
public static void main(String[] args) {
BigDecimal b1 = new BigDecimal("1.00");
BigDecimal b2 = new BigDecimal("3.00");
BigDecimal b3 = b1.divide(b2, 2, RoundingMode.HALF_UP);
System.out.println(b3); // 0.33
}
}
RoundingMode.UP:远离零的舍入模式,向远离零的方向舍入,绝对值越大,舍入后的值越大。
RoundingMode.DOWN:向零的舍入模式,直接去掉小数部分。
RoundingMode.CEILING:向正无穷方向舍入,如果是正数则行为和 ROUND_UP 一样,如果是负数则行为和 ROUND_DOWN 一样。
RoundingMode.FLOOR:向负无穷方向舍入,如果是正数则行为和 ROUND_DOWN 一样,如果是负数则行为和 ROUND_UP 一样。
RoundingMode.HALF_UP:四舍五入,如果舍弃部分大于等于 0.5,则舍入行为和 ROUND_UP 一样,否则舍入行为和 ROUND_DOWN 一样。
RoundingMode.HALF_DOWN:五舍六入,如果舍弃部分大于 0.5,则舍入行为和 ROUND_UP 一样,否则舍入行为和 ROUND_DOWN 一样。
RoundingMode.HALF_EVEN:银行家舍入法,如果舍弃部分大于 0.5,则舍入行为和 ROUND_UP 一样,如果舍弃部分小于 0.5,则舍入行为和 ROUND_DOWN 一样,如果舍弃部分等于 0.5,则舍入行为和 ROUND_HALF_UP 一样。
RoundingMode.UNNECESSARY:不需要舍入,如果对精确度有要求,使用该模式,如果对精确度没有要求,使用该模式可能会抛出 ArithmeticException 异常。
总结
- 使用 BigDecimal 的字符串构造函数,避免使用 double 类型构造函数。
- 如果无法避免使用浮点类型,可以使用 BigDecimal.valueOf() 方法。
- 比较 BigDecimal 对象时,使用 equals() 方法会比较精度,使用 compareTo() 方法比较值。
- 在进行运算时,要指定精度和正确的舍入模式,使用 setScale() 方法设置精度,使用 setRoundingMode() 方法设置舍入模式。
金额使用 Long 类型存储还是 BigDecimal 类型存储?
选择使用 Long 类型还是 BigDecimal 类型来存储金额,主要取决于你的具体需求和场景。
- 使用 Long 类型
适用场景:如果你的应用程序中涉及的金额数值范围较小,且不需要进行高精度的浮点数运算(比如,只处理整数金额,如分或更小的货币单位),那么使用 Long 类型是一个简单且高效的选择。
优点:
- 整数运算比浮点数运算更快,且没有精度丢失的问题。
- Long 类型占用的内存比 BigDecimal 小。
缺点:
- 无法精确表示小数金额,除非你将金额转换为最小单位(如分)进行存储。
- 在进行金额计算时,需要手动处理四舍五入等逻辑。
- 使用 BigDecimal 类型
适用场景:当你的应用程序需要处理高精度的浮点数运算,特别是金融领域,如货币计算、税率计算等,BigDecimal 是更好的选择。
优点:
- 可以精确表示小数,非常适合财务计算。
- 提供了丰富的数学运算方法,如加、减、乘、除、四舍五入等,且这些运算都是精确的。
- 可以通过设置精度和舍入模式来控制计算结果,满足不同的业务需求。
缺点:
- 相对于 Long 类型,BigDecimal 占用的内存更多。
- 运算速度相对较慢,因为需要处理复杂的数学逻辑。
- 总结
- 如果你的应用只处理整数金额,且金额范围不大,那么使用 Long 类型可能更合适。
- 如果你的应用需要处理小数金额,或者对金额计算的精度有严格要求,那么应该使用 BigDecimal 类型。
日期格式化使用 yyyy 和 YYYY 的区别?
在日期格式化中,使用 yyyy 和 YYYY 表示年份时存在显著的区别,主要体现在它们对年份的定义和计算方式上。
- yyyy 的含义
标准年份:yyyy 表示的是标准的公历年份,即我们通常所说的年份,如 2024 年。
应用广泛:在大多数日期格式化和日期时间处理的场景中,yyyy 是表示年份的标准方式。
- YYYY 的含义
周基年份(Week-based Year):YYYY 表示的是周基年份,这个概念是基于 ISO 8601 标准中的周数系统来定义的。在这个系统中,一年中的第一周是包含该年的第一个周四的那周,且一周的起始日是周一。如果 1月1日 不是周一,且它所在的一周不包含上一年的最后一个周四,那么这一周仍被视为上一年的最后一周。因此,在某些情况下,尤其是跨年周,使用 YYYY 可能会导致年份与标准的公历年份不一致。
特定场景应用:YYYY 主要用于需要按照周来计算年份的场景,如某些财务、统计或日志分析等领域。
JDK 动态代理与 CGLIB 动态代理的区别?
在 Java 中,动态代理是一种实现 AOP(面向切面编程)的重要技术,它允许在运行时动态创建代理对象,以实现对目标对象的增强功能。Java 中的动态代理主要有两种实现方式:JDK 动态代理和 CGLIB 动态代理,它们在实现动态代理的原理和使用方式上有区别:
JDK 动态代理是基于接口的代理技术,要求目标类必须实现一个或多个接口。它使用 java.lang.reflect.Proxy
类和 java.lang.reflect.InvocationHandler
接口来创建代理对象和实现代理逻辑。 在运行时,JDK 动态代理会生成一个代理类,该代理类实现了目标类实现的接口,并在方法调用前后插入额外的代码(即代理逻辑)。然而,JDK 动态代理只能代理接口,无法代理普通类。
CGLIB 动态代理是基于继承的代理技术,它通过继承目标类来创建代理对象。CGLIB 动态代理不要求目标类实现接口,它使用 net.sf.cglib.proxy.Enhancer
类和 MethodInterceptor
接口来创建代理对象和实现代理逻辑。 由于继承关系,CGLIB 动态代理可以代理普通类,但无法代理 final 类和 final 方法。
总结
JDK 动态代理适用于基于接口的代理需求,而 CGLIB 动态代理适用于代理普通类的需求。选择使用哪种代理方式取决于具体的需求。如果目标类已经实现了接口且需要基于接口进行代理,可以选择 JDK 动态代理。 如果目标类没有实现接口,或需要代理普通类的方法,可以选择 CGLIB。
谈谈自定义注解的场景及实现?
自定义注解是 Java 语言的一个强大的特性,可以为代码添加元数据信息,提供额外的配置或标记,适用于多种场景。
- 配置和扩展框架:通过自定义注解,可以为框架提供配置参数或进行扩展。例如 Spring 框架中 @Autowired 注解用于自动装配依赖项,@RequestMapping 注解用于映射请求路径。
- 运行时检查:自定义注解可以在运行时对代码进行检查,并进行相应处理。例如 JUnit 框架的 @Test 注解标记测试方法,在运行测试时会自动识别并执行这些方法。
- 规范约束:自定义注解用于规范代码风格和约束。例如,Java 代码规范检查工具 Checkstyle 可以通过自定义注解来标记代码中的问题。
实现自定义注解的步骤如下:
- 定义注解:使用 @interface 关键字定义注解,可以在注解中定义属性,方法等。
- 在注解中定义属性,并指定默认值。
- 根据需求,可添加元注解来控制注解的使用方式。如 @Target、@Retention、@Documented、@Inherited 等。
- 在代码中使用自定义注解。
- 使用反射机制解析注解信息。