Skip to content

变量与数据类型 ​

S1-001必学入门约 60 分钟JDK 21编程语言基础 · 语法基础

1. 本节定位 ​

Java 是强类型语言:每个变量都有确定的类型,类型决定了它能存什么、占多少内存、能做什么运算。

本节讲清四件事:有哪些类型、怎么声明、什么时候会"悄悄出错"(溢出与精度丢失)、以及金额这类业务数据该用什么类型。最后一条是实际工作中踩坑最多的地方。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说出 8 种基本类型的名称、字节数与取值范围;
  • 区分基本类型与引用类型,并说清成员变量与局部变量的默认值差异;
  • 判断一次赋值或运算会不会发生自动类型提升或精度丢失;
  • 解释 Integer.MAX_VALUE + 1 为什么是负数;
  • 说出为什么金额不能用 double 存,以及正确的两种做法;
  • 正确使用 var 与 final。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

变量的类型不是形式主义,它决定了三件真实的事:

第一,能存多大的数。 byte 装不下 200,int 装不下 300 亿。选错类型,程序在某个临界值上突然出错。

第二,会不会丢精度。 用 double 存金额,0.1 + 0.2 != 0.3——这不是 Java 的 bug,而是二进制浮点的固有特性。

第三,会不会溢出。 int 超过 21 亿会变成负数,且不报错。这类 bug 在生产环境里非常难查。

5. 核心概念 ​

5.1 8 种基本类型 ​

类型字节取值范围默认值典型用途
byte1-128 ~ 1270文件流、网络传输
short2-32768 ~ 327670很少用
int4-2147483648 ~ 21474836470整数默认类型
long8约 ±9.2×10¹⁸0LID、时间戳、金额(以分为单位)
float4约 ±3.4×10³⁸0.0f很少用
double8约 ±1.8×10³⁰⁸0.0d小数默认类型(但别用来存金额)
char20 ~ 65535(Unicode)'\u0000'单个字符
boolean1true / falsefalse条件判断

记忆要点:整数默认 int,小数默认 double。写 long 字面量要加 L,写 float 要加 f。

5.2 基本类型与引用类型 ​

java
int count = 10;                       // 基本类型:变量里直接存值
String name = "知识库";                // 引用类型:变量里存的是对象的地址
int[] numbers = {1, 2, 3};            // 数组也是引用类型
基本类型引用类型
存什么值本身对象的引用(地址)
默认值0 / false / '\u0000'null
能否为 null不能能(引用类型都可以是 null)
比较== 比较值== 比较地址(内容要用 equals)

5.3 成员变量有默认值,局部变量没有 ​

java
public class Demo {
    static int memberValue;          // 成员变量:默认为 0

    void method() {
        int localValue;              // 局部变量:没有默认值
        System.out.println(localValue);   // 编译错误:可能尚未初始化
    }
}

规则:成员变量(字段)会被自动初始化;局部变量必须显式赋值后才能使用。这是编译器在帮你避免"读到垃圾值"。

5.4 类型转换:自动提升与强制转换 ​

text
小范围 → 大范围:自动提升,安全
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。这叫溢出截断,编译器不会警告你。

5.5 整数溢出:不报错的错误 ​

java
int max = Integer.MAX_VALUE;    // 2147483647
int overflow = max + 1;         // -2147483648

二进制补码在最大值加一后会"回绕"到最小值。关键在于它不抛异常、不报警告——如果你的分页计算、库存累加用了 int,量级一上来就会出这种问题。

防御方式:涉及累加、ID、金额时直接用 long;或者用 Math.addExact(...),它会溢出时抛异常:

java
Math.addExact(Integer.MAX_VALUE, 1);   // ArithmeticException: integer overflow

5.6 浮点精度:为什么金额不能用 double ​

java
System.out.println(0.1 + 0.2);          // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);   // false

double 用二进制表示小数,而 0.1、0.2 在二进制里是无限循环小数,只能近似存储。结论:

场景正确做法
金额计算用 long 存"分",或 BigDecimal(《浮点精度与 BigDecimal》)
比较小数不用 ==,改用"差值小于某个精度"或 BigDecimal.compareTo
科学计算double 可以,但要理解误差累积

5.7 var 与 final ​

java
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。

6. 递进示例 ​

6.1 最小示例:声明与输出 ​

java
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);

6.2 业务示例:金额用 long 存分 ​

java
// 商品单价 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,单价 × 数量在千万级订单场景就可能溢出。

6.3 反例:用 double 存金额 ​

java
double price = 19.90;
double total = price * 3;
System.out.println(total);              // 59.699999999999996
System.out.println(total == 59.7);      // false

对账时出现"差一分钱",往往就是从这里开始的。

7. 完整可运行代码 ​

文件:labs/java-syntax/src/main/java/cn/kb/learn/syntax/DataTypes.java

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);
    }
}

运行命令:

powershell
cd labs
./mvnw -pl java-syntax compile
java -cp java-syntax/target/classes cn.kb.learn.syntax.DataTypes

