主题
界面
主题
Java 是强类型语言:每个变量都有确定的类型,类型决定了它能存什么、占多少内存、能做什么运算。
本节讲清四件事:有哪些类型、怎么声明、什么时候会"悄悄出错"(溢出与精度丢失)、以及金额这类业务数据该用什么类型。最后一条是实际工作中踩坑最多的地方。
完成本节后,你应该能够:
Integer.MAX_VALUE + 1 为什么是负数;double 存,以及正确的两种做法;var 与 final。变量的类型不是形式主义,它决定了三件真实的事:
第一,能存多大的数。 byte 装不下 200,int 装不下 300 亿。选错类型,程序在某个临界值上突然出错。
第二,会不会丢精度。 用 double 存金额,0.1 + 0.2 != 0.3——这不是 Java 的 bug,而是二进制浮点的固有特性。
第三,会不会溢出。 int 超过 21 亿会变成负数,且不报错。这类 bug 在生产环境里非常难查。
| 类型 | 字节 | 取值范围 | 默认值 | 典型用途 |
|---|---|---|---|---|
byte | 1 | -128 ~ 127 | 0 | 文件流、网络传输 |
short | 2 | -32768 ~ 32767 | 0 | 很少用 |
int | 4 | -2147483648 ~ 2147483647 | 0 | 整数默认类型 |
long | 8 | 约 ±9.2×10¹⁸ | 0L | ID、时间戳、金额(以分为单位) |
float | 4 | 约 ±3.4×10³⁸ | 0.0f | 很少用 |
double | 8 | 约 ±1.8×10³⁰⁸ | 0.0d | 小数默认类型(但别用来存金额) |
char | 2 | 0 ~ 65535(Unicode) | '\u0000' | 单个字符 |
boolean | 1 | true / false | false | 条件判断 |
记忆要点:整数默认 int,小数默认 double。写 long 字面量要加 L,写 float 要加 f。
int count = 10; // 基本类型:变量里直接存值
String name = "知识库"; // 引用类型:变量里存的是对象的地址
int[] numbers = {1, 2, 3}; // 数组也是引用类型| 基本类型 | 引用类型 | |
|---|---|---|
| 存什么 | 值本身 | 对象的引用(地址) |
| 默认值 | 0 / false / '\u0000' | null |
| 能否为 null | 不能 | 能(引用类型都可以是 null) |
| 比较 | == 比较值 | == 比较地址(内容要用 equals) |
public class Demo {
static int memberValue; // 成员变量:默认为 0
void method() {
int localValue; // 局部变量:没有默认值
System.out.println(localValue); // 编译错误:可能尚未初始化
}
}规则:成员变量(字段)会被自动初始化;局部变量必须显式赋值后才能使用。这是编译器在帮你避免"读到垃圾值"。
小范围 → 大范围:自动提升,安全
byte → short → int → long → float → double
大范围 → 小范围:必须强制转换,可能丢数据
int i = 300;
byte b = (byte) i; // 结果是 44,不是 300(byte) 300 为什么是 44?因为 byte 只有 8 位:300 的二进制是 100101100,截断后 8 位是 00101100 = 44。这叫溢出截断,编译器不会警告你。
int max = Integer.MAX_VALUE; // 2147483647
int overflow = max + 1; // -2147483648二进制补码在最大值加一后会"回绕"到最小值。关键在于它不抛异常、不报警告——如果你的分页计算、库存累加用了 int,量级一上来就会出这种问题。
防御方式:涉及累加、ID、金额时直接用 long;或者用 Math.addExact(...),它会溢出时抛异常:
Math.addExact(Integer.MAX_VALUE, 1); // ArithmeticException: integer overflowSystem.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // falsedouble 用二进制表示小数,而 0.1、0.2 在二进制里是无限循环小数,只能近似存储。结论:
| 场景 | 正确做法 |
|---|---|
| 金额计算 | 用 long 存"分",或 BigDecimal(《浮点精度与 BigDecimal》) |
| 比较小数 | 不用 ==,改用"差值小于某个精度"或 BigDecimal.compareTo |
| 科学计算 | double 可以,但要理解误差累积 |
var message = "Java 21"; // 编译器推断为 String
final int MAX_RETRY = 3; // 常量,不可重新赋值var 的边界(重要):
var list = new ArrayList<String>() 清晰,var result = service.process() 就该写明类型。常量命名约定:static final 的常量用 UPPER_SNAKE_CASE。
int age = 18;
long population = 8_000_000_000L; // 下划线只是分隔符,便于阅读
boolean active = true;
char grade = 'A';
double price = 19.9;
System.out.println(age + " / " + population + " / " + active + " / " + grade + " / " + price);// 商品单价 19.90 元 → 存 1990 分
long unitPriceInCents = 1990L;
int quantity = 3;
long totalInCents = unitPriceInCents * quantity; // 5970 分
// 展示时再转成元
System.out.printf("总价: %.2f 元%n", totalInCents / 100.0);为什么乘 quantity 不溢出:long 的范围足够大。如果这里用 int,单价 × 数量在千万级订单场景就可能溢出。
double price = 19.90;
double total = price * 3;
System.out.println(total); // 59.699999999999996
System.out.println(total == 59.7); // false对账时出现"差一分钱",往往就是从这里开始的。
文件:labs/java-syntax/src/main/java/cn/kb/learn/syntax/DataTypes.java
package cn.kb.learn.syntax;
public class DataTypes {
static int memberValue;
public static void main(String[] args) {
// 1. 基本类型的取值范围
System.out.println("byte : " + Byte.MIN_VALUE + " ~ " + Byte.MAX_VALUE);
System.out.println("short: " + Short.MIN_VALUE + " ~ " + Short.MAX_VALUE);
System.out.println("int : " + Integer.MIN_VALUE + " ~ " + Integer.MAX_VALUE);
System.out.println("long : " + Long.MIN_VALUE + " ~ " + Long.MAX_VALUE);
System.out.println("char : " + (int) Character.MIN_VALUE + " ~ " + (int) Character.MAX_VALUE);
// 2. 字面量写法
int decimal = 10_000;
int hex = 0xFF;
int binary = 0b1010;
long big = 9_000_000_000L;
System.out.println("字面量: " + decimal + " / " + hex + " / " + binary + " / " + big);
// 3. 强制转换会截断
int source = 300;
byte narrowed = (byte) source;
System.out.println("(byte) 300 = " + narrowed);
// 4. 浮点数不能表示精确的十进制小数
System.out.println("0.1 + 0.2 = " + (0.1 + 0.2));
System.out.println("相等判断: " + (0.1 + 0.2 == 0.3));
// 5. 整数溢出
int overflow = Integer.MAX_VALUE + 1;
System.out.println("Integer.MAX_VALUE + 1 = " + overflow);
// 6. 成员变量有默认值
System.out.println("成员变量默认值: " + memberValue);
// 7. var 类型推断(Java 10+)
var message = "var 推断为 String";
System.out.println(message + " / 实际类型 " + message.getClass().getSimpleName());
// 8. 常量
final int maxRetry = 3;
System.out.println("常量 maxRetry = " + maxRetry);
}
}运行命令:
cd labs
./mvnw -pl java-syntax compile
java -cp java-syntax/target/classes cn.kb.learn.syntax.DataTypes预期输出:
byte : -128 ~ 127
short: -32768 ~ 32767
int : -2147483648 ~ 2147483647
long : -9223372036854775808 ~ 9223372036854775807
char : 0 ~ 65535
字面量: 10000 / 255 / 10 / 9000000000
(byte) 300 = 44
0.1 + 0.2 = 0.30000000000000004
相等判断: false
Integer.MAX_VALUE + 1 = -2147483648
成员变量默认值: 0
var 推断为 String / 实际类型 String
常量 maxRetry = 3int overflow = Integer.MAX_VALUE + 1; 下断点,先执行完这一行,看 Variables 面板里 overflow 变成负数——零成本的"事故现场";(byte) 300 那行断点,把 narrowed 加入 Watch,观察 300 → 44 的截断;0.1 + 0.2 的表达式选中,按 Alt+F8 求值,亲眼看 0.30000000000000004;System.out.println(memberValue); 前的 static int memberValue;,观察编译错误——体会"成员变量有默认值"这条规则。long 字面量忘了加 L long big = 9000000000; // 编译错误:整数太大
long big = 9000000000L; // 正确原因:9000000000 先按 int 解析,超出 int 范围直接编译失败。
== 比较浮点数 if (a + b == 0.3) { ... } // 可能永远不成立
if (Math.abs(a + b - 0.3) < 1e-9) { } // 正确:按精度比较Integer count = null;
int value = count; // NullPointerException:对 null 拆箱这是带包装类型的集合里最常见的 NPE 来源:
Map<String, Integer> map = new HashMap<>();
int value = map.get("不存在的键"); // NPE
int safe = map.getOrDefault("不存在的键", 0); // 正确int total;
total += 1; // 编译错误:可能尚未初始化修复:int total = 0;——编译器强制你明确初值,这是好事。
目标:亲手制造一次溢出与一次精度丢失,并给出修复方案。
步骤:
int 累加,打印结果;再用 long 累加,对比两个结果;Math.addExact 重写第 2 步,观察它在什么时候抛异常;double 计算 0.1 + 0.2,再用"乘以 100 后取整"的方式比较是否等于 30;long 而不是 int。验收方式:能解释第 2 步两个结果的差异来自哪里。
扩展任务:用 BigDecimal 重做第 4 步(new BigDecimal("0.1").add(new BigDecimal("0.2"))),对比哪种方式更可靠,并说明为什么不能用 new BigDecimal(0.1)(提示:那个构造方法接收的是已经失真的 double 值,要用字符串构造)。
基础题
int 溢出时会抛出异常。(对 / 错)long 字面量要加后缀 ______。代码阅读题
byte b = 100;
b = (byte) (b + 28);
System.out.println(b);编程题
long totalInCents(long unitPriceInCents, int quantity),返回总价(分)。要求: IllegalArgumentException;Math.multiplyExact 防止溢出。综合任务
long 存分,支持多件商品累加;总价: 123.45 元 的结果;double。提示 1(方向):第 4 题的 b + 28 会先把 b 提升成 int(128),再强转回 byte 才放得下。
提示 2(关键点):第 5 题的 Math.multiplyExact 在溢出时抛 ArithmeticException,比静默回绕更安全。
提示 3(接近答案):第 6 题的折扣可以写成 total * 85 / 100(全程整数运算),需要四舍五入时用 Math.round 配合 double 只做最后一步换算,或者用 BigDecimal.setScale(..., RoundingMode.HALF_UP)。
Math.addExact 等带 Exact 的方法。int、double、L。boolean 为 false、引用类型为 null);局部变量没有任何默认值,必须显式初始化后才能使用,否则编译不通过。输出 -128。
过程:
b + 28:b 被自动提升为 int,得到 128;(byte) 128:byte 最大是 127,128 溢出回绕到 -128;b 并打印。这个例子说明:自动提升让中间结果"装得下",但强转回小类型时仍会溢出。
package cn.kb.learn.syntax;
public class PriceCalculator {
static long totalInCents(long unitPriceInCents, int quantity) {
if (unitPriceInCents <= 0) {
throw new IllegalArgumentException("单价必须为正");
}
if (quantity <= 0) {
throw new IllegalArgumentException("数量必须为正");
}
return Math.multiplyExact(unitPriceInCents, quantity);
}
public static void main(String[] args) {
System.out.println(totalInCents(1990L, 3));
try {
totalInCents(Long.MAX_VALUE / 2, 3);
} catch (ArithmeticException exception) {
System.out.println("预期内的溢出:" + exception.getMessage());
}
}
}预期输出:
5970
预期内的溢出:long overflowpackage cn.kb.learn.syntax;
public class OrderAmount {
private static final int DISCOUNT_PERCENT = 85; // 8.5 折
public static void main(String[] args) {
long[] pricesInCents = {1990L, 2500L, 999L};
int[] quantities = {2, 1, 3};
long subtotal = 0;
for (int i = 0; i < pricesInCents.length; i++) {
subtotal += pricesInCents[i] * quantities[i];
}
// 折扣:全程整数运算,最后一步四舍五入到分
long discounted = Math.round(subtotal * DISCOUNT_PERCENT / 100.0);
System.out.printf("原价: %.2f 元%n", subtotal / 100.0);
System.out.printf("折后: %.2f 元%n", discounted / 100.0);
}
}为什么不能用 double 全程计算:double 存的是近似值,多件商品累加与折扣相乘会把误差放大,最终 printf 出来的"分"可能差 1 分。整数运算(long 存分)没有这个问题;/ 100.0 只出现在展示的最后一步,不影响计算精度。
预期输出:
原价: 99.77 元
折后: 84.80 元long 分或 BigDecimal。《运算符与流程控制》:有了数据,接下来要让程序"做判断"和"重复做事"。