06 变量与数据类型

前置知识:第一个程序与 jshell
本章目标:掌握 Java 的 8 种基本类型、字面量写法、类型转换规则、包装类与自动装箱、String 初识与作用域规则。

概述

如果你写过 C,这一章会有强烈的「既熟悉又陌生」感:熟悉的 int、double、char 都在;陌生的是——Java 的类型大小是固定的、没有 unsigned、boolean 不再是整数。这些差异不是语法糖,而是「一次编写,到处运行」的地基。

与 C 的核心对比总览

维度CJava
类型大小由平台决定(long 在 32/64 位不同)固定不变,跨平台一致
无符号类型unsigned int不存在,全部有符号
boolean本质是 int(0/非 0)独立类型 boolean,只有 true/false
char 大小1 字节(ASCII)2 字节(UTF-16 编码单元)
变量声明位置C89 必须在块首任意位置,还有块级作用域
常量const / #definefinal 关键字
隐式转换陷阱if (x = 5) 合法编译错误,boolean 不参与算术

8 种基本类型详表

Java 的基本类型(primitive types)大小由语言规范固定,任何 JVM 上都一样:

类型大小范围默认值对应 C 类型
byte1 字节-128 ~ 1270signed char
short2 字节-32768 ~ 327670short
int4 字节-2147483648 ~ 21474836470int
long8 字节约 ±9.22×10^180Llong long
float4 字节IEEE 754 单精度,约 6~7 位有效数字0.0ffloat
double8 字节IEEE 754 双精度,约 15 位有效数字0.0ddouble
char2 字节’\u0000’ ~ ‘\uffff’(0 ~ 65535)‘\u0000’无直接对应
boolean未规定(通常按 1 字节处理)只有 true / falsefalse无(C 用 int 模拟)

差异一:大小固定,跨平台一致

public class SizeDemo {
    public static void main(String[] args) {
        // 这些值在任何机器上打印结果都相同——C 中 sizeof(long) 则随平台变化
        System.out.println("int 占 " + Integer.BYTES + " 字节");   // 4
        System.out.println("long 占 " + Long.BYTES + " 字节");     // 8
        System.out.println("char 占 " + Character.BYTES + " 字节"); // 2
 
        // 注意:Java 没有 sizeof 运算符!大小是规范规定的常量,
        // 通过包装类的 BYTES/SIZE 字段获取(SIZE 是位数)
        System.out.println("int 位数: " + Integer.SIZE);           // 32
    }
}

C 里你习惯了 sizeof(int) 在编译期求值;Java 干脆删掉了这个运算符——因为答案永远是同一个。

差异二:没有 unsigned

public class NoUnsigned {
    public static void main(String[] args) {
        int x = -1;                 // 内存中就是 32 个 1
        System.out.println(x);      // -1
 
        // C 中 (unsigned int)x 会得到 4294967295
        // Java 要达到同样效果需要用位运算技巧:
        long asUnsigned = x & 0xFFFFFFFFL;
        System.out.println(asUnsigned);          // 4294967295
 
        // 或者用 Integer.toUnsignedString(Java 8+)
        System.out.println(Integer.toUnsignedString(x)); // 4294967295
 
        // 无符号除法/比较也有对应方法
        System.out.println(Integer.divideUnsigned(-1, 2));       // 2147483647
        System.out.println(Integer.compareUnsigned(-1, 1));      // 1(无符号下 -1 更大)
    }
}

为什么 Java 拒绝 unsigned?设计者认为无符号运算带来的混合表达式陷阱(C 中经典的 unsigned 隐式转换 bug)远大于收益。绝大多数场景用 long 扩大范围即可解决。

差异三:boolean 不对应 0/1

public class BooleanDemo {
    public static void main(String[] args) {
        boolean flag = true;
 
        // 下面三行全部无法通过编译——这是 Java 刻意的安全设计:
        // flag = 1;              // 错误: int 不能赋给 boolean
        // if (flag = true) {}    // 这行其实能编译!但语义是"赋值为true恒真"
        // if (1) {}              // 错误: int 不是 boolean
 
        if (flag) {               // 条件必须是真正的 boolean 表达式
            System.out.println("flag 为真");
        }
 
        // boolean 参与 && || ! 运算,但不参与任何算术运算
        // flag + 1;  // 编译错误
    }
}

C 中 if (x = 5) 这种手滑赋值能静默编译通过;Java 里除了对 boolean 变量的赋值,其他都会被编译器拦下。习惯建议:即使写 flag == true 也没问题,但更地道的写法是直接 if (flag)

字面量写法

