【014】基本类型与包装类:缓存、相等性、NPE

写业务代码时,你可能写过 int a = 10; 也写过 Integer b = 10;,但有没有想过:这两种写法有什么区别为什么有时候 == 比较失灵为什么 Integer 能用 equalsint 不能明明赋值了却还是 NPE

这篇帮你把基本类型和包装类彻底搞明白,后面写 Spring Boot 业务代码、接口参数校验、数据库映射时才不会踩坑。下面我按「基本类型 vs 包装类 → 装箱拆箱 → 缓存池 → equals 与 == → NPE 防御」的顺序往下聊。


1. 基本类型与包装类:天生一对 💑

1.1 为什么要有两套类型?

Java 是纯面向对象 的语言,但基本类型 (Primitive Types)的存在是为了性能。试想一下,如果所有数据都用对象表示:

java 复制代码
// 假设没有基本类型,所有都是对象
Integer a = new Integer(10);  // 每次都要 new 对象
int b = 10;  // 基本类型,直接存值
  • 基本类型 :直接存值,栈上分配,
  • 包装类 :对象,堆上分配,支持 null提供丰富方法

所以 Java 保留了 8 种基本类型,同时提供对应的包装类,兼顾性能灵活性

1.2 8 种基本类型一览

Java 有 8 种基本类型,每种都有对应的包装类:

基本类型 包装类 字节数 取值范围 默认值
byte Byte 1 -128 ~ 127 0
short Short 2 -32768 ~ 32767 0
int Integer 4 -2^31 ~ 2^31-1(约 21 亿) 0
long Long 8 -2^63 ~ 2^63-1 0L
float Float 4 约 ±3.4E38 0.0f
double Double 8 约 ±1.8E308 0.0d
char Character 2 0 ~ 65535(Unicode) '\u0000'
boolean Boolean 1 true / false false

1.3 基本类型 vs 包装类 核心区别

