本节目标:能为
Book的每个字段选出合适的类型,解释基本类型与包装类型的取舍,并避开装箱、缓存和空指针三个高频坑。
适用版本:JDK 25 LTS(Temurin 25.0.5)。
Book 需要哪些字段
library 的第一条业务规则是「录入一本书,能列出、能借、能还」。在写借阅逻辑之前,先要把「一本书在程序里长什么样」定下来。一个够用的 Book 至少需要:
- 一个唯一标识:ISBN。
- 书名、作者。
- 出版年份。
- 价格,用于展示或罚金计算。
- 当前是否可借。
这些字段看起来都简单,但每个都涉及类型选择:ISBN 用 String 还是 long?年份用 int 还是 Integer?价格用 double 还是 int?可借用 boolean 还是 Boolean?选错类型不会立刻报错,但会在边界上出问题。
八种基本类型
Java 有八种基本类型(primitive type),它们是值类型,不是对象:
| 类型 | 大小 | 范围 | 默认值 | 典型用途 |
|---|---|---|---|---|
byte | 8 位 | -128 ~ 127 | 0 | 字节数据、文件读写 |
short | 16 位 | -32,768 ~ 32,767 | 0 | 较少使用 |
int | 32 位 | -2,147,483,648 ~ 2,147,483,647 | 0 | 计数、年份、金额分 |
long | 64 位 | 约 ±9.22e18 | 0L | 大整数、时间戳、总次数 |
float | 32 位 | 约 ±3.4e38,6–7 位有效数字 | 0.0f | 图形、科学计算 |
double | 64 位 | 约 ±1.8e308,15–16 位有效数字 | 0.0d | 一般浮点计算 |
char | 16 位 | 0 ~ 65,535(UTF-16 单元) | '\u0000' | 单个字符 |
boolean | 未规定 | true / false | false | 开关、状态 |
几个容易忽略的点:
- 默认值只对字段生效。 类的成员变量不初始化时会有默认值;方法里的局部变量必须先赋值才能使用。下面这样写会编译失败:
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 选类型
把八个类型往字段上套,选择其实不复杂:
| 字段 | 类型 | 理由 |
|---|---|---|
isbn | String | ISBN-10 可能有前导零和校验位 X,不是纯数字 |
title | String | 文本 |
author | String | 文本 |
publishedYear | int | 年份范围远小于 int,值类型没有空值歧义 |
priceCents | int | 金额用「分」存整数,避免浮点误差 |
available | boolean | 只有借出/可借两种状态 |
totalLoans | long | 累计借阅次数可能很大,用 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()。看起来方便,但有两个代价:
null拆箱会抛NullPointerException。 包装类型可以是null,基本类型不能。==比较的是引用,不是值。 两个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 与类型推断 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。