1. 核心概念与生态

1.1 JDK, JRE, JVM 的包含关系

大厂不仅仅问定义,更看重你对“运行环境”的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+--------------------------------------------------------------------------------+
| JDK (Java Development Kit) |
| |
| +--------------------------------------------------------------------------+ |
| | JRE (Java Runtime Environment) | |
| | | |
| | +-------------------------+ +-------------------------------------+ | |
| | | JVM (Java Virtual Machine)| + | Core Libraries (String, System...) | | |
| | +-------------------------+ +-------------------------------------+ | |
| +--------------------------------------------------------------------------+ |
| |
| +--------------------------------------------------------------------------+ |
| | Development Tools (javac, javap, jdb, jstack...) | |
| +--------------------------------------------------------------------------+ |
+--------------------------------------------------------------------------------+
  • JVM: 只有翻译能力,没有基石(类库)。
  • JRE: 能运行程序,但不能开发(没有编译器 javac)。
  • JDK: 全套装备。

🔥 大厂面试题: Q: 为什么 Java 被称为“一次编写,到处运行”? A: 关键在于 字节码 (.class)JVM。Java 源代码编译成与平台无关的字节码,不同平台的 JVM(Windows版, Linux版)负责将相同的字节码翻译成特定平台的机器指令。JVM 屏蔽了底层操作系统的差异。

1.2 编译优化:JIT vs AOT

  • JIT (Just-In-Time):
    • 原理: 解释执行 -> 探测热点代码 (HotSpot) -> C1/C2 编译器编译为本地机器码 -> 缓存。
    • 缺点: “预热”过程导致启动慢。
  • AOT (Ahead-Of-Time):
    • 原理: 编译期直接生成机器码(如 GraalVM)。
    • 场景: Serverless、微服务快速启动。

2. 数据类型与底层存储(重点)

2.1 基本数据类型表 (必背)

Java 的基本类型长度是固定的,不随操作系统变化(这与 C/C++ 不同)。

类型 字节 (Bytes) 位 (Bits) 范围 默认值
byte 1 8 -128 ~ 127 0
short 2 16 -32768 ~ 32767 0
int 4 32 -2^31 ~ 2^31-1 (约21亿) 0
long 8 64 -2^63 ~ 2^63-1 0L
float 4 32 IEEE 754 单精度 0.0f
double 8 64 IEEE 754 双精度 0.0d
char 2 16 Unicode 字符 (0 ~ 65535) ‘\u0000’
boolean - - 只有 true/false false

🔥 大厂面试题: Q: Java 中的 boolean 类型占用多少内存? A: JVM 规范没有明确规定

  1. 在编译后的字节码中,boolean 变量通常被当作 int 处理(占 4 字节,使用 iconst_0/iconst_1)。
  2. boolean[] 数组中,通常每个元素占 1 个字节(byte)。 底气: 回答时提到“JVM 规范未定义”和“数组与单独变量的区别”是加分项。

2.2 浮点数的坑

1
2
3
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a == b); // false!
  • 原因: 浮点数采用二进制科学计数法,无法精确表示 0.1 等十进制小数,存在精度丢失。
  • 最佳实践: 涉及金额计算,必须使用 java.math.BigDecimal,且必须使用 String 参数的构造器 (new BigDecimal("0.1"))。

2.3 内存布局图解:基本类型 vs 包装类型

当代码执行 int i = 10; Integer j = 10; 时,内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
    栈内存 (Stack Frame)                     堆内存 (Heap)
+------------------------+ +--------------------------+
| | | |
| 变量 i [ 10 ] | (直接存储值) | |
| | | |
| 变量 j [ 0x1A2B ] ---+------------>| [ Integer 对象 ] |
| | (存储引用) | +----------------------+ |
+------------------------+ | | 对象头 (Header) | |
| | 实例数据 (value=10) | |
| | 对齐填充 (Padding) | |
| +----------------------+ |
+--------------------------+

🔥 大厂面试题: Q: new Integer(10) 这个对象在内存中占多大?(64位 JVM) A: 这是一个典型的考察对象内存布局的问题。

  1. 对象头 (Mark Word + Class Pointer): 开启指针压缩时占 12 字节 (8 + 4),未开启占 16 字节。
  2. 实例数据 (int value): 4 字节。
  3. 对齐填充: Java 对象大小必须是 8 的倍数。
  • 计算: 12 (头) + 4 (数据) = 16 字节。正好是 8 的倍数,无需填充。
  • 结论: 约 16 字节(这是单纯对象的大小,不包含栈上的引用)。