特性 基本类型 包装类
存储位置
默认值 0/false/'\u0000' null
是否为 null ❌ 不能为 null ✅ 可以为 null
性能 高(直接存值) 低(需要 new 对象)
方法 有丰富的方法(如 Integer.parseInt()
泛型支持 ❌ 不支持 ✅ 支持
java 复制代码
// 基本类型:直接存值
int a = 10;  // 栈上存 10

// 包装类:对象
Integer b = 10;  // 堆上存 Integer 对象,引用 b 存在栈上

// 基本类型不能为 null
// int c = null;  // 编译错误!

// 包装类可以为 null
Integer d = null;  // 合法

1.4 什么时候用基本类型?

  • 数据库主键 IDlong id(数据库主键通常非 null,但用 Long 更安全)
  • 业务状态码、计数int statusint count
  • 性能敏感的核心循环:用基本类型(避免装箱拆箱开销)
  • 方法返回值类型固定 :如 int calculate()long timestamp
  • 数组int[]double[](数组元素是基本类型)
java 复制代码
// 性能敏感的场景
public long calculateSum(List<Long> numbers) {
    long sum = 0;  // 基本类型,避免每次循环装箱
    for (Long num : numbers) {
        if (num != null) {
            sum += num;  // num 自动拆箱
        }
    }
    return sum;
}

1.5 什么时候用包装类?

  • 可能为 null 的字段:如表单提交的年龄、分数、金额
  • 泛型 :集合 List<Integer> 不能用 List<int>
  • 数据库映射:MyBatis/JPA 的实体类字段(数据库字段可能为 null)
  • DTO/VO 字段:前端传过来的参数可能为空
  • Optional 容器 :与 Optional<Integer> 配合
java 复制代码
// 实体类:数据库字段可能为 null,用包装类
@Entity
public class User {
    @Id
    private Long id;           // 主键非 null,但用 Long
    
    private String name;       // 名字可能为空
    
    private Integer age;       // 年龄可能为空
    
    private Boolean active;   // 状态可能未知
}

// DTO:前端参数可能为空
public class UserDTO {
    private String name;       // 可能为空
    private Integer age;       // 可能为空
}

2. 自动装箱与拆箱:编译器偷偷做的事 📦

2.1 什么是装箱和拆箱?

装箱 (Boxing):基本类型 → 包装类
拆箱(Unboxing):包装类 → 基本类型

java 复制代码
// 装箱:编译器帮你 Integer.valueOf(10)
Integer a = 10;  // 自动装箱

// 拆箱:编译器帮你 a.intValue()
int b = a;       // 自动拆箱

这都是编译器帮你做的,写代码时和基本类型用法一样,但底层实现不同。

2.2 装箱的实现原理

Integer.valueOf() 的内部实现:

java 复制代码
public static Integer valueOf(int i) {
    // 如果在缓存范围内(-128 ~ 127),返回缓存对象
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    // 否则创建新对象
    return new Integer(i);
}

关键点

  • 如果值在缓存范围内(-128 ~ 127),直接返回缓存对象
  • 如果超出范围,创建新对象

2.3 拆箱的实现原理

java 复制代码
// Integer 类的 intValue() 方法
public int intValue() {
    return value;  // 返回包装的值
}

所有包装类都有 xxxValue() 方法:

包装类 拆箱方法
Integer intValue()
Long longValue()
Double doubleValue()
Boolean booleanValue()
... ...

2.4 装箱的坑:缓存范围

java 复制代码
// 缓存范围内:复用同一对象
Integer a = 100;
Integer b = 100;
System.out.println(a == b);  // true(缓存范围内,复用同一对象)

// 缓存范围外:创建新对象
Integer c = 200;
Integer d = 200;
System.out.println(c == d);  // false(超出范围,创建新对象)

所以永远不要用 == 比较包装类 ,用 equals

2.5 装箱的性能问题

java 复制代码
// ❌ 性能问题:循环中频繁装箱
Long sum = 0L;
for (int i = 0; i < 1000000; i++) {
    sum += i;  // 每次 i 自动装箱成 Long
}

// ✅ 性能优化:使用基本类型
long sum = 0L;
for (int i = 0; i < 1000000; i++) {
    sum += i;  // 直接加法,无装箱
}

在高性能场景下,尽量避免循环中的自动装箱拆箱

2.6 装箱的常见场景

java 复制代码
// 场景 1:方法参数
void method(Integer num) { }  // 接收包装类型
method(10);  // 自动装箱

// 场景 2:集合操作
List<Integer> list = new ArrayList<>();
list.add(10);  // 自动装箱

// 场景 3:三元运算符(容易出错!)
Integer a = null;
int b = a != null ? a : 0;  // 编译错误!a 可能为 null,无法拆箱

// 正确写法
int b = a != null ? a.intValue() : 0;  // 显式拆箱
int b = (a != null ? a : 0);  // 三元运算符会自动拆箱

3. 缓存池:Integer 的小秘密 🎁

3.1 为什么要缓存?

为了性能。-128 ~ 127 是最常用的整数范围,每次都 new 一个新对象太浪费,直接复用缓存对���省内存又省时间。

java 复制代码
// 不用缓存:每次 new 新对象
Integer a = new Integer(10);
Integer b = new Integer(10);
System.out.println(a == b);  // false

// 用缓存:复用同一对象
Integer c = 10;
Integer d = 10;
System.out.println(c == d);  // true

3.2 缓存范围

包装类 缓存范围 说明
Integer -128 ~ 127 默认,可通过 -XX:AutoBoxCacheMax 调整
Short -128 ~ 127 固定
Long -128 ~ 127 固定
Character 0 ~ 127 固定
Byte -128 ~ 127 固定(全部)
Boolean true / false 两个常量
Float 浮点数范围太大,缓存无意义
Double 浮点数范围太大,缓存无意义

3.3 IntegerCache 实现

IntegerCacheInteger内部类

java 复制代码
private static class IntegerCache {
    // 缓存下限:-128(固定)
    static final int low = -128;
    
    // 缓存上限:默认 127,可通过 -XX:AutoBoxCacheMax 调整
    static final int high;
    
    // 缓存数组
    static final Integer cache[];

    static {
        // 默认 high = 127
        high = 127;
        
        // 创建缓存数组:256 个元素(-128 到 127)
        cache = new Integer[(high - low) + 1];
        
        // 初始化缓存
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
    }
}

3.4 缓存配置

bash 复制代码
# 修改 Integer 缓存上限(不推荐生产使用)
java -XX:AutoBoxCacheMax=200 MyApp

这样 -XX:AutoBoxCacheMax=200 设置后,缓存范围变成 -128 ~ 200。

3.5 缓存的实际应用场景

java 复制代码
// 场景 1:数据库状态码
public enum OrderStatus {
    PENDING(0),      // 缓存
    PAID(1),         // 缓存
    SHIPPED(2),      // 缓存
    COMPLETED(3),    // 缓存
    CANCELLED(4);    // 缓存
    
    private final int code;
    
    OrderStatus(int code) {
        this.code = code;
    }
    
    public static OrderStatus fromCode(int code) {
        // 这里用 == 比较也可以,因为枚举是单例
        for (OrderStatus status : values()) {
            if (status.code == code) {
                return status;
            }
        }
        return null;
    }
}

// 场景 2:缓存小整数(适合高频使用)
// 例如:分页页码、状态码、类型码等

4. equals 与 ==:到底该用哪个?⚖️

4.1 核心区别

比较方式 比较内容 适用场景
== 引用地址(是不是同一个对象) 基本类型、枚举(单例)
equals() 对象内容(值是否相等) 包装类、字符串、自定义类
java 复制代码
// == 比较的是引用地址
Integer a = new Integer(10);
Integer b = new Integer(10);
System.out.println(a == b);  // false(两个不同的对象)

// equals 比较的是内容
System.out.println(a.equals(b));  // true(值相等)

4.2 基本规则

比较场景 推荐方式 原因
基本类型 vs 基本类型 == 值比较,性能最高
包装类 vs 包装类 equals() 值比较,避免缓存坑
字符串 equals() String 重写了 equals
枚举 == 枚举成员是单例
业务主键 ID 比较 equals() 安全可靠

4.3 常见错误

java 复制代码
// ❌ 错误:包装类用 ==
Integer a = new Integer(10);
Integer b = new Integer(10);
System.out.println(a == b);  // false(不同对象)

// ✅ 正确:包装类用 equals
System.out.println(a.equals(b));  // true

// ✅ 正确:基本类型用 ==
int x = 10;
int y = 10;
System.out.println(x == y);  // true

4.4 字符串的特殊性

字符串是最常用的比较场景,有三种创建方式:

java 复制代码
// 方式 1:字面量(常量池)
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);  // true(常量池复用)

