06 变量与数据类型
前置知识:第一个程序与 jshell
本章目标:掌握 Java 的 8 种基本类型、字面量写法、类型转换规则、包装类与自动装箱、String 初识与作用域规则。
概述
如果你写过 C,这一章会有强烈的「既熟悉又陌生」感:熟悉的 int、double、char 都在;陌生的是——Java 的类型大小是固定的、没有 unsigned、boolean 不再是整数。这些差异不是语法糖,而是「一次编写,到处运行」的地基。
与 C 的核心对比总览
| 维度 | C | Java |
|---|---|---|
| 类型大小 | 由平台决定(long 在 32/64 位不同) | 固定不变,跨平台一致 |
| 无符号类型 | unsigned int 等 | 不存在,全部有符号 |
| boolean | 本质是 int(0/非 0) | 独立类型 boolean,只有 true/false |
| char 大小 | 1 字节(ASCII) | 2 字节(UTF-16 编码单元) |
| 变量声明位置 | C89 必须在块首 | 任意位置,还有块级作用域 |
| 常量 | const / #define 宏 | final 关键字 |
| 隐式转换陷阱 | if (x = 5) 合法 | 编译错误,boolean 不参与算术 |
8 种基本类型详表
Java 的基本类型(primitive types)大小由语言规范固定,任何 JVM 上都一样:
| 类型 | 大小 | 范围 | 默认值 | 对应 C 类型 |
|---|---|---|---|---|
byte | 1 字节 | -128 ~ 127 | 0 | signed char |
short | 2 字节 | -32768 ~ 32767 | 0 | short |
int | 4 字节 | -2147483648 ~ 2147483647 | 0 | int |
long | 8 字节 | 约 ±9.22×10^18 | 0L | long long |
float | 4 字节 | IEEE 754 单精度,约 6~7 位有效数字 | 0.0f | float |
double | 8 字节 | IEEE 754 双精度,约 15 位有效数字 | 0.0d | double |
char | 2 字节 | ’\u0000’ ~ ‘\uffff’(0 ~ 65535) | ‘\u0000’ | 无直接对应 |
boolean | 未规定(通常按 1 字节处理) | 只有 true / false | false | 无(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_type) | var,仅限局部变量 |
| 类型是否固定 | — | 固定,编译期确定,只是不用手写 |
| 类似机制 | 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 #define | C const | Java 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 种基本类型各有一个对应的引用类型(包装类),泛型集合只能存它们:
| 基本类型 | 包装类 | 基本类型 | 包装类 |
|---|---|---|---|
| byte | Byte | float | Float |
| short | Short | double | Double |
| int | Integer | char | Character |
| long | Long | boolean | Boolean |
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 缓存池 - 局部变量使用前必须初始化,编译器强制检查
- 下一章:07 运算符与表达式
- 相关:11 字符串(StringBuilder 解决拼接性能)
练习
| 题号 | 题目 | 链接 | 知识点 |
|---|---|---|---|
| P1000 | 超级玛丽游戏 | https://www.luogu.com.cn/problem/P1000 | 顺序结构、输出 |
| P1003 | 铺地毯 | https://www.luogu.com.cn/problem/P1003 | 数组、循环 |