2.4 字符串 String 的至暗时刻 (新增重要章节)

String 是面试中考察最细致的类,没有之一。

🔥 大厂面试题 1: Q: String s = new String(“abc”); 这行代码创建了几个对象? A:

  • 情况 1: 如果字符串常量池中已经存在 “abc”,则只在中创建一个 String 对象。
  • 情况 2: 如果字符串常量池中不存在 “abc”,则先在常量池中创建一个 “abc” 对象,再在中创建一个 String 对象。共 2 个

🔥 大厂面试题 2: Q: String s1 = “a” + “b”; 和 String s2 = x + y; (x, y 为变量) 有什么区别? A:

  • "a" + "b": 编译器优化。编译时直接变成 "ab",放入常量池。
  • x + y: 运行时拼接。底层通过 StringBuilder (Java 8) 或 StringConcatFactory (Java 9+) 实现,最终会在堆上生成一个新的 String 对象。

🔥 大厂面试题 3: Q: String 为什么要设计成不可变的 (Immutable)? A:

  1. 字符串常量池 (String Pool): 只有不可变才能实现池化,节省堆内存。
  2. 安全性: String 常作为网络连接、文件路径、反射参数,不可变防止被恶意修改。
  3. 线程安全: 不可变对象天生线程安全,多线程共享无需同步。
  4. HashCode 缓存: String 广泛用于 HashMap Key,不可变性保证了 hash 值只需计算一次即可缓存,提升性能。

3. 深入理解:装箱、拆箱与缓存

3.1 缓存池原理 (IntegerCache)

这是通过静态内部类实现的单例模式变种。

1
2
3
4
5
6
7
8
9
10
11
// 伪代码演示 IntegerCache 结构
class IntegerCache {
static final Integer[] cache;
static {
// 默认范围 -128 到 127
cache = new Integer[256];
for(int i = 0; i < cache.length; i++) {
cache[i] = new Integer(i - 128); // 提前创建好所有对象
}
}
}

🔥 大厂面试题: Q: Integer i1 = 128; Integer i2 = 128; 它们相等吗?为什么? A: i1 == i2false

  • 原因: 128 超出了默认缓存范围 [-128, 127]
  • 过程: Integer.valueOf(128) 会直接 return new Integer(128),在堆中创建了两个不同的对象,地址不同。

3.2 自动装箱的性能陷阱

在循环中不小心使用包装类会导致大量的 GC(垃圾回收)。

1
2
3
4
5
6
Long sum = 0L; // 致命错误:使用了 Long 包装类
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
// 隐式操作:sum = Long.valueOf(sum.longValue() + i);
// 结果:创建了 20 亿个 Long 对象,内存爆满,CPU 疯狂 GC
}

4. 面向对象核心机制

4.1 Java 只有值传递 (Pass By Value)

这是初学者最大的误区。Java 永远是值传递。

  • 传递基本类型: 传递的是数值的副本
  • 传递引用类型: 传递的是引用地址(指针)的副本,而不是对象本身。

经典面试题代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ValuePassTest {
public static void main(String[] args) {
String s = "Hello";
change(s);
System.out.println(s); // 输出仍然是 "Hello"
}

public static void change(String str) {
// str 是 s 的一个拷贝,它也指向 "Hello"
str = "World";
// 这一步只是让局部变量 str 指向了新对象 "World"
// 外面的 s 依然指向 "Hello",从未改变
}
}

4.2 静态 (Static) 加载顺序

类的加载顺序是:父类静态 -> 子类静态 -> 父类构造 -> 子类构造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Father {
static { System.out.println("父类静态块"); }
public Father() { System.out.println("父类构造器"); }
}

class Son extends Father {
static { System.out.println("子类静态块"); }
public Son() { System.out.println("子类构造器"); }
}

// 执行 new Son() 的输出顺序:
// 1. 父类静态块
// 2. 子类静态块
// 3. 父类构造器
// 4. 子类构造器

4.3 关键字深度辨析:final, finally, finalize (新增)

这三个词长得像,但毫无关系。

