基本数据类型
Java 基本数据类型:从 int 到 BigDecimal
📖 本文是 Java 基础知识系列的第三篇,承接上一篇的基本语法,深入 Java 的数据类型体系。8 种基本类型、包装类型、自动装箱拆箱、Integer 缓存——这些是 Java 面试中出镜率最高的基础知识点。
一、8 种基本数据类型
Java 的数据类型分为两大类:基本数据类型(Primitive Type)和引用数据类型(Reference Type)。基本类型是 Java 语言的基石——它们不是对象,直接存储在栈上,访问速度极快。
1.1 完整列表
| 类型 | 位数 | 包装类 | 默认值 | 取值范围 | 用途 |
|---|---|---|---|---|---|
byte | 8 | Byte | 0 | -128 ~ 127 | 字节流操作、网络传输 |
short | 16 | Short | 0 | -32,768 ~ 32,767 | 节省内存的场景 |
int | 32 | Integer | 0 | -2¹³¹ ~ 2¹³¹-1(约 ±21 亿) | 最常用的整数类型 |
long | 64 | Long | 0L | -2⁶³ ~ 2⁶³-1 | 超大数值(时间戳、ID) |
float | 32 | Float | 0.0f | ±3.4E-38 ~ ±3.4E38 | 精度要求不高的浮点数 |
double | 64 | Double | 0.0d | ±1.7E-308 ~ ±1.7E308 | 默认浮点类型 |
char | 16 | Character | '�' | 0 ~ 65,535(无符号) | Unicode 字符 |
boolean | — | Boolean | false | true / false | 逻辑判断 |
💡 记忆技巧:4 种整型(byte/short/int/long)、2 种浮点(float/double)、1 种字符(char)、1 种布尔(boolean)。
1.2 整型的细节
// 整型字面量
byte b = 127; // byte 范围:-128 ~ 127
short s = 32767; // short 范围:-32768 ~ 32767
int i = 2_147_483_647; // int 最大值,可以用 _ 分隔(JDK 7+)
long l = 9_223_372_036_854_775_807L; // long 必须加 L 后缀
// 不同进制表示
int dec = 100; // 十进制
int bin = 0b1100100; // 二进制(JDK 7+,0b 前缀)
int oct = 0144; // 八进制(0 前缀)
int hex = 0x64; // 十六进制(0x 前缀)
System.out.println(dec == bin && bin == oct && oct == hex); // true1.3 ⭐️ 浮点型的精度陷阱
这是新手最容易踩坑的知识点——浮点数在计算机中不精确:
// 这个结果会让你怀疑人生
System.out.println(0.1 + 0.2); // 输出:0.30000000000000004
System.out.println(1.0 - 0.9); // 输出:0.09999999999999998为什么会这样?
计算机用二进制表示小数,而 0.1 在二进制中是一个无限循环小数(就像十进制中的 1/3 = 0.333...):
0.1(十进制) = 0.0001100110011001100110011...(二进制)float(32 位)和 double(64 位)只能存储有限位数,截断后就产生了误差。这不是 Java 的问题——所有遵循 IEEE 754 标准的语言(C/C++/Python/Go)都有同样的问题。
为什么不能用 float/double 表示金额?
// ❌ 金融计算中这样做会出生产事故
double balance = 1.00;
double price = 0.10;
for (int i = 0; i < 10; i++) {
balance -= price; // 每次扣除 0.10
}
System.out.println(balance); // 期望 0.00,实际输出:-5.551115123125783E-17正确做法在本章第 5 节「BigDecimal」中展开。
1.4 char 和 Unicode
char 类型用 16 位存储一个 Unicode 字符。但 Unicode 后来扩充到了 21 位(超过 16 位的范围),引入了 补充字符(Supplementary Characters) 的概念——占两个 char,称为「代理对(Surrogate Pair)」:
// 基本多语言平面(BMP)—— 一个 char 足够
char c1 = 'A';
char c2 = '中';
char c3 = 'A'; // Unicode 转义,表示 'A'
// 补充字符 —— 需要用两个 char 表示
// 😂 (U+1F602) 的 UTF-16 编码是 0xD83D 0xDE02
String emoji = "😂"; // 😂
System.out.println(emoji.length()); // 输出 2 —— 因为占两个 char
System.out.println(emoji.codePointCount(0, emoji.length())); // 输出 1 —— 正确计数💡 开发建议:处理文本时使用
codePointAt()/codePointCount()而不是charAt()/length(),避免在处理 emoji 等补充字符时出错。
1.5 boolean 在 JVM 中怎么表示?
虽然 Java 规范说 boolean 只有 true 和 false,但 JVM 规范没有规定 boolean 在内存中的存储形式:
// 用 javap -c 反编译查看字节码
boolean flag = true;
// 字节码:iconst_1(将 int 常量 1 压入栈)
// 说明 HotSpot VM 实际上用 int(0/1)来表示 boolean- 单个 boolean 变量:用
int存储(32 位),0 为 false,非 0 为 true - boolean 数组:用
byte数组存储,节省空间(每个元素 8 位)
这不是 bug,而是实现选择——用 int 表示不需要额外的指令支持,JVM 的指令集本身就是基于 int 设计的。
二、包装类型
2.1 为什么需要包装类型?
基本类型性能高效,但它们有一个致命缺陷:不能参与面向对象的机制。
Java 的集合框架(List、Set、Map)只能存储对象,泛型也只能使用引用类型:
// ❌ 编译不通过——基本类型不能做泛型参数
List<int> list = new ArrayList<>();
Map<int, String> map = new HashMap<>();
// ✅ 使用包装类型
List<Integer> list = new ArrayList<>();
Map<Integer, String> map = new HashMap<>();为了填补「基本类型不可用于面向对象场景」的鸿沟,Java 为每种基本类型提供了一个对应的引用类型——包装类(Wrapper Class)。
2.2 包装类全貌
| 基本类型 | 包装类 | 父类 |
|---|---|---|
byte | Byte | Number |
short | Short | Number |
int | Integer | Number |
long | Long | Number |
float | Float | Number |
double | Double | Number |
char | Character | Object |
boolean | Boolean | Object |
💡 注意:6 个数字类型的包装类都继承自
Number抽象类,而Character和Boolean直接继承Object。
2.3 自动装箱与自动拆箱
JDK 5 引入的**自动装箱(Autoboxing)和自动拆箱(Unboxing)**让基本类型和包装类型可以无缝互转。这是 Java 编译器在编译期插入代码实现的「语法糖」:
// 自动装箱:基本类型 → 包装类型
Integer i = 100; // 编译器生成:Integer i = Integer.valueOf(100);
// 自动拆箱:包装类型 → 基本类型
int n = i; // 编译器生成:int n = i.intValue();
// 在运算中自动拆箱
Integer a = 10, b = 20;
Integer c = a + b; // 步骤:a.intValue() + b.intValue() → int 加法 → Integer.valueOf(30)装箱和拆箱分别调用了什么方法?
// 装箱 → valueOf()
Integer i = Integer.valueOf(100);
// 拆箱 → xxxValue()
int n = i.intValue();
long l = i.longValue();
double d = i.doubleValue();用 javap -c 反编译验证:
// 源码
Integer i = 100;
int n = i;
// 编译后的字节码:
// bipush 100
// invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
// ...
// invokevirtual #3 // Method java/lang/Integer.intValue:()I2.4 装箱拆箱的性能陷阱
自动装箱很方便,但在循环中频繁装箱会产生大量临时对象,严重影响性能:
// ❌ 糟糕——每次循环都创建一个 Long 对象
Long sum = 0L; // 装箱
for (long i = 0; i < 1_000_000; i++) {
sum += i; // sum = Long.valueOf(sum.longValue() + i)
} // 每次 += 都经历:拆箱 → 加法 → 装箱 → 创建新对象
// ✅ 正确——使用基本类型
long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
sum += i; // 纯栈上操作,没有对象创建
}三、⭐️ 缓存机制(面试核心)
3.1 Integer 缓存池 [-128, 127]
这是 Java 面试中最常考的包装类知识点——Integer 内部维护了一个缓存池:
// Integer.valueOf() 的源码实现(JDK 8+)
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) // 默认 [-128, 127]
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}在缓存范围内,valueOf() 返回同一个对象;超出范围则返回新对象。
3.2 经典面试题
Integer a = 100; // Integer.valueOf(100) → 命中缓存,返回缓存对象
Integer b = 100; // Integer.valueOf(100) → 命中缓存,返回同一个缓存对象
System.out.println(a == b); // true —— 指向同一个对象!
Integer c = 200; // Integer.valueOf(200) → 超出缓存 [−128, 127],new Integer(200)
Integer d = 200; // Integer.valueOf(200) → new Integer(200),又是一个新对象
System.out.println(c == d); // false —— 两个不同的对象!
System.out.println(c.equals(d)); // true —— equals() 比较的是值
// 进一步验证
Integer e = new Integer(100); // ❌ 已废弃(JDK 9+),强制新建对象,不走缓存
System.out.println(a == e); // false —— 缓存对象 vs 新对象🎯 面试口诀:「装箱调用 valueOf,[-128, 127] 有缓存,超过范围 new 新对象,
==比较地址要小心。」
3.3 缓存范围可以调整
Integer 缓存的上限可以通过 JVM 参数调整(下限固定为 -128):
# 将缓存上限改为 500
java -Djava.lang.Integer.IntegerCache.high=500 MyApp但不建议在生产环境中随意修改——缓存范围越大,启动时分配的内存越多。
3.4 哪些包装类有缓存?
| 包装类 | 缓存范围 | 说明 |
|---|---|---|
Byte | -128 ~ 127(全部 256 个值) | 范围固定,不可调 |
Short | -128 ~ 127 | 范围固定 |
Integer | -128 ~ 127(上限可调) | -Djava.lang.Integer.IntegerCache.high=N |
Long | -128 ~ 127 | 范围固定 |
Character | 0 ~ 127(ASCII 范围) | 范围固定 |
Float | 无缓存 | 浮点值太多,缓存不现实 |
Double | 无缓存 | 浮点值太多,缓存不现实 |
Boolean | TRUE 和 FALSE 两个静态常量 | 不是缓存,是常量 |
// Float/Double 没有缓存——每次都新建对象
Float f1 = 100.0f;
Float f2 = 100.0f;
System.out.println(f1 == f2); // false
// Boolean 有静态常量
Boolean b1 = true;
Boolean b2 = true;
System.out.println(b1 == b2); // true —— 都指向 Boolean.TRUE四、类型转换
4.1 隐式转换(自动类型提升)
当两个不同类型的操作数参与运算时,Java 会自动将较窄的类型提升为较宽的类型:
byte → short → int → long → float → double
↗
char规则:char 和 short 同一级别(都是 16 位),但 char 是无符号的(0~65535),short 是有符号的(-32768~32767)。
// 隐式转换示例
byte b = 10;
int i = b; // byte → int (自动,不丢失精度)
long l = 100L;
float f = l; // long → float (自动,可能丢失精度)
// 表达式中自动提升
byte b1 = 10, b2 = 20;
byte b3 = b1 + b2; // ❌ 编译错误!
// b1 + b2 的结果被自动提升为 int,不能直接赋值给 byte
int i3 = b1 + b2; // ✅ 正确⚠️ long → float 不丢失数据吗? 会丢失精度!long 有 63 位有效位(去掉符号位),float 只有 23 位尾数。但 Java 允许这种转换而不报错——大转小可以隐式转换?不是,转换方向是 int → long → float → double。float 的"取值范围"比 long 大(因为有指数),所以 Java 认为这是「拓宽转换」,但在转换过程中精度确实可能丢失。
4.2 强制转换(显式类型转换)
将较宽的类型强制转为较窄的类型,需要使用括号语法——数据可能溢出或精度丢失:
// 强制转换语法:(目标类型) 表达式
double d = 3.14;
int i = (int) d; // 3 —— 截断小数部分
// 溢出风险
int big = 300;
byte small = (byte) big; // 44 —— 完全溢出!300 = 256 + 44,所以只剩 44
System.out.println(small); // 输出 44
// 更直观的溢出
int maxInt = Integer.MAX_VALUE; // 2147483647
short s = (short) maxInt; // -1 —— 高位截断
System.out.println(s);4.3 表达式中的自动类型提升
当表达式中同时出现不同类型时,所有操作数会提升到与最高级别操作数相同的类型:
// int + long → long
long l = 100L + 200; // 200 自动提升为 long,结果是 long
// int + double → double
double d = 100 + 3.14; // 100 提升为 100.0,结果是 double
// byte + byte → int(重要!)
byte b1 = 1, b2 = 2;
// byte b3 = b1 + b2; // ❌ 编译错误!结果被提升为 int
byte b3 = (byte)(b1 + b2); // ✅ 需要强制转换
// 复合赋值不会报错
b1 += b2; // ✅ 没问题,因为 += 内部做了强制转换:b1 = (byte)(b1 + b2)这正是我们在上一篇文章中提到过的问题——+= 运算符会自动替你做类型转换。
五、⭐️ BigDecimal——金融计算的正确姿势
5.1 为什么 double 不能做金额?
答案很简单:浮点数不精确。在第一节我们已经看到 0.1 + 0.2 ≠ 0.3。对于金额这种一分钱都不能差的场景,double 的误差会随运算次数不断累积,最终导致账目不平。
// 模拟金融场景中的灾难
double total = 0.0;
for (int i = 0; i < 100; i++) {
total += 0.01; // 每次加 1 分钱
}
System.out.println(total); // 期望 1.00,实际输出 1.0000000000000007
// 账目不平!100 笔 0.01 加起来居然不等于 1.005.2 BigDecimal 构造方法——也有陷阱!
使用 BigDecimal 来解决精度问题,但构造方法本身也有坑:
// ❌ 陷阱!用 double 构造 BigDecimal,精度丢失已经发生
BigDecimal bad = new BigDecimal(0.1);
System.out.println(bad);
// 输出:0.1000000000000000055511151231257827021181583404541015625
// 因为 0.1 这个 double 字面量本身就已经是不精确的了!
// ✅ 正确做法一:用字符串构造
BigDecimal good = new BigDecimal("0.1");
System.out.println(good); // 输出:0.1
// ✅ 正确做法二:用 valueOf(内部也是 new BigDecimal(Double.toString(val)))
BigDecimal good2 = BigDecimal.valueOf(0.1);
System.out.println(good2); // 输出:0.1🎯 核心原则:永远用
new BigDecimal("0.1")或BigDecimal.valueOf(0.1),不要用new BigDecimal(0.1)。
BigDecimal.valueOf() 的源码:
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
// Double.toString(0.1) 返回 "0.1",再用这个字符串构造 BigDecimal —— 安全!
}5.3 常用运算
BigDecimal a = new BigDecimal("10.00");
BigDecimal b = new BigDecimal("3.00");
// 四则运算
BigDecimal sum = a.add(b); // 13.00
BigDecimal diff = a.subtract(b); // 7.00
BigDecimal prod = a.multiply(b); // 30.00
BigDecimal quot = a.divide(b, 2, RoundingMode.HALF_UP); // 3.33
// ⚠️ 如果除不尽而不指定舍入模式,会抛出 ArithmeticException
// a.divide(b); // 10 / 3 除不尽,抛出异常!
BigDecimal safe = a.divide(b, RoundingMode.HALF_UP); // ❌ 也危险!
// ✅ 正确:指定精度和舍入模式
BigDecimal result = a.divide(b, 4, RoundingMode.HALF_UP);5.4 舍入模式速查
| 模式 | 含义 | 0.5 舍入结果 |
|---|---|---|
HALF_UP | 四舍五入(最常用) | 0.5 → 1 |
HALF_DOWN | 五舍六入 | 0.5 → 0, 0.6 → 1 |
HALF_EVEN | 银行家舍入(IEEE 754 默认) | 0.5 → 0, 1.5 → 2 奇进偶不进 |
CEILING | 向上取整 | 1.1 → 2, -1.1 → -1 |
FLOOR | 向下取整 | 1.9 → 1, -1.9 → -2 |
UP | 远离零方向 | 1.1 → 2, -1.1 → -2 |
DOWN | 向零方向 | 1.9 → 1, -1.9 → -1 |
// 银行家舍入的经典案例
BigDecimal a = new BigDecimal("2.5");
BigDecimal b = new BigDecimal("3.5");
System.out.println(a.setScale(0, RoundingMode.HALF_EVEN)); // 2(偶数向,舍去)
System.out.println(b.setScale(0, RoundingMode.HALF_EVEN)); // 4(奇数向,进位)
// 为什么叫"银行家舍入"?
// 大量数据处理时,传统四舍五入会使总和大一点点(每次 0.5 都进 1)
// 银行家舍入让进位和舍去的概率各 50%,减少了统计偏差5.5 等值比较
BigDecimal 的 equals() 不仅比较数值,还比较精度(scale)。数值比较建议用 compareTo():
BigDecimal a = new BigDecimal("1.0"); // scale = 1
BigDecimal b = new BigDecimal("1.00"); // scale = 2
System.out.println(a.equals(b)); // false —— scale 不一样!
System.out.println(a.compareTo(b)); // 0 —— 数值相等六、值传递 or 引用传递?
这是一个经典的 Java 面试题:Java 是值传递还是引用传递?
答案是:Java 只有值传递。
6.1 基本类型参数——毫无疑问的值传递
public static void change(int x) {
x = 100; // 修改的是局部副本,不影响原来的变量
}
int a = 10;
change(a);
System.out.println(a); // 10 —— 没有变6.2 引用类型参数——看起来像引用传递,其实不是
public static void change(StringBuilder sb) {
sb.append(" World"); // 修改了对象的内容
}
StringBuilder sb = new StringBuilder("Hello");
change(sb);
System.out.println(sb); // Hello World —— 变了!粗看像引用传递——但接下来的例子揭露本质:
public static void change(StringBuilder sb) {
sb = new StringBuilder("New Object"); // 让 sb 指向新对象
}
StringBuilder sb = new StringBuilder("Hello");
change(sb);
System.out.println(sb); // Hello —— 没有变!原来的对象还在6.3 完整的解释
Java 中的参数传递永远是「拷贝值」:
- 基本类型:拷贝的是值本身 → 怎么改都不影响原变量
- 引用类型:拷贝的是引用(地址值)→ 可以通过拷贝的引用修改对象的内容,
但不能改变原引用本身
关键区分两个概念:
① 修改引用指向的对象的内容 → 影响外部 ✅
② 修改引用本身(让它指向新对象)→ 不影响外部 ❌// 对比图解
// 传引用进去时:
// 原引用 ──→ [Hello] ←── 拷贝的引用(形参)
// 通过拷贝的引用修改对象 → 改的是同一个对象 → 外部可见
//
// 让拷贝的引用指向新对象:
// 原引用 ──→ [Hello]
// 拷贝的引用 ──→ [New Object]
// 两者指向不同对象了,互不影响🎯 面试标准回答:「Java 只有值传递。基本类型传值的副本,引用类型传引用的副本。通过引用副本可以修改对象内容,但不能让原引用指向新对象。」
七、总结
| 知识点 | 核心要点 |
|---|---|
| 8 种基本类型 | byte/short/int/long、float/double、char、boolean;整型和浮点型遵循 IEEE 754 |
| 浮点精度 | 0.1 + 0.2 ≠ 0.3,因为二进制无法精确表示十进制小数 |
| 自动装箱拆箱 | JDK 5 语法糖:装箱 → valueOf(),拆箱 → xxxValue(),循环中避免装箱 |
| Integer 缓存 | [-128, 127] 默认缓存,valueOf() 在范围内返回缓存对象;== 比较地址要小心 |
| 类型转换 | 隐式:byte → short → int → long → float → double;强制:可能溢出或截断 |
| BigDecimal | 用 new BigDecimal("0.1") 或 BigDecimal.valueOf(0.1);除法要指定精度和舍入模式 |
| 值传递 | Java 只有值传递。基本类型传值,引用类型传引用的副本 |
掌握本文的内容后,你已经理解了 Java 中所有数据类型的本质。下一篇我们将进入 Java 面向对象编程——封装、继承、多态、抽象类与接口、static/final 修饰符,以及那些让你在面试中脱颖而出的 OOP 设计细节。
参考
- Java Language Specification - Chapter 4: Types, Values, and Variables — Java 类型系统的官方规范
- IEEE 754 Floating-Point Standard — 浮点数标准详解
- BigDecimal JavaDoc — BigDecimal 官方文档
- JavaGuide - 基本数据类型 — JavaGuide 相关知识点
- javabetter.cn - 基本数据类型 — 二哥的 Java 进阶之路