基本数据类型

世界杯开幕式视频

Java 基本数据类型:从 int 到 BigDecimal📖 本文是 Java 基础知识系列的第三篇,承接上一篇的基本语法,深入 Java 的数据类型体系。8 种基本类型、包装类型、自动装箱拆箱、Integer 缓存——这些是 Java 面试中出镜率最高的基础知识点。

一、8 种基本数据类型Java 的数据类型分为两大类:基本数据类型(Primitive Type)和引用数据类型(Reference Type)。基本类型是 Java 语言的基石——它们不是对象,直接存储在栈上,访问速度极快。

1.1 完整列表类型位数包装类默认值取值范围用途byte8Byte0-128 ~ 127字节流操作、网络传输short16Short0-32,768 ~ 32,767节省内存的场景int32Integer0-2¹³¹ ~ 2¹³¹-1(约 ±21 亿)最常用的整数类型long64Long0L-2⁶³ ~ 2⁶³-1超大数值(时间戳、ID)float32Float0.0f±3.4E-38 ~ ±3.4E38精度要求不高的浮点数double64Double0.0d±1.7E-308 ~ ±1.7E308默认浮点类型char16Character'�'0 ~ 65,535(无符号)Unicode 字符boolean—Booleanfalsetrue / 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 和 Unicodechar 类型用 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 为 trueboolean 数组:用 byte 数组存储,节省空间(每个元素 8 位)这不是 bug,而是实现选择——用 int 表示不需要额外的指令支持,JVM 的指令集本身就是基于 int 设计的。

二、包装类型2.1 为什么需要包装类型?基本类型性能高效,但它们有一个致命缺陷:不能参与面向对象的机制。

Java 的集合框架(List、Set、Map)只能存储对象,泛型也只能使用引用类型:

// ❌ 编译不通过——基本类型不能做泛型参数

List list = new ArrayList<>();

Map map = new HashMap<>();

// ✅ 使用包装类型

List list = new ArrayList<>();

Map map = new HashMap<>();为了填补「基本类型不可用于面向对象场景」的鸿沟,Java 为每种基本类型提供了一个对应的引用类型——包装类(Wrapper Class)。

2.2 包装类全貌基本类型包装类父类byteByteNumbershortShortNumberintIntegerNumberlongLongNumberfloatFloatNumberdoubleDoubleNumbercharCharacterObjectbooleanBooleanObject💡 注意: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=NLong-128 ~ 127范围固定Character0 ~ 127(ASCII 范围)范围固定Float无缓存浮点值太多,缓存不现实Double无缓存浮点值太多,缓存不现实BooleanTRUE 和 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 → 1HALF_DOWN五舍六入0.5 → 0, 0.6 → 1HALF_EVEN银行家舍入(IEEE 754 默认)0.5 → 0, 1.5 → 2 奇进偶不进CEILING向上取整1.1 → 2, -1.1 → -1FLOOR向下取整1.9 → 1, -1.9 → -2UP远离零方向1.1 → 2, -1.1 → -2DOWN向零方向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 进阶之路