关键字 类型 作用
final 修饰符 修饰类(不可继承)、方法(不可重写)、变量(不可修改/常量)。
finally 关键字 try-catch 结构的一部分,保证代码一定被执行(常用于关闭资源)。
finalize 方法 Object 类的方法,GC 回收对象前调用。已过时 (Deprecated),极不稳定,永远不要使用。

🔥 大厂面试题: Q: try { return 1; } finally { return 2; } 返回什么? A: 返回 2

  • 原理: finally 块中的代码会在 try 块的 return 语句执行之后、返回之前执行。如果 finally 里也有 return,会直接覆盖 try 中的返回值。这是严重的逻辑坏味道。

4.4 深拷贝 vs 浅拷贝 (Deep vs Shallow Copy) (新增)

  • 浅拷贝 (Shallow Copy): 也就是 Object.clone() 的默认行为。只复制对象本身,对象内部的引用类型成员变量依然指向原来的对象。
  • 深拷贝 (Deep Copy): 复制对象本身,同时递归复制对象内部引用的所有对象。

🔥 大厂面试题: Q: 如何实现一个对象的深拷贝? A:

  1. 序列化 (推荐): 将对象序列化为流 (ByteArrayOutputStream),再反序列化回来。前提是对象必须实现 Serializable
  2. JSON 转换: JSON.parse(JSON.stringify(obj)) (思路类似)。
  3. 递归重写 clone(): 代码量大,易错,不推荐。

5. 常见误区与坑 (大厂避雷指南)

  1. 误区:所有整数的 == 比较都不可靠

    • 纠正: int (基本类型) 的 == 绝对可靠。只有 Integer (包装类) 在比较时才需要注意。
    • 铁律: 只要是对象比较,闭着眼睛用 equals(),别用 == 赌运气。
  2. 陷阱:HashMap 中的 Key 如果不重写 hashCode 会怎样?

    • 现象: map.put(new Key("a"), 1); 然后 map.get(new Key("a")); 返回 null
    • 原因: HashMap 先算 hashCode 找桶位。如果没重写,两个逻辑相同的对象哈希值不同,get 的时候直接去错误的桶里找,或者在正确的桶里比较 == 失败,导致找不到数据。
  3. 陷阱:short s1 = 1; s1 = s1 + 1; 会报错吗?

    • 答案: 会编译报错。因为 1 是 int,s1 + 1 结果自动提升为 int,不能直接赋值给 short
    • 变种: s1 += 1; 不会报错。因为 += 运算符隐式包含了强制类型转换 s1 = (short)(s1 + 1)
  4. 异常陷阱:Exception vs Error (新增)

    • Error: JVM 层面无法恢复的严重错误(如 StackOverflowError, OutOfMemoryError),不应该被 catch。
    • Exception: 程序运行时的错误。
      • Checked Exception (受检异常): 编译期强制处理 (如 IOException)。
      • Unchecked Exception (运行时异常): 编译期不检查 (如 NullPointerException, ClassCastException)。

    🔥 面试追问: NoClassDefFoundErrorClassNotFoundException 区别?

    • ClassNotFoundException (Exception): 显式加载类(如 Class.forName())时找不到类。
    • NoClassDefFoundError (Error): 编译时类存在,但运行时类文件找不到了(通常是 Jar 包版本冲突或打包遗漏)。

6. 最佳实践

  1. 使用 long 定义时间戳: 系统时间 System.currentTimeMillis() 返回的是毫秒数,必须用 longint 放不下。
  2. 金额计算:
    • ❌ 禁止使用 double / float
    • ✅ 强制使用 BigDecimal (构造传入 String)。
    • ✅ 或者存数据库时单位存为“分”,用 long 存储(如 100 代表 1.00 元)。
  3. POJO 类规范:
    • 所有成员变量都用包装类型(如 Integer 而非 int)。
    • 理由: 数据库查询结果可能是 NULL,如果用 int 接收 NULL 会报 NullPointerException,而 Integer 可以接收 null
  4. 资源关闭:
    • 优先使用 try-with-resources (Java 7+),自动关闭实现了 AutoCloseable 接口的资源(如 IO 流、JDBC 连接),避免 finally 中漏写关闭逻辑。

7. 延伸阅读与练习

推荐阅读

思考与练习

  1. 练习题:

    1
    2
    3
    Integer i1 = new Integer(10);
    Integer i2 = 10;
    System.out.println(i1 == i2); // 输出什么?false