上一篇我们走完了“代码怎么跑起来”的全程,并且知道了一个关键事实:javac 会在程序运行之前,把源码从头到尾检查一遍,检查合格才产出 .class 文件。它检查的远不只是拼写。从这一篇开始,我们要逐步认识它重点把关的第一大类东西——数据。
真实程序很少像 HelloWorld 那样只打印一句固定的话。你的年龄、这门课的得分、账户余额,都是会变的数据。程序要处理它们,第一步是把它们存进内存,随取随改。这个存放处叫 变量(variable)。先看一段完整的小程序:
1public class MyInfo {
2 public static void main(String[] args) {
3 int age = 18; // 整数:年龄
4 double height = 1.75; // 小数:身高(米)
5 boolean isStudent = true; // 是或否:是不是学生
6 char initial = '李'; // 单个字符:姓氏的第一个字
7
8 System.out.println(age);
9 System.out.println(height);
10 System.out.println(isStudent);
11 System.out.println(initial);
12 }
13}每一行输出一个变量里装的值。下面把这几行拆开看:变量到底是什么,Java 又为什么要求每声明一个变量,都得先报出它的类型。
变量:内存里一只带标签的盒子
程序运行时的所有数据都存在内存(RAM)里。内存可以想象成一长排带门牌号的小房间,每个房间能装一定量的数据。问题是门牌号是一串给机器看的数字,又长又难记,于是 Java 允许你给某个房间贴一张自己认识的标签——变量名。以后你说“把 19 放进 age”,编译器就知道该写哪间房。
这只“盒子”有两个特点。第一,标签只在源码世界里存在:javac 编译成字节码后,指令里不再出现 age、height 这些名字,而是直接按房间编号读写。这正是上一篇说的“翻译”的一部分——编译器把你的命名翻译成了位置。第二,盒子能换内容,不能换规格:age 一旦声明成 int,就永远是 int,能变的只是里面装的那个数。
声明一个变量,本质上就是告诉编译器:请为我在内存安排一个位置,并允许我用这个名字称呼它。
为什么声明时必须先写出类型
任何变量的声明都遵循同一个句式:先写类型,再写名字,可以顺手给初始值,最后以分号结束:
1int age = 18; // 声明 int 变量 age,初始装入 18
2int score; // 只声明,先不装值读法是固定的:“声明一个 int 类型的变量,名字叫 age,初始值 18。”注意顺序:类型在最前面。为什么 Java 不允许更省事的写法 age = 18;,靠猜也知道它是整数?因为编译器需要一张“规格单”。Java 官方教程里有一句很本质的话:变量的数据类型,决定了它能容纳哪些值,以及能对它执行哪些运算 1。类型就是那张规格单——知道 age 只能装整数,编译器才能判断后面每一行对 age 的操作是否合法。声明 int score; 之后写 score = 90; 合法,写 score = "优秀"; 就会被拦下(原因见最后一节)。规格单先立好,检查才有依据。
四种最常用的基本类型
Java 里的类型分两大类:基本类型(primitive type,语言内置、变量直接装值)和 引用类型(reference type,变量装的是指向某个对象的“门牌号”)。String 就属于引用类型,今天先不碰它,只认识基本类型中最常用的四个 2:
| 类型 | 装什么 | 例子 | 内存占用 |
|---|---|---|---|
| int | 整数:年龄、数量、序号 | int age = 18; | 4 字节(32 位) |
| double | 带小数的数:身高、成绩 | double height = 1.75; | 8 字节(64 位) |
| boolean | 只有 true(真)或 false(假) | boolean isStudent = true; | 规范未精确定义 |
| char | 单个字符 | char initial = '李'; | 2 字节(16 位) |
几处细节值得记牢。int 只有 32 位,能装的整数范围是 -2147483648 到 2147483647,约负 21 亿到正 21 亿,装不下就得换 64 位的 long(以后再讲)。double 是双精度浮点数,有大约 15~16 位有效数字;官方教程特别提醒,金额这类需要精确计算的场合不要用 double 2,因为浮点数在二进制里常常只是近似值。boolean 只接受两个词:true 和 false,用来记录“是/否、开/关”这类开关状态。char 用 单引号 且只能装一个字符,因为它是 16 位 Unicode 编码,所以汉字也装得下;双引号写出的 "李" 是字符串 String,能装一串字符,跟 char 完全是两回事——单引号一个,双引号一串。
顺带认识一个术语:18、1.75、true、'李' 这些直接写进源码的裸值叫 字面量(literal)。像 1.75 这样的小数字面量,Java 默认按 double 处理——这一点下一节马上要用到。
“=”是装进去,不是“等于”
初学者最容易在 = 上栽跟头。看这行代码:
1age = age + 1;按数学的读法,这是句矛盾话:age 怎么可能等于 age 加一?但在程序里它完全合法,正确的读法是:“把 age 盒子里的当前值取出来,加上 1,再把结果装回 age 这个盒子。”执行顺序是 先算右边,再写左边:如果 age 里现在是 18,那么右边先算出 19,然后 19 被装进盒子,age 变成 19。
所以 = 在 Java 里是“赋值”,是“装入”,不是“相等”。它要求左边必须是一个变量(得有个盒子让你放东西),右边可以是任何能算出值的表达式。这也解释了上一节说的“盒子能换内容”:age = age + 1 干的正是换内容这件事。
类型写错,javac 当场拦住你
现在可以回答本篇最重要的问题了:强类型语言到底意味着什么?看三行都过不了编译的代码:
1int age = "十八"; // 编译错误:String 不能转成 int
2int score = 3.5; // 编译错误:double 转 int 可能丢失精度
3int x;
4System.out.println(x); // 编译错误:x 可能尚未初始化第一行:"十八" 是 String(引用类型),往只装整数的 int 盒子里塞字符串,编译器报“类型不兼容”,直接拒绝生成 .class。第二行:3.5 是 double 字面量,硬塞进 int 会丢掉小数点后的 0.5,Java 不允许这种“有损”的转换悄悄发生。第三行最有教育意义:方法里声明的局部变量 不赋值就不许读——Java 不给局部变量默认值 1,防止你读出一个来历不明的数。
注意这三处错误发生在哪一步:javac 阶段,程序根本没运行。这是强类型语言给初学者的第一重礼物——错误在“写的时候”就被抓住,而不是“跑起来之后”才爆出来。你在 IDEA、VS Code 里写代码时看到红波浪线,也是同一套类型规则在背后工作:编辑器一边打字一边用编译器的规则检查,类型对不上立刻标红。如果类型检查被推迟到运行时,程序得先跑起来、一路执行到出错那一行才会报错,而在此之前可能已经打印了一半结果、甚至拿着坏数据继续算,让错误变得更难追查。
把这几节串起来看:声明变量时写类型,不是形式主义——那是你与编译器签订的一份契约。你承诺“这个盒子只装 int”,编译器回报以全程的编译期把关。代价是多打几个字,换来的是一大批错误永远没有机会进入运行阶段。
认识完盒子和四种基本类型,下一步自然是让这些数字参与运算和比较——那是运算符的地盘,留到下一篇。