《Java 编程入门》4.1 基本类型与包装类型

以 Book 的字段设计为线索,讲清八种基本类型的范围与默认值、什么时候该用 int 而不是 Integer、ISBN 为什么必须用 String;再拆开装箱拆箱、Integer 缓存、== 与 equals 的差异,用真实 NPE 和 128 比较结果说明包装类型最容易踩的三个坑。

本节目标:能为 Book 的每个字段选出合适的类型,解释基本类型与包装类型的取舍,并避开装箱、缓存和空指针三个高频坑。
适用版本:JDK 25 LTS(Temurin 25.0.5)。

Book 需要哪些字段

library 的第一条业务规则是「录入一本书,能列出、能借、能还」。在写借阅逻辑之前,先要把「一本书在程序里长什么样」定下来。一个够用的 Book 至少需要:

  • 一个唯一标识:ISBN。
  • 书名、作者。
  • 出版年份。
  • 价格,用于展示或罚金计算。
  • 当前是否可借。

这些字段看起来都简单,但每个都涉及类型选择:ISBN 用 String 还是 long?年份用 int 还是 Integer?价格用 double 还是 int?可借用 boolean 还是 Boolean?选错类型不会立刻报错,但会在边界上出问题。

八种基本类型

Java 有八种基本类型(primitive type),它们是值类型,不是对象:

类型大小范围默认值典型用途
byte8 位-128 ~ 1270字节数据、文件读写
short16 位-32,768 ~ 32,7670较少使用
int32 位-2,147,483,648 ~ 2,147,483,6470计数、年份、金额分
long64 位约 ±9.22e180L大整数、时间戳、总次数
float32 位约 ±3.4e38,6–7 位有效数字0.0f图形、科学计算
double64 位约 ±1.8e308,15–16 位有效数字0.0d一般浮点计算
char16 位0 ~ 65,535(UTF-16 单元)'\u0000'单个字符
boolean未规定true / falsefalse开关、状态

几个容易忽略的点:

  • 默认值只对字段生效。 类的成员变量不初始化时会有默认值;方法里的局部变量必须先赋值才能使用。下面这样写会编译失败:
int count;
System.out.println(count);
错误: 可能尚未初始化变量count
  • char 是 16 位无符号整数。 它可以参与算术运算,'A' + 1 得到 66。但它不是 String,char 只能表示一个 UTF-16 单元,表示不了大部分 emoji。
  • boolean 的大小没有规定。 JVM 规范只说它有 true/false 两个值,不保证 1 位或 1 字节。不要假设它能和 int 互转,Java 不允许 boolean 参与数值运算。
  • float 和 double 是二进制浮点。 它们能表示很大的范围,但不能精确表示 0.1 这样的十进制小数。金额不要用它们。

字面量有默认类型:整数写 10 是 int,写 10L 是 long;小数写 0.1 是 double,写 0.1f 是 float。超出 int 范围的整数字面量必须加 L,否则编译失败。

为 Book 选类型

把八个类型往字段上套,选择其实不复杂:

字段类型理由
isbnStringISBN-10 可能有前导零和校验位 X,不是纯数字
titleString文本
authorString文本
publishedYearint年份范围远小于 int,值类型没有空值歧义
priceCentsint金额用「分」存整数,避免浮点误差
availableboolean只有借出/可借两种状态
totalLoanslong累计借阅次数可能很大,用 long 留余量

ISBN 是最容易被选错的字段。很多人看到「13 位数字」就想用 long,但 ISBN-10 的最后一位可能是 X,而且书号可能有前导零。long 会把 0123456789 变成 123456789,前导零丢失;X 根本存不进去。标识符用 String,不要用数值类型,除非你确定它只做算术。

价格用 int 存「分」,而不是 double 存「元」。89.00 在 double 里不是精确值,累加多次会出现 88.99999999999999 这样的结果。用 8900 表示 89.00 元,加减乘都是整数运算,结果精确。展示时再除以 100,格式化成两位小数。

年份用 int 而不是 Integer。Book 的年份不应该是「未知」,如果业务上确实允许未知,应该用 Integer 加显式的空值检查,而不是用 0 当哨兵值。类型选择要表达业务语义:能用值类型表达「一定存在」时,不要用包装类型引入 null。

写 Book 类

按上面的选择写第一版 Book:

