1、8种基本数据类型
| 数据类型 | 内存占用 | 取值范围 |
|---|---|---|
byte | 1 字节 | -128 到 127 |
short | 2 字节 | -32,768 到 32,767 |
int | 4 字节 | -2,147,483,648 到 2,147,483,647 |
long | 8 字节 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
float | 4 字节 | ±1.4 × 10^−45 到 ±3.4 × 10^38 |
double | 8 字节 | ±4.9 × 10^−324 到 ±1.8 × 10^308 |
char | 2 字节 | 0 到 65,535 |
boolean | 1 字节(实际 JVM 中通常占 4 字节) | true 或 false |
2、包装类
在Java中,包装类是为基本数据类型(int,long,double...)提供对象表示方式的对应类。每一个都有对应的包装类。
| 基本数据类型 | 包装类 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
为什么需要包装类?
Java 是一种面向对象的语言,但其 8 种基本类型 (如 int, boolean) 却不具备对象的特性。包装类 的存在,是为了解决基本类型在面向对象环境中的不足,实现一切皆对象的理念。
-
对象化
Java 中的集合类(如
ArrayList、HashMap等)只接受对象类型,而不接受基本数据类型。因此,如果要在这些集合类中使用基本数据类型,就必须使用包装类。例如,ArrayList<Integer>可以存储int类型的值。 -
支持null值
基本类型都有默认值(如
int默认值为 0),无法表示 “无” 或 “缺失” 的状态。包装类作为对象,可以被赋值为null,这在数据库操作、函数返回、网络传输中非常重要。 -
实现对象特性和方法
有些方法只能接受对象类型作为参数和返回值。包装类可以作为这些方法的参数类型和返回类型,提供更灵活的方式来操作基本数据类型。
自动装箱/拆箱的原理?
- 自动装箱:当需要一个对象类型时,Java 会自动创建一个包装类对象并将基本类型的值存入其中。
- 自动拆箱:当需要基本数据类型时,Java 会自动从包装类对象中提取出基本类型的值。
这两个过程背后由编译器和
Integer.valueOf()、Integer.intValue()等方法支持。编译器会根据上下文的需要(是对象还是基本类型)自动选择合适的操作。
⚠️ 潜在的陷阱:自动装箱的缓存机制
Integer、Short、Byte、Character 等包装类在 -128 到 127 范围内的值会进行缓存。当自动装箱该范围内的值时,会直接从缓存中获取已存在的对象,而不是创建新对象。
这会导致使用 == 比较时出现“魔幻”的结果:
| 表达式 | 结果 | 解释 |
|---|---|---|
Integer a = 100; Integer b = 100; | a == b true | 100 在缓存范围内,a 和 b 指向同一个对象。 |
Integer c = 200; Integer d = 200; | c == d false | 200 超出缓存范围,c 和 d 是两个不同的对象。 |
⭐ 核心要点: 无论是否使用缓存,比较两个包装类对象的内容时,永远应该使用 equals() 方法,而不是 ==。
3、数组
🔍 核心问题
Java 数组在 JVM 层面是如何存储的?它带来了什么风险?为什么说 Java 没有真正的多维数组?
1、数组的特性
✅核心特性
- 同质性:只能存储相同类型数据;JVM类型检查,避免运行时类型错误。
- 定长性:创建后长度不可变;内存分配时确定大小,无动态扩容机制。
- 有序性:数组中的元素是按照索引顺序存储的,可以通过下标
[i]直接访问任意元素。 - 内存连续性:数组元素在内存中是连续存储的。 💻内存结构(JVM层面) 在 JVM 堆内存中,数组对象具有特殊的内存布局:
- 对象头 (Object Header):包含 Mark Word(锁、GC标记)和 Class Pointer。
- Length 字段:数组对象头中特有的字段(4字节),记录数组容量。这解释了为什么获取
arr.length是 操作且不需要括号(它是属性,不是方法)。
- Length 字段:数组对象头中特有的字段(4字节),记录数组容量。这解释了为什么获取
- 数组体 (Body):
- 基本类型数组(如
int[]):直接在连续内存中存储数值。 - 引用类型数组(如
String[]):连续内存中存储的是指向对象的引用,而非对象本身。
- 基本类型数组(如
⚡️性能优势(为什么快)
int[] arr = new int[1000000];
long start = System.nanoTime();
for (int i = 0; i < arr.length; i++) {
arr[i] = i * 2;
}
long time = System.nanoTime() - start; // 通常 < 5ms
- O(1) 随机访问:直接内存地址计算
- CPU缓存友好:连续内存极大提高缓存命中率(比链表快5-10倍)
- 无装箱开销:基础类型数组避免对象头开销
2、数组的局限性
🚫局限性 A:长度不可变 : 数组一旦创建,无法增加或减少其容量。
- JVM分配机制:
int[] arr = new int[10]; // JVM调用:
// CollectedHeap::array_allocate(type, length, CHECK)
- 扩容成本:必须分配新内存块+复制数据
int[] newArr = new int[20]; // 新分配40字节
System.arraycopy(arr, 0, newArr, 0, 10); // 调用memcpy()
时间复杂度:O(N) 内存开销:临时双倍内存 B:插入/删除效率低: 在数组中间位置插入或删除元素时,需要移动后续所有元素。 C:类型单一: 只能存储单一数据类型。
String[] strings = new String[2];
Object[] objects = strings; // 协变:编译通过
objects[0] = 100; // ❌ 编译通过,但运行时抛出 java.lang.ArrayStoreException
3、多维数组
A. 本质:数组的数组 (Array of Arrays)
Java 不支持像 C/C++ 那样在内存中连续存储的矩阵块。Java 的多维数组本质上是一维数组,其元素是指向其他数组的引用。
B.内存布局:
int[][] matrix = new int[2][3];
matrix (引用)
│
▼
[ref0][ref1] ← 主数组
│ │
│ ▼
│ [4][5][6] ← 子数组2
▼
[1][2][3] ← 子数组1
- 主数组:在堆中分配一个长度为 2 的数组,存储两个内存地址(引用)。
- 子数组:在堆的任意位置分配两个独立的长度为 3 的
int数组。 - 非连续性:子数组之间在物理内存上不一定相邻。
C.性能影响
- 访问成本:访问
matrix[i][j]需要读取两次内存(先读行引用,再读具体值)。- 读取主数组的
ref[i](获取子数组地址) - 读取子数组的
element[j]
- 读取主数组的
- 缓存失效:跨行遍历时,由于下一行的内存地址可能跳跃很大,CPU 预取机制失效,导致缓存命中率大幅下降。