第2章 变量与类型
元数据卡
- 前置知识:第1章——能写、编译、运行 Java 程序
- 预计时间:60 分钟
- 核心难度:
- 完成标志:能够声明各种类型的变量、赋值、在终端打印变量的值,理解类型转换的基本规则
本章分层
- 必读:Java 基本数据类型(int/double/boolean/char/String);变量声明与初始化;类型转换规则
- 选读:final 常量;var 关键字;表达式的自动类型提升
- 深水区:JVM 内存中不同数据类型的存储布局;浮点数 IEEE 754 精确表示
本章不会要求你掌握
- 所有基本类型的二进制表示细节
- 引用类型与基本类型的底层差异
- 自动装箱/拆箱的编译器行为
你在哪
变量村的清晨,老陈师傅的工坊里飘着一股铁锈和旧木头的味道。他把 HelloWorld 那张纸钉在了墙上。"第一句话你已经会说。"他转身从抽屉里翻出一张发黄的旅行地图——画的是一个村庄,每栋房子外面挂着一个牌子:整数仓库、小数银行、字符驿站、真假哨站。
"变量村不是白叫的。"他把地图摊在桌上,"计算机里面,每个变量都是一栋小房子——有个门牌号(叫名字),有个房间(占内存),里面住着东西(叫值)。今天的任务:认识这里所有类型的房子。"
你的任务
"Hello, World!" 是一个写死的句子。如果把程序比作菜谱,那它只写了"放盐"——但盐放多少、什么时候放、放在哪个碗里,全都没写。
真实程序里,数据是变化的:今天用户叫张三,明天可能叫李四;今天是 18 度,明天可能是 25 度。你不能每次数据变了就改源代码重新编译——那太傻了。
你需要一个办法:在内存里划出一块区域,给这块区域起个名字,往里面放东西,随时能拿出来用或者换成别的——这就是 变量。
遭遇战 → 获得技能
① 第一个问题:存一个数字
我凭直觉写:
x = 5编译——报错。老陈师傅并不意外。
"Java 不让你这么干。它要你提前说清楚:你要存的东西是什么种类、占多大空间。"
在 Java 里,声明一个变量需要两样东西:类型 和 名字。
// VariablesDemo.java
public class VariablesDemo {
public static void main(String[] args) {
int score = 95;
System.out.println(score);
}
}如何运行:
javac VariablesDemo.java→java VariablesDemo预期输出:95
"int 是类型名,意思是整数。score 是变量名,你用这个名字来引用这块内存。"老陈师傅在纸上画了个小方框。
int score = 95; // 声明 + 赋值,一步到位他接着写:
int score; // 只声明:告诉 Java 我要一个叫 score 的整数容器
score = 95; // 再赋值:往容器里放数据"声明和赋值可以分开做。但你不能拿了空容器就乱用——Java 编译器会检查你有没有给变量初始化。"
int score;
System.out.println(score); // ❌ 编译错误:variable score might not have been initialized② 八种基本类型
"整数有四种。"老陈师傅用粉笔敲了敲黑板:
| 类型 | 大小 | 取值范围 | 默认值 |
|---|---|---|---|
byte | 1 字节 | -128 ~ 127 | 0 |
short | 2 字节 | -32,768 ~ 32,767 | 0 |
int | 4 字节 | -2^31 ~ 2^31-1 | 0 |
long | 8 字节 | -2^63 ~ 2^63-1 | 0L |
"绝大多数情况下你只用 int。byte 和 short 用在内存紧张的地方(比如嵌入式设备、网络协议解析)。long 只在数字可能超过 20 亿的时候用——long 的字面量末尾要加大写 L。"
// PrimitiveTypes.java
public class PrimitiveTypes {
public static void main(String[] args) {
byte b = 100;
short s = 30000;
int i = 1_000_000; // Java 7 起可以用下划线分隔数字——纯语法糖
long l = 3_000_000_000L; // 超过 int 范围时必须加 L
System.out.println("byte: " + b);
System.out.println("short: " + s);
System.out.println("int: " + i);
System.out.println("long: " + l);
}
}如何运行:
javac PrimitiveTypes.java→java PrimitiveTypes预期输出:byte: 100 short: 30000 int: 1000000 long: 3000000000
他看了我一眼:"小数有两种。"
| 类型 | 大小 | 精度 | 默认值 |
|---|---|---|---|
float | 4 字节 | ~7 位有效数字 | 0.0f |
double | 8 字节 | ~15 位有效数字 | 0.0d |
"float 字面量末尾必须加 f 或 F;double 加不加 d 都行——Java 默认小数就是 double。"
float pi = 3.1415f; // 不加 f 会编译错误——3.1415 默认是 double
double e = 2.71828; // 不加 d 没问题,double 是默认的"然后是另外两种——跟数字无关。"
char grade = 'A'; // 单个字符,用单引号包住
boolean isReady = true; // 真或假,只有两个值🐍 Python 差异
Python 不需要你声明类型——你直接写
score = 95就好,Python 自动推断类型并且允许随时改变。这看起来方便,但缺点是你可能不小心把score赋值为字符串"hello",后面用到score + 5就抛运行时异常了。Java 是静态类型——变量类型一旦声明,终生不变。Python 也没有
byte、short、long的区别——它只有一个int,大小只受内存限制。float就是双精度(约等于 Java 的double)。char也不存在——Python 里字符就是长度为 1 的字符串。
** C++ 差异**
C++ 的基本类型看起来和 Java 很像,但有一个关键区别:C++ 的
int大小不固定——16 位系统上是 2 字节,32/64 位上是 4 字节。Java 的int在任何平台上都是固定的 4 字节,跨平台一致性是 Java 的设计目标之一。C++ 还有
unsigned修饰符——unsigned int只能存非负数,但上限翻倍。Java 没有无符号类型(直到 Java 8 才在一些 API 上做了补救)。
③ var——偷懒的正确方式
"Java 10 之后,你有一种偷懒的办法。"老陈师傅又写:
var name = "Alice"; // 编译器自己推断:name 是 String 类型
var count = 42; // 编译器自己推断:count 是 int 类型
var pi = 3.14; // 编译器自己推断:pi 是 double 类型"这叫类型推断。你写 var,编译器看等号右边的值,自己判断类型。但有个重要的限制:var 必须在声明时就赋值,而且赋值之后类型就锁死了——你不能声明 var x; 然后再赋值。"
var x; // ❌ 编译错误:cannot infer type for local variable x
x = 10;"还有,var 只能用在方法内部的局部变量——不能用它做类成员变量、方法参数、或者方法返回值。"
var n = 100;
n = "hello"; // ❌ n 已经被推断为 int,不能再赋字符串"你看,Java 帮你做了声明的工作,但没妥协类型安全。这是 Python 和 Java 的一个中间地带。"
④ 类型转换——从小盒子换到大盒子
"有时候你需要把一种类型的值放到另一种类型的变量里。"老陈师傅把几个大小不一的木盒子排开。
自动转换(隐式):小→大
int i = 100;
long l = i; // ✅ int 自动变成 long——小盒子装进大盒子,安全
double d = i; // ✅ int 自动变成 double——整数变小数,不丢精度规则很简单:byte → short → int → long → float → double——箭头方向上的转换自动发生,因为目标类型的范围更大。
强制转换(显式):大→小
"但反过来呢?"
double pi = 3.14159;
int i = pi; // ❌ 编译错误:incompatible types"能编过才怪。你把一个可能装了 3.14 的大箱子,强行塞进只能装整数的小箱子——数字丢了小数部分。Java 不会替你做这个决定。你得明确说:我知道我会丢精度,我同意。"
double pi = 3.14159;
int i = (int) pi; // ✅ (int) 是强制类型转换
System.out.println(i); // 输出:3(小数部分被截断,不是四舍五入!)"括号里写目标类型,这叫强制类型转换(cast)。"
// TypeConversion.java
public class TypeConversion {
public static void main(String[] args) {
// 隐式转换——自动
int apples = 10;
long total = apples; // int → long
double price = apples; // int → double
// 显式转换——手动
double weight = 85.7;
int kg = (int) weight; // double → int,丢掉 0.7
// 还有可能丢数据的
int big = 1000;
byte small = (byte) big; // 数据溢出!1000 远远超过 byte 的范围
System.out.println("long: " + total);
System.out.println("double: " + price);
System.out.println("int (from double): " + kg);
System.out.println("byte (from int): " + small);
}
}如何运行:
javac TypeConversion.java→java TypeConversion预期输出:long: 10 double: 10.0 int (from double): 85 byte (from int): -24
"看到了吧?(byte) big 输出 -24——这不是 Java 的 bug,而是数据溢出时发生溢出回绕。1000 的二进制超过了 byte 能表示的范围(-128~127),剩余部分从最左边被"截掉"了,结果变成了 -24。"
"所以——强制类型转换是你在跟编译器说:别管我,我知道我在干什么。 大多数时候你知道,但你也可能犯错。"
🐍 Python 差异
Python 没有"类型转换"的说法——你在 Python 里用
int(3.14)会得到3,这不叫 cast,叫构造函数调用。Java 的类型转换发生在编译期(编译器知道类型的情况下),而 Python 的int(3.14)发生在运行时。Python 不存在数据溢出——它的整数可以任意大。
1000变成byte?Python 没有byte类型。
** C++ 差异**
C++ 的强制转换和 Java 类似:
int i = (int)3.14;。但 C++ 有四种不同的 cast 操作符(static_cast、dynamic_cast、const_cast、reinterpret_cast)——Java 只有一种(type)括号语法。C++ 也会发生溢出,并且行为由 CPU 决定(在 C++ 里对有符号整数的溢出是未定义行为——编译器可能做任何事),但 Java 严格规定了溢出回绕的行为(像二进制补码那样)。
常见陷阱
事故一:浮点数运算不是你想的那样
double a = 0.1;
double b = 0.2;
System.out.println(a + b); // 你以为是 0.3输出:
0.30000000000000004"不是 0.3?"
"不是 0.3。原因和 Java 无关——这是二进制浮点数的固有限制。0.1 在二进制里是一个无限循环小数,跟 1/3 在十进制里是 0.3333... 一样。你用 double 只能存一个近似值,多个近似值加起来误差就冒出来了。"
事故二:char 不是字符串
char ch = "A"; // ❌ 编译错误——char 用单引号,双引号是字符串char ch = 'AB'; // ❌ 编译错误——char 只能装一个字符事故三:整数除法
int a = 5;
int b = 2;
System.out.println(a / b); // 输出:2,不是 2.5"两个整数相除,Java 做整数除法——直接舍去小数部分。如果你要小数结果,至少一个操作数要用小数类型。"
System.out.println(5 / 2.0); // 输出:2.5
System.out.println(5.0 / 2); // 输出:2.5
System.out.println((double) 5 / 2); // 输出:2.5通关挑战
🗡 热身(5 分钟必做)
声明以下变量并打印出来:
- 你的年龄(
int) - 你的身高(
double) - 你的姓的首字母(
char) - 你是否喜欢编程(
boolean)
- 你的年龄(
声明一个
long变量赋值为10000000000(100 亿),注意加L后缀声明
byte b = 200;——观察编译错误声明
var city = "Beijing";,然后尝试city = 42;——看编译器说什么
** 挑战(30 分钟选做)**
- 写一段代码,演示
int类型的最大值Integer.MAX_VALUE加 1 会发生什么(这叫"整数溢出") - 写一个温度转换器:输入华氏度(
double),转换为摄氏度并打印。公式:C = (F - 32) * 5 / 9——注意整数除法陷阱 - 用
var声明一个float变量,然后尝试和double变量做加法——看编译器如何推断结果的类型
** 排障**
| 问题 | 最常见原因 |
|---|---|
variable X might not have been initialized | 声明了但没赋值就使用 |
incompatible types: possible lossy conversion | 大范围类型隐式转小范围——必须加 cast |
integer number too large | int 字面量超过了 21 亿,需要加 L 变成 long |
cannot find symbol | 变量名拼写错了,或者还没声明就用 |
: expected | 忘了写类型(Java 不是 Python——类型必须声明) |
验收标准
- [ ] 能解释什么是变量(类比贴标签的盒子)
- [ ] 能写出所有 8 种基本类型的声明和初始化
- [ ] 知道
var的用法和限制 - [ ] 能区分隐式转换和强制转换,知道什么时候该用哪种
- [ ] 能演示:整数除法丢小数、浮点运算有精度误差
- [ ] 知道
char用单引号、String用双引号
常见卡点
1. long 字面量忘加 L
long big = 10000000000; // ❌ 编译错误:integer number too large因为 10000000000 被编译器当作 int,但它的值超出了 int 范围。改正:
long big = 10000000000L; // ✅ 加 L 告诉编译器这是 long 字面量2. float 字面量忘加 f
float f = 3.14; // ❌ 编译错误:incompatible types3.14 默认是 double,不能隐式放进 float。改正:
float f = 3.14f; // ✅3. 把赋值 = 和比较 == 搞混
这个会在后面控制流章节反复出现——赋值 = 是把右边的值放进左边的变量,比较 == 是问"它们相等吗?" 两个是完全不同的操作。
4. 中文标点符号
全角分号 ;、全角引号 "、全角括号 ()——这些在中文文本里看起来正常,但在 Java 代码里都是语法错误。养成习惯:写代码时输入法切到英文模式。
现在不需要理解
- 什么是"栈"和"堆"——第5章讲方法时会揭晓
- 基本类型和引用类型的根本区别——第7章面向对象时会提
String为什么是大写开头(它是一个类,不是基本类型)——第9章字符串专题- 浮点数的 IEEE 754 二进制表示——感兴趣可以查,但入门不需要
final关键字——第2章+常量篇会覆盖
旅人笔记
变量就像一个带标签的盒子。
你告诉 Java:"我要一个叫 score 的盒子,它只能装 int 类型的东西。" Java 就在内存里给你分配一个 4 字节的"盒子"。然后你把 95 放进去。以后每次用 score 这个名字,Java 就帮你打开那个盒子取出值。
这八个基本类型 (byte, short, int, long, float, double, char, boolean) 是 Java 的"原子"——它们不是由其他类型组合而成的。所有复杂的东西(字符串、对象、数组)最终都是由这些原子拼出来的。
命名规范是个好习惯:变量名用驼峰风格——第一个单词全小写,后面的单词首字母大写:myScore、playerName、isGameOver。
老陈师傅在我写完代码之后,拿了一张纸条贴在墙上:
变量让你能记住东西。
类型让你不会记错。
名字让你能找到它。
→ 下一站预告
有了变量之后,你不再只能打印固定文字——你可以打印不同的数字和字符,可以把一个变量的计算结果存到另一个变量里。
下一步,你会学到表达式和操作符:如何把变量组合起来做计算、比对、判断。当你需要把"温度大于 30 度吗"这样的问题翻译成计算机能理解的语言时,你手上就有了工具。
第3章:表达式与操作符。