记录Java基础知识
一、Java基础
1.1 单例模式
public class SingleInstance {
//volatile防止JVM指令重排
private static volatile SingleInstance singleInstance;
private SingleInstance() {
}
public static SingleInstance getSingleInstance() {
if (singleInstance == null) {
synchronized (SingleInstance.class) {
//doubleCheck
if (singleInstance == null) {
singleInstance = new SingleInstance();
}
}
}
return singleInstance;
}
}另外,需要注意 uniqueInstance 采⽤ volatile 关键字修饰也是很有必要。
uniqueInstance 采⽤ volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这
段代码其实是分为三步执⾏:
为 uniqueInstance 分配内存空间
初始化 uniqueInstance
将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1->3->2。指令重排在单线程环境下不
会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执
⾏了 1 和 3,此时 T2 调⽤ getUniqueInstance () 后发现 uniqueInstance 不为空,因此返回
uniqueInstance ,但此时 uniqueInstance 还未被初始化。使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。
1.2 抽象类和接口
抽象类和接口的区别主要从他们的变量与方法两个方面看:
抽象类主要用于抽取多个类的共同特性和行为。包含:成员变量、抽象方法、静态方法、具体方法、构造方法。
接口主要用于定义一套行为规范或能力,一个类可以实现多个接口。接口可以包含:成员变量、抽象方法、静态方法、default默认方法、私有方法(仅供接口内部的default方法或静态方法调用)。 JDK 8 前,接口中只能有抽象方法。从 JDK 8 开始,接口可以包含静态方法和 default 方法。从 JDK 9 开始,接口可以包含私有方法
注意:
接口的成员变量必须是 public static final 的常量(必须赋初值)。例如 int num = 0; 等价于 public static final int num = 0;接口的default默认方法是一个有具体实现的方法,实现类可以重写,也可以直接继承使用抽象类的成员变量默认是default,可以在子类被重新定义赋值。抽象类的成员变量权限默认是default(包内可见),可以在子类中被重新定义或赋值。抽象类中的抽象方法不能被private、static、synchronized和native等修饰抽象类可以有实例变量和静态变量,接口只能有静态变量( static final )
接口的default方法修饰和可见范围default不是一个东西。抽象类可以有构造器,但不能被实例化,子类构造器调用父类构造器(必须)。接口不能有构造器。
1.3 静态/非静态内部类
非静态内部类:
- 依赖于外部类实例,外部类实例化后才可实例化
- 可以访问外部类实例变量(静态/非静态)和方法 (通过隐藏的
Outer.this) - 不能定义static变量(因为非静态内部类生命周期随外部类实例),只能定义static final变量。
- static int x = 5; 错误
- static final int x = 100; 正确
- 可以访问外部类私有变量 (持有对外部类实例的隐式引用)
静态内部类:
- 只能访问外部类静态成员和方法
- 可以定义static变量
- 生命周期独立于外部类实例
| 特性 | 非静态内部类 | 静态内部类 |
|---|---|---|
| 依赖关系 | 依赖外部类实例 | 独立,不依赖实例 |
| 实例化方式 | outer.new Inner() | new Outer.Inner() |
| 访问外部实例成员 | ✅ 直接访问 | ❌ 不能直接访问 |
| 访问外部静态成员 | ✅ 直接访问 | ✅ 直接访问 |
| 定义静态变量 | ❌ 不允许(除常量) | ✅ 允许 |
| 定义静态方法 | ❌ 不允许 | ✅ 允许 |
| 内存持有引用 | 持有 Outer.this 引用 | 无外部类实例引用 |
| 常见用途 | 紧密关联的辅助类 | 工具类、与外部类逻辑相关的独立类 |
QA:非静态内部类可以直接访问外部类实例,是如何做到的?
是因为编译期在生成字节码时会为非静态内部类维护一个指向外部类实例的引用,就可以访问外部类实例。编译器在生成内部类构造方法时,将外部类实例作为参数传入,以此建立联系。
// 源代码中的调用 Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); // 实际编译后的调用 Outer outer = new Outer(); Outer.Inner inner = new Outer$Inner(outer); // 传入外部类实例
1.4 String、StringBuilder、StringBuffer详解
一、String不可变字符
JDK8及之前用的是char[]数组,JDK9及之后改为了byte[]数组。都被final修饰,每次修改都会创建返回新的。天然线程安全。
// JDK 8 及之前
public final class String {
private final char value[]; // 字符数组存储
private int hash; // 缓存哈希值
}
// JDK 9 及之后(紧凑字符串优化)
public final class String {
private final byte[] value; // byte数组存储
private final byte coder; // 编码标识:LATIN1或UTF16
private int hash; // 缓存哈希值
}示例:
String s1 = "hello"; // 字符串常量池
String s2 = new String("hello"); // 堆中新对象
String s3 = s1 + " world"; // 编译优化为StringBuilder
二、StringBuffer(可变字符,线程安全)
StringBuffer实现线程安全是通过所有公开方法都加 synchronized 锁。
public final class StringBuffer extends AbstractStringBuilder {
// 继承自父类
// char[] value; // JDK 8
// byte[] value; // JDK 9+(与String相同优化)
int count; // 实际字符数
// 关键:所有方法都加 synchronized
public synchronized StringBuffer append(String str) {
toStringCache = null; // 清空缓存
super.append(str);
return this;
}
}三、StringBuilder(可变字符串,非线程安全)
与StringBuffer同源,都继承 AbstractStringBuilder,但是方法没有被synchronized修饰,是线程不安全的,性能最好。
public final class StringBuilder extends AbstractStringBuilder {
// 继承自父类
// char[] value; // JDK 8
// byte[] value; // JDK 9+(与String相同优化)
int count; // 实际字符数
// 没有 synchronized!
public StringBuilder append(String str) {
super.append(str);
return this;
}
}四、总结
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 存储结构 | JDK8: char[] JDK9+: byte[] | 同String | 同String |
| 可变性 | ❌ 不可变 | ✅ 可变 | ✅ 可变 |
| 线程安全 | ✅(天然) | ✅(synchronized) | ❌ |
| 性能 | 最低(大量新建对象) | 中等(锁开销) | 最高 |
| 扩容机制 | 不扩容,每次创建新对象 | 2倍+2,可指定初始容量 | 同StringBuffer |
| 初始化 | "" 或构造函数 | 默认new StringBuffer(16) | 默认new StringBuilder(16) |
| 使用场景 | 常量字符串、键值 | 多线程环境字符串操作 | 单线程环境字符串操作 |
1.5 泛型、通配符
一、泛型(Generics)
使用类型参数(如 T, K, V)创建可复用、类型安全的代码组件。T、K、V这种来作为泛型字符,可以用在类、方法、对象上,通过最终使用的时候传入的对象类型来确定最终是什么。一般单个泛型用T(比如说List),多个用<K,V>(比如说Map)。
声明位置:
类:class Box<T> { } // 泛型类
方法:public <T> T get(T t) { } // 泛型方法
接口:interface List<T> { } // 泛型接口二、边界
限定类型参数的范围,只支持上界(extends)。
语法:
- 上界:
<T extends Number>→ T 必须是 Number 或其子类 - 多重边界:
<T extends Number & Comparable<T> & Serializable> - 注意:Java 不支持
<T super Number>作为类型参数声明
三、通配符
在使用泛型类型时表示未知类型,增加灵活性。
三种通配符:
| 通配符 | 含义 | 读取 | 写入 | 用途 |
|---|---|---|---|---|
<?> | 任意类型 | Object | 只能 null | 完全未知类型 |
<? extends T> | T 或其子类 | 作为 T 安全 | 不安全(除 null) | 生产者(只读) |
<? super T> | T 或其父类 | 作为 Object | 可写入 T 及其子类 | 消费者(只写) |
四、T 与 ? 的核心区别
| 特性 | T(类型参数) | ?(通配符) |
|---|---|---|
| 本质 | 具体已知类型 | 未知任意类型 |
| 变量声明 | ✅ 可以声明 T 变量 | ❌ 不能声明 ? 变量 |
| 返回值 | ✅ 可作为返回类型 | ❌ 不能直接作为返回类型 |
| 类型一致性 | ✅ 确保多处类型相同 | ❌ 每个 ? 独立,可能不同 |
| 边界约束 | 只支持上界(extends) | 支持上界/下界(extends/super) |
| 使用场景 | 需要具体类型操作 | 只需类型约束,不关心具体类型 |
五、类型擦除(Type Erasure)
Java 泛型在编译期实现的机制,为保持向后兼容性。
- 核心原因:Java 1.5引入泛型时,已经存在了海量的、使用原始类型(
List,而不是List<String>)和强制类型转换的非泛型代码(Java 1.4及之前)。- 如果不擦除:编译器需要为
List<String>和List<Integer>生成完全不同的类。那么现有的List list = new ArrayList()代码将无法与新的泛型List交互,因为它们本质上成了不同的类型。这会直接破坏现有代码库。- 通过擦除:
List<String>在运行时就是List,新代码(泛型)和旧代码(非泛型)可以无缝互操作。例如,你可以将一个List<String>传递给一个接收List的旧方法。这保证了“二进制兼容性”。
擦除规则:
- 无界类型参数:
<T>→ 擦除为Object - 有界类型参数:
<T extends Number>→ 擦除为Number - 泛型方法:独立擦除,保留方法签名
- 桥方法生成:用于保持多态性
影响:
- 运行时无泛型信息:
instanceof T、new T()无法使用 - 不能创建泛型数组:
T[] array = new T[10];错误 - 重载限制:
void method(List<String>)和void method(List<Integer>)编译后签名相同