大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
基本类型与引用类型
这一课你将能做到
- 默写 8 大基本类型及各自适用场景,说清基本类型和引用类型的本质区别
- 预判整数溢出、浮点精度丢失的结果,写出正确的类型转换代码
- 解释
Integer缓存陷阱和NullPointerException(NPE)的来源——这两个是后端线上事故与面试的高频考点
为什么要学
变量是程序存放数据的最小单位。类型选错,轻则数据被截断(订单金额少一个零),重则精度错乱(金额算分不齐)、线上 NPE 把接口打挂。本站后端里这些对应关系你以后天天见:
- MySQL 的
BIGINT主键 → Javalong(int最大约 21 亿,不够存雪花 ID) - 金额
DECIMAL(10,2)→ JavaBigDecimal(绝不能用double) - Redis 缓存的布尔标志 → Java
Boolean(引用类型,可能是 null)
类型系统是 Java「强类型」的根基:编译期就拦住大部分数据错误。
先建立图像
Java 的数据分两个世界:
基本类型(primitive) 引用类型(reference)
┌─────────────┐ ┌─────────────┐
│ 变量 = 值本身 │ │ 变量 = 对象的「遥控器」│
│ 存在栈上/直接 │ │ 变量存地址,对象在堆上 │
│ 不可能为 null │ │ 可能为 null(没遥控器)│
└─────────────┘ └─────────────┘
int a = 10; String s = "hi";
int b = a; b 复制值 10 String t = s; t 复制的是地址
改 b 不影响 a s 和 t 指向同一个对象
8 大基本类型全家福:
| 类型 | 字节 | 范围/说明 | 后端常用场景 |
|---|---|---|---|
byte | 1 | -128 ~ 127 | 二进制数据处理 |
short | 2 | ±3.2 万 | 少用,省内存的旧字段 |
int | 4 | ±21 亿 | 普通计数、状态码 |
long | 8 | ±922 亿亿 | 数据库主键、雪花 ID |
float | 4 | 约 7 位有效数字 | 基本不用 |
double | 8 | 约 15 位有效数字 | 科学计算;金额禁用 |
char | 2 | 一个 Unicode 字符 | 单字符处理 |
boolean | — | true / false | 开关、标志位 |
跟着做
步骤 1:声明各类型变量并打印
新建 TypeDemo.java:
public class TypeDemo {
public static void main(String[] args) {
int viewCount = 1024; // int 字面量直接写
long userId = 20261006123456789L; // long 字面量必须带 L 后缀!
double rating = 9.5; // 小数字面量默认是 double
float price = 19.9F; // float 字面量必须带 F 后缀
boolean vip = true;
char grade = 'A'; // char 用单引号,只装一个字符
byte level = 3;
System.out.printf("浏览 %d, 用户 %d, 评分 %.1f, 价格 %.2f, VIP %b, 等级 %c%n",
viewCount, userId, rating, price, vip, grade);
}
}
输出:浏览 1024, 用户 20261006123456789, 评分 9.5, 价格 19.90, VIP true, 等级 A。
去掉 L 后缀会编译报错「整数太大」——编译器先按 int 解析字面量,超范围直接拒绝。
步骤 2:亲手触发类型转换的两种情况
public class CastDemo {
public static void main(String[] args) {
// 自动提升:小容器倒进大容器,安全
int i = 100;
long l = i; // int -> long 自动完成
double d = l; // long -> double 自动完成
System.out.println(d); // 100.0
// 强制转换:大容器倒小容器,可能溢出/丢精度
long bigId = 3_000_000_000L; // 30 亿,超出 int 范围
int truncated = (int) bigId; // 必须写 (int),且结果已失真
System.out.println(truncated); // -1294967296,溢出成负数!
int score = (int) 99.9; // 浮点转整数直接砍掉小数(不是四舍五入)
System.out.println(score); // 99
}
}
步骤 3:体验浮点精度陷阱(金额问题的根源)
public class FloatTrap {
public static void main(String[] args) {
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // false!
// 正确姿势:金额用 BigDecimal,且必须用字符串构造
java.math.BigDecimal a = new java.math.BigDecimal("0.1");
java.math.BigDecimal b = new java.math.BigDecimal("0.2");
System.out.println(a.add(b)); // 0.3
// 注意:new BigDecimal(0.1)(传 double)照样有精度问题,务必传字符串
}
}
为什么:二进制无法精确表示 0.1,double 存的是近似值。订单金额差 0.0000001 累积起来就是对账事故。
步骤 4:包装类与自动装箱
每个基本类型都有对应的引用类型「包装类」:
public class WrapperDemo {
public static void main(String[] args) {
Integer boxed = 100; // 自动装箱:int -> Integer
int unboxed = boxed; // 自动拆箱:Integer -> int
// 为什么要包装类?因为泛型集合只能装对象(第 9 课),
// 数据库字段允许 NULL 时,int 表示不了 null 而 Integer 可以。
Integer dbAge = null; // 模拟数据库 age 列为 NULL
int age = dbAge; // 运行时炸了:NullPointerException!
// 经典缓存陷阱
Integer x = 127, y = 127;
System.out.println(x == y); // true:走 IntegerCache 缓存,同一个对象
Integer m = 128, n = 128;
System.out.println(m == n); // false:超出缓存,两个新对象
System.out.println(m.equals(n)); // true:比较内容必须用 equals
}
}
步骤 5:var 与常量(JDK 10+)
public class VarDemo {
public static void main(String[] args) {
var message = "类型推断"; // 编译器推断为 String,仍是强类型
var numbers = new int[]{1, 2, 3};
System.out.println(message + numbers.length);
final int MAX_USERS = 1000; // final:只能赋值一次,即常量
System.out.println(MAX_USERS);
}
}
var 只能用于局部变量,不能用于字段、参数、返回值。类型还是要明确的,只是让编译器替你写。
讲清楚要点
- 基本类型存值,引用类型存地址:
=赋值时,基本类型复制值,引用类型复制地址。这是「改了b为何a也变」类 bug 的根源。 - 整数默认
int、小数默认double:所以long字面量加L、float加F。反过来(大转小)必须显式强制转换。 - 整数除法会截断:
10 / 3 == 3,10 % 3 == 1。想要小数先把一个操作数转 double:10.0 / 3。 - 包装类解决三件事:泛型容器只能装对象;数据库 NULL 语义;提供
parseInt、MAX_VALUE等工具。 - 比较一律用
equals,==只比地址:对 Integer、String 等引用类型尤其如此。==偶尔「碰巧对」(小整数缓存),反而最害人。 - 拆箱前判空:
Integer为 null 时直接赋给int必抛 NPE,这是后端接口 500 的常见元凶。
你可能会懵的地方
-
char和String什么关系?char是单个 16 位 Unicode 字符('A'),String是字符序列("ABC")。String本质是char数组的封装(新版本 JDK 用 byte 数组优化了存储),可以做s.charAt(0)取出 char。 -
为什么
Integer只缓存 -128 到 127?IntegerCache预创建了这个高频区间的对象,属于 JVM 性能优化。规范(Java 语言规范)允许缓存更宽区间,但超 127 后==结果在不同 JVM 上不可预期——所以永远用 equals。 -
boolean 到底占几个字节? 规范没定死,JVM 实现里单个 boolean 通常按 1 字节存,boolean 数组每项也是 1 字节。面试答「规范未规定,实现通常为 1 字节」即可。
-
什么时候用基本类型,什么时候用包装类? 局部计算、循环计数用基本类型(性能好、不可能 null);实体类字段、集合元素、可空语义(数据库列可 NULL)用包装类。阿里巴巴 Java 开发手册同款结论:POJO 类属性一律用包装类型。
-
long 的 L 后缀小写 l 行不行? 行,但
l和数字1几乎无法区分(101lvs1011),行业规范强制用大写L。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
long id = 10000000000; | 字面量超 int 范围,编译错 | 加 L:10000000000L |
double money = 0.1 + 0.2; 当金额 | 精度丢失 | BigDecimal + 字符串构造 |
if (m == n) 比较 Integer | 超 127 后恒 false | m.equals(n) |
int x = nullableInteger; | NPE | 先判空或用 Objects.requireNonNullElse |
(int) 99.9 以为四舍五入 | 得到 99 | 用 Math.round(99.9) 得 100 |
byte 直接参与运算写回 byte | byte b = b + 1 编译错(结果提升为 int) | b = (byte)(b + 1) 或 b += 1 |
卡点急救
- 编译报「不兼容的类型:从 double 转换到 int 可能会有损失」:把小数赋给整数变量了。要么换类型,要么显式
(int),要么改用Math.round。 - 运行报
NullPointerException:打印变量确认哪个引用是 null(报错堆栈第一行会指向具体行号),先判空再拆箱/调用。 - 两个 Integer 明明值相等却进了 else 分支:你用了
==。全部改成equals,一劳永逸。 - 数字太大显示成科学计数法:
System.out.println对大 double 会输出1.0E10,用printf("%.2f")或BigDecimal.toPlainString()。
面试常问
-
基本类型和引用类型的区别? 基本类型直接存值、在栈上(或作为对象一部分在堆上)、不能为 null、8 种;引用类型存对象地址、对象在堆、可为 null、有成员方法和继承体系。
-
Integer i = 127与Integer j = 127为什么==是 true,128 就不是? 自动装箱走Integer.valueOf,内部IntegerCache缓存 -128~127 的对象,区间内返回同一实例;超出则 new 新对象,地址不同。结论:包装类型比较用 equals。 -
为什么金额不能用 float/double?怎么处理? 二进制浮点无法精确表示十进制小数(0.1 是无限循环二进制),累积误差会导致对账不平。用
BigDecimal(字符串构造、指定 scale 和舍入模式),或以「分」为单位存 long。 -
int和Integer的区别? 类型本质、默认值(0 vs null)、能否进泛型集合、可否为数据库 NULL 字段做映射。高频追问「Integer 的默认值是 0 吗」——是 null,不是 0。
自测三问
- 8 种基本类型中,哪些字面量需要后缀?分别是什么?
System.out.println(0.1 + 0.2 == 0.3)输出什么?为什么?金额场景怎么写才对?- 数据库
age列为 NULL,映射到Integer age = null后执行int x = age会发生什么?
小结一句话
基本类型存值、引用类型存地址;long 加 L、float 加 F、整数除法截断、浮点算钱必翻车;包装类为 null 和泛型而生,比较永远用 equals——这四条能避开 80% 的初级线上事故。
延伸
- Oracle:原始数据类型
- 下一课:条件与循环——if/switch/for/while 控制程序走向,用箭头 switch 重构本站的状态机代码。