package com.plume.library.domain;

public class Book {
    private final String isbn;
    private final String title;
    private final String author;
    private final int publishedYear;
    private final int priceCents;
    private boolean available;

    public Book(String isbn, String title, String author,
                int publishedYear, int priceCents) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.publishedYear = publishedYear;
        this.priceCents = priceCents;
        this.available = true;
    }

    public String isbn() {
        return isbn;
    }

    public String title() {
        return title;
    }

    public String author() {
        return author;
    }

    public int publishedYear() {
        return publishedYear;
    }

    public int priceCents() {
        return priceCents;
    }

    public boolean isAvailable() {
        return available;
    }

    public void markBorrowed() {
        if (!available) {
            throw new IllegalStateException("book already borrowed: " + isbn);
        }
        available = false;
    }

    public void markReturned() {
        available = true;
    }

    public String priceText() {
        return String.format("%d.%02d", priceCents / 100, priceCents % 100);
    }
}

这一版刻意不用 record。record 要到第 9 章才讲,它适合「字段全部不可变」的数据载体;Book 现在有一个可变的 available,而且第 7 章还要讨论封装和不可变设计。先用普通类把字段和类型定下来,后面再决定哪些字段该冻结。

priceText 用整数除法和取模把「分」转成「元」。8900 / 100 得 89,8900 % 100 得 0,格式化成 89.00。如果价格是 8950,结果是 89.50。这段逻辑在第 4.3 节讨论除法和取模时会再展开。

包装类型:什么时候需要对象

每种基本类型都有一个对应的包装类型:int → Integer,long → Long,boolean → Boolean,char → Character,其余类似。包装类型是对象,可以为 null,可以放进泛型集合(第 10 章),可以调用方法。

自动装箱(autoboxing)让两者可以隐式转换:

Integer boxed = 42;      // 自动装箱:int -> Integer
int unboxed = boxed;     // 自动拆箱:Integer -> int

编译器会在背后插入 Integer.valueOf(42) 和 boxed.intValue()。看起来方便,但有两个代价:

  1. null 拆箱会抛 NullPointerException。 包装类型可以是 null,基本类型不能。
  2. == 比较的是引用,不是值。 两个 Integer 用 == 比较,结果取决于缓存和对象身份,不是数值。

Integer 有一个缓存:-128 到 127 之间的值会复用同一个对象。所以:

Integer a = 127;
Integer b = 127;
System.out.println(a == b);   // true

Integer c = 128;
Integer d = 128;
System.out.println(c == d);   // false

127 在缓存范围内,a 和 b 指向同一个对象;128 超出范围,每次都新建对象,引用不同。这个行为是 JVM 规范允许的,不是 bug。比较包装类型的值用 equals,不要用 ==。 如果只是想比较数值,先拆箱成基本类型再比。

包装类型真正有用的场景:

  • 泛型集合:List<Integer> 不能写 List<int>,第 10 章会用到。
  • 可空语义:数据库里的 NULL、JSON 里的 null,需要用包装类型表达「没有值」。
  • 工具方法:Integer.parseInt、Long.parseLong、Boolean.parseBoolean 这些静态方法。
  • 反射和序列化:很多框架要求对象类型。

反过来,字段能用基本类型就用基本类型。Book.priceCents 用 int,因为价格一定存在;如果业务允许「价格未知」,才改成 Integer 并在读取时处理 null。

在 library 里验证

写一个探针,观察值类型、装箱缓存和拆箱:

package com.plume.library.domain;

public final class BookProbe {
    public static void main(String[] args) {
        Book book = new Book("978-7-115-54608-9", "Java 编程入门",
                "Leeting Yan", 2026, 8900);
        System.out.println(book.title() + " / " + book.author()
                + " / " + book.publishedYear());
        System.out.println("price = " + book.priceText());
        System.out.println("available = " + book.isAvailable());
        book.markBorrowed();
        System.out.println("after borrow = " + book.isAvailable());

        Integer smallA = 127;
        Integer smallB = 127;
        Integer bigA = 128;
        Integer bigB = 128;
        System.out.println("127 boxed == : " + (smallA == smallB));
        System.out.println("128 boxed == : " + (bigA == bigB));
        System.out.println("128 equals   : " + bigA.equals(bigB));

        long isbnDigits = 9787115546089L;
        System.out.println("isbn as long = " + isbnDigits);
        System.out.println("leading zero lost = " + Long.parseLong("0123456789"));
    }
}

