变量

上节课我们就说过:在 Java 中,我们操作的所有 数据 都可以看作是 对象(除了基本数据类型)。

每个整数、小数、字符串,还有我们后面要学的 Map、List 等, 都是对象(或数据)。

在 Java 程序运行的时候,这些对象其实就是 内存中的一段数据

我们的程序是经常要操作这些内存中的数据对象的,比如打印整数的值到屏幕上,或者把字符串内容写入文件。

那么我们的程序语言如何告诉计算机,我们 要操作(使用)的是哪个数据对象 呢?

比如下面的代码 会产生一个 字符串对象 在内存中:

"你好,今天天气真不错"

如果后面的程序代码要把这个字符串对象打印到屏幕上,怎么引用它呢?

问题在于,我们怎么在代码中 使用 一个内存中的数据对象?

变量/定义变量

大家来思考一个问题:

人类语言里面,当我们提到一个人或物体, 是怎么说的?

对了,用他们的 名字

比如: 王晓刚同学, 湖人队等等。

如果不允许使用事物的名字, 我们的话就没法说了。


人类语言是这样,计算机语言也是一样。编程语言为了方便操作数据对象,也需要给对象起一个名字

我们把 Java 语言中给数据起的名字, 称之为 变量

所以, Java 中的变量,可以理解为 可以通过它 访问某个内存数据对象 的名字

我们可以这样给数据起名字并存入变量中:

int age = 43;
String weather = "你好,今天天气真不错";

这行代码 int age = 43; 做了两件事:

  1. 类型声明 (Declaration): int age; 告诉编译器,我们要创建一个名为 age 的变量,它只能存放 int (整数) 类型的数据。

  2. 赋值 (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);

变量 命名规则 和 风格

✳️ 命名规则 (必须遵守)

变量名不是随便取的,有一定的规则和约定。

一个好的工程师,变量名不是乱取的,我们通常称之为 见名知意 原则,就是看到变量名,就知道变量对应的数据的含义。

比如 startTime, yourName, 这样的变量名,看了就知道是啥意思。

而像 abc 这样的变量名就不好,因为看了不知道是啥意思。


✳️ 命名风格 (建议遵守)

虽然不遵守约定也能运行,但遵守约定能让代码更专业、更易读。

下面这张表是全面的风格说明,遵守它,会让你写的代码就“看起来像专业 Java 程序员写的”。

类型推荐命名风格正确示例错误/不推荐示例
类(class)PascalCase(大驼峰)CustomerService HttpClientcustomerService http_client
接口(interface)PascalCaseUserRepository DisposableuserRepository IUserRepository
方法(method)camelCase(小驼峰)calculateTotal() getUserById()CalculateTotal() get_user_by_id
局部变量(local var)camelCase(小驼峰)userName orderCount isActiveUserName order_count
参数(parameter)camelCasevoid login(String userName)login(String UserName)
成员变量(field)camelCaseuserNameUserName m_userName
常量(static final)UPPER_SNAKE_CASEMAX_RETRY_COUNT DEFAULT_TIMEOUTmaxRetryCount MaxRetryCount
枚举类型(enum)PascalCaseFileMode DayOfWeek
枚举成员UPPER_SNAKE_CASEAPPEND SUNDAYAppend Sunday
包名(package)全小写(点分隔)com.myapp.services java.iocom.MyApp.Services
泛型类型参数(T)单个大写字母T, K, V, Et, 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 语言中,常用的基本数据类型有:

数据类型大小描述
int4 字节存储从 -2,147,483,648 到 2,147,483,647 的整数
long8 字节存储从 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 的整数
float4 字节存储小数。足以存储 6 到 7 位小数
double8 字节存储小数。足以存储 15 位小数
char2 字节存储单个字符/字母,用单引号括起来
boolean1 位*存储 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() 代码,打印出 数据对象 到终端窗口 上。

printlnSystem.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 隐式类型 的情况:

  1. 类型非常明显时:当赋值操作的右侧已经清楚地表明了类型,使用 var 可以避免重复,让代码更简洁。

    // 推荐
    var customer = new Customer();
    var numbers = new ArrayList<Integer>();
    var name = "John Doe";
    
  2. Stream API 操作:Stream 操作的结果往往类型很长,使用 var 非常方便。

    // 推荐
    var adultUsers = users.stream()
                          .filter(u -> u.age > 18)
                          .collect(Collectors.toList());
    

✳️ 推荐使用显式类型的情况:

  1. 类型不明显时:如果变量的类型不能从赋值的右侧一眼看出,明确写出类型有助于代码的理解和维护。

    // 推荐使用具体类型,因为 getBalance() 的返回类型不明确
    BigDecimal accountBalance = bankService.getBalance(accountId);
    
    // 不推荐,可读性差
    var accountBalance = bankService.getBalance(accountId); 
    
  2. 编程到接口:当你希望变量的类型是接口而不是具体的实现类时,需要使用显式类型。

    // 变量类型是 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个变量 kidstudentprettyboy 指向它。

但是 字符串数据 在 内存中 只有一份


如果后面的代码将其中一个变量进行重新赋值:

kid = "小周";

这只会改变 kid 这个变量的指向,让它指向一个新的字符串对象 "小周"studentprettyboy 的指向不受影响。


所以,如果我们接下来执行下面的代码

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

变量指向的对象本身发生了变化

这种情况,只发生在变量是 引用类型 的时候,比如:ListMap,或者自定义的类实例对象。

这里先给大家举个 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"; // 编译错误! 不能把字符串赋值给一个整数变量