// 方式 2:new 对象(堆)
String s3 = new String("hello");
System.out.println(s1 == s3);  // false(s3 在堆上)

// 方式 3:intern() 方法
String s4 = new String("hello");
s4.intern();  // 加入常量池
System.out.println(s1 == s4);  // true(s4 指向常量池)

字符串比较永远用 equals

java 复制代码
// ❌ 错误
if (username == "admin") {  // 可能失败

// ✅ 正确
if ("admin".equals(username)) {  // 推荐
    // ...
}

// ✅ 也正确(更安全,避免 NPE)
if (Objects.equals(username, "admin")) {
    // ...
}

4.5 枚举的比较

枚举推荐用 == ,因为枚举成员是单例

java 复制代码
public enum OrderStatus {
    PENDING,
    PAID,
    SHIPPED,
    COMPLETED,
    CANCELLED
}

// ✅ 推荐用 ==(枚举是单例)
if (status == OrderStatus.PENDING) {
    // ...
}

// ✅ 也可以用 equals
if (status.equals(OrderStatus.PENDING)) {
    // ...
}

为什么枚举可以用 ==

  • 枚举成员在 JVM 中是单例
  • == 比较的是引用地址,单例的引用地址相同

4.6 业务代码示例

java 复制代码
// 场景 1:订单状态比较
OrderStatus status = OrderStatus.PENDING;

// ❌ 错误:包装类用 ==
if (status == OrderStatus.PENDING) {  // 可能出问题

// ✅ 正确:包装类用 equals
if (status.equals(OrderStatus.PENDING)) {  // 推荐

// ✅ 正确:枚举用 ==
if (status == OrderStatus.PENDING) {  // 枚举可以用 ==


// 场景 2:用户 ID 比较
Long userId1 = 100L;
Long userId2 = 100L;

// ❌ 错误
if (userId1 == userId2) {  // 可能 false

// ✅ 正确
if (userId1.equals(userId2)) {  // 推荐

// ✅ 也正确(Objects 工具类)
if (Objects.equals(userId1, userId2)) {  // 更安全

4.7 自定义类的 equals

如果自定义类需要比较内容,需要重写 equals 方法

java 复制代码
public class User {
    private Long id;
    private String name;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id) &&
               Objects.equals(name, user.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}

注意 :重写 equals 时,一定要重写 hashCode(如果对象要存到 HashMap/HashSet 中)。


5. NPE 的根因与防御 🛡️

5.1 什么是 NPE?

NPE(NullPointerException,空指针异常) 是 Java 最常见的异常之一:

java 复制代码
String str = null;
System.out.println(str.length());  // NullPointerException

5.2 为什么会有 NPE?

包装类可以为 null ,而基本类型不能。当包装类为 null 时,自动拆箱会触发 NPE:

java 复制代码
Integer age = null;
int a = age;  // 自动拆箱:age.intValue() → NPE

5.3 常见触发场景

java 复制代码
// 场景 1:数据库查询结果为 null
User user = userMapper.selectById(100L);
String name = user.getName();  // user 为 null → NPE

// 场景 2:业务逻辑中 null 没有处理
Integer discount = getDiscount();  // 可能返回 null
int d = discount + 10;  // 拆箱 → NPE

// 场景 3:自动拆箱
Integer count = null;
if (count > 0) {  // 拆箱 → NPE

// 场景 4:方法链式调用
String city = user.getAddress().getCity();  // getAddress() 可能返回 null

// 场景 5:集合操作
List<String> list = null;
list.add("hello");  // NPE

// 场景 6:数组
String[] arr = null;
arr[0] = "hello";  // NPE

5.4 NPE 防御手段

5.4.1 优先使用基本类型
java 复制代码
// 字段尽量用基本类型
public class User {
    private long id;        // 主键非 null,用 long
    private int status;     // 状态码用 int
    private boolean active; // 布尔用 boolean
    
    // 可能为 null 的字段用包装类
    private String name;    // 可能为空
    private Integer age;    // 可能为空
}
5.4.2 用 Optional 包装可能为 null 的值
java 复制代码
// 之前
public User getUser(Long id) {
    return userMapper.selectById(id);  // 可能返回 null
}

// 之后:返回 Optional
public Optional<User> getUser(Long id) {
    return Optional.ofNullable(userMapper.selectById(id));
}

// 调用方
getUser(1L).ifPresent(user -> System.out.println(user.getName()));

// 或者
User user = getUser(1L).orElse(null);

Optional 的常用方法

java 复制代码
Optional<String> opt = Optional.of("hello");

// 判断是否有值
if (opt.isPresent()) { }

// 获取值(推荐)
String value = opt.get();  // 可能抛异常

// 有值执行操作
opt.ifPresent(v -> System.out.println(v));

// 有值返回值,无值返回默认值
String result = opt.orElse("default");

// 有值返回值,无值返回计算结果
String result = opt.orElseGet(() -> computeDefault());

// 有值返回 Optional,无值返回空 Optional
Optional<String> filtered = opt.filter(s -> s.length() > 3);

// 转换值
Optional<Integer> len = opt.map(String::length);
5.4.3 用 Objects.requireNonNull 或 Assert
java 复制代码
// 方法参数校验
public void setAge(Integer age) {
    if (age == null) {
        throw new IllegalArgumentException("年龄不能为空");
    }
    this.age = age;
}

// 用 Spring 的 Assert
public void setAge(Integer age) {
    Assert.notNull(age, "年龄不能为空");
    this.age = age;
}

// 用 Objects 工具类
public void setAge(Integer age) {
    this.age = Objects.requireNonNull(age, "年龄不能为空");
}
5.4.4 用 @NotNull / @NotBlank 注解
java 复制代码
// DTO 字段加注解
public class UserDTO {
    @NotNull(message = "ID 不能为空")
    private Long id;
    
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    @NotNull(message = "年龄不能为空")
    @Min(value = 0, message = "年龄不能为负数")
    @Max(value = 150, message = "年龄超出范围")
    private Integer age;
}

// Controller 层加 @Valid
@PostMapping("/user")
public Result<Void> addUser(@Valid @RequestBody UserDTO dto) {
    // 参数为 null 或空字符串会直接返回 400 错误
    userService.add(dto);
    return Result.success();
}

常用校验注解

注解 作用
@NotNull 不能为 null
@NotBlank 不能为空字符串(仅 String)
@NotEmpty 不能为空(Collection/Map/Array)
@Min / @Max 数值范围
@Size 字符串/集合大小
@Email 邮箱格式
@Pattern 正则匹配
5.4.5 用 null 安全的方法
java 复制代码
// 之前:链式调用,每一步都可能 NPE
String city = user.getAddress().getCity();

// 之后:用 Optional
String city = Optional.ofNullable(user)
    .map(User::getAddress)
    .map(Address::getCity)
    .orElse("未知");

// 之后:用 Spring 的 ObjectUtils
String city = ObjectUtils.getIfPresent(
    () -> user.getAddress().getCity(),
    () -> "未知"
);

// 之后:用 Apache Commons Lang
String city = ObjectUtils.defaultIfNull(
    user.getAddress() != null ? user.getAddress().getCity() : null,
    "未知"
);
5.4.6 用空对象模式
java 复制代码
// 定义一个空对象
public class EmptyAddress implements Address {
    @Override
    public String getCity() {
        return "未知";
    }
    
    @Override
    public String getDetail() {
        return "未知";
    }
}

// 返回空对象而不是 null
public Address getAddress() {
    if (address == null) {
        return new EmptyAddress();
    }
    return address;
}

5.5 NPE 排查命令

bash 复制代码
# 查看异常堆栈
java -jar app.jar 2>&1 | grep -A 20 "NullPointerException"

# 用 Arthas 快速定位
$ watch com.example.User getName "{params, throwExp}" -e

# 用 jstack 查看线程堆栈
jstack <pid>

# 用 jdb 调试
jdb -attach <pid>

5.6 最佳实践总结

  1. 尽量避免返回 null:用空集合、空对象、Optional 代替
  2. 方法参数加校验 :用 @NotNull、Assert
  3. 链式调用注意空指针:用 Optional 包装
  4. 集合初始化 :用 Collections.emptyList() 等空集合
  5. 用工具类Objects.requireNonNull()Objects.equals()

6. 实战:Spring Boot 中的类型选择

6.1 实体类字段选择

java 复制代码
// User 实体类
@Entity
@Table(name = "t_user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;           // 主键用 Long(可能为 null)
    
    @Column(nullable = false)
    private String username;   // 用户名不能为空
    
    private Integer age;       // 年龄可能为空,用 Integer
    
    private Boolean active;   // 状态可能未知,用 Boolean
    
    private LocalDateTime createTime;  // 创建时间
}

6.2 DTO 字段选择

java 复制代码
// 用户创建 DTO
public class CreateUserDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    private String password;
    
    @NotNull(message = "年龄不能为空")
    @Min(value = 0, message = "年龄不能为负数")
    private Integer age;  // 前端可能不传,用 Integer
}

6.3 Service 层处理

java 复制代码
@Service
public class UserService {
    
    public User getById(Long id) {
        // 用 Optional 包装
        return Optional.ofNullable(userMapper.selectById(id))
            .orElseThrow(() -> new BusinessException("用户不存在"));
    }
    
    public void updateAge(Long id, Integer age) {
        // 参数校验
        Assert.notNull(age, "年龄不能为空");
        Assert.isTrue(age >= 0 && age <= 150, "年龄超出范围");
        
        // 业务逻辑
        User user = userMapper.selectById(id);
        if (user != null) {
            user.setAge(age);
            userMapper.updateById(user);
        }
    }
}

6.4 Controller 层处理

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {
    
    @GetMapping("/{id}")
    public Result<User> getById(@PathVariable Long id) {
        // 参数校验
        if (id == null || id <= 0) {
            return Result.error("ID 无效");
        }
        
        // 业务处理
        User user = userService.getById(id);
        return Result.success(user);
    }
    
    @PostMapping
    public Result<Void> create(@Valid @RequestBody CreateUserDTO dto) {
        // @Valid 自动校验,失败返回 400
        userService.create(dto);
        return Result.success();
    }
}

小结

  • 基本类型 存值、性能高、不能为 null包装类 是对象、可以为 null、提供丰富方法
  • 自动装箱valueOf()自动拆箱xxxValue()
  • Integer 缓存池 范围是 -128 ~ 127,超出范围创建新对象
  • 包装类比较用 equals基本类型用 ==字符串永远用 equals
  • 枚举比较可以用 ==,因为枚举成员是单例
  • NPE 防御 :优先用基本类型、用 Optional 包装、用 @NotNull 校验、用 null 安全的方法
  • 数据库实体字段 用包装类(可能为 null),业务计算字段尽量用基本类型

下一篇(015)预告:String 与字符集:接口里乱码的根源------UTF-8 和 GBK 的区别、Java 内部编码、Spring Boot 接口怎么防乱码。

相关推荐
故事和你911 小时前
洛谷-算法1-7-搜索3
数据结构·c++·算法·leetcode·动态规划
emmjng3692 小时前
使用飞算JavaAI实现在线图书借阅平台
java
CoderYanger2 小时前
14届蓝桥杯省赛Java A 组Q1~Q3
java·开发语言·线性代数·算法·职场和发展·蓝桥杯
钮钴禄·爱因斯晨2 小时前
他到底喜欢我吗?赛博塔罗Java+前端实现,一键解答!
java·开发语言·前端·javascript·css·html
词元Max2 小时前
Java 转 AI Agent 开发学习路线(2026年3月最新版)
java·人工智能·学习
亚历克斯神2 小时前
Java 云原生开发最佳实践:构建现代化应用
java·spring·微服务
布说在见2 小时前
企业级 Java 登录注册系统构建指南(附核心代码与配置)
java·开发语言
是宇写的啊2 小时前
SpringBoot配置文件
java·spring boot·spring
草莓熊Lotso2 小时前
一文读懂 Java 主流编译器:特性、场景与选择指南
java·开发语言·经验分享