JavaSE-Java基础类型与数据结构

49 阅读6分钟

1、8种基本数据类型

数据类型内存占用取值范围
byte1 字节-128 到 127
short2 字节-32,768 到 32,767
int4 字节-2,147,483,648 到 2,147,483,647
long8 字节-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
float4 字节±1.4 × 10^−45 到 ±3.4 × 10^38
double8 字节±4.9 × 10^−324 到 ±1.8 × 10^308
char2 字节0 到 65,535
boolean1 字节(实际 JVM 中通常占 4 字节)truefalse

2、包装类

在Java中,包装类是为基本数据类型(int,long,double...)提供对象表示方式的对应类。每一个都有对应的包装类。

基本数据类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter

为什么需要包装类?

Java 是一种面向对象的语言,但其 8 种基本类型 (如 int, boolean) 却不具备对象的特性。包装类 的存在,是为了解决基本类型在面向对象环境中的不足,实现一切皆对象的理念。

  1. 对象化

    Java 中的集合类(如 ArrayListHashMap 等)只接受对象类型,而不接受基本数据类型。因此,如果要在这些集合类中使用基本数据类型,就必须使用包装类。例如,ArrayList<Integer> 可以存储 int 类型的值。

  2. 支持null值

    基本类型都有默认值(如 int 默认值为 0),无法表示 “无”“缺失” 的状态。包装类作为对象,可以被赋值为 null,这在数据库操作、函数返回、网络传输中非常重要。

  3. 实现对象特性和方法

    有些方法只能接受对象类型作为参数和返回值。包装类可以作为这些方法的参数类型和返回类型,提供更灵活的方式来操作基本数据类型。

自动装箱/拆箱的原理?

  • 自动装箱:当需要一个对象类型时,Java 会自动创建一个包装类对象并将基本类型的值存入其中。
  • 自动拆箱:当需要基本数据类型时,Java 会自动从包装类对象中提取出基本类型的值。 这两个过程背后由编译器和 Integer.valueOf()Integer.intValue() 等方法支持。编译器会根据上下文的需要(是对象还是基本类型)自动选择合适的操作。

⚠️ 潜在的陷阱:自动装箱的缓存机制 IntegerShortByteCharacter 等包装类在 -128 到 127 范围内的值会进行缓存。当自动装箱该范围内的值时,会直接从缓存中获取已存在的对象,而不是创建新对象。

这会导致使用 == 比较时出现“魔幻”的结果:

表达式结果解释
Integer a = 100; Integer b = 100;a == b \rightarrow true100 在缓存范围内,a 和 b 指向同一个对象。
Integer c = 200; Integer d = 200;c == d \rightarrow false200 超出缓存范围,c 和 d 是两个不同的对象。

⭐ 核心要点: 无论是否使用缓存,比较两个包装类对象的内容时,永远应该使用 equals() 方法,而不是 ==

3、数组

🔍 核心问题

Java 数组在 JVM 层面是如何存储的?它带来了什么风险?为什么说 Java 没有真正的多维数组?

1、数组的特性

✅核心特性

  • 同质性:只能存储相同类型数据;JVM类型检查,避免运行时类型错误。
  • 定长性:创建后长度不可变;内存分配时确定大小,无动态扩容机制。
  • 有序性:数组中的元素是按照索引顺序存储的,可以通过下标 [i] 直接访问任意元素。
  • 内存连续性:数组元素在内存中是连续存储的。 💻内存结构(JVM层面) 在 JVM 堆内存中,数组对象具有特殊的内存布局:
  • 对象头 (Object Header):包含 Mark Word(锁、GC标记)和 Class Pointer。
    • Length 字段:数组对象头中特有的字段(4字节),记录数组容量。这解释了为什么获取 arr.lengthO(1)O(1) 操作且不需要括号(它是属性,不是方法)。
  • 数组体 (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
  1. 主数组:在堆中分配一个长度为 2 的数组,存储两个内存地址(引用)。
  2. 子数组:在堆的任意位置分配两个独立的长度为 3 的 int 数组。
  3. 非连续性:子数组之间在物理内存上不一定相邻

C.性能影响

  • 访问成本:访问 matrix[i][j] 需要读取两次内存(先读行引用,再读具体值)。
    1. 读取主数组的ref[i](获取子数组地址)
    2. 读取子数组的element[j]
  • 缓存失效:跨行遍历时,由于下一行的内存地址可能跳跃很大,CPU 预取机制失效,导致缓存命中率大幅下降。