public class LiteralDemo {
    public static void main(String[] args) {
        // ---- 整数四种进制 ----
        int dec = 100;         // 十进制
        int hex = 0xFF;        // 十六进制 = 255
        int oct = 010;         // 八进制 = 8(前导 0,容易看错,慎用)
        int bin = 0b1010;      // 二进制 = 10(Java 7+,C 没有)
 
        // 下划线分隔符(Java 7+),编译时忽略,纯为可读性
        int creditCard = 1234_5678_9012_3456;
        long bytes = 0b1101_0010_1010_0001;
 
        // ---- long 字面量必须带 L 后缀 ----
        long big = 9999999999L;    // 没有 L 会编译错误:默认按 int 解析,溢出
        long ok = 100;             // 小数值可以省略(int 自动提升)
 
        // ---- 浮点默认 double,float 必须 f 后缀 ----
        double d = 3.14;
        float f = 3.14f;           // 不加 f 编译错误:double 不能隐式转 float
        double sci = 1.2e3;        // 科学计数法 = 1200.0
 
        // ---- char 字面量 ----
        char letter = 'A';         // 单引号单字符
        char cn = '中';            // 一个汉字也是一个 char(占 2 字节)
        char newline = '\n';       // 转义序列与 C 相同: \t \n \\ \' \" \r \b \f
        char unicode = '\u4e2d';   // Unicode 转义,等价于 '中'
 
        // ---- boolean 与 null 字面量 ----
        boolean t = true;
        boolean fa = false;
        String s = null;           // 引用类型的空值,不能赋给基本类型
 
        System.out.println(dec + " " + hex + " " + oct + " " + bin);
        System.out.println(big + " " + f + " " + sci);
        System.out.println(letter + " " + cn + " " + (unicode == cn));
    }
}

var 局部变量类型推断(Java 10+)

import java.util.ArrayList;
 
