ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用街霸对战理解Java变量:定义、命名与赋值全梳理

用街霸对战理解Java变量:定义、命名与赋值全梳理 我当年学 Java 的时候最不理解的不是继承、不是多态反而是“变量”这种最基础的东西。教科书上说“变量就是存储数据的容器”道理我都懂但“容器”长什么样为什么要有类型为什么要讲究命名以我当时的心智根本没建立起画面感。直到有一天我边写代码边打《街头霸王》忽然就通了隆的血量条、气槽、眩晕值、连击数这些东西不就是活生生的变量吗这篇博客我打算就用街头霸王做主线把Java变量的定义、命名规范和更新赋值彻底讲透。你只要跟着我构建完这个“街霸对战模型”以后再看到变量、看到内存、看到赋值表达式脑子里就不再是干巴巴的语法而是一整个清晰的格斗画面。这不是那种罗列 API 的教程我会把“变量为什么会这样设计”“踩过的坑在哪”都讲明白。适合正在学 Java 基础的人也适合准备面试时想把自己薄弱概念理顺的人。1. 变量到底是什么——先拿街头霸王搭思维模型1.1 一个角色就是一台“行走的变量集合”你可以把《街头霸王》里的隆Ryu想象成一个对象但这个对象本身不是由复杂知识构成的它是由一堆状态数据构成的。我们不考虑动画、音效、建模只考虑战斗逻辑需要的数据当前血量hitPoint比如 100当前能量槽superGauge比如 0.0当前眩晕值stunValue被打中会累积当前连击数comboCount现在是不是格挡状态isBlocking上次出的是什么招lastMove在我眼里这些就是一个一个的变量。格斗过程中发生的一切全是在修改这些变量的值。血量从 100 变成 82能量从 0.0 变成 50.0连击数从 0 变成 1。你把这些变量摆成一排那就是一个完整的角色状态。这一点非常重要程序的世界里所谓“状态”其实就是一个又一个变量在某一时刻的取值快照。理解了这个回头看 Java 的变量定义你就会发现它不过是在创建游戏角色时给每个属性先开好一个“存档格子”。1.2 变量三要素类型、名字、值Java 里的变量由三个要素构成数据类型、变量名、存储的值。这也是你必须清楚认识的不然代码写多了很容易糊涂。类型决定了这块区域能存什么样的数据、占多少空间、怎么解释。比如int存整数占 4 个字节double存带小数的数占 8 个字节。这就像街霸里“血量”只能存整数你不可能有 82.5 点血但能量槽可以是 37.5%所以它更适合用带小数点的类型。名字就是方便你找到这个数据的标签。你叫playerHealth也好叫ryuHealth也好反正在代码里你一提到这个名字说的就是那个内存区域。值则是这个变量当前实际存的内容。战斗开始时hitPoint的值是 100被升龙拳打中之后值变成 82。用一句大白话总结变量就是一块起了名字、规定了类型的内存空间里面放着某个值。程序运行时这块空间里的值可以变所以它才叫“变量”。如果你把值固定死不能改那就叫常量Java 里用final来修饰比如final int MAX_HEALTH 100。后面的章节我会专门说这个。2. 变量定义——给角色建档2.1 基本语法数据类型 变量名Java 定义一个变量的格式很简单数据类型 变量名;比如给隆建档int hitPoint; double superGauge; boolean isBlocking;这里int表示“这是一个整数类型的变量”hitPoint是变量名分号表示语句结束。如果你需要同时给一个初值就在后面加等号和一个值也就是“初始化”int hitPoint 100; double superGauge 0.0; boolean isBlocking false;到底选什么类型要看你准备存什么数据。我在写街霸这个案例时血量和伤害都是整数点数所以用int能量槽是百分比有可能出现 37.5% 这种带小数的值所以用double格挡状态只有“是”和“否”天然就是boolean角色名、招式名那些是文本用String。很多人一开始会困惑为什么 Java 一定要求声明类型你看 Python 那种动态语言写health 100就行了多省事。但 Java 的静态类型恰恰是它的优点类型像是一份协议提前规定了变量里面能放什么、不能放什么编译器会在你写错的时候及时拦住你。比如你把一百这种字符串往int变量里塞编译器直接报错而不是等到程序跑起来才出诡异问题。2.2 声明和初始化是两件不同的事这里必须掰开讲声明和初始化不是一回事。int health; // 声明只开了一个格子还没有放值 health 100; // 赋值往格子里放值如果你只声明不赋值直接打印它在 Java 里是无法通过编译的int health; System.out.println(health); // 编译错误变量可能尚未初始化这个设计听起来比 Python 麻烦但它是一个非常贴心的保护。你想想如果格斗游戏里角色血量是空的就开始战斗那会是怎样一场混乱Java 的做法是不初始化就不准使用逼着你在用变量之前先把状态定下来。这看起来是规矩多实则是帮你减少 bug。我个人的建议是定义变量时尽量同时初始化哪怕初值是零。比如写街霸逻辑时你直接就写int hitPoint 100;、int comboCount 0;而不是先声明一堆空变量过几行再赋。这样代码读起来就像角色档案一样清晰每行交代一个初始属性。2.3 定义多个变量的两种姿势定义多个变量可以这样int hitPoint 100, attack 30, defense 20;也可以这样int hitPoint 100; int attack 30; int defense 20;第一种写法合法但可读性差我曾经为了少写几行把变量全都塞进一行结果改其中一个的时候看花眼差点把角色的攻击力改成防御力。更稳妥的做法是一个变量单独占一行旁边可以加注释。第二点一个变量只能声明一次同一个作用域内不能出现两个同名变量。就像街霸对战里你不能同时有两个哭着的“隆”站在同一边名字冲突会导致程序无法判断你指的是谁。3. 变量命名规范——波动拳不能叫“wdq”3.1 先说硬性规则哪些名字根本不能写Java 对变量名的要求有几条硬规矩违反了直接编译失败不能以数字开头。所以1Up不行player1可以。不能包含空格和特殊字符。空格只能通过下划线替代。不能是 Java 关键字。比如你不能写int int 10;也不能用class、public、static当变量名。可以由字母、数字、下划线_和美元符号$组成。举几个例子你体会一下int specialMoveLevel; // 合法 double hadoukenDamage; // 合法 int 波动拳伤害; // 中文变量名合法但不推荐 int 1stPlayer; // 不合法数字开头 String true; // 不合法关键字 int player-name; // 不合法不能有减号说实话我写过一阵子中文变量名。后来发现团队协作时风格不统一容易乱而且在某些环境里中文变量名还会引发编码问题。除非整个项目都是中文命名风格且大家都认可否则还是老老实实用英文驼峰。3.2 约定俗成的命名风格驼峰式硬性规则之外Java 社区还有一个非常通用的约定叫“小驼峰命名法”专门用于变量名和方法名。规则很简单第一个单词首字母小写后面的单词首字母大写单词之间直接拼接不加下划线。用街霸来举例hitPoint不要写成hitpoint或者hit_pointsuperGauge不要写成supergaugecomboCount看起来就像连击计数一目了然ryuHealth、kenHealth一眼就能分辨是谁的血量再配合有意义的单词命名就成了代码自带的注释。我见过有人写int wdq; int sll;这是“波动拳”和“升龙拳”的拼音缩写。写的时候觉得自己很聪明第二天回来看就崩溃了谁能记住sll是升龙拳还是扫雷我个人的经验是宁可名字长一点也不要让它含义不明。你写hadoukenDamage、shoryukenDamage虽然敲键盘多了几秒钟但代码的可读性提升是巨大的。还有一类命名需要特别注意常量。常量用全大写加下划线比如final int MAX_HEALTH 100;、final int HADOUKEN_DAMAGE 10;。这个风格和普通变量的小驼峰区别明显让人一眼就知道“这是一个不可变的值”不会有人傻到去给MAX_HEALTH重新赋值。3.3 命名也要讲“度”规范两个字听起来比较教条但实际作用是在保护代码的可读性。我见过两种极端一种是名字短到没有信息量int a、int b、int c满天飞。这在讲算法题的时候勉强说得过去但放到街霸这种业务逻辑里就全是灾难。比如你维护一个战斗功能看到a - b;根本不知道是谁受了伤、谁掉了血。另一种是名字长到失去意义比如currentPlayerSuperComboSuperGaugeAmount虽然每个词都对但读起来像绕口令写起来也费劲。更好的做法是拆分析上下文里如果是隆的战斗逻辑superGauge就已经足够了前面的修饰不需要堆叠。最好的命名是让别人在不看注释的情况下也能猜出这个变量大概在干嘛。这不是玄学是经验。每次写变量定义前停下来想一想三天后的我还能看懂这个名字吗4. 更新赋值——战斗正式开始4.1 等号不是数学的等号很多人学赋值时会在这里卡一下hitPoint hitPoint - 18;在数学里是荒谬的它根本不可能成立。但在编程里这就是最常见的更新赋值。其实等号在这里不是一个“等于关系”而是一个“把右边的计算结果倒入左边的容器”的动作。你可以把它念成把hitPoint原来的值取出来减去 18再把新的结果放回hitPoint这个格子里。看一个最直观的例子int hitPoint 100; hitPoint hitPoint - 18; System.out.println(hitPoint); // 82第一行把 100 放进变量第二行先把变量里的值读出来计算100 - 18 82再把 82 写回变量。整个过程是“读、算、写”三步。正因为这个操作太常见了Java 提供了一组复合赋值运算符可以少写一点重复代码hitPoint - 18; // 等价于 hitPoint hitPoint - 18; superGauge 50; // 等价于 superGauge superGauge 50; comboCount; // 等价于 comboCount comboCount 1;这三种写法你都会在真正的项目里大量见到。我个人在写实战代码时只要是简单的加减一律用复合赋值只有需要把中间过程单独算出来的时候才写完整表达式。4.2 一个回合里的变量是如何变动的咱们来做一个一分钟的脑内实验。假设现在就有一场隆对肯Ken的比赛开局隆血量 100能量 0.0连击数 0第一击中隆打出一记波动拳造成 10 点伤害肯的血量由 100 变成 90。同时隆自己积攒了部分能量能量由 0.0 变成 50.0连击数由 0 变为 1反击到来肯一个升龙拳打中隆造成 18 点伤害隆的血量由 100 变成 82隆的连击被打断连击数回到 0防御时刻隆改变策略开始防御isBlocking由false变成true此次肯的伤害减半只掉了 9 点血能量爆发隆能量攒到 100释放超必杀肯血量再掉 45隆的能量被清空回到 0.0这个过程里所有数值的增减落到代码上都是变量更新。我想让你注意一点变量值的含义完全是由你的业务逻辑决定的。同样一个hitPoint它什么时候减、减多少都对应着格斗游戏中的具体事件。所谓“写代码”很多时候就是把现实中的变化翻译成变量赋值语句。我还可以顺手给你一个“状态快照”式的角度每一次变量更新之后这组变量新的取值就是游戏此刻的完整状态。调试人员为什么那么喜欢看变量面板因为变量面板上的数据比任何日志都更真实。4.3 交换变量值一个必须掌握的经典操作战斗中经常需要把两个角色的某些数值互换或者把临时值保存下来。这时候就涉及到“交换变量”的需求。直接写a b; b a;是不行的你想想第一行把 b 的值覆盖给 a 之后a 原来的值就已经丢了第二行再赋值时b 拿到的是已经变化后的值结果两个变量都变成同一个值原来的值彻底消失。正确做法是引入一个临时变量int a 1; int b 2; int temp a; a b; b temp;这就像你要交换两个牌堆里的牌手里必须有个空位先暂存一张牌。Java 这样设计看起来笨但它保证了每一步都清晰可控不像某些语言提供花哨的交换语法。基础题会有种面试题的既视感但实际开发中临时变量保存旧状态也是家常便饭。比如战斗系统里你想计算“格挡后受到的伤害”就要先把原始伤害存到一个临时变量里。4.4 关于更新赋值再讲一个容易混的点我发现很多初学者会把赋值表达式本身当做一个值来理解。实际上在 Java 中hitPoint 82这个表达式的结果就是 82所以你可以写成链式赋值int a, b; a b 100; // b 先被赋为 100然后 a 也变成 100这个特性某些场景下能用但可读性差容易引起争议。我个人的习惯是绝不在正式项目里写链式赋值一行一个变量清清楚楚比省那两行代码重要得多。另外要提醒更新一个基本类型变量并不会影响另一个变量。比如int originalHealth 100; int currentHealth originalHealth; currentHealth 82; System.out.println(originalHealth); // 还是 100currentHealth拿到的是originalHealth的值拷贝之后改currentHealth跟originalHealth一点关系都没有。这看起来是理所当然的但一旦你将来接触了引用类型比如数组和对象你会发现这个规则会发生变化。所以趁现在还简单把这个“值拷贝”的概念先在脑子里钉住。5. 常见问题与排查——街霸实战中的翻车现场5.1 未初始化就使用血条是空的就开始打这个问题我在 2.2 里已经提过但因为它确实是初学者第一坑我再从排查角度说一遍。int kenHealth; // 很多行代码... kenHealth - 10; // 编译错误局部变量可能尚未初始化在 Java 里局部变量不像某些编程语言会默认给你一个零值Java 局部变量不初始化就不能使用。编译器会直接拒绝编译把这个错误挡在运行之前。解决办法也很简单定义时就给一个明确的初始值比如int kenHealth MAX_HEALTH;或者在使用前确保所有分支都完成赋值。我见过一种隐蔽写法int damage; if (isCriticalHit) { damage 20; } else { damage 5; } kenHealth - damage;这种没问题因为无论走哪个分支damage 都被赋值了。但如果你把else部分漏掉编译器就会报damage may not have been initialized。这时候不要烦躁恰恰说明编译器在帮你检查逻辑漏洞因为确实存在一个场景里你没给伤害值。5.2 作用域搞混变量不是钱不能到处花每个变量都有它的“有效范围”专业上叫作用域。Java 的方法里通常一对大括号就是一个块。在一个if块里面定义的变量出了这个if块就不能访问。if (isBlocking) { int reducedDamage 18 / 2; } System.out.println(reducedDamage); // 编译错误找不到符号这就像战斗系统里你在“格挡判断”的代码块里算出来的减伤值如果只存在于那个块里其他逻辑自然拿不到。解决办法是把变量提到外面int reducedDamage 0; if (isBlocking) { reducedDamage 18 / 2; } System.out.println(reducedDamage); // 正常输出 9这里特别容易踩坑的是循环。很多人喜欢在for循环里用int i循环结束后还想继续用i结果编译不过。理解作用域之后你就不纠结了变量的生命范围是有限的出了那个块它就该“下班”。这对内存管理和代码清晰度都是好事。5.3 类型不匹配一个萝卜一个坑Java 对类型要求严格。int变量不能直接存小数boolean变量不能存数字这些都好理解。真正的坑在于“要转换的时候怎么办”。比如int damage 9; double rate 1.5; double result damage * rate; // 结果 13.5这是合法的int 会自动转成 double但如果反向操作double gauge 37.5; int gaugeInt gauge; // 编译错误可能丢失精度这时候你必须强制转换int gaugeInt (int) gauge; // 小数为0.5被舍弃变成37强制转换是你明确告诉编译器“我知道可能丢精度我就要这个整数”。但如果不是业务确实需要我建议不要随便强转因为小数被截断的后果有时候很隐蔽。比如街霸里伤害是 9.5 你强转成 9玩家看不出来但整场打下来误差会积累最终伤害数值就会跟你预期不一致。5.4 用等号比较数字这可能是面试里最经典的陷阱写战斗判断条件的时候新手特别容易把赋值和相等判断搞混。比如if (hitPoint 0) { // 这是赋值不是判断 System.out.println(角色倒下); }这个代码在 Java 中直接编译不过因为hitPoint 0的结果是整数不能塞进 if 条件里当布尔值。这其实是 Java 在设计上帮你拦掉了一个大坑。你要写的是if (hitPoint 0) { System.out.println(角色倒下); }我倒不觉得这是个特别容易犯的错但它是一个非常经典的面试点特别是拿 C 语言经验转 Java 的人经常被这个区别绕晕。赋值是往变量里放值判断是比较两个值是否相等语义完全不同。5.5 常见问题速查表为了方便你以后排查我把这些最常见的变量问题整理成一张表问题现象可能原因解决办法编译报“可能尚未初始化”局部变量定义后未赋值就在某些分支使用定义时给默认初值或检查所有分支是否都完成赋值编译报“找不到符号”变量超出作用域或拼写错误把变量提到所需作用域之外并严格检查拼写编译报“不兼容的类型”把高精度类型赋给低精度类型使用强制类型转换并确认业务上允许丢精度运行结果与预期不符赋值误用为比较、复合赋值语义理解错误检查操作符区分与区分与多个同名变量导致不可读命名不够具体重复出现相似变量名使用有区分度的命名如ryuHealth、kenHealth排查的时候还有个小技巧把相关变量打印出来观察它们在每个节点前后的变化。游戏逻辑里一个变量值异常往往意味着你在某个步骤把数据算错或赋错了。打印变量比猜逻辑快得多。6. 完整实战——从零写一个街头霸王对战模拟器6.1 完整代码一个可运行的 Java 文件现在我们把前面讲的所有知识揉进一个完整的代码示例。这个 demo 不依赖任何第三方库你只需要有一个 JDK 环境就能直接编译运行。public class StreetFighterDemo { public static void main(String[] args) { // 常量初始生命、各招式伤害、能量增长比例 final int MAX_HEALTH 100; final int HADOUKEN_DAMAGE 10; final int SHORYUKEN_DAMAGE 18; final int ULTRA_DAMAGE 45; final int SUPER_GAUGE_INCREASE 50; // 定义变量角色状态 String playerName Ryu; int ryuHealth MAX_HEALTH; int kenHealth MAX_HEALTH; double ryuGauge 0.0; int comboCount 0; boolean isBlocking false; System.out.println( 街霸模拟战斗开始 ); System.out.println(playerName HP: ryuHealth Ken HP: kenHealth 能量: ryuGauge % 连击: comboCount); // 第1回合Ryu 使用波动拳命中 Ken System.out.println(▶ 第1回合 playerName 使用波动拳造成 HADOUKEN_DAMAGE 点伤害); kenHealth - HADOUKEN_DAMAGE; ryuGauge SUPER_GAUGE_INCREASE; comboCount; System.out.println(Ken 剩余 HP: kenHealth Ryu 能量: ryuGauge %连击数: comboCount); // 第2回合Ken 用升龙拳反击命中 Ryu System.out.println(▶ 第2回合Ken 使用升龙拳造成 SHORYUKEN_DAMAGE 点伤害); ryuHealth ryuHealth - SHORYUKEN_DAMAGE; comboCount 0; System.out.println(Ryu 剩余 HP: ryuHealth 连击被打断当前连击数: comboCount); // 第3回合Ryu 选择防御并继续积攒能量 isBlocking true; ryuGauge SUPER_GAUGE_INCREASE; System.out.println(▶ 第3回合 playerName 选择防御格挡状态: isBlocking); // 防御状态下受到的伤害减半注意除法运算结果 int reducedDamage SHORYUKEN_DAMAGE / 2; ryuHealth - reducedDamage; System.out.println(Ken 升龙拳被格挡造成减半伤害 reducedDamage Ryu 剩余 HP: ryuHealth 能量: ryuGauge %); // 第4回合能量达到100释放超必杀 if (ryuGauge 100.0) { System.out.println(▶ 第4回合能量已满 playerName 释放超必杀技造成 ULTRA_DAMAGE 点伤害); kenHealth - ULTRA_DAMAGE; ryuGauge 0.0; comboCount; } System.out.println( 战斗结算 ); System.out.println(Ryu 剩余 HP: ryuHealth 能量: ryuGauge %); System.out.println(Ken 剩余 HP: kenHealth Ryu 最终连击数: comboCount); } }6.2 运行结果与关键逻辑拆解你把这文件保存为StreetFighterDemo.java然后执行javac StreetFighterDemo.java和java StreetFighterDemo会看到类似这样的输出 街霸模拟战斗开始 Ryu HP: 100 Ken HP: 100 能量: 0.0% 连击: 0 ▶ 第1回合Ryu 使用波动拳造成 10 点伤害 Ken 剩余 HP: 90Ryu 能量: 50.0%连击数: 1 ▶ 第2回合Ken 使用升龙拳造成 18 点伤害 Ryu 剩余 HP: 82连击被打断当前连击数: 0 ▶ 第3回合Ryu 选择防御格挡状态: true Ken 升龙拳被格挡造成减半伤害 9Ryu 剩余 HP: 73能量: 100.0% ▶ 第4回合能量已满Ryu 释放超必杀技造成 45 点伤害 战斗结算 Ryu 剩余 HP: 73能量: 0.0% Ken 剩余 HP: 45Ryu 最终连击数: 1这个代码虽然很短但已经把变量定义、常量、初始化、复合赋值、强制类型转换初体验、作用域内的临时变量、条件判断等知识点全部串起来了。我建议你亲手运行一遍然后试着改几个地方观察变化比如把HADOUKEN_DAMAGE从 10 改成 12看看 Ken 的剩余血量会怎么变或者把SUPER_GAUGE_INCREASE从 50 改成 30看看第四回合还会不会触发超必杀。6.3 这个 demo 还能怎么扩展如果你想更进一步把变量玩得更透可以尝试这几个方向第一把血量、能量、伤害这些数据放进数组。比如int[] healthPoints {100, 100};然后用下标区分隆和肯。这就引出了数组变量的更新healthPoints[0] - 10;。数组本质上是引用类型和基础类型的更新行为完全不同是下一阶段理解内存模型非常关键的一步。第二用Math.random()生成随机伤害。每次攻击的落点不用固定值而是浮动的比如int realDamage (int)(Math.random() * 10) 5;。这里你就能看到强转是怎么把double变成int的也能深刻理解为什么业务上要小心丢精度。你可以让整个模拟战斗像真实的游戏一样每一局结果都不同。第三把重复的逻辑封装进方法。比如写一个attack(int targetHealth, int damage)接收参数并返回新的血量。这时候变量就不再只是 main 方法里的局部变量还要理解参数传递和返回值是怎么通过变量完成的。等你把变量彻底吃透再去学方法、学对象会有一种水到渠成的感觉。我个人写这段代码时最大的体会是变量命名对调试的影响太大了。第一次我把ryuHealth错写成了kenHealth结果总是隆掉血我还以为是逻辑写错了足足查了五分钟。所以后来我定义变量时都会刻意让名字一眼能看出归属比如ryuHealth、kenHealth、ryuGauge而不是整个health从头用到尾。别小看这几个字符的差别它能帮你省掉的排查时间比你想象的多得多。
返回列表