补充一些java基础知识
目录
接口和抽象类的区别
特性 | 抽象类(Abstract Class) | 接口(Interface) |
---|---|---|
是否可实例化 | ❌ 不可实例化 | ❌ 不可实例化 |
构造方法 | ✅ 可以有构造方法 | ❌ 不能有构造方法 |
字段类型 | ✅ 可以有任意类型的字段(public、protected、private 等) | ✅ 字段默认为 public static final(常量) |
方法实现 | ✅ 可以有具体方法(Java 8+ 支持默认方法) | ✅ Java 8+ 支持默认方法和静态方法,否则仅允许抽象方法 |
继承方式 | ✅ 一个类只能继承一个抽象类 | ✅ 一个类可以实现多个接口 |
访问修饰符 | ✅ 方法可以定义为 public、protected、private | ✅ 方法默认是 public,不能定义为 private(Java 9+ 允许 private 方法) |
多重继承 | ❌ Java 不支持类的多重继承 | ✅ 接口支持多重继承(通过 extends) |
设计目的 | ✅ 定义类的共享行为和状态 | ✅ 定义行为契约(能力),不涉及状态 |
int、Integer区别
类型 | 本质 | 存储方式 |
---|---|---|
int | 基本数据类型(Primitive Type) | 直接存储数值,占用 4 字节 |
Integer | 包装类(Wrapper Class) | 存储对象引用,包含一个 int 字段 |
Integer i = 10; // 自动装箱(int → Integer)
int j = new Integer(20); // 自动拆箱(Integer → int)
特性 | int | Integer |
---|---|---|
类型 | 基本数据类型 | 引用数据类型(类) |
默认值 | 0 | null(作为对象时) |
内存占用 | 4 字节 | 对象头 + 4 字节(约 16 字节,取决于 JVM 实现) |
是否可为 null | ❌ 不可为 null | ✅ 可为 null |
比较方式 | == 比较数值 | == 比较引用地址,.equals() 比较数值 |
适用场景 | 数值计算、性能敏感场景 | 集合类(如 List)、泛型、反射 |
java泛型
Java 泛型的核心实现原理是 类型擦除,其设计目标是兼容旧代码并简化 JVM 实现。尽管牺牲了运行时类型信息,但通过编译期类型检查和桥接方法,Java 泛型在安全性与灵活性之间取得了平衡。理解这些原理有助于避免常见陷阱(如泛型数组、类型捕获),并在实际开发中高效使用泛型。
类型擦除(Type Erasure)
基本原理
- 编译阶段:泛型信息被完全擦除,替换为原始类型(Raw Type)。
- 类型参数(如 T、E)被替换为 Object 或其上界(如 T extends Number 替换为 Number)。
- 泛型方法中的类型参数也会被擦除。
- 运行时:JVM 无法感知泛型的具体类型,泛型信息仅在编译期存在。
优势与代价
- 优势:
- 兼容旧版本 Java 代码(无需修改原有非泛型类)。
- 避免为每个泛型类型生成额外的类文件(如 C++ 模板)。
- 代价:
- 运行时无法获取泛型类型信息(如 List 和 List 在运行时被视为同一类型)。
- 限制泛型数组的创建(如 new T[10] 非法)。
特性 | Java 泛型 | C++ 模板 |
---|---|---|
实现机制 | 类型擦除(编译期擦除) | 实例化(编译期生成具体类型代码) |
运行时类型信息 | 无(完全擦除) | 有(保留具体类型信息) |
性能 | 无额外开销(运行时无泛型实例) | 可能有冗余代码(每个类型生成一份代码) |
兼容性 | 兼容旧版本 Java 代码 | 无兼容性问题 |
- 桥接方法(Bridge Method)
- 2.1 问题背景
由于类型擦除,子类覆盖的方法签名可能与父类方法不一致,导致多态失效。 - 2.2 解决方案
编译器自动生成 桥接方法,确保方法调用的正确性。 - 2.3 示例
class Parent<T> {
public void set(T value) { ... }
}
class Child extends Parent<String> {
@Override
public void set(String value) { ... } // 被扩展为桥接方法
}
编译后(伪代码):
// Child 类中的桥接方法
public void set(Object value) {
set((String) value); // 调用重写的方法
}
List为什么不能int,要Integer
关键点 | 说明 |
---|---|
泛型限制 | Java泛型仅支持引用类型,int无法直接用于集合。 |
自动装箱/拆箱 | 简化了基本类型与包装类的转换,但底层仍依赖Integer对象。 |
null值支持 | Integer允许null,而int不能,适合处理可能缺失的数据。 |
性能与陷阱 | 装箱拆箱带来轻微开销,需注意缓存范围和空指针异常。 |
替代方案 | 使用基本类型数组或专用集合库(如Trove)可优化性能。 |
lambda表达式
在 Java 中,Lambda 表达式 是一种简化函数式接口
实现的语法结构。其底层实现涉及编译器的转换机制、JVM 的 invokedynamic
指令,以及运行时的动态绑定策略。
- 函数式接口
Lambda 表达式必须绑定到一个 函数式接口(Functional Interface),即只包含一个抽象方法的接口。例如:
@FunctionalInterface
interface MyFunction {
String apply(String input);
}
- Lambda 转换为方法
当 Java 编译器遇到 Lambda 表达式时,会将其转换为一个 私有静态方法,例如:
private static String lambda$main$0(String input) {
return input + " - processed";
}
此方法包含了 Lambda 表达式的实际逻辑。
invokedynamic 的作用
- 调用链:Lambda 表达式在字节码中被表示为 invokedynamic 指令。
- 引导方法(Bootstrap Method):该指令会调用一个引导方法(由 JVM 提供,如 LambdaMetafactory),动态生成实现函数式接口的类。
List<String> names = Arrays.asList("Alice", "Bob");
names.forEach(name -> System.out.println(name));
对应指令如下:
INVOKEVIRTUAL java/util/List.forEach(Ljava/util/function/Consumer;)V
INVOKESTATIC java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
LambdaMetafactory 的作用
JVM 提供的 LambdaMetafactory 是 Lambda 表达式实现的关键组件。它负责:
- 根据目标函数式接口(如 Consumer)动态生成一个实现了该接口的类。
将 Lambda 表达式转换为该接口的一个实例。
将 Lambda 方法(如 lambda$main$0)绑定到接口的方法(如 accept)上。 - 实现类的生成
LambdaMetafactory 会生成一个匿名类(如 java/lang/invoke/LambdaForm D M H 或 c o m / e x a m p l e / M y L a m b d a DMH 或 com/example/MyLambda DMH或com/example/MyLambda$Lambda$1),其结构如下:
class com.example.MyLambda$$Lambda$1 implements Consumer<String> {
private final MyLambda this$0; // 捕获的外部对象
private final String capturedVar; // 捕获的外部变量
public void accept(String s) {
lambda$main$0(s); // 调用 Lambda 方法
}
}
Lambda 表达式的底层实现可以概括为以下几步:
- 编译器转换:将 Lambda 转换为私有静态方法。
- 字节码生成:使用 invokedynamic 指令触发动态绑定。
- 运行时实现:通过 LambdaMetafactory 动态生成实现类。
- 变量捕获:根据上下文生成闭包逻辑(静态或非静态)。
这种设计使得 Lambda 表达式既保持了语法简洁,又具备高性能和线程安全的特性,是 Java 8 函数式编程的核心支撑。
Java多态
多态的实现需要满足以下三个条件:
- 继承关系:存在父子类或接口与实现类的继承关系。
- 方法重写:子类对父类的虚方法进行重写。
- 父类引用指向子类对象:通过父类引用调用方法时,实际执行的是子类的实现。
eg:
Animal a = new Dog();
a.speak(); // 实际调用的是 Dog.speak()
多态的底层实现机制
- 运行时常量池与符号引用
- 每个类在编译时会生成一个 运行时常量池(Runtime Constant Pool),其中包含类、方法、字段的符号引用(Symbolic Reference)。
- 符号引用是逻辑名称(如 com.example.Animal.speak:()V),需要在运行时解析为 直接引用(内存地址)。
- 方法调用指令与动态绑定
Java 提供了多种方法调用指令:
- invokevirtual:用于调用实例方法(包括虚方法)。
- invokeinterface:用于调用接口方法。
- invokestatic:用于调用静态方法。
- invokedynamic:支持动态语言特性(如 Lambda 表达式)。
对于 虚方法(非 private、static、final 的方法),JVM 会在运行时通过 动态绑定 决定调用哪个方法。
- 对象头与类元数据
- 每个 Java 对象都有一个 对象头(Object Header),其中包含一个 类指针(Class Pointer),指向该对象所属类的 元数据(Class Metadata)。
- 通过类指针,JVM 可以访问类的方法表(Method Table)和字段信息。
- 方法表(Method Table)
- 每个类在类加载时会生成一个 方法表,记录该类所有虚方法的地址。
- 方法表的结构如下:plaintext
Method Table for Dog:
* - speak() -> Dog.speak()
* - eat() -> Animal.eat()
- 子类重写方法后,方法表中对应的条目会被替换为子类的实现。
- 动态绑定流程
当调用虚方法时,JVM 执行以下步骤:- 获取对象的实际类型:通过对象头中的类指针。
- 查找方法表:在实际类型的类元数据中查找方法表。
- 调用方法:根据方法表中的地址执行对应的方法。
JVM 的动态绑定优化
- 内联缓存(Inline Caching)
JVM 会缓存最近调用过的类和方法地址,减少方法表查找的开销。
如果某个引用经常指向同一类(如 Dog),JVM 会直接调用缓存的方法地址,无需每次查找方法表。 - JIT 编译优化
JIT(Just-In-Time)编译器 会将热点代码(频繁调用的方法)编译为本地机器码。
对于多态调用,JIT 可能通过 方法内联(Inlining)进一步优化性能,前提是能确定调用的目标方法。
Java 多态的底层实现依赖于以下机制:
- 运行时常量池:存储方法的符号引用。
- 方法表:类加载时生成,记录虚方法的地址。
- 对象头与类指针:通过对象头确定实际类型。
- 动态绑定:运行时根据方法表查找并调用实际方法。
- JIT 优化:通过内联缓存和内联提升性能。
这种设计使得 Java 能够灵活支持多态,同时通过 JVM 的优化机制保持较高的执行效率。
happends before
在 Java 中,Happens-Before 原则 是 Java 内存模型(JMM, Java Memory Model)的核心概念之一,用于定义多线程程序中操作的 可见性
和顺序性
。它确保一个线程的操作对另一个线程是可见的,并且在某些情况下保证操作的顺序不会被重排序。
Happens-Before 原则 是一种 逻辑顺序关系,而非物理时间上的先后顺序。如果操作 A happens-before 操作 B,则:
- 可见性:操作 A 的结果对操作 B 可见。
- 顺序性:操作 A 在操作 B 之前发生(逻辑上),但不一定在物理时间上先于 B。
Happens-Before 的内置规则
- 程序顺序规则(Program Order Rule)
同一线程内的操作按代码顺序执行,即 前一个操作 happens-before 后一个操作。
int a = 1; // A
int b = a + 1; // B
// A happens-before B
- 监视器锁规则(Monitor Lock Rule)
解锁操作 happens-before 后续的加锁操作。
synchronized (lock) {
x = 1; // A (unlock)
}
synchronized (lock) {
y = x + 1; // B (lock)
}
// A happens-before B
- volatile 变量规则(Volatile Variable Rule)
写操作 happens-before 后续的读操作。
volatile int flag = false; // A
...
flag = true; // B (write)
...
if (flag) { ... } // C (read)
// B happens-before C
- 线程启动规则(Thread Start Rule)
线程的 start() 方法 happens-before 该线程的 run() 方法中的任何操作。
Thread t = new Thread(() -> {
System.out.println("Hello"); // A
});
t.start(); // B
// B happens-before A
- 线程终止规则(Thread Termination Rule)
线程中的所有操作 happens-before join() 方法的返回。
Thread t = new Thread(() -> {
x = 1; // A
});
t.start();
t.join(); // B
// A happens-before B
-
中断规则(Interrupt Rule)
一个线程调用另一个线程的 interrupt() 方法 happens-before 该线程检测到中断。 -
终结器规则(Finalizer Rule)
对象的构造函数执行完毕 happens-before 该对象的 finalize() 方法执行。 -
传递性(Transitivity)
如果 A happens-before B,且 B happens-before C,则 A happens-before C。
Minor gc 和full gc区别
大对象为什么直接分配堆中
error 和 exception 的区别
java.lang.Throwable
├── java.lang.Exception
│ ├── java.lang.RuntimeException(运行时异常)
│ └── 其他检查型异常(Checked Exceptions)
└── java.lang.Error(错误)
java.lang.Throwable
├── java.lang.Exception
│ ├── java.lang.RuntimeException(运行时异常)
│ └── 其他检查型异常(Checked Exceptions)
└── java.lang.Error(错误)
Exception(异常)
- 定义:表示程序运行过程中可能出现的可恢复的异常。
- 特点:
- 可以通过 try-catch 捕获并处理。
- 检查型异常(Checked Exceptions):编译器强制要求处理(如 IOException)。
- 运行时异常(Unchecked Exceptions):由 RuntimeException 及其子类构成,无需显式捕获(如 NullPointerException)。
Error(错误)
- 定义:表示严重的系统级问题,通常无法通过程序恢复。
- 特点:
- 不建议捕获(除非有特殊需求,如日志记录)。
- 通常由 JVM 或底层系统触发,如内存溢出、栈溢出等。
特性 | Exception | Error |
---|---|---|
是否可恢复 | 可恢复(通过异常处理机制) | 通常不可恢复 |
是否需要显式处理 | 检查型异常必须处理,运行时异常可选 | 通常不需要处理 |
典型场景 | 用户输入错误、文件读取失败等 | JVM 错误、内存溢出、栈溢出等 |
是否继承自 Throwable | 是 | 是 |
是否继承自 Exception | 否 | 否(直接继承自 Throwable) |
Java反射,为什么C/C++没有?
反射(Reflection)是一种 Java 程序运行期间的动态技术,可以在运行时(runtime)检査、修改其自身结构或行为。通过反射,程序可以访问、检测和修改它自己的类、对象、方法、属性等成员。
Java 反射机制的实现与其运行时环境设计密切相关,而C/C++因语言特性限制缺乏原生反射功能。以下是具体原因:
-
JVM的元数据支持
Java的反射依赖于 JVM 在运行时存储的类元数据(如类名、方法签名、字段信息等),这些数据存储在内存的 方法区 (Method Area)的 Class 对象中。运行时可通过这些元数据动态查询和操作对象结构。 -
C/C++的设计哲学差异
C++标准追求零开销编译期反射(如通过编译时注解实现反射),这与Java通过运行时环境动态获取信息的方式本质不同。C++标准委员会更倾向通过工具链(如 Clang )或第三方库(如 Qt 、 UE4 )实现反射功能,而非内置反射机制。 -
类型系统差异
C/C++强调类型安全,其结构体仅存储数据不保留类型信息,导致无法像Java那样通过运行时类型推导实现动态操作。而Java的 类文件格式 (如 Class文件 )本身包含足够元数据支持反射操作。 -
反射的实现可能性
虽然C++26标准已加入反射提案(Reflection TS),但目前仍处于开发阶段,尚未成为语言核心特性。部分编译器(如Clang)已通过实验性支持反射功能,但需额外配置编译环境。