基本数据类型与包装类
八种基本数据类型
Java 是强类型语言,所有变量在使用前必须声明类型。基本数据类型(primitive types)是语言内置的,不是对象。作为局部变量时存储在栈帧中;作为对象的实例字段时,随对象一起分配在堆上。
| 类型 | 大小 | 默认值 | 范围 |
|---|---|---|---|
byte |
1 字节 | 0 | -128 ~ 127 |
short |
2 字节 | 0 | -32768 ~ 32767 |
int |
4 字节 | 0 | -2^31 ~ 2^31-1 |
long |
8 字节 | 0L | -2^63 ~ 2^63-1 |
float |
4 字节 | 0.0f | IEEE 754 单精度 |
double |
8 字节 | 0.0d | IEEE 754 双精度 |
char |
2 字节 | '\u0000' | 0 ~ 65535(Unicode) |
boolean |
未定义 | false | true / false |
看到上面 byte 的范围是 -128 ~ 127,int 的范围是 -2^31 ~ 2^31-1,你可能会好奇:为什么负数比正数多一个? 要回答这个问题,需要先理解整数在计算机中的存储方式——补码(Two's Complement)。
整数的二进制存储:原码、反码、补码
计算机只能识别 0 和 1,那么怎么表示负数?最朴素的想法是:拿出一位来当符号位。这个思路催生了三种编码方案,它们在历史上递进演化,最终补码胜出。
原码(Sign-Magnitude):最直觉的方案
原码的规则非常简单——最高位表示符号(0 正 1 负),其余位表示绝对值。就像我们在数字前面写 + 或 - 号一样:
以 8 位为例(1 位符号 + 7 位数值):
+5 → 0 0000101
↑ ↑──────↑
符号 绝对值 5
-5 → 1 0000101
↑ ↑──────↑
符号 绝对值 5
原码对人类很友好,一眼就能看出正负和大小。但它有两个致命问题:
问题一:零有两种表示。 00000000(+0)和 10000000(-0)都表示零,这让硬件在比较"是否等于零"时需要额外判断。
问题二:加减法需要额外逻辑。 计算 5 + (-3) 时,硬件不能直接把两个原码相加,必须先比较绝对值大小、确定结果符号、再做减法。这意味着 CPU 需要同时配备加法器和减法器,电路复杂度翻倍。
用原码直接相加 5 + (-3),结果是错的:
0 0000101 (+5 的原码)
+ 1 0000011 (-3 的原码)
───────────
1 0001000 = -8 ??? ← 应该是 +2,完全不对!
反码(Ones' Complement):过渡方案
反码的规则:正数不变,负数在原码的基础上,符号位不变,其余位按位取反(0↔1)。
+5 的反码 = 0 0000101 (正数:与原码相同)
-5 的原码 = 1 0000101
-5 的反码 = 1 1111010 (符号位不变,其余取反)
↕↕↕↕↕↕↕
翻翻翻翻翻翻翻
反码的好处是可以让加法器直接处理减法。试试 5 + (-3):
0 0000101 (+5 的反码)
+ 1 1111100 (-3 的反码)
───────────
1 0 0000001 产生了进位
↓ 把溢出的进位加回最低位("循环进位")
0 0000010 = +2 ✅ 正确!
反码解决了加减法统一的问题,但仍面临两个主要挑战:
- +0 和 -0 的困扰:
00000000表示 +0,11111111表示 -0。这让零的判断变得复杂。 - 循环进位(End-around Carry):这是反码最致命的性能弱点。在进行加法运算时,如果最高位产生了溢出进位,必须将其加回到最低位才能得到结果。
详解:什么是循环进位?
反码的数学基础是基于 $2^n - 1$ 取模的。当两个反码相加产生溢出时,这个溢出的“1”实际上代表了 $2^n$,但在反码系统中,$2^n \equiv 1 \pmod{2^n-1}$,所以必须把这个 1 加回末位。
例子:计算 $5 + (-3)$
00000101 (+5 的反码)
+ 11111100 (-3 的反码:原码 10000011 取反)
───────────
1 00000001 产生了溢出进位 1
↓
00000001 (去掉溢出位后的结果)
+ 1 (把溢出位加回最低位)
───────────
00000010 = +2 ✅ 正确!
硬件代价: 为了处理这个循环进位,CPU 的加法器电路需要额外的逻辑来实现这“第二次加法”,或者采用特殊的环形进位加法器。这增加了硬件设计的复杂度,也降低了运算速度。这也是反码最终被补码取代的核心原因——补码的溢出位可以直接丢弃,不需要任何修正。
补码(Two's Complement):最终方案
补码的规则:正数不变,负数 = 反码 + 1。 也可以理解为:负数 = 原码取反 + 1。
+5 的原码 = 0 0000101
+5 的反码 = 0 0000101 (正数三码相同)
+5 的补码 = 0 0000101
-5 的原码 = 1 0000101
-5 的反码 = 1 1111010 (符号位不变,其余取反)
-5 的补码 = 1 1111011 (反码 + 1)
补码的精妙之处在于:它让加法器直接计算,不需要任何修正。再算 5 + (-3):
0 0000101 (+5 的补码)
+ 1 1111101 (-3 的补码)
───────────
1 0 0000010 溢出位直接丢弃
↓
0 0000010 = +2 ✅ 正确!不需要循环进位!
再验证一个负数结果,3 + (-5):
0 0000011 (+3 的补码)
+ 1 1111011 (-5 的补码)
───────────
1 1111110 这是 -2 的补码吗?
↓ 验证:符号位 1 → 负数,取反加一:0000001 + 1 = 0000010 = 2
↓ 所以是 -2 ✅ 正确!
为什么补码能做到这一点? 可以用钟表来类比。在一个 12 小时制的时钟上,时针指向 3 点时,往回拨 5 格(3 - 5)和往前拨 7 格(3 + 7)到达的位置是一样的——都是 10 点。因为 -5 和 +7 对 12 取模的结果相同(它们互为"模 12 的补数")。
钟表上:3 - 5 = 3 + 7 = 10(mod 12)
12
/ | \
11 | 1
/ | \
10 | 2
| | |
9 ←——3——→ 3 从 3 点出发
| -5↙ ↘+7 | 减 5 和 加 7 到达同一个位置
8 | 4
\ | /
7 | 5
\ | /
6
8 位二进制的 mod 值是 2⁸ = 256
-5 的补数 = 256 - 5 = 251 = 11111011₂
所以 -5 的补码就是 11111011,和前面推导出的完全一致!
补码的数学本质是:在 N 位系统中,-X 的补码 = 2^N - X。这让减法变成了加法,CPU 只需要一个加法器就能同时处理加减法。
补码对数值范围的影响
回到最开始的问题:为什么 byte 的范围是 -128 ~ 127,而不是 -127 ~ 127?
在补码系统中,8 位二进制能表示 256 个不同的值(00000000 到 11111111)。因为零只有一种表示(00000000),原来 -0 占据的那个位模式 10000000 就被"释放"出来,可以多表示一个数。
把三种编码对同一组十进制数字的表示并排放在一起看,差异一目了然:
十进制 原码 反码 补码
───────────────────────────────────────────────
+127 01111111 01111111 01111111 ← 正数:三码完全相同
+126 01111110 01111110 01111110
+5 00000101 00000101 00000101
+1 00000001 00000001 00000001
───────────────────────────────────────────────
+0 00000000 00000000 00000000
-0 10000000 11111111 —— ← 原码和反码都有 -0,补码没有!
───────────────────────────────────────────────
-1 10000001 11111110 11111111 ← 负数:三码各不相同
-2 10000010 11111101 11111110
-5 10000101 11111010 11111011
-127 11111111 10000000 10000001
───────────────────────────────────────────────
-128 —— —— 10000000 ← 只有补码能表示 -128!
仔细观察上面这张表,你会发现正数区三码完全相同,负数区各不相同。但最关键的区别在零和 -128 那两行。下面我们彻底搞清楚"补码为什么能多表示一个数"。
为什么补码能多表示一个数?
核心思路:数格子。 8 位二进制一共有多少种不同的 0/1 组合?答案是 2⁸ = 256 种。也就是说我们有 256 个"格子"(位模式),每个格子可以分配给一个十进制数字。问题就变成了:这 256 个格子怎么分配?
原码的分配方式:
格子总数:256 个
分给正数:00000001 ~ 01111111 → +1 到 +127 → 127 个格子
分给负数:10000001 ~ 11111111 → -1 到 -127 → 127 个格子
分给零: 00000000(+0)和 10000000(-0) → 2 个格子 ← 浪费了!
> [!TIP]
> **追问:在原码中,我不能强行把 10000000 分配给 -128 吗?**
> 理论上你可以这么干,但它会带来巨大的逻辑混乱。
> 1. **定义冲突**:原码的定义是“符号位 + 绝对值”。如果 `10000000` 是 -128,那么符号位是 1(负),数值位却是 `0000000`(零),这在逻辑上是自相矛盾的。硬件解码器必须为这个“特例”增加额外的电路判断。
> 2. **数学断层**:在补码中,`-128` 是从 `-127`(`10000001`)减 1 自然得到的二进制结果。但在原码中,`-127` 是 `11111111`,减 1 应该是 `11111110`(即 -126),永远也减不到 `10000000` 去。
>
> 所以,在补码里 `-128` 是**算出来的**,而在原码里这只能是一个**强行打的补丁**。
合计:127 + 127 + 2 = 256 个格子全部用完
但只表示了 127 + 127 + 1 = 255 个不同的数(因为 +0 和 -0 是同一个数)
问题很清楚了:+0 和 -0 在数学上是同一个数,却占了两个格子。相当于 256 个停车位里,有一个车占了两个车位,浪费了一个。
补码的分配方式:
格子总数:256 个
分给正数:00000001 ~ 01111111 → +1 到 +127 → 127 个格子
分给负数:10000000 ~ 11111111 → -128 到 -1 → 128 个格子 ← 多了一个!
分给零: 00000000 → 0 → 1 个格子
合计:127 + 128 + 1 = 256 个格子全部用完
表示了 127 + 128 + 1 = 256 个不同的数,一个都没浪费!
补码让 0 只占一个格子,省出来的那个格子(位模式 10000000)就给了 -128。
深度思考:为什么 10000000 偏偏是 -128?
这是补码学习中最容易卡壳的地方。按照“取反加一”推导:10000000 符号位是 1(负数),数值位取反 1111111 变 0000000,加一变 0000001,难道是 -1?不对!
其实,10000000 是补码体系里的一个特例,我们可以从三个视角看清它的真面目:
视角一:位权定义(最本质的数学逻辑)
在补码中,最高位(符号位)的权值不是正数,而是负数。对于 8 位补码,从高到低每一位的权值分别是:
(-2⁷), 2⁶, 2⁵, 2⁴, 2³, 2², 2¹, 2⁰
即:-128, 64, 32, 16, 8, 4, 2, 1
计算 10000000 的值:
1 * (-128) + 0*64 + 0*32 + ... + 0*1 = -128 ✅
视角二:数值的连续性 观察负数的补码序列:
11111111是 -111111110是 -2- ...
10000001是 -12710000000是多少? 按照这个递减规律,-127 减 1 得到的就是 -128。
视角三:溢出回环
尝试计算 +127 加 1:
01111111 (+127)
+ 00000001 (+1)
───────────
10000000
在 8 位运算中,正数最大值再加 1 会产生“算术溢出”,跳到了负数的最小值。就像时钟的 12 点既是终点也是起点,10000000 在 256 个格子的圆环上,恰好处于 0 的正对面。
终极追问:为什么 10000000 必须给负数?不能给正数吗?
答案是:在补码的数学框架下,这是“不得不”的选择。 我们可以从两个维度拆解这个必然性:
1. 维持符号位的一致性(第一准则) 补码设计的核心初衷是:看最高位就能知道正负。
- 如果最高位是
0,就是正数或零。 - 如果最高位是
1,就是负数。
如果我们将 10000000 定义为 +128,那么“最高位代表符号位”这个至高准则就自相矛盾了——你会发现有一个最高位为 1 的数居然是正数。为了保持硬件判定逻辑的极简,最高位为 1 的所有格子都必须分配给负数。
2. 模运算的“圆环连续性”(数学必然性) 补码的本质是模 256 运算。想象一个一圈 256 个格子的圆环:
- 从
0开始顺时针走是正数:1, 2, 3... 一直走到第 127 个格子(01111111)。 - 从
0开始逆时针走是负数:-1 (11111111), -2 (11111110)... - 当顺时针走 128 步和逆时针走 128 步时,它们会撞在同一个格子上,这个格子就是
10000000。
这就产生了一个竞态:这个格子既可以叫 +128,也可以叫 -128。如果我们为了保住“最高位即符号位”这一硬件实现效率最高的规则,就必须把它分配给负数。否则,你的 CPU 在判断正负号时,就不能只看最高位,还得额外判断“如果是 10000000 则是正数,其他最高位为 1 的才是负数”,这会显著增加电路复杂度。
为什么补码的 0 天然只有一种?
因为无论你是从 +0 还是 -0 的原码出发推导,最终都会收敛于同一个补码:
1. 从 +0 的原码推导:
+0 的原码:00000000
+0 的反码:00000000 (正数:三码合一)
+0 的补码:00000000
2. 从 -0 的原码推导:
-0 的原码:10000000
-0 的反码:11111111 (符号位 1 不变,其余取反)
-0 的补码:00000000 (反码 + 1,进位溢出丢弃)
↑ 计算过程:11111111 + 1 = 1 00000000
结论:在补码规则下,+0 和 -0 殊途同归,都指向唯一的位模式 00000000。
这不是人为规定的,而是"取反加一"这个数学操作的自然结果。而被"空出来"的 10000000,按补码的运算规则刚好等于 -128(你可以验证:10000000 加上 01111111(+127)等于 11111111(-1),即 -128 + 127 = -1 ✅)。
| 编码方式 | 格子总数 | 零占几个格子 | 能表示的不同数字个数 | 范围 |
|---|---|---|---|---|
| 原码 | 256 | 2(+0 和 -0) | 255 个 | -127 ~ +127 |
| 反码 | 256 | 2(+0 和 -0) | 255 个 | -127 ~ +127 |
| 补码 | 256 | 1 | 256 个 | -128 ~ +127 |
[!IMPORTANT] 既然有了 -128,那为什么正数不能到 128?
这是一个非常经典的问题。答案非常简单:格子不够用了。
8 位二进制总共只有 256 个组合(格子)。我们已经分配了:
- 128 个格子 给负数(从 -1 到 -128)
- 1 个格子 给零(00000000)
此时剩下的格子数是:256 - 128 - 1 = 127 个。所以正数最高只能到 127。
如果非要表示 128,它的二进制是
10000000。但在补码系统中,由于最高位被规定为符号位,这个位模式已经分给了-128。如果你非要在 8 位里存 128,计算机就会把它当成 -128 处理,这就是“正溢出”现象。
所以 Java 的 byte 范围是 -128 ~ 127(共 256 个值),int 范围是 -2^31 ~ 2^31 - 1(共 2^32 个值)。负数总是比正数多一个,根本原因就是零占的是"正数侧"的格子(最高位是 0),把省出来的格子让给了负数侧。
核心结论:补码的设计哲学
补码的设计绝非随意的“约定”,而是数学逻辑与硬件效率的完美平衡。我们可以从以下三个维度深度理解:
-
数学层面的“减法加法化” 补码的本质是利用模运算(Modular Arithmetic)的特性。在 $n$ 位系统中,减去一个数 $X$ 等同于加上它的补数 $(2^n - X)$。
- 意义:这使得计算机底层只需要一套加法器电路就能同时完成加法和减法。如果使用原码,硬件必须额外设计一套复杂的减法器,且需要先比较两个数的绝对值大小,电路复杂度将成倍增加。
-
硬件层面的“极简判断逻辑” 补码成功实现了符号位与数值位的统一参与运算。
- 意义:通过规定最高位(MSB)为符号位(0正1负),CPU 判断一个数的正负号只需要检查 1 个比特位。如果不遵守这个规定(比如强行让
10000000代表+128),那么 CPU 在判断符号时就必须增加额外的逻辑门来处理特例,这会直接降低处理器的指令执行速度。
- 意义:通过规定最高位(MSB)为符号位(0正1负),CPU 判断一个数的正负号只需要检查 1 个比特位。如果不遵守这个规定(比如强行让
-
架构层面的“空间利用率最大化” 补码彻底解决了原码/反码中 +0 和 -0 占据两个格子的问题。
- 意义:通过将 0 锁定在
00000000,原本属于-0的那个格子10000000被“释放”了出来。在圆环回环的数学模型中,这个格子恰好处于0的对立面,距离0的顺时针距离和逆时针距离都是 128。补码坚定地将其分配给负数侧(即-128),从而成全了“所有最高位为 1 的数都是负数”的严密逻辑,实现了 256 个格子的零浪费分配。
- 意义:通过将 0 锁定在
一句话总结: 补码通过牺牲掉一点点“对称美”(负数比正数多一个),换取了电路设计的极致精简和数值运算的统一高效。
三码对比总结
| 正数 | 负数 | 零 | 加减运算 | |
|---|---|---|---|---|
| 原码 | 直接表示 | 符号位 + 绝对值 | +0 和 -0 两种 | 需要额外判断逻辑 |
| 反码 | 与原码相同 | 符号位不变,其余取反 | +0 和 -0 两种 | 可以统一,但需要循环进位 |
| 补码 | 与原码相同 | 反码 + 1 | 唯一 | 直接相加,溢出丢弃 |
Java 中所有整数类型(byte、short、int、long)都使用补码存储。 这也是所有现代计算机的标准做法。char 比较特殊,它是无符号类型(0 ~ 65535),所有 16 位都用来表示数值,没有符号位。
boolean 的真实大小
JVM 规范没有规定 boolean 的确切大小。在 HotSpot 实现中,单个 boolean 变量会占用 4 字节(当作 int 处理),而 boolean[] 数组中的每个元素占 1 字节(当作 byte[])。这是 JVM 层面的实现细节,和 Java 语言层面"只有 true/false"的语义不矛盾。
浮点数精度陷阱
浮点数采用 IEEE 754 标准,核心思想和十进制科学计数法一样——把一个数拆成「符号 + 指数 + 尾数」三部分存储:
十进制科学计数法:-3.14 × 10²
↑ ↑ ↑
符号 尾数 指数
IEEE 754 二进制版:(-1)^符号 × 1.尾数 × 2^(指数-偏移量)
Java 的 float 和 double 分别用 32 位和 64 位来存储这三部分:
| 类型 | 总位数 | 符号位 | 指数位 | 尾数位 |
|---|---|---|---|---|
float |
32 | 1 | 8 | 23 |
double |
64 | 1 | 11 | 52 |
以存储 6.75 为例:十进制 6.75 = 二进制 110.11(即 4+2+0.5+0.25),写成二进制科学计数法就是 1.1011 × 2²。小数点左边永远是 1 所以不用存("隐含的 1"),最终 32 位这样排列:
0 10000001 10110000000000000000000
↑ ↑ ↑
符号 指数(129) 尾数(1011 后补零)
指数为什么存 129 而不是 2? 因为指数可以是负数(比如 0.5 = 1.0 × 2⁻¹),但指数段没有符号位。IEEE 754 的解决方案是偏移量编码:存储时给真实指数加一个固定偏移量,读取时再减回来:
存储值 = 真实指数 + 偏移量
偏移量 = 2^(指数位数-1) - 1
| 类型 | 指数位数 | 偏移量 | 真实指数范围 |
|---|---|---|---|
float |
8 位 | 2⁷ - 1 = 127 | -126 ~ +127 |
double |
11 位 | 2¹⁰ - 1 = 1023 | -1022 ~ +1023 |
回到 6.75 的例子:真实指数 = 2,存储值 = 2 + 127 = 129(二进制 10000001)。再看一个负指数的例子,0.375 = 1.1 × 2⁻²:真实指数 = -2,存储值 = -2 + 127 = 125(二进制 01111101)。
这样做的好处是指数部分可以直接当作无符号整数比较大小,指数越大存储值越大,硬件比较更高效。
问题在于尾数位是有限的,而有些十进制小数在二进制中是无限循环的。 要理解这一点,先看二进制小数的含义。
十进制中,小数点后的每一位分别代表 10⁻¹、10⁻²、10⁻³ …… 二进制完全一样,只是底数换成 2:
十进制 0.375 = 3×10⁻¹ + 7×10⁻² + 5×10⁻³ = 3/10 + 7/100 + 5/1000
二进制 0.011 = 0×2⁻¹ + 1×2⁻² + 1×2⁻³ = 0/2 + 1/4 + 1/8 = 0.375
所以把十进制小数转二进制,就是找出它能被哪些 1/2、1/4、1/8、1/16…… 相加凑出来。
系统的做法是**"乘 2 取整法"**:每次乘以 2,整数部分就是下一个二进制位,小数部分留着继续乘。先看一个能精确转换的例子:
转换 0.75:
0.75 × 2 = 1.50 → 整数部分 1 → 第 1 位是 1,剩下 0.50
0.50 × 2 = 1.00 → 整数部分 1 → 第 2 位是 1,剩下 0.00 ← 到 0 了,停!
结果:0.75₁₀ = 0.11₂
验证:1/2 + 1/4 = 0.5 + 0.25 = 0.75 ✅
现在用同样的方法转换 0.1:
0.1 × 2 = 0.2 → 取 0,剩 0.2
0.2 × 2 = 0.4 → 取 0,剩 0.4 ← 记住这个 0.2
0.4 × 2 = 0.8 → 取 0,剩 0.8
0.8 × 2 = 1.6 → 取 1,剩 0.6
0.6 × 2 = 1.2 → 取 1,剩 0.2 ← 又出现 0.2 了!
0.2 × 2 = 0.4 → 取 0,剩 0.4 ← 和上面完全一样,开始重复
...
第 5 步之后剩下 0.2,和第 1 步之后一模一样。既然起点相同,后续每一步的计算也必然相同,所以会无限循环步骤 1→2→3→4→5→1→2→...,不断产出 0011 0011 0011...。这和长除法里 1 ÷ 3 = 0.333... 除不尽是同一个道理——余数永远在循环,永远到不了 0。
最终:0.1₁₀ = 0.0 0011 0011 0011 0011...₂(0011 无限循环)
但 double 只有 52 位尾数,无限循环必须截断,截断就丢失精度。所以:
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // false
这不是 Java 的 bug,而是所有遵循 IEEE 754 的语言(C、Python、JavaScript……)的共同特性。涉及金融计算时必须使用 BigDecimal。
包装类
Java 的集合框架(如 ArrayList、HashMap)只能存储对象,不能存储基本类型。为了让基本类型也能参与面向对象的操作,Java 为每种基本类型提供了对应的包装类(Wrapper Class):
| 基本类型 | 包装类 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
包装类都是不可变的(immutable)—— 一旦创建,内部的值就不能改变。这和 String 的不可变性设计动机类似:保证线程安全,允许安全地作为 HashMap 的 key。
自动装箱与拆箱
在 Java 5 之前,基本类型和包装类之间的转换必须手动完成。Java 5 引入了自动装箱(autoboxing)和自动拆箱(unboxing),让编译器自动完成这个过程。
// 自动装箱:编译器转换为 Integer.valueOf(10)
Integer a = 10;
// 自动拆箱:编译器转换为 a.intValue()
int b = a;
字节码层面的真相
自动装箱和拆箱并非"魔法",而是编译器在编译期插入的方法调用:
// Integer a = 10 的字节码
bipush 10
invokestatic Integer.valueOf:(I)Ljava/lang/Integer;
astore_1
// int b = a 的字节码
aload_1
invokevirtual Integer.intValue:()I
istore_2
装箱调用 Integer.valueOf(),拆箱调用 intValue()。理解这一点后,下面的缓存池机制就顺理成章了。
拆箱的 NPE 陷阱
自动拆箱会调用 intValue(),如果包装类对象为 null,就会抛出 NullPointerException:
Integer x = null;
int y = x; // 💥 NullPointerException
这在三目运算符中特别隐蔽:
Integer a = null;
Integer b = 2;
// 编译器认为结果是 int,对 a 执行了拆箱
int result = (true) ? a : b; // 💥 NPE
缓存池机制
查看 Integer.valueOf() 的源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
当值在 [-128, 127] 范围内时,valueOf() 直接从预先创建好的缓存数组中返回对象,而不是 new 一个新的。这就像图书馆的常用书架——最常被借阅的书放在入口处的架子上,拿了就走。
各包装类的缓存范围
| 包装类 | 缓存范围 |
|---|---|
| Byte | -128 ~ 127(全范围) |
| Short | -128 ~ 127 |
| Integer | -128 ~ 127(上限可通过 JVM 参数调整) |
| Long | -128 ~ 127 |
| Character | 0 ~ 127 |
| Boolean | TRUE / FALSE |
| Float | 无缓存 |
| Double | 无缓存 |
Float 和 Double 没有缓存,因为浮点数的值域太大且使用场景分散,缓存命中率极低,反而浪费内存。
经典架构问题
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true ← 同一个缓存对象
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false ← 两个不同的 new Integer(128)
Integer e = new Integer(127);
Integer f = new Integer(127);
System.out.println(e == f); // false ← new 绕过缓存,一定是新对象
== 比较的是引用地址,不是值。在缓存范围内,valueOf() 返回同一个对象,所以 == 为 true。超出范围或用 new 创建,就是不同的对象。
Integer 缓存上限可调
Integer 是唯一一个缓存上限可调的包装类。通过 JVM 参数 -XX:AutoBoxCacheMax=<N> 或 -Djava.lang.Integer.IntegerCache.high=<N> 可以调高上限(但不能调低到 127 以下)。在某些高频使用特定整数值的场景中(如端口号、状态码),调高缓存上限可以减少对象创建的开销。
类型转换
自动类型提升(隐式转换)
小范围类型可以自动提升为大范围类型:
byte → short → int → long → float → double
char ↗
注意 long(64 位整数)可以自动提升为 float(32 位浮点),虽然可能损失精度:
long big = 123456789012345L;
float f = big; // 1.23456792E14,精度丢失了
这是因为 float 虽然只有 4 字节,但它能表示的范围比 long 大(最大约 3.4 × 10^38),只是精度不够。Java 的类型提升规则优先考虑范围,而非精度。
强制类型转换(显式转换)
大范围类型赋给小范围类型需要强制转换,可能造成数据截断:
int i = 300;
byte b = (byte) i; // 44 ← 300 的二进制低 8 位是 00101100 = 44
小结
| 概念 | 核心要点 |
|---|---|
| 基本类型 | 8 种,不是对象;局部变量在栈上,实例字段随对象在堆上 |
| 包装类 | 不可变对象,支持集合和泛型 |
| 装箱/拆箱 | 编译器插入 valueOf() / xxxValue() |
| 缓存池 | 小范围值复用对象,== 比较要小心 |
| 类型转换 | 小→大自动,大→小强制,注意精度丢失 |