预期输出:

text
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 = 3

8. Debug 观察点 ​

  1. 在 int overflow = Integer.MAX_VALUE + 1; 下断点,先执行完这一行,看 Variables 面板里 overflow 变成负数——零成本的"事故现场";
  2. 在 (byte) 300 那行断点,把 narrowed 加入 Watch,观察 300 → 44 的截断;
  3. 把 0.1 + 0.2 的表达式选中,按 Alt+F8 求值,亲眼看 0.30000000000000004;
  4. 删除 System.out.println(memberValue); 前的 static int memberValue;,观察编译错误——体会"成员变量有默认值"这条规则。

9. 常见错误 ​

错误 1:long 字面量忘了加 L ​

java
long big = 9000000000;    // 编译错误:整数太大
long big = 9000000000L;   // 正确

原因:9000000000 先按 int 解析,超出 int 范围直接编译失败。

错误 2:用 == 比较浮点数 ​

java
if (a + b == 0.3) { ... }              // 可能永远不成立
if (Math.abs(a + b - 0.3) < 1e-9) { }  // 正确:按精度比较

错误 3:拆箱时的空指针 ​

java
Integer count = null;
int value = count;        // NullPointerException:对 null 拆箱

这是带包装类型的集合里最常见的 NPE 来源:

java
Map<String, Integer> map = new HashMap<>();
int value = map.get("不存在的键");   // NPE
int safe = map.getOrDefault("不存在的键", 0);   // 正确

错误 4:局部变量未初始化 ​

java
int total;
total += 1;      // 编译错误:可能尚未初始化

修复:int total = 0;——编译器强制你明确初值,这是好事。

10. 动手实验 ​

目标:亲手制造一次溢出与一次精度丢失,并给出修复方案。

步骤:

  1. 运行第 7 节代码,核对输出;
  2. 写一段计算"1 到 100000 的平方和"的代码,先用 int 累加,打印结果;再用 long 累加,对比两个结果;
  3. 用 Math.addExact 重写第 2 步,观察它在什么时候抛异常;
  4. 用 double 计算 0.1 + 0.2,再用"乘以 100 后取整"的方式比较是否等于 30;
  5. 写一份说明:在你的日常工作中,哪些字段适合用 long 而不是 int。

验收方式:能解释第 2 步两个结果的差异来自哪里。

扩展任务:用 BigDecimal 重做第 4 步(new BigDecimal("0.1").add(new BigDecimal("0.2"))),对比哪种方式更可靠,并说明为什么不能用 new BigDecimal(0.1)(提示:那个构造方法接收的是已经失真的 double 值,要用字符串构造)。

11. 练习题 ​

基础题

  1. 判断:int 溢出时会抛出异常。(对 / 错)
  2. 填空:整数默认类型是 ______,小数默认类型是 ______,long 字面量要加后缀 ______。
  3. 简答:成员变量与局部变量在默认值上有什么区别?

代码阅读题

java
byte b = 100;
b = (byte) (b + 28);
System.out.println(b);
  1. 输出是多少?为什么?

编程题

  1. 写一个方法 long totalInCents(long unitPriceInCents, int quantity),返回总价(分)。要求:
    • 单价必须为正,否则抛 IllegalArgumentException;
    • 数量必须为正;
    • 用 Math.multiplyExact 防止溢出。

综合任务

  1. 设计一个"订单金额"的小程序:
    • 用 long 存分,支持多件商品累加;
    • 计算折扣(打 8.5 折),要求四舍五入到分;
    • 打印形如 总价: 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)。

12. 参考答案 ​

基础题
  1. 错。整数溢出不会抛异常,而是静默回绕到另一端。想让它抛异常要用 Math.addExact 等带 Exact 的方法。
  2. int、double、L。
  3. 成员变量有默认值(数字 0、boolean 为 false、引用类型为 null);局部变量没有任何默认值,必须显式初始化后才能使用,否则编译不通过。
代码阅读题(第 4 题)

输出 -128。

过程:

  1. b + 28:b 被自动提升为 int,得到 128;
  2. (byte) 128:byte 最大是 127,128 溢出回绕到 -128;
  3. 赋值给 b 并打印。

这个例子说明:自动提升让中间结果"装得下",但强转回小类型时仍会溢出。

编程题(第 5 题)
java
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());
        }
    }
}

预期输出:

text
5970
预期内的溢出:long overflow
综合任务(第 6 题)
java
package 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 只出现在展示的最后一步,不影响计算精度。

预期输出:

text
原价: 99.77 元
折后: 84.80 元

13. 掌握标准 ​

  • [ ] 能说出 8 种基本类型的字节数与大致范围;
  • [ ] 能判断一次运算是否会发生提升或截断;
  • [ ] 知道整数溢出与浮点误差的成因,并知道怎么防;
  • [ ] 金额场景能自觉选择 long 分或 BigDecimal。

14. 扩展资源 ​

15. 下一步学习 ​

《运算符与流程控制》:有了数据,接下来要让程序"做判断"和"重复做事"。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引