编译并运行:

javac -d out src/main/java/com/plume/library/domain/Book.java \
  src/main/java/com/plume/library/domain/BookProbe.java
java -cp out com.plume.library.domain.BookProbe

本机 JDK 25 输出:

Java 编程入门 / Leeting Yan / 2026
price = 89.00
available = true
after borrow = false
127 boxed == : true
128 boxed == : false
128 equals   : true
isbn as long = 9787115546089
leading zero lost = 123456789

最后两行是 ISBN 用 long 的反例。9787115546089 看起来能存,但换成带前导零的书号就丢了信息;如果校验位是 X,Long.parseLong 会直接抛 NumberFormatException。String 没有这些问题。

再验证拆箱空指针。把年份声明成 Integer,赋 null,再赋给 int:

Integer publishedYear = null;
int year = publishedYear;

运行时抛出:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "<local1>" is null
    at com.plume.library.UnboxProbe.main(UnboxProbe.java:6)

报错信息里的 intValue() 就是编译器插入的拆箱调用。它不看值是不是 null,直接调用方法,所以 NPE 发生在拆箱那一行,而不是你声明 null 的那一行。排查时要看栈顶的行号。

Integer.parseInt 也有边界。传入非数字字符串:

Integer.parseInt("12a");
java.lang.NumberFormatException: For input string: "12a"

这类异常是 RuntimeException,不强制捕获。第 13 章会讲什么时候该捕获、什么时候该让它冒泡;现在先记住:用户输入转数字,必须处理格式错误。

常见错误与边界

「用 == 比较 Integer。」 127 == 127 为 true,128 == 128 为 false,结果依赖缓存范围。这类 bug 在测试数据小于 128 时完全看不出来,上线后遇到大 ID 才爆发。统一用 equals,或者先转成 int 再比。

「包装类型字段默认是 null。」 Integer count; 的字段默认值是 null,不是 0。直接参与算术会 NPE。如果字段语义是「计数从 0 开始」,用 int;如果确实需要「未知」,在构造器或读取处显式处理 null。

「用 double 存金额。」 0.1 + 0.2 不等于 0.3,累加多次后误差会放大。金额用最小货币单位的整数(分),或者用 BigDecimal 做精确十进制运算。卷一的罚金计算用「分」,简单且够用。

「用 char 存中文。」 一个 char 是 16 位,能表示基本汉字,但表示不了扩展区汉字和 emoji。文本一律用 String,单个字符的场景才考虑 char。String.length() 返回的是 UTF-16 单元数,不一定是「字数」。

「整数字面量以 0 开头。」 Java 把 0123456789L 当成八进制字面量,而 8 和 9 不是八进制数字,编译时报:

错误: 八进制文字中的数字非法
        long isbn = 0123456789L;
                            ^

即使数字都在 0–7 范围内,010 也表示十进制的 8,不是 10。ISBN 用 String 存,既避开前导零丢失,也避开八进制解析。

小结

  • 八种基本类型是值类型;默认值只对字段生效,局部变量必须先赋值。
  • Book 的 ISBN 用 String,年份用 int,金额用「分」存 int,状态用 boolean,累计次数用 long。
  • 包装类型用于可空语义、泛型集合和工具方法;能用基本类型表达「一定存在」时优先用基本类型。
  • 自动装箱会在背后调用 valueOf,自动拆箱会在背后调用 xxxValue;null 拆箱直接 NPE。
  • Integer 缓存 -128 到 127,== 比较包装类型不可靠,值比较用 equals。
  • 金额不要用 double;标识符不要用数值类型;文本不要用 char。

字段和类型定下来后,下一个问题是「这些变量该怎么声明」。var 能让局部变量声明更短,final 能表达「不再改变」,但两者都有边界。下一节把类型推断和 final 放在一起讲。

阅读导航:上一节:3.3 Maven 生命周期:compile、test、package · 下一节:4.2 var、final 与类型推断 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Netty 高性能网络编程》15.3 linkd-v2 验收与复盘
  2. 《Netty 高性能网络编程》15.2 限流、超时与降级
  3. 《Netty 高性能网络编程》15.1 网关路由与转发