变量
上节课我们就说过:在 Java 中,我们操作的所有 数据 都可以看作是 对象(除了基本数据类型)。
每个整数、小数、字符串,还有我们后面要学的 Map、List 等, 都是对象(或数据)。
在 Java 程序运行的时候,这些对象其实就是 内存中的一段数据 。
我们的程序是经常要操作这些内存中的数据对象的,比如打印整数的值到屏幕上,或者把字符串内容写入文件。
那么我们的程序语言如何告诉计算机,我们 要操作(使用)的是哪个数据对象 呢?
比如下面的代码 会产生一个 字符串对象 在内存中:
"你好,今天天气真不错"
如果后面的程序代码要把这个字符串对象打印到屏幕上,怎么引用它呢?
问题在于,我们怎么在代码中 使用 一个内存中的数据对象?
变量/定义变量
大家来思考一个问题:
人类语言里面,当我们提到一个人或物体, 是怎么说的?
对了,用他们的 名字。
比如: 王晓刚同学, 湖人队等等。
如果不允许使用事物的名字, 我们的话就没法说了。
人类语言是这样,计算机语言也是一样。编程语言为了方便操作数据对象,也需要给对象起一个名字。
我们把 Java 语言中给数据起的名字, 称之为 变量。
所以, Java 中的变量,可以理解为 可以通过它 访问某个内存数据对象 的名字。
我们可以这样给数据起名字并存入变量中:
int age = 43;
String weather = "你好,今天天气真不错";
这行代码 int age = 43; 做了两件事:
-
类型声明 (Declaration):
int age;告诉编译器,我们要创建一个名为age的变量,它只能存放int(整数) 类型的数据。 -
赋值 (Assignment):
= 43;把43这个值存入到age变量中。
= 在这里是 赋值操作符,不是数学上的等于。
定义了变量之后,我们写代码,需要用到这些数据时,就可以用它们的名字: 变量名。
比如
int age = 43;
String weather = "你好,今天天气真不错";
System.out.println(age);
System.out.println(weather);
代码执行的时候,程序看到变量名就知道代表的就是对应的数据。
大家运行一下,可以发现,可以打印出变量所代表的数据。
可以一次声明多个同类型的变量
int age = 43, height = 170;
如果在定义变量的时候并不知道初始值是什么,可以先不赋值。在后续的代码中再赋值。
比如
int age;
String weather;
// 其它代码
age = 10;
weather = "ok";
// 使用变量前,变量一定要赋值过
System.out.println("Hello, 白月黑羽!" + weather + age);
变量 命名规则 和 风格
✳️ 命名规则 (必须遵守)
变量名不是随便取的,有一定的规则和约定。
-
变量名可以包含 字母、数字、下划线
_和美元符号$。 -
变量名可以以字母、下划线或美元符号打头,比如
var1,_var,$var。 -
但不能以数字打头, 像
1var这样是不行的。 -
变量名中不能包含空格。
-
变量名 不能 和 关键字 同名。
比如
class,public,int都是 Java 的关键字,是 Java 语言中有特殊意义的名字, 不能用作变量名。 -
变量名 大小写敏感。
startTime和starttime是两个不同的变量。
一个好的工程师,变量名不是乱取的,我们通常称之为 见名知意 原则,就是看到变量名,就知道变量对应的数据的含义。
比如 startTime, yourName, 这样的变量名,看了就知道是啥意思。
而像 a , b , c 这样的变量名就不好,因为看了不知道是啥意思。
✳️ 命名风格 (建议遵守)
- 局部变量、参数、方法、字段 通常使用 小驼峰命名法 (camelCase),例如:
myVariable,age,userName,calculateSum。 - 类、接口 等通常使用 大驼峰命名法 (PascalCase),例如:
MyClass,UserRepository。 - 常量 通常使用 全大写下划线分隔 (UPPER_SNAKE_CASE),例如:
MAX_RETRY_COUNT。
虽然不遵守约定也能运行,但遵守约定能让代码更专业、更易读。
下面这张表是全面的风格说明,遵守它,会让你写的代码就“看起来像专业 Java 程序员写的”。
| 类型 | 推荐命名风格 | 正确示例 | 错误/不推荐示例 |
|---|---|---|---|
| 类(class) | PascalCase(大驼峰) | CustomerService HttpClient | customerService http_client |
| 接口(interface) | PascalCase | UserRepository Disposable | userRepository IUserRepository |
| 方法(method) | camelCase(小驼峰) | calculateTotal() getUserById() | CalculateTotal() get_user_by_id |
| 局部变量(local var) | camelCase(小驼峰) | userName orderCount isActive | UserName order_count |
| 参数(parameter) | camelCase | void login(String userName) | login(String UserName) |
| 成员变量(field) | camelCase | userName | UserName m_userName |
| 常量(static final) | UPPER_SNAKE_CASE | MAX_RETRY_COUNT DEFAULT_TIMEOUT | maxRetryCount MaxRetryCount |
| 枚举类型(enum) | PascalCase | FileMode DayOfWeek | |
| 枚举成员 | UPPER_SNAKE_CASE | APPEND SUNDAY | Append Sunday |
| 包名(package) | 全小写(点分隔) | com.myapp.services java.io | com.MyApp.Services |
| 泛型类型参数(T) | 单个大写字母 | T, K, V, E | t, Key |
❌ 错误风格(在面试/代码审查里会被直接打回去的)
int user_age; // 下划线分词(Java 习惯用驼峰)
String UserName; // 局部变量用了大驼峰
void GetData() // 方法大写开头(这是 C# 风格)
class userService // 类名小写开头
interface IUserService // 接口加了 I(Java 不推荐)
static final int maxValue = 100; // 常量没用大写
✅ 一句话背诵版(贴在显示器旁边)
类、接口 → PascalCase
方法、变量、参数 → camelCase
常量、枚举值 → UPPER_SNAKE_CASE
包名 → 全小写
照着这张表写,你的 Java 代码瞬间就“专业范儿”拉满!
常用数据类型
Java 是一门 强类型 语言,这意味着每个变量和对象都必须有一个明确的类型。
不同类型的数据对象, 就像现实世界中不同类型的事物一样。 他们有不同的特征和用途。
基本数据类型 (Primitive Types)
Java 语言中,常用的基本数据类型有:
| 数据类型 | 大小 | 描述 |
|---|---|---|
| int | 4 字节 | 存储从 -2,147,483,648 到 2,147,483,647 的整数 |
| long | 8 字节 | 存储从 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 的整数 |
| float | 4 字节 | 存储小数。足以存储 6 到 7 位小数 |
| double | 8 字节 | 存储小数。足以存储 15 位小数 |
| char | 2 字节 | 存储单个字符/字母,用单引号括起来 |
| boolean | 1 位* | 存储 true 或 false 值 (*具体大小取决于 JVM 实现) |
注意:String 不是基本数据类型,它是引用类型(对象)。
基本类型的定义格式如下
int myIntNum = 5;
long myLongNum = 15000000000L;
float myFloatNum = 5.99F;
double myDoubleNum = 5.99;
char myLetter = 'D';
boolean myBool = true;
// 也可以用表达式来初始化变量
int sum2 = 5 + 10; // 15
int sum3 = myIntNum + 10; // 15
集合类型
最常用的是:
🔹数组: int[] (比如 {1, 2, 3})
🔹列表: List<Integer> (注意:泛型不能用基本类型 int,要用包装类 Integer)
🔹Map: Map<String, Integer>
后面会有详细的学习。
自定义类型
除了上面这些内置的 数据类型,
Java 语言还可以 自己定义数据类型(通过 class, record, interface 等),
后面会学到。
System.out.println 输出
前面我们一直用 System.out.println() 代码,打印出 数据对象 到终端窗口 上。
println 是 System.out 对象 提供的一个 方法。
关于 类 和 方法 的概念, 在后面会有详细的讲解。
这里,我们只要知道 这样写就可以 将括号里面的对象的值,打印显式在控制台窗口即可。
调用时,会将 括号里面的参数对象,转化为 对应的 字符串表示形式
比如:
✳️ 数字 类型
boolean isDone = true;
char initial = 'A';
double price = 9.99;
int count = 5;
System.out.println(isDone); // Output: true
System.out.println(initial); // Output: A
System.out.println(price); // Output: 9.99
System.out.println(count); // Output: 5
✳️ String 类型
String greeting = "Hello, World!";
System.out.println("This is a string literal.");
System.out.println(greeting);
var 隐式定义 (Java 10+)
前面我们的代码中定义变量,用的都是 显式定义类型 , 就是明确地告诉编译器这个变量是什么类型。
String message = "Hello, World!";
int number = 10;
从 Java 10 开始,我们还可以 使用 var 隐式定义类型 是让编译器根据变量的初始化值自动推断出它应该是什么类型。
var 本身不是一种类型,它指示编译器在编译时确定变量的真实类型。
var message = "Hello, World!"; // 编译器推断为 String
var number = 10; // 编译器推断为 int
var names = new ArrayList<String>(); // 编译器推断为 ArrayList<String>
var 只能在有初始化值时使用:
var name; // ❌ 编译错误:无法推断类型
name = "张三";
隐式定义类型 和 显式定义类型, 两者生成的可执行代码是完全相同的。
它们的区别在于编码时的便利性、可读性以及编译器的角色。
var 隐式定义类型 能让代码更简洁,但也不能一概都使用它。
它的使用目标应该是提升代码可读性,而不是降低它。
核心原则是:如果一个普通的开发者不能通过 var定义代码,就立即知道变量的类型,那么就应该使用显式类型。
✳️ 推荐使用 var 隐式类型 的情况:
-
类型非常明显时:当赋值操作的右侧已经清楚地表明了类型,使用
var可以避免重复,让代码更简洁。// 推荐 var customer = new Customer(); var numbers = new ArrayList<Integer>(); var name = "John Doe"; -
Stream API 操作:Stream 操作的结果往往类型很长,使用
var非常方便。// 推荐 var adultUsers = users.stream() .filter(u -> u.age > 18) .collect(Collectors.toList());
✳️ 推荐使用显式类型的情况:
-
类型不明显时:如果变量的类型不能从赋值的右侧一眼看出,明确写出类型有助于代码的理解和维护。
// 推荐使用具体类型,因为 getBalance() 的返回类型不明确 BigDecimal accountBalance = bankService.getBalance(accountId); // 不推荐,可读性差 var accountBalance = bankService.getBalance(accountId); -
编程到接口:当你希望变量的类型是接口而不是具体的实现类时,需要使用显式类型。
// 变量类型是 List 接口 List<String> userList = new ArrayList<>(); // 如果使用 var,变量类型会被推断为 ArrayList<String> 类 var userList = new ArrayList<String>();
变量值的变化
对象的名字为什么叫变量呢?
因为它会变 :)
Java 语言中,变量的值可以产生变化。
比如
int age = 43;
age = 50; // age 变量现在是 50 了
这里特别要注意的是,Java 中的数据类型有 基本数据类型 (Primitive Types) 和 引用类型 (Reference Types) 的区别,这会影响变量“变化”的方式。
基本数据类型 (Primitive Types)
基本数据类型 包括 int, double, boolean, char 等。
变量直接包含数据本身。
基本类型的变量 被重新赋值,就是把内存中原来的数据 直接修改掉。
比如
int age = 43; // age 变量存着 43 这个值
age = 50; // age 变量现在存着 50 这个值
是直接把内存里那 4 个字节的内容从 43 改成了 50!
// 第一行代码执行后:
内存里:
┌─────────────┐
│ age │ → 00000000 00000000 00000000 00101011 (二进制就是 43)
└─────────────┘
// 第二行 age = 50; 执行后:
内存里:
┌─────────────┐
│ age │ → 00000000 00000000 00000000 00110010 (二进制就是 50)
└─────────────┘
原来的 43 已经被直接覆盖掉了!
基本类型的数据赋值给另外的变量时,是 复制数据本身 。
比如:
int a = 100;
int b = a; // 复制!基本类型赋值是复制
b = 200;
System.out.println(a); // 仍然是 100!b 改了不影响 a
引用类型 (Reference Types)
引用类型 包括 String, 数组, List, Map,自定义的 class 等。
这些类型的变量,包含的是对数据所在内存地址的 引用(像一个指针)。
引用类型的变量 被重新赋值,就是把变量引用地址 改掉,指向新的对象。
String name = "白月黑羽"; // 堆上创建第一个字符串对象,name 保存它的地址
name = "小白"; // 堆上创建第二个字符串对象,name 现在指向新地址
原来的对象还在堆上,只是没人引用它了(之后会被 GC 回收)
内存图示:
// name = "白月黑羽"; 之后
┌────────────────────┐
│ 字符串对象 "白月黑羽" │
└────────────────────┘
↑
name 变量现在指向这里
// name = "小白"; 之后
┌────────────────────┐
│ 字符串对象 "白月黑羽" │←───→ (无人引用,等待GC回收)
└────────────────────┘
┌────────────────────┐
│ 字符串对象 "小白" │
└────────────────────┘
↑
name 变量现在指向这里
因为 引用类型对象 有时候可能很占内存。如果也 像 基本类型 那样直接拷贝的话,会非常的浪费内存。
引用类型的数据赋值给另外的变量时,是 复制数据的引用地址
比如:
String kid = "小明";
String student = kid;
String prettyboy = kid;
这样,"小明" 这个字符串对象就有3个变量 kid、 student 和 prettyboy 指向它。
但是 字符串数据 在 内存中 只有一份 !
如果后面的代码将其中一个变量进行重新赋值:
kid = "小周";
这只会改变 kid 这个变量的指向,让它指向一个新的字符串对象 "小周"。student 和 prettyboy 的指向不受影响。
所以,如果我们接下来执行下面的代码
System.out.println(kid);
System.out.println(student);
System.out.println(prettyboy);
运行结果就是
小周
小明
小明
重新赋值的其它写法
变量重新赋值的时候,有时候会出现下面这种写法
int var = 1;
var = var + 1; // 把变量原来的值增加 1,结果对象再赋值给变量var
数字变量的自增赋值还有更简洁的写法,如下
int var = 1;
var = var + 1; // var 变成了 2
var += 2; // 等价于 var = var + 2, var 变成了 4
var++; // 等价于 var = var + 1, var 变成了 5
var -= 1; // 等价于 var = var - 1, var 变成了 4
var--; // 等价于 var = var - 1, var 变成了 3
var *= 2; // 等价于 var = var * 2, var 变成了 6
var /= 2; // 等价于 var = var / 2, var 变成了 3
var %= 2; // 等价于 var = var % 2, var 变成了 1
变量指向的对象本身发生了变化
这种情况,只发生在变量是 引用类型 的时候,比如:List、Map,或者自定义的类实例对象。
这里先给大家举个 Map 对象的例子。 这里面 import 语句 和 HashMap 的用法,后面会讲到,这里只是为了说明变量指向的对象本身发生变化的情况。
void main() {
var info = new HashMap<String, String>(
Map.of(
"name", "白羽黑月",
"height", "170cm"
)
);
// 这里没有改变 info 变量的指向,而是改变了它指向的那个 Map 对象内部的数据
info.put("height", "175cm");
System.out.println(info.get("height")); // 输出 175cm
}
高亮语句就是让变量 info 指向的那个 Map 对象的值发生了变动。
不能改变变量类型
注意:Java 是强类型语言,变量的类型一旦声明就不能改变。下面的代码是 错误 的:
int age = 43;
age = "hello"; // 编译错误! 不能把字符串赋值给一个整数变量