public class VarDemo {
    public static void main(String[] args) {
        // var 不是"动态类型",也不是"万能类型"——它只是让编译器从右侧推断类型
        var count = 10;                    // 推断为 int
        var price = 19.99;                 // 推断为 double
        var name = "RootStack";            // 推断为 String
        var list = new ArrayList<String>(); // 推断为 ArrayList<String>
 
        // count = "abc";   // 编译错误!count 已经是 int,不是 Object
 
        // var 只能用于有初始值的局部变量:
        // var x;                  // 错误:无法推断
        // var y = null;           // 错误:null 类型不明
        // 成员变量/方法参数/返回值不能用 var
 
        System.out.println(count + " " + price + " " + name + " " + list.size());
    }
}
对比项C(C23 前)Java var
自动类型推断无(GCC 扩展 __auto_typevar,仅限局部变量
类型是否固定固定,编译期确定,只是不用手写
类似机制C++ 的 auto语义与 C++ auto 基本一致

使用建议:右侧类型显而易见时用 var,复杂表达式或需要明确阅读类型时写出完整类型。

final 常量 vs C 的 const 与 define

public class FinalDemo {
    // 编译期常量惯例:全大写 + 下划线,static final 组合
    static final double TAX_RATE = 0.13;
    static final int MAX_USERS = 1024;
 
    public static void main(String[] args) {
        final int SIZE = 10;       // 局部常量
        // SIZE = 20;             // 编译错误:final 变量只能赋值一次
 
        // final 也可以先声明后赋值(空白 final):
        final int result;
        result = compute();
        // result = 99;           // 已赋过值,再赋值报错
 
        System.out.println(SIZE * TAX_RATE);
    }
 
    static int compute() { return 42; }
}

三种机制的对比:

特性C #defineC constJava final
处理阶段预处理文本替换编译期编译期
有类型吗无(纯文本)
能否调试查看不能可以可以
作用域从定义处到文件尾遵循作用域遵循作用域
数组大小可用C 中不可(VLA 除外)可用

Java 没有 #define,宏替换的常见用途(常量、简单函数)分别由 static final 和方法承担。

类型转换

自动(隐式)转换:小范围 → 大范围

public class WideningDemo {
    public static void main(String[] args) {
        int i = 100;
        long l = i;        // int -> long,自动
        double d = l;      // long -> double,自动
        char c = 'A';
        int code = c;      // char -> int,得到 65(C 中同样如此)
 
        // 转换方向(实线无精度损失,虚线可能有):
        // byte -> short -> int -> long -> float -> double
        //                ^
        //                char ---/
        System.out.println(code);   // 65
        System.out.println(d);      // 100.0
 
        // 注意两个反直觉点:
        long big = 9007199254740993L;
        float f = big;              // long(64位) -> float(32位) 是自动的!
        System.out.println((long) f == big);  // false,float 有效数字不够
        // 结论:long->float/double 可能丢精度,但语法上仍是"自动转换"
    }
}

强制转换:大范围 → 小范围,溢出行为与 C 相同

public class NarrowingDemo {
    public static void main(String[] args) {
        double d = 3.99;
        int i = (int) d;            // 直接截断小数,不是四舍五入 -> 3
        System.out.println(i);
 
        // 整数溢出:高位截断,和 C 的行为一模一样
        int max = Integer.MAX_VALUE;        // 2147483647
        int overflow = max + 1;             // 回绕到最小值
        System.out.println(overflow);       // -2147483648
 
        // byte 截断示例
        int big = 300;                      // 二进制 1_0010_1100
        byte b = (byte) big;                // 取低 8 位 0010_1100 = 44
        System.out.println(b);
 
        // char 与 short 之间必须显式转换(同为 2 字节但范围不同)
        char c = 'A';
        // short s = c;   // 编译错误,需强制
        short s = (short) c;
 
        // 表达式中的提升:byte/short/char 参与算术一律先提升为 int
        byte x = 10, y = 20;
        // byte z = x + y;   // 编译错误!x+y 的结果是 int
        byte z = (byte) (x + y);            // 必须显式转回
        System.out.println(z);
 
        // 除零行为:整数除以 0 抛异常(C 是未定义行为),浮点除以 0 得 Infinity/NaN
        System.out.println(1.0 / 0.0);      // Infinity
        System.out.println(0.0 / 0.0);      // NaN
        // System.out.println(1 / 0);       // ArithmeticException
    }
}
场景C 行为Java 行为
int 溢出未定义行为(实际多为回绕)明确定义为回绕
整数除零未定义行为抛出 ArithmeticException
浮点除零未定义行为返回 Infinity / NaN
(int)3.99截断为 3截断为 3

包装类与自动装箱拆箱

8 种基本类型各有一个对应的引用类型(包装类),泛型集合只能存它们:

基本类型包装类基本类型包装类
byteBytefloatFloat
shortShortdoubleDouble
intIntegercharCharacter
longLongbooleanBoolean
import java.util.ArrayList;
 
public class WrapperDemo {
    public static void main(String[] args) {
        // 手动装箱(老写法,了解即可)
        Integer a = Integer.valueOf(100);
        // 自动装箱(编译器自动插入 valueOf)
        Integer b = 100;
        // 自动拆箱(编译器自动插入 intValue())
        int c = b;
 
        // 泛型集合必须用包装类——ArrayList<int> 是非法的
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);          // 装箱:add(Integer.valueOf(1))
        int first = list.get(0); // 拆箱
 
        // 包装类可以为 null(基本类型不行)
        Integer maybe = null; // 常见于数据库字段可能为空的场景
 
        // 危险:拆箱 null 会抛 NullPointerException
        try {
            Integer n = null;
            int x = n;        // 这里爆炸
        } catch (NullPointerException e) {
            System.out.println("捕获到拆箱 null: " + e);
        }
 
        // 常用静态方法
        int parsed = Integer.parseInt("12345");       // 字符串转 int
        String str = Integer.toHexString(255);        // 转 16 进制字符串 "ff"
        int maxOfTwo = Integer.max(3, 7);
        System.out.println(parsed + " " + str + " " + maxOfTwo);
    }
}

缓存池陷阱:-128 到 127

public class CacheTrap {
    public static void main(String[] args) {
        Integer a = 127, b = 127;
        Integer c = 128, d = 128;
 
        System.out.println(a == b);   // true —— 命中了缓存池里的同一对象
        System.out.println(c == d);   // false —— 超出缓存,valueOf 新建了对象!
 
        // 原理:Integer.valueOf 对 [-128, 127] 范围内的值返回缓存的同一对象,
        // 范围外每次 new。== 比较的是引用地址,不是值。
        //
        // 缓存范围速查:
        // Byte/Short/Long/Integer: -128 ~ 127
        // Character: 0 ~ 127
        // Float/Double: 无缓存(浮点值无限多,没法缓存)
        //
        // 正确的比较方式——永远用 equals:
        System.out.println(c.equals(d));  // true
 
        // 混合运算会自动拆箱成 int 再比较,所以这种反而没事:
        int e = 128;
        System.out.println(c == e);   // true(c 拆箱后与 e 比较值)
    }
}

结论一句话:两个包装类对象之间比较值,永远用 equals,不要用 ==

String 初识与不可变性

String 属于引用类型,但它特殊到值得在本章提前认识:

public class StringIntro {
    public static void main(String[] args) {
        // 最常用创建方式:字符串字面量
        String s1 = "hello";
        // new 方式:强制在堆上新建对象(一般不推荐)
        String s2 = new String("hello");
 
        // String 不可变:所有看似修改的方法都是返回新对象
        String s3 = "hello";
        s3.toUpperCase();            // 返回了新对象 "HELLO",但没人接收
        System.out.println(s3);      // 还是 hello
        String s4 = s3.toUpperCase();
        System.out.println(s4);      // HELLO
 
        // 拼接也是生成新对象
        String s5 = s3 + " world";
        System.out.println(s5);      // hello world
 
        // 不可变的好处:线程安全、可以缓存 hashCode、字符串常量池得以实现
        // 代价:频繁拼接性能差——解决方案见 [[java/1入门/11_字符串|字符串]] 一章的 StringBuilder
    }
}

作用域规则

Java 的变量作用域由 {} 决定,比 C89 宽松(不必块首声明),但比 C++ 严格的一点是:不允许内层块遮蔽同名局部变量

public class ScopeDemo {
    static int field = 1;        // 类级字段:整个类可见,有默认值
 
    public static void main(String[] args) {
        int outer = 10;
 
        if (outer > 5) {
            int inner = 20;
            System.out.println(outer + inner);  // 内层可以读外层
        }
        // System.out.println(inner);  // 编译错误:inner 出了作用域
 
        // int outer2 = 1;
        // {
        //     int outer2 = 2;  // 若重名会编译错误:已在方法中定义
        // }
 
        for (int i = 0; i < 3; i++) { }  // i 只属于 for 循环
        // System.out.println(i);        // 编译错误
 
        System.out.println(field);       // 字段未手动初始化也有默认值 0
    }
 
    static void showDefault() {
        // 局部变量没有默认值,使用前必须初始化——这点与 C 不同:
        int local;
        // System.out.println(local);   // 编译错误:可能尚未初始化
        local = 5;
        System.out.println(local);
    }
}
变量种类默认值使用前必须初始化
类的字段(成员变量)有(0/false/null)
局部变量是,编译器强制

C 的局部变量不初始化是「读到垃圾值的未定义行为」;Java 直接编译报错,把这类 bug 扼杀在编译期。

综合示例

/**
 * 类型系统综合演示:模拟一个简单的商品价格计算器
 */
public class TypeShowcase {
 
    static final double DISCOUNT = 0.85;   // 折扣常量
 
    public static void main(String[] args) {
        var productName = "机械键盘";       // var 推断为 String
        var unitPrice = 299.0;              // var 推断为 double
        int quantity = 3;
        final int FREE_SHIP_THRESHOLD = 500;
 
        // 计算总价:int 与 double 运算,quantity 自动提升为 double
        double total = unitPrice * quantity;
        double discounted = total * DISCOUNT;
 
        // 强制转换取整(截断)
        int payAmount = (int) discounted;
 
        // 包装类演示
        Integer boxedQuantity = quantity;           // 自动装箱
        boolean overThreshold = boxedQuantity != null
                && discounted > FREE_SHIP_THRESHOLD; // 拆箱参与比较
 
        System.out.println("商品: " + productName);
        System.out.printf("原价: %.2f 元%n", total);
        System.out.printf("折后: %.2f 元%n", discounted);
        System.out.println("取整应付: " + payAmount + " 元");
        System.out.println("免运费: " + overThreshold);
 
        // 溢出演示:如果数量极大
        int hugeQty = Integer.MAX_VALUE;
        long safeTotal = unitPrice == 0 ? 0 : (long) hugeQty * 2; // 先转 long 再乘
        System.out.println("两倍数量的库存总量(int 会溢出): " + safeTotal);
    }
}

本章要点回顾

  • 8 种基本类型大小固定、跨平台一致;无 unsigned;boolean 独立于整数
  • 字面量支持二进制 0b 与下划线分隔;long 加 L,float 加 f
  • 自动转换沿「小到大」方向;强转截断高位,溢出回绕行为与 C 相同但被明确定义
  • 包装类用于泛型场景;比较值永远用 equals,警惕 -128~127 缓存池
  • 局部变量使用前必须初始化,编译器强制检查


练习

题号题目链接知识点
P1000超级玛丽游戏https://www.luogu.com.cn/problem/P1000顺序结构、输出
P1003铺地毯https://www.luogu.com.cn/problem/P1003数组、循环