《java创世手记》---java基础篇(上)
我将尝试“唤醒感知力”的方式,结合“造物主”的视角,来引导进入 Java 的世界。我们会尽量避免枯燥的细节罗列,而是从“模拟现实”和“解决问题”的角度出发,来体会 Java 这门语言是如何构建和表达数字世界的。
《Java 创世手记》
序章:选择你的创世工具 —— 为何是 Java?
想象你是一位经验丰富的“造物主”,在 Python 的世界里已经游刃有余,用它描绘了诸多灵动的脚本和小巧的世界。现在,你站在一片更广阔、更需要秩序与结构的新大陆前,你发现 Python 虽然灵活,但在构建那些需要坚固地基、严密规则、能承载亿万生灵同时运作的宏伟城邦时,似乎需要一种更为“工业级”的工具。
这时,Java 递上了它的名片。
Java 对你说:“嘿,造物主!我知道你喜欢 Python 的优雅和快速。但我这里,有另一些你可能需要的特性:”
- “万物皆对象”的极致秩序 (Object-Oriented Philosophy):
- Python 也是面向对象的,但 Java 将这种思想贯彻得更为彻底。在 Java 的世界里,几乎所有事物(除了最最基础的“原子”——基本数据类型)都是一个“对象”。这意味着你的世界将拥有高度的模块化和清晰的蓝图。想象一下,你要建造的不再是玲珑小屋,而是摩天大楼,每一块砖、每一扇窗、每一部电梯,都需要精确的规格和明确的职责。Java 的面向对象特性,就是为此而生。
- “一次编译,到处运行”的魔法契约 (Platform Independence):
- 你用 Python 创造的脚本,在不同的“土地”(操作系统)上运行时,有时需要一些“水土适应”。而 Java 有一个著名的口号:“Write Once, Run Anywhere”。它通过一个叫做“Java 虚拟机 (JVM)”的“翻译官”,让你创造的程序可以在任何安装了 JVM 的土地上顺畅运行,无论是 Windows、Linux 还是 macOS。这就像你拥有了一份通用的建筑图纸,可以在世界各地建造同样的奇迹。
- “严谨的类型系统”带来的稳固地基 (Static Typing & Strong Typing):
- Python 的动态类型让你在创造时自由挥洒,但有时,当世界变得复杂,这种自由可能会带来一些隐藏的“结构风险”。Java 则像一位严谨的工程师,在你“打地基”(编写代码)的时候,就要求你明确每一个“建材”(变量)的“规格”(类型),并且在“施工”(编译)阶段就会帮你检查很多潜在的“设计缺陷”。这虽然在初期会显得有些“啰嗦”,但它能确保你的宏伟建筑在未来更加稳固,不易崩塌。
- “强大的生态系统与社区”提供的无尽资源 (Rich Ecosystem & Community):
- 想象一下,你要建造一座城市,你需要各种各样的“专业工匠”和“现成模块”——处理复杂网络的工程师、管理海量数据的图书管理员、构建华丽界面的艺术家……Java 拥有一个极其庞大和成熟的生态系统,无数的“轮子”(开源库和框架)可以直接取用,无数的“同道中人”(开发者社区)可以交流经验。这意味着你不是一个人在战斗,你的创世之旅将得到强大的助力。
- “卓越的性能与并发处理能力”驱动的繁华世界 (Performance & Concurrency):
- 当你的世界变得庞大,有成千上万的“居民”(用户/请求)同时活动时,你需要你的世界能够高效运转,从容应对。Java,尤其是在服务器端应用和大型系统中,以其优良的性能和强大的并发处理能力著称。它能让你的世界保持活力,即使在最繁忙的时刻。
造物主,你手中的 Python 已经是模拟微观动态和快速原型的好帮手。而 Java,则更像是一套用于构建宏大、复杂、需要长期稳定运行的“工业化创世套件”。 它可能在最初的“建模”阶段比 Python 要求更多“规矩”,但这些“规矩”最终会回馈给你一个更健壮、更可扩展、更易于维护的宏伟世界。
现在,你是否准备好拿起 Java 这把新的创世之锤,去开辟一片不同的天地?
第一章:世界的基石 —— 数据、变量与基本规则
在任何一个世界被创造之初,都需要最基本的“物质”和“法则”。在 Java 的世界里,这些基石是什么呢?
1. 一切从“声明”开始:给世界万物一个明确的“身份” (Variables & Data Types)
想象一下,在你的新世界里,你要记录一个“居民”的信息:他的年龄、他的名字、他是否还活着。
- Python 的方式 (回顾):
age = 25
name = "造物者"
is_alive = True
Python 很随和,你直接告诉它“这是年龄”,它就知道了。
- Java 的方式:先“报备身份”!
在 Java 的世界里,规矩更严一些。在你使用任何“东西”(变量)之前,你必须先向“世界法则”(编译器)声明它的“种类”(数据类型)和“名字”(变量名)。
// Java 代码:声明变量并赋值
int age; // 声明一个整数类型的变量,名为 age
age = 25; // 给 age 赋值String name = "造物主"; // 声明一个字符串类型的变量 name,并直接赋值
boolean isAlive = true; // 声明一个布尔类型的变量 isAlive,并赋值
感知唤醒:
int age;
这就像你在创世之初,对虚空说:“此处将有一个‘整数容器’,我给它贴上‘age’的标签。” 此时容器还是空的。age = 25;
然后你将“数字25”这个“灵魂”放入了这个标记为“age”的“整数容器”中。String name = "造物主";
这是一步到位的操作:“此处将有一个‘文本容器’,标签是‘name’,并且我直接把‘造物主’这段文字放进去。”
为何 Java 如此“啰嗦”?
这种“先声明后使用”以及“类型固定”的规则,就像建筑施工前必须有详细的图纸和材料清单。
- 早期发现错误:如果图纸上标明这里要用“钢筋”,你却拿来了“木头”,工程师(编译器)在施工前就能发现问题,避免了建到一半楼塌了的风险。
- 性能优化:当“世界法则”提前知道了每个“容器”里装的是什么“东西”时,它能更有效地管理和分配“世界的资源”(内存),让世界运转得更快。
- 代码清晰度:对于庞大的世界而言,明确的“身份”让其他“协作者”(程序员)更容易理解你的设计。
Java 世界的基本“物质种类”(基本数据类型 Primitive Data Types):
这些是构成 Java 世界最基础的“原子”,它们不是“对象”,而是纯粹的值。
类型 | 英文名称 | 描述 | 创世比喻 | 示例 |
---|---|---|---|---|
byte | 字节型 | 存储小范围整数 | 一小撮沙子 | byte b = 100; |
short | 短整型 | 存储比 byte 大的整数 | 一小捧沙子 | short s = 1000; |
int | 整型 | 最常用的整数类型 | 一袋沙子 (标准规格) | int i = 100000; |
long | 长整型 | 存储非常大的整数 | 一卡车沙子 | long l = 100L; |
float | 单精度浮点型 | 存储带小数的数字 | 一杯水 (精度一般) | float f = 3.14f; |
double | 双精度浮点型 | 更精确的小数 (常用) | 一大桶水 (精度高) | double d = 3.14; |
char | 字符型 | 存储单个字符 | 一个字母/符号的印记 | char c = 'A'; |
boolean | 布尔型 | 存储 true 或 false | “是/否”开关 | boolean flag = true; |
感知细节:
long l = 100L;
数字后面的L
(或l
) 告诉 Java:“这是个长整型数字,别搞错了!”float f = 3.14f;
数字后面的f
(或F
) 告诉 Java:“这是个单精度浮点数。” 如果不加,Java 默认小数是double
类型。char c = 'A';
字符用单引号包裹。
2. “命名”的艺术与“禁忌” (Identifiers & Keywords)
和你熟悉的 Python 一样,Java 的世界也需要给各种事物命名(标识符),也有些词语是“神圣不可侵犯”的(关键字)。
- 标识符 (Identifiers): 你给变量、方法、类起的名字。
- 规则:可以由字母、数字、下划线 (
_
)、美元符号 ($
) 组成,但不能以数字开头。 - Java 的“推荐风格”(约定俗成):
- 变量名和方法名:采用“小驼峰命名法 (lowerCamelCase)”,例如
age
,userName
,calculateSum
。 - 类名:采用“大驼峰命名法 (UpperCamelCase)”,例如
Student
,MyFirstProgram
。 - 常量名:所有字母大写,单词间用下划线分隔,例如
MAX_VALUE
,PI
。
- 变量名和方法名:采用“小驼峰命名法 (lowerCamelCase)”,例如
- 规则:可以由字母、数字、下划线 (
- 关键字 (Keywords): Java 语言保留的,具有特殊含义的词。你不能用它们作为标识符。
感知唤醒:
- 常见的有:
public
,class
,static
,void
,int
,if
,else
,for
,while
,return
,new
,import
… (随着学习深入,你会逐渐认识它们) - 标识符 就像你在新世界里给山川河流、花草树木起的名字,要有意义,也要符合“命名规范”,这样你的世界才显得井井有条。
- 关键字 则是这个世界的“基本法则”的代号,比如“创生万物用
class
”,“定义行为用void
或其他返回类型”,“做出选择用if
”,这些是不能随意挪作他用的。
3. “注释”:给你的创世蓝图做标记 (Comments)
和 Python 一样,注释是给“人”(包括未来的你和其他协作者)看的,Java 编译器会忽略它们。
- 单行注释 (Single-line comment): 以
//
开头
// 这是我的第一个 Java 变量
int playerScore = 100;
- 多行注释 (Multi-line comment): 以
/*
开始,以*/
结束
/** 这是对玩家得分变量的详细说明。* 它记录了玩家在当前游戏中的总得分。*/
int playerScore = 100;
- 文档注释 (Documentation comment): 以
/**
开始,以*/
结束。这种注释很特别,可以用工具 (如javadoc
) 自动生成 API 文档。
/*** 这是一个计算两个整数和的方法。* @param a 第一个整数* @param b 第二个整数* @return 两个整数的和*/
public int add(int a, int b) {return a + b;
}
感知唤醒:
注释就像你在建筑图纸上做的标记和说明,让后来者能看懂你的设计意图。文档注释更是重量级,它能帮你自动生成一份“世界说明书”,告诉别人你的世界是如何运作的。
4. “运算符”:驱动世界运转的“力” (Operators)
运算符是连接数据、执行操作的符号。它们是 Java 世界中各种“相互作用力”的体现。大部分运算符和你熟悉的 Python 中的非常类似。
- 算术运算符:
+
,-
,*
,/
(除法,结果可能带小数),%
(取余)。
int a = 10;
int b = 3;
int sum = a + b; // 13
int difference = a - b; // 7
int product = a * b; // 30
double quotient = (double)a / b; // 3.333... (注意类型转换,否则整数相除结果也是整数)
int remainder = a % b; // 1
感知细节:
在 Java 中,两个整数相除,如果结果不进行类型转换,得到的仍然是整数(小数部分会被截断)。例如 10 / 3
结果是 3
。若想得到精确的小数结果,需要将其中一个操作数(或两者)转换为浮点类型,如 (double)a / b
。
- 关系运算符:
==
(等于),!=
(不等于),>
(大于),<
(小于),>=
(大于等于),<=
(小于等于)。结果都是布尔值 (true
或false
)。
boolean isEqual = (a == b); // false
boolean isGreater = (a > b); // true
- 逻辑运算符:
boolean condition1 = true;
boolean condition2 = false;
boolean resultAnd = condition1 && condition2; // false
boolean resultOr = condition1 || condition2; // true
boolean resultNot = !condition1; // false
&&
(逻辑与,AND): 两边都为true
,结果才为true
。||
(逻辑或,OR): 两边只要有一个为true
,结果就为true
。!
(逻辑非,NOT): 取反。
- 赋值运算符:
=
(赋值),+=
,-=
,*=
,/=
,%=
(复合赋值)。
int score = 100;
score += 10; // score = score + 10; 现在 score 是 110
- 自增/自减运算符:
++
(自增1),--
(自减1)。
int count = 5;
System.out.println(count++); // 输出 5, 然后 count 变为 6
System.out.println(++count); // count 先变为 7, 然后输出 7
感知唤醒:
自增自减运算符就像给你的变量一个“快速升级/降级”的按钮。理解“前缀”和“后缀”的区别,就像理解“先做事再说”和“先说再做事”的区别。
i++
(后自增): 先使用i
的值,再将i
加 1。++i
(前自增): 先将i
加 1,再使用i
的新值。
造物主的小结与展望:
到目前为止,你已经了解了 Java 世界最基本的构成单元和运作规则:
- 如何通过明确的类型声明来定义世界中的“物质”(变量)。
- 认识了构成这些物质的基本“原子”种类(基本数据类型)。
- 学会了给这些物质和行为命名(标识符)的艺术和禁忌(关键字)。
- 掌握了为你的创世蓝图添加注释的方法。
- 理解了驱动世界变化的各种**“力”**(运算符)。
这些是 Java 世界的“物理定律”和“化学元素表”。虽然看起来比 Python 多了一些“仪式感”,但正是这些“仪式感”为构建更宏大、更稳固的世界打下了坚实的基础。
接下来,我们将探索 Java 如何构建更复杂的“结构体”和“生命体”——对象 (Objects),以及如何通过控制流语句 (Control Flow Statements) 来编排世界事件的发生顺序和逻辑。你的创世之旅,才刚刚开始!
第二章:编排世界韵律 —— 程序结构、控制流与初级数据集合
造物主,你已经掌握了构成 Java 世界的基本“物质”(数据类型)和驱动它们的“力”(运算符)。现在,我们需要学习如何将这些元素组织起来,形成有序的“程序”,并赋予这个世界“思考”和“重复行动”的能力。这就像你需要为你的世界设定时间流逝的规则、季节更替的逻辑,以及如何管理一群同类型的“小精灵”。
1. Java 程序的“骨架”:基本结构与 main
方法的神秘入口
每一个 Java 程序都需要一个“主心骨”,一个开始执行的地方。这个“主心骨”通常是一个包含 main
方法的类。
// 这是一个最简单的 Java 程序结构
public class MyFirstJavaProgram { // 定义一个公开的类,类名通常与文件名相同// 这是程序的入口点:main 方法// JVM (Java 虚拟机) 会从这里开始执行你的程序public static void main(String[] args) {// 在这里编写你的指令,告诉计算机要做什么System.out.println("我的 Java 世界,你好!"); // 向控制台输出一行文字// 演示:调用我们之前定义的方法 (假设 add 方法也在此类中或可访问)int sum = addNumbers(5, 3);System.out.println("5 + 3 = " + sum);}// 我们可以在同一个类中定义其他方法public static int addNumbers(int a, int b) { // 注意这里的 staticreturn a + b;}
}
感知唤醒:
public class MyFirstJavaProgram
:- 在 Java 中,所有的代码都必须存在于类 (class) 之中。你可以把类想象成一个“蓝图容器”,里面装着属性和行为的定义。
public
表示这个类是公开的。通常,一个.java
文件只包含一个public
类,并且该类的名字与文件名完全相同(例如,上面的代码应保存在MyFirstJavaProgram.java
文件中)。
public static void main(String[] args)
:- 这是 Java 世界的“创世神谕”或“启动按钮”。当你告诉计算机运行这个程序时,JVM 会像一位忠实的仆人,首先找到这个名为
main
的特殊方法,并从这里开始执行。 public
: 表示这个main
方法是公开的,可以被 JVM 从外部调用。static
: 这个关键字意味着main
方法属于类本身,而不是属于这个类的某个特定对象。因此,JVM 不需要先创建MyFirstJavaProgram
类的对象就能直接调用main
方法。想象一下,这是“世界法则”本身赋予的启动能力,不需要先造出一个具体的“东西”来按按钮。(关于static
,我们后面会有更详细的探讨)void
: 表示main
方法执行完毕后,不返回任何值给调用它的 JVM。main
: 这是固定的方法名,JVM 只认这个名字作为入口。(String[] args)
: 这是main
方法的参数。它是一个字符串数组 (String array),名为args
(arguments 的缩写)。这个参数允许你在从命令行运行 Java 程序时,向程序传递一些初始信息。例如,你可以运行java MyFirstJavaProgram hello world
,那么args
数组就会包含"hello"
和"world"
这两个字符串。目前,你可以暂时忽略它,但知道它的存在。
- 这是 Java 世界的“创世神谕”或“启动按钮”。当你告诉计算机运行这个程序时,JVM 会像一位忠实的仆人,首先找到这个名为
System.out.println("...");
:- 这是 Java 中最常用的向控制台输出信息的语句。你可以把它看作是你的程序向你(造物主)“汇报工作”或“展示成果”的方式。
System
是一个预定义的类,代表系统。out
是System
类的一个静态成员变量,它是一个“打印流对象”。println()
是这个打印流对象的一个方法,用于打印一行文本并换行 (print()
只打印不换行)。
感知细节:
- 分号
;
: Java 中的每一条完整语句(指令)的末尾通常都需要一个分号,表示这条指令结束了。这就像中文的句号。 - 花括号
{}
: 用于定义代码块的边界,比如类的主体、方法的主体、以及后面会学到的if
语句、循环语句等的主体。
2. 世界的“选择题”:条件判断语句 (if
, else if
, else
)
你的世界需要根据不同的“情况”做出不同的“反应”。比如,“如果天气晴朗,精灵们就出去采果子;否则(如果下雨),他们就待在树洞里唱歌。”
Java 的条件判断语句和 Python 非常相似,只是语法上有些许不同。
public class WeatherDecision {public static void main(String[] args) {boolean isSunny = true;boolean isRaining = false;// 基本的 if-else 结构if (isSunny) { // 条件表达式,结果为 boolean 类型System.out.println("天气晴朗,精灵们出去采果子!");} else {System.out.println("天气不好,精灵们待在树洞里。");}int temperature = 25; // 摄氏度// if-else if-else 结构if (temperature > 30) {System.out.println("太热了,喝点冰镇果汁。");} else if (temperature >= 20 && temperature <= 30) { // 可以使用逻辑运算符组合条件System.out.println("温度适宜,正是活动的好时候!");} else if (temperature < 10) {System.out.println("有点冷,需要加件衣服。");} else {System.out.println("嗯,今天温度比较普通。");}// 嵌套的 if 语句boolean hasUmbrella = true;if (isRaining) {System.out.println("下雨了!");if (hasUmbrella) {System.out.println("幸好带了伞。");} else {System.out.println("糟糕,没带伞,要淋湿了!");}} else {System.out.println("今天没下雨。");}}
}
感知唤醒:
- 条件必须是布尔表达式:
if
和else if
后面的括号()
中,必须是一个结果为true
或false
的表达式。 - 代码块由花括号
{}
包裹:不像 Python 用缩进,Java 用花括号来明确标记if
,else if
,else
控制的代码范围。即使代码块只有一条语句,也强烈建议使用花括号,这能避免很多潜在的逻辑错误,并增加代码可读性。 else if
可以有多个,else
最多一个且必须在最后。
3. 世界的“重复节拍”:循环语句 (while
, for
)
你的世界里有很多事情需要重复进行。比如,“太阳每天都会东升西落”,“工匠需要敲打铁砧100次”。
while
循环:当条件为真时,重复!
当你不知道确切要重复多少次,但知道一个“继续重复”的条件时,while
循环是你的好帮手。
public class WhileLoopDemo {public static void main(String[] args) {int mana = 100; // 初始魔法值int spellCost = 15; // 每次施法消耗int spellsCast = 0; // 已施法次数// 当魔法值足够释放一次法术时,就继续施法while (mana >= spellCost) {mana -= spellCost; // 消耗魔法spellsCast++; // 施法次数增加System.out.println("施放了一次法术!剩余魔法:" + mana + ",已施法:" + spellsCast + "次。");}System.out.println("魔法不足,无法继续施法。总共施法了 " + spellsCast + " 次。");}
}
感知唤醒:
while (条件)
,只要“条件”为 true
,花括号 {}
里的“剧情”就会一遍遍上演。一定要确保循环内部有改变“条件”的语句,否则可能会陷入“无限循环”的灾难(比如魔法值永远不减少)。
for
循环:按次数或遍历集合,精准重复!
当你明确知道要重复多少次,或者要依次处理一个“集合”中的每一个元素时,for
循环更常用。Java 的for
循环有几种形式,最经典的是基于计数器的。
public class ForLoopDemo {public static void main(String[] args) {// 经典 for 循环:打印数字 0 到 4// for (初始化语句; 循环条件; 迭代表达式)for (int i = 0; i < 5; i++) { // i 从 0 开始,每次循环 i 增加 1,直到 i 不再小于 5System.out.println("当前数字 (经典for): " + i);}System.out.println("---");// 增强型 for 循环 (for-each loop):遍历数组或集合中的元素// 后面学习数组和集合时会更常用String[] fruits = {"苹果", "香蕉", "橙子"}; // 这是一个字符串数组,后面会讲for (String fruit : fruits) { // 依次取出 fruits 数组中的每个元素,赋值给 fruit 变量System.out.println("我喜欢的水果:" + fruit);}}
}
感知细节 (经典 for
循环):
for (int i = 0; i < 5; i++)
int i = 0;
(初始化): 循环开始前执行一次,通常用于声明和初始化一个循环控制变量(计数器)。i < 5;
(循环条件): 每次循环开始前判断。如果为true
,执行循环体;如果为false
,循环结束。i++
(迭代表达式): 每次循环体执行完毕后执行,通常用于更新循环控制变量。
感知唤醒 (增强型 for
循环):
for (String fruit : fruits)
可以读作:“对于 fruits
集合中的每一个 fruit
(其类型为 String
)”。这是一种更简洁、更易读的遍历方式,当你不需要关心元素的索引时非常有用。
4. 循环的“微控制器”:break
与 continue
有时,在循环的“重复节拍”中,你需要一些“特殊指令”来微调节奏。
break
:立刻“刹车”,终止整个循环!
当满足某个条件时,你可能希望完全跳出当前的循环,不再执行后续的迭代。
public class BreakDemo {public static void main(String[] args) {// 寻找宝藏,找到就停止搜索String[] locations = {"森林", "山洞", "沼泽-有宝藏!", "沙漠"};for (String location : locations) {System.out.println("正在搜索:" + location);if (location.contains("宝藏")) {System.out.println("找到了宝藏!停止搜索!");break; // 立刻跳出 for 循环}}}
}
continue
:跳过“本轮”,直接进入“下一轮”!
当满足某个条件时,你可能只想跳过当前这次循环中余下的代码,直接开始下一次迭代。
public class ContinueDemo {public static void main(String[] args) {// 给精灵分配任务,跳过生病的精灵String[] elves = {"艾拉-健康", "索林-生病", "菲欧娜-健康"};for (String elf : elves) {if (elf.contains("生病")) {System.out.println(elf.split("-")[0] + " 生病了,跳过本次任务分配。");continue; // 跳过当前循环的剩余部分,直接开始下一个精灵}System.out.println("给 " + elf.split("-")[0] + " 分配了日常巡逻任务。");}}
}
感知唤醒:
break
就像游戏里的“退出关卡”按钮。continue
则是“跳过这个小怪,打下一个”的策略。
5. “数组”:最简单的数据“兵营” (Arrays)
在你创造的世界中,经常需要管理一组相同类型的“事物”,比如一队士兵的生命值、一排树木的高度、一周七天的名称。数组 (Array) 就是 Java 提供的一种最基础的、用来存储固定数量、同种类型元素的“数据容器”。
- 声明数组:
// 方式一:推荐
int[] monsterHPs; // 声明一个能存储 int 类型元素的数组,名为 monsterHPs
String[] dayNames; // 声明一个能存储 String 类型元素的数组,名为 dayNames// 方式二:也可以,但不推荐(C/C++风格)
// int monsterHPs[];
感知: 此时只是声明了这个“兵营”的“番号”,兵营里还没有“士兵”(元素),兵营的大小也还没定。
- 创建数组 (分配空间):
使用new
关键字为数组分配内存空间,并指定数组的长度 (length)。一旦创建,数组的长度就不能改变了。
monsterHPs = new int[5]; // 创建一个长度为 5 的 int 数组,可以存放 5 个整数// 此时数组中的元素会被自动初始化为其类型的默认值 (int 为 0)
dayNames = new String[7]; // 创建一个长度为 7 的 String 数组// 引用类型的默认值为 null
- 声明并同时创建和初始化:
double[] prices = {19.9, 25.5, 9.8}; // 直接用花括号提供初始元素值,长度自动确定为3
char[] vowels = new char[] {'a', 'e', 'i', 'o', 'u'}; // 也可以这样写
- 访问数组元素:
数组的元素通过索引 (index) 来访问,索引从 0 开始,到数组长度 - 1
结束。
monsterHPs[0] = 100; // 给第一个元素(索引为0)赋值
monsterHPs[1] = 120;
// monsterHPs[5] = 80; // 错误!数组越界 (ArrayIndexOutOfBoundsException),因为最大索引是 4System.out.println("第一个怪物的血量:" + monsterHPs[0]); // 输出 100
System.out.println("第二个怪物的血量:" + monsterHPs[1]); // 输出 120
- 获取数组长度:
使用数组名.length
属性。
System.out.println("怪物数组的长度:" + monsterHPs.length); // 输出 5
- 遍历数组 (常用
for
循环):
System.out.println("所有怪物的血量:");
for (int i = 0; i < monsterHPs.length; i++) { // 使用经典 for 循环System.out.println("怪物 " + (i + 1) + ": " + monsterHPs[i]);
}System.out.println("一周的每一天:");
for (String day : dayNames) { // 使用增强型 for 循环 (如果只是读取元素)if (day == null) { // String 数组未赋值的元素是 nullSystem.out.println("未指定");} else {System.out.println(day);}
}
dayNames[0] = "星期一"; // 赋值示例
感知唤醒:
- 数组就像一个编号固定、格子大小统一的“储物柜”。
- 长度固定:一旦建好,柜子数量不能增减。这是数组的一个重要特性(也是局限性,后面我们会学习更灵活的“集合”)。
- 同种类型:每个格子只能放同样类型的“物品”。
- 索引从0开始:这是编程中非常常见的约定,务必牢记。
- 数组越界:试图访问不存在的“格子”(索引小于0或大于等于长度)会导致程序在运行时抛出
ArrayIndexOutOfBoundsException
异常,你的世界会因此“小规模崩塌”。
造物主的小结与展望:
通过这一章,你掌握了编排 Java 世界运行节奏和初步组织数据的核心法则:
- 理解了 Java 程序的基本骨架和
main
方法的入口作用。 - 学会了使用
if-else if-else
语句让你的世界能够根据条件做出不同的决策。 - 掌握了
while
和for
循环,使你的世界能够执行重复性的任务。 - 了解了
break
和continue
如何微调循环的流程。 - 初步接触了数组这种简单的数据集合,用于管理固定数量的同类型“事物”。
这些控制流语句和数组是构建更复杂逻辑和数据结构的基础。它们是你作为造物主,指挥世界万物按你的意愿行动和组织起来的“指挥棒”和“初级兵营”。
接下来,我们将正式进入第三章:构建世界的蓝图与实体 —— 类与对象,将这些基础的控制逻辑和数据组织方式,与更强大的面向对象思想结合起来,创造出真正富有“生命力”和“个性”的实体!
第三章:构建世界的蓝图与实体 —— 类与对象 (Classes & Objects)
在 Python 的世界里,你已经体会过“类 (class)”和“对象 (object)”的概念,它们是模拟现实事物的重要工具。在 Java 中,这种“面向对象”的思想被推向了极致,它是 Java 世界构建的核心法则。
1. 什么是“类”,什么是“对象”?—— 蓝图与建筑
- 类 (Class) - 造物的“蓝图”或“模具”:
- 想象你要在你的世界里创造一种新的“生物”——比如说,“精灵 (Elf)”。你需要先定义“精灵”这种生物共同拥有的特征(属性,比如:名字、年龄、魔法值)和它们都能做的动作(行为/方法,比如:施法、行走)。
- 这个定义“精灵是什么”的抽象描述,就是类。它是一张详细的“精灵设计图纸”,或者一个可以批量生产“精灵”的“模具”。它本身并不是一个具体的精灵,而是一个模板。
- 对象 (Object) - 蓝图的“实体”或“产品”:
- 有了“精灵”的设计图纸 (类),你就可以根据这张图纸创造出具体的一个个精灵实体了。比如,你可以创造一个名叫“莱戈拉斯”、年龄2931岁、魔法值很高的精灵;也可以创造另一个名叫“阿尔温”、年龄也很大的精灵。
- 这些具体存在的、独一无二的精灵个体,就是对象。每个对象都是根据同一个“类”这个蓝图创造出来的,所以它们都拥有类所定义的属性和行为,但每个对象的属性值(比如名字、具体年龄)可以是不同的。
感知唤醒:
- 类是抽象的,对象是具体的。
- 类定义了“是什么”,对象是“这一个”。
- 一个类可以创建出无数个对象,就像一张图纸可以用来建造无数栋同样的房子。
2. 用 Java “绘制蓝图”:定义一个类 (Defining a Class)
在 Java 中,我们使用 class
关键字来定义一个类。
// Java 代码:定义一个“汽车 (Car)”类
public class Car { // public class Car 是类的声明,Car 是类名// --- 属性 (Fields/Instance Variables) ---// 用来描述汽车的状态或特征String brand; // 品牌 (例如:"Toyota", "BMW")String color; // 颜色 (例如:"Red", "Black")int currentSpeed; // 当前速度 (例如:60 km/h)// --- 行为/方法 (Methods) ---// 用来描述汽车能做什么public void startEngine() { // public void startEngine() 是方法的声明System.out.println(brand + " 的引擎启动了!");}public void accelerate(int amount) {currentSpeed += amount; // 增加速度System.out.println(brand + " 加速了,当前速度:" + currentSpeed + " km/h");}public void brake() {currentSpeed = 0; // 刹车,速度归零System.out.println(brand + " 刹车了,已停止。");}public void displayInfo() {System.out.println("这是一辆 " + color + " 的 " + brand + ",当前速度:" + currentSpeed + " km/h。");}
}
感知细节:
public class Car { ... }
:public
: 这是一个“访问修饰符”,表示这个“汽车蓝图”是公开的,在你的世界的任何地方都可以看到和使用它来“造车”。(后面我们会详细了解其他访问修饰符)class
: 关键字,告诉 Java:“我要开始画一张蓝图了!”Car
: 你给这张蓝图起的名字,遵循大驼峰命名法。
- 属性 (Fields / Instance Variables):
String brand;
,String color;
,int currentSpeed;
- 这些是在类内部声明的变量,它们定义了每一个“汽车对象”将会拥有的数据。每个汽车对象都会有自己的一套
brand
,color
,currentSpeed
的值。 - 它们通常被称为“成员变量”或“实例变量”,因为它们属于类的每一个“实例”(对象)。
- 方法 (Methods):
public void startEngine() { ... }
,public void accelerate(int amount) { ... }
等。- 这些定义了“汽车对象”能够执行的动作。
public
: 同样是访问修饰符。void
: 表示这个方法执行完毕后,不返回任何结果。如果方法需要返回结果,这里会是返回结果的数据类型(比如int
,String
,boolean
等)。startEngine()
: 方法名,遵循小驼峰命名法。()
: 括号内可以包含“参数 (parameters)”,这是方法执行时需要从外界接收的信息。例如accelerate(int amount)
中的int amount
就是一个参数,表示加速的量。{ ... }
: 花括号内是“方法体”,包含了具体执行的指令。System.out.println(...)
: 这是 Java 中向“控制台”(你可以想象成一个信息输出窗口)打印文本的常用方法。
3. 根据蓝图“制造实体”:创建和使用对象 (Creating and Using Objects)
有了 Car
类的蓝图,我们就可以“制造”出具体的汽车对象了。
// Java 代码:在另一个地方(比如一个主程序入口)使用 Car 类
public class MyWorld { // 假设这是我们世界的主场景public static void main(String[] args) { // public static void main 是 Java 程序的入口点,先记住这个固定写法// --- 制造第一辆车 ---Car myRedCar = new Car(); // 使用 new 关键字创建 Car 类的对象// --- 给这辆车的属性赋值 ---myRedCar.brand = "比亚迪";myRedCar.color = "红色";myRedCar.currentSpeed = 0; // 初始速度为0// --- 让这辆车行动起来 (调用对象的方法) ---myRedCar.displayInfo(); // 输出:这是一辆 红色的 比亚迪,当前速度:0 km/h。myRedCar.startEngine(); // 输出:比亚迪 的引擎启动了!myRedCar.accelerate(50); // 输出:比亚迪 加速了,当前速度:50 km/hmyRedCar.brake(); // 输出:比亚迪 刹车了,已停止。System.out.println("--------------------"); // 分隔线// --- 制造第二辆车 ---Car myBlueCar = new Car();myBlueCar.brand = "特斯拉";myBlueCar.color = "蓝色";// myBlueCar.currentSpeed 默认是 int 类型的初始值 0,我们也可以不显式赋值myBlueCar.displayInfo(); // 输出:这是一辆 蓝色的 特斯拉,当前速度:0 km/h。myBlueCar.startEngine(); // 输出:特斯拉 的引擎启动了!myBlueCar.accelerate(80); // 输出:特斯拉 加速了,当前速度:80 km/h}
}
感知细节:
public static void main(String[] args)
: 这是 Java 程序的“创世起点”。当你运行一个 Java 程序时,JVM (java虚拟机)会首先寻找并执行这个main
方法。目前,你只需要知道,我们大部分的“剧情演示”都会从这里开始。Car myRedCar = new Car();
:Car myRedCar
: 声明一个类型为Car
的变量myRedCar
。这就像你在说:“我要一个能指向‘汽车实体’的‘遥控器’,这个遥控器叫myRedCar
。”new Car()
: 这是真正“制造汽车”的动作!new
: Java 中用于创建新对象的关键字。它会在“世界的内存空间”中开辟一块地方来存放这个新的汽车实体。Car()
: 调用Car
类的“构造方法 (Constructor)”。即使我们没有在Car
类中显式定义一个无参数的构造方法,Java 也会为我们提供一个默认的无参数构造方法,用于创建对象。
=
:将新创建的Car
对象的“地址”(或者说“引用”)赋值给变量myRedCar
。现在,myRedCar
这个“遥控器”就指向了那辆新制造的红色比亚迪。
- 访问属性和调用方法:
myRedCar.brand = "比亚迪";
: 使用“对象名.属性名”的方式来访问和修改对象的属性。myRedCar.startEngine();
: 使用“对象名.方法名()”的方式来调用对象的方法,让对象执行相应的行为。
感知唤醒:
new
是“创生”的咒语。 它从无到有变出一个具体的实体。- 变量 (如
myRedCar
) 只是一个“引用”或“标签”,它指向内存中真正的对象实体。 就像你给一只猫起了个名字,名字不是猫本身,但通过名字你可以找到那只猫。 - 每个对象都是独立的。
myRedCar
和myBlueCar
是两辆完全不同的车,它们各自拥有自己的品牌、颜色和速度。修改myRedCar
的属性不会影响myBlueCar
。
4. “构造器”的奥秘:对象诞生时的“初始化仪式” (constructor
- __init__
in Python)
在上面的例子中,我们创建对象后,再逐个给属性赋值,显得有些繁琐。Java 提供了“构造方法 (Constructor)”机制,让我们可以在创建对象的时候就方便地初始化它的属性。这非常像 Python 类中的 __init__
方法。
Java 构造方法的特点:
- 方法名必须与类名完全相同。
- 没有返回类型 (连
void
都不写)。
// Java 代码:改进 Car 类,添加构造方法
public class Car {String brand;String color;int currentSpeed;// --- 构造方法 (Constructor) ---// 这是一个可以接收品牌和颜色作为参数的构造方法public Car(String initialBrand, String initialColor) {System.out.println("一辆新的汽车正在被制造...");brand = initialBrand; // 将传入的品牌赋值给对象的 brand 属性color = initialColor; // 将传入的颜色赋值给对象的 color 属性currentSpeed = 0; // 新车速度默认为0System.out.println("汽车制造完成!品牌:" + brand + ",颜色:" + color);}// 我们仍然可以保留一个无参数的构造方法(如果需要的话)// 如果定义了带参数的构造方法,Java 不会自动提供默认的无参数构造方法了// 如果想用 new Car() 这样创建对象,就需要自己显式定义一个public Car() {System.out.println("一辆默认参数的汽车被制造了 (可能是通用型号)。");brand = "通用品牌";color = "白色";currentSpeed = 0;}// ... (其他方法 startEngine, accelerate, brake, displayInfo 保持不变) ...public void startEngine() { System.out.println(brand + " 的引擎启动了!"); }public void accelerate(int amount) { currentSpeed += amount; System.out.println(brand + " 加速了,当前速度:" + currentSpeed + " km/h"); }public void brake() { currentSpeed = 0; System.out.println(brand + " 刹车了,已停止。"); }public void displayInfo() { System.out.println("这是一辆 " + color + " 的 " + brand + ",当前速度:" + currentSpeed + " km/h。"); }
}// Java 代码:使用带构造方法的 Car 类
public class MyAdvancedWorld {public static void main(String[] args) {// 使用带参数的构造方法创建汽车Car luxuryCar = new Car("劳斯莱斯", "黑色"); // "劳斯莱斯" 传给 initialBrand, "黑色" 传给 initialColorluxuryCar.displayInfo(); // 输出:这是一辆 黑色的 劳斯莱斯,当前速度:0 km/h。luxuryCar.accelerate(100);System.out.println("--------------------");Car defaultCar = new Car(); // 调用无参数的构造方法defaultCar.displayInfo(); // 输出:这是一辆 白色的 通用品牌,当前速度:0 km/h。}
}
感知细节:
- 当我们执行
new Car("劳斯莱斯", "黑色");
时,Java 会自动寻找并调用Car
类中参数列表匹配的构造方法,即public Car(String initialBrand, String initialColor)
这个。 - 构造方法内部的
brand = initialBrand;
就是将我们传入的 “劳斯莱斯” 赋值给了新创建的luxuryCar
对象的brand
属性。 - 重要:如果你自己定义了任何构造方法(无论带不带参数),Java 就不会再自动提供那个默认的、无参数的构造方法了。 如果你还想通过
new Car();
(不带参数) 的方式创建对象,你就必须像上面例子中那样,自己显式地定义一个无参数的构造方法public Car() { ... }
。 - 一个类可以有多个构造方法,只要它们的参数列表不同(参数的个数、类型或顺序不同),这称为“方法重载 (Method Overloading)”,我们稍后会详细讨论。
造物主的小结与展望:
通过“类”这张蓝图,你现在已经掌握了在 Java 世界中创造具体“实体”(对象)的魔法。你知道了:
- 类是属性和行为的集合体,是创建对象的模板。
- 对象是类的具体实例,拥有独立的属性值。
- 使用
new
关键字和类的构造方法来创建对象。 - 通过“对象名.属性名”和“对象名.方法名()”来与对象交互。
这仅仅是面向对象世界的冰山一角。接下来,我们将深入探索:
this
** 关键字**:对象如何称呼“自己”。- 封装 (Encapsulation):如何更好地保护对象的内部状态。
- 继承 (Inheritance):如何基于现有蓝图创造出更具体的“亚种”蓝图。
- 多态 (Polymorphism):同一种行为在不同“物种”上的多样表现。
这些概念将让你手中的“造物术”更加强大和灵活,能够构建出更加复杂和生动的 Java 世界。准备好了吗?
第四章:对象的自我认知与世界的秩序 —— this
、封装、继承与多态
现在,你已经能够用 Java 的“类”作为蓝图,创造出各种“对象”实体了。但是,一个成熟的造物主不仅要能创造,更要懂得如何让这些实体拥有“自我意识”,如何维护世界的“秩序”,以及如何让世界更富有“多样性”和“层次感”。
1. “我,就是我!” —— this
关键字:对象的“身份证”
在 Python 中,类的方法的第一个参数通常是 self
,它代表了调用该方法的对象实例本身。Java 中也有一个类似的机制,那就是 this
关键字。
this
关键字在 Java 类的方法内部或构造方法内部使用,它代表当前正在执行代码的那个对象实例。
感知场景一:区分同名变量
当构造方法或方法的参数名与类的成员变量名相同时,我们需要用 this
来明确指出哪个是成员变量,哪个是参数。
public class Cat {String name; // 成员变量 nameString color; // 成员变量 color// 构造方法public Cat(String name, String color) { // 参数 name 和 color// 如果不用 this,编译器会混淆// name = name; // 这实际上是把参数 name 赋值给参数 name 自己,成员变量没变!// color = color; // 同上// 使用 this 来明确指定成员变量this.name = name; // 将参数 name 的值 赋给 当前对象的 name 属性this.color = color; // 将参数 color 的值 赋给 当前对象的 color 属性System.out.println("一只名叫 " + this.name + " 的 " + this.color + " 猫咪诞生了!");}public void introduce() {// 在方法内部,this 指向调用这个 introduce 方法的 Cat 对象System.out.println("大家好,我是 " + this.name + ",一只 " + this.color + " 的猫。");}public static void main(String[] args) {Cat tom = new Cat("汤姆", "灰色"); // 调用构造方法时,构造方法内的 this 就指向 tom 这个对象tom.introduce(); // 调用 introduce 方法时,方法内的 this 也指向 tom 这个对象Cat garfield = new Cat("加菲", "橘色");garfield.introduce();}
}
感知唤醒:
- 把
this
想象成对象在说“我的xx属性”或“我自己”。 this.name
表示“当前这个对象的name
属性”。- 当参数名和成员变量名一样时,
this
是区分它们的唯一可靠方式。如果不一样,比如构造方法写成public Cat(String catName, String catColor)
,那么name = catName;
就不需要this
了,因为没有歧义。但使用this
是一个好习惯,能增加代码可读性。
感知场景二:在一个构造方法中调用另一个构造方法
有时,一个类有多个构造方法,其中一个构造方法可以调用另一个构造方法来复用初始化代码。这时,this()
就派上用场了,它代表调用本类中的其他构造方法。
public class Dog {String name;int age;String breed; // 品种// 主构造方法,包含所有属性的初始化public Dog(String name, int age, String breed) {System.out.println("狗狗正在通过详细信息构造...");this.name = name;this.age = age;this.breed = breed;}// 第一个辅助构造方法,只传入名字和品种,年龄默认为0public Dog(String name, String breed) {// 使用 this() 调用本类中参数匹配的构造方法// 必须是构造方法中的第一条语句!this(name, 0, breed); // 调用 public Dog(String name, int age, String breed)System.out.println("狗狗通过名字和品种构造,年龄默认为0。");}// 第二个辅助构造方法,只传入名字,品种默认为“中华田园犬”,年龄默认为0public Dog(String name) {this(name, "中华田园犬"); // 调用 public Dog(String name, String breed)System.out.println("狗狗只通过名字构造,默认为中华田园犬,年龄为0。");}public void bark() {System.out.println(this.name + " 汪汪叫!");}public static void main(String[] args) {Dog dog1 = new Dog("旺财", 3, "金毛");dog1.bark();System.out.println("---");Dog dog2 = new Dog("小白", "萨摩耶");dog2.bark();System.out.println("---");Dog dog3 = new Dog("阿黄");dog3.bark();}
}
感知细节:
this(...)
必须作为构造方法中的第一条可执行语句。- 它使得构造逻辑更集中,避免了代码重复。
2. “封装”的智慧:保护你的造物,隐藏内部细节 (Encapsulation)
想象一下,你创造了一台精密的“魔法时钟”。你不希望任何人都能随意拨弄它内部的齿轮和发条,因为那样很容易把它弄坏。你只想提供一些简单的按钮,比如“设置时间”、“查看时间”。
这就是封装 (Encapsulation) 的思想:
- 隐藏内部实现细节:将对象的属性(数据)和实现这些属性操作的方法(逻辑)捆绑在一起,并对外部世界隐藏对象内部的复杂结构和不希望被直接访问的部分。
- 提供公共的访问接口:只暴露一些定义良好、受控制的方法(通常称为 getters 和 setters)供外部与对象交互。
为何要封装?
- 安全性:防止外部代码随意修改对象内部状态,导致不可预期的错误。就像你不会让用户直接修改银行账户的余额数字,而是提供“存款”和“取款”的操作。
- 模块化/低耦合:对象的内部实现可以自由修改,只要对外提供的接口不变,就不会影响到使用该对象的其他代码。这使得你的世界更容易维护和升级。
- 易用性:使用者不需要关心对象内部是如何工作的,只需要知道如何通过提供的接口来使用它即可。
Java 中实现封装的常用手段:
- **将属性声明为 **
private
:
private
是一个访问修饰符,表示该属性只能在当前类的内部被访问,外部类无法直接通过“对象名.属性名”的方式访问。 - 提供
public
的 getter 和 setter 方法:- Getter 方法:用于获取私有属性的值 (例如
getName()
)。 - Setter 方法:用于设置私有属性的值,并且可以在设置前进行有效性检查或执行其他逻辑 (例如
setName(String newName)
)。
- Getter 方法:用于获取私有属性的值 (例如
public class BankAccount {private String accountNumber; // 账号 (私有)private double balance; // 余额 (私有)// 构造方法public BankAccount(String accountNumber, double initialBalance) {this.accountNumber = accountNumber;if (initialBalance >= 0) {this.balance = initialBalance;} else {this.balance = 0;System.out.println("初始余额不能为负,已设置为0。");}}// --- Getter 方法 ---public String getAccountNumber() {return this.accountNumber; // 可以读取账号}public double getBalance() {return this.balance; // 可以查看余额}// --- Setter 方法 (通常不直接提供 setBalance,而是通过存款和取款操作) ---// 但如果需要修改账号(虽然不常见),可以提供一个 setter// public void setAccountNumber(String accountNumber) {// this.accountNumber = accountNumber;// }// --- 其他公共方法 ---public void deposit(double amount) { // 存款if (amount > 0) {this.balance += amount;System.out.println("存款 " + amount + " 成功。当前余额:" + this.balance);} else {System.out.println("存款金额必须大于0。");}}public void withdraw(double amount) { // 取款if (amount > 0 && amount <= this.balance) {this.balance -= amount;System.out.println("取款 " + amount + " 成功。当前余额:" + this.balance);} else if (amount <= 0) {System.out.println("取款金额必须大于0。");} else {System.out.println("余额不足,取款失败。");}}public static void main(String[] args) {BankAccount myAccount = new BankAccount("123456789", 1000.0);// System.out.println(myAccount.balance); // 错误!balance 是 private 的,不能直接访问System.out.println("我的账户余额:" + myAccount.getBalance()); // 通过 getter 获取myAccount.deposit(500);myAccount.withdraw(200);myAccount.withdraw(2000); // 尝试取款超额}
}
感知唤醒:
private
就像给你的“魔法时钟”内部零件上了一把锁。public
的 getter 和 setter 方法,以及其他业务方法(如deposit
,withdraw
),则是你精心设计在时钟外壳上的“操作按钮”和“显示屏”。- 封装让你的“造物”更健壮、更易于管理,也更符合现实世界中复杂系统的工作方式——我们使用手机,但不需要知道它内部芯片如何运作。
3. “继承”的力量:站在巨人的肩膀上,创造更丰富的物种 (Inheritance)
想象一下,你已经创造了“动物 (Animal)”这个通用的蓝图,定义了所有动物都有“吃 (eat)”和“睡 (sleep)”的行为。现在,你想创造更具体的动物,比如“狗 (Dog)”和“猫 (Cat)”。
“狗”和“猫”首先都是“动物”,所以它们自然也应该会“吃”和“睡”。但它们还有自己独特的行为,比如“狗会汪汪叫 (bark)”,而“猫会喵喵叫 (meow)”。
继承 (Inheritance) 机制允许你创建一个新的类(称为子类 Subclass 或 派生类 Derived Class),这个新类可以继承一个已存在的类(称为父类 Superclass 或 基类 Base Class)的属性和方法。子类不仅拥有父类的所有非私有成员,还可以添加自己特有的属性和方法,或者重写 (Override) 父类的方法以实现不同的行为。
Java 中使用 extends
关键字来实现继承。
// 父类 (Superclass / Base Class)
class Animal {String name;public Animal(String name) {this.name = name;System.out.println(this.name + " (一种动物) 诞生了。");}public void eat() {System.out.println(this.name + " 正在吃东西...");}public void sleep() {System.out.println(this.name + " 正在睡觉...");}
}// 子类 (Subclass / Derived Class) - Dog 继承自 Animal
class Dog extends Animal { // Dog is an AnimalString breed; // 狗特有的属性:品种public Dog(String name, String breed) {// 调用父类的构造方法来初始化从父类继承的属性 (如 name)// super(...) 必须是子类构造方法的第一条语句!super(name); // 调用 Animal 类的 public Animal(String name) 构造方法this.breed = breed;System.out.println("它是一只 " + this.breed + "。");}// Dog 特有的方法public void bark() {System.out.println(this.name + " (一只" + this.breed + ") 正在汪汪叫!");}// 重写 (Override) 父类的 eat 方法@Override // @Override 注解,告诉编译器这里是打算重写父类方法,如果写错了会有提示public void eat() {// super.eat(); // 可以选择性地调用父类的原始 eat 方法System.out.println(this.name + " (一只" + this.breed + ") 正在狼吞虎咽地吃狗粮!");}
}// 子类 (Subclass) - Cat 继承自 Animal
class Cat extends Animal { // Cat is an Animalpublic Cat(String name) {super(name); // 调用父类构造方法}// Cat 特有的方法public void meow() {System.out.println(this.name + " 正在喵喵叫~");}
}public class Zoo {public static void main(String[] args) {Dog myDog = new Dog("旺财", "金毛");myDog.eat(); // 调用的是 Dog 类中重写后的 eat() 方法myDog.sleep(); // 调用的是从 Animal 类继承来的 sleep() 方法myDog.bark(); // 调用 Dog 类特有的 bark() 方法System.out.println("---");Cat myCat = new Cat("咪咪");myCat.eat(); // 调用的是从 Animal 类继承来的 eat() 方法 (Cat 没有重写)myCat.sleep();myCat.meow();}
}
感知细节:
class Dog extends Animal
: 声明Dog
类继承自Animal
类。这意味着Dog
对象自动拥有Animal
类中定义的name
属性以及eat()
和sleep()
方法(只要它们不是private
的)。super(name);
: 在子类的构造方法中,使用super(...)
来调用父类的构造方法。这非常重要,因为父类可能有一些必要的初始化逻辑。super()
必须是子类构造方法中的第一条语句。- 方法重写 (Overriding):
- 子类可以提供一个与父类中某个方法具有完全相同的方法签名(方法名、参数列表、返回类型都相同,或者返回类型是父类方法返回类型的子类型)的新实现。
- 如
Dog
类中的eat()
方法重写了Animal
类的eat()
方法。当myDog.eat()
被调用时,执行的是Dog
版本的eat()
。 @Override
注解是一个好习惯,它能让编译器帮你检查是否真的构成了重写。
super.父类方法()
: 在子类重写的方法中,如果你还想调用父类中被重写的那个原始方法,可以使用super.eat();
这样的语法。
继承的好处:
- 代码复用:父类中通用的属性和方法不需要在每个子类中重复编写。
- 层次化结构:更好地组织和管理你的“造物”,形成“种属”关系,符合现实世界的分类。
- 可扩展性:当需要新的“物种”时,可以通过继承现有类来快速创建,并添加新特性。
4. “多态”的魅力:同一种呼唤,不同的回应 (Polymorphism)
多态 (Polymorphism),字面意思是“多种形态”。在面向对象编程中,它指的是不同类的对象对同一个消息(方法调用)做出不同的响应。
多态是建立在继承和方法重写的基础之上的。
核心理念:父类引用指向子类对象。
public class AnimalShow {// 这个方法接收一个 Animal 类型的参数// 但我们可以传入 Dog 对象或 Cat 对象,因为它们都是 Animal 的子类public static void makeSound(Animal animal) { // animal 可以是 Dog,也可以是 CatSystem.out.print(animal.name + " 发出的声音是:");// 关键:这里 animal.eat() 调用的到底是哪个 eat() ?// 如果 animal 实际指向的是 Dog 对象,就调用 Dog 的 eat()// 如果 animal 实际指向的是 Cat 对象(而Cat没有重写eat),就调用 Animal 的 eat()// 对于没有统一行为的 "makeSound",我们通常不在父类 Animal 中定义一个通用的 makeSound,// 而是在子类中定义各自的发声方法。// 但是,如果 Animal 有一个通用的 performAction 方法,子类可以重写它。// 更典型的多态例子是利用共同的父类方法,但子类有不同实现animal.eat(); // 演示多态性,会调用实际对象类型的 eat 方法}public static void main(String[] args) {Animal genericAnimal = new Animal("某种动物");Dog myDog = new Dog("点点", "斑点狗");Cat myCat = new Cat("花花");// 父类引用可以指向子类对象Animal pet1 = myDog; // pet1 的“声明类型”是 Animal,但它实际指向一个 Dog 对象Animal pet2 = myCat; // pet2 的“声明类型”是 Animal,但它实际指向一个 Cat 对象System.out.println("--- 通过父类引用调用方法 ---");pet1.eat(); // 实际调用的是 Dog 的 eat() (因为 myDog 重写了)pet2.eat(); // 实际调用的是 Animal 的 eat() (因为 myCat 没有重写)pet1.sleep(); // 调用 Animal 的 sleep()// pet1.bark(); // 错误!因为 pet1 的“声明类型”是 Animal,Animal 类没有 bark() 方法。// 编译器只认声明类型有什么方法。System.out.println("--- 将对象作为参数传递 ---");makeSound(genericAnimal);makeSound(myDog); // 传递 Dog 对象给接收 Animal 类型的方法makeSound(myCat); // 传递 Cat 对象// 如何调用子类特有的方法?需要“类型转换” (Casting)if (pet1 instanceof Dog) { // 先检查 pet1 是否真的是 Dog 类型或其子类型Dog specificDog = (Dog) pet1; // 将 Animal 类型的引用强制转换为 Dog 类型specificDog.bark(); // 现在可以调用 Dog 特有的方法了}}
}
感知唤醒:
- 编译时看左边,运行时看右边:
Animal pet1 = myDog;
- 编译时:编译器检查
pet1
这个变量时,只认它的声明类型Animal
。所以你只能通过pet1
调用Animal
类中定义的方法(或者从其父类继承来的)。你不能写pet1.bark()
,因为Animal
类没有bark()
方法。 - 运行时:当程序实际执行
pet1.eat()
时,Java 虚拟机会查看pet1
实际指向的那个对象(即myDog
这个Dog
对象)。如果Dog
类重写了eat()
方法,那么就会执行Dog
版本的eat()
;如果没重写,就会去父类Animal
中找eat()
方法执行。这就是多态的魔力!
instanceof
操作符:用于在运行时检查一个对象是否是某个特定类(或其子类)的实例,或者是否实现了某个接口。这在进行向下转型(父类引用转为子类引用)前非常重要,可以避免ClassCastException
(类型转换异常)。- 向上转型 (Upcasting):将子类对象赋值给父类引用(例如
Animal pet1 = myDog;
)。这是自动发生的,也是安全的。 - 向下转型 (Downcasting):将父类引用转换回子类引用(例如
Dog specificDog = (Dog) pet1;
)。这需要显式强制转换,并且有风险(如果pet1
实际指向的不是Dog
对象或其子类对象,就会抛出异常),所以通常配合instanceof
使用。
多态的好处:
- 灵活性和可扩展性:你可以编写更通用的代码(例如
makeSound(Animal animal)
方法),它可以处理多种不同类型的对象,只要它们都继承自同一个父类或实现了同一个接口。当将来添加新的Animal
子类时,这个方法无需修改就能处理新的动物类型。 - 简化代码:减少了大量的
if-else
或switch
判断对象类型的代码。
造物主的小结与展望:
这一章,你掌握了 Java 面向对象编程的四大支柱中的三个:
this
:让对象拥有了“自我”的参照。- 封装:为你的造物披上了“铠甲”,保护其核心,并提供有序的交互方式。
- 继承:让你能够构建出富有层次感的“物种体系”,实现代码的复用和扩展。
- 多态:赋予了你的世界“随物赋形”的灵动,让同样的行为可以有不同的表现。
这些概念是 Java 强大的根基。它们共同协作,使得我们可以构建出结构清晰、易于维护、高度灵活和可扩展的复杂系统。
结语
造物主,恭喜你!至此,我们一同在《Java 创世手记 - 基础篇(上)》的指引下,为你广袤的数字世界奠定了第一块坚实的基石。你不再仅仅是旁观者,你已经手握 Java 这柄初露锋芒的创世之锤,开始亲手勾勒和塑造你的想象。
回顾这段旅程,我们已经:
- 洞悉了 Java 作为创世工具的独特禀赋,理解了它为何能支撑起那些宏伟而严谨的数字城邦。
- 掌握了构成世界万物的基本“元素”与“法则”——从声明每一个“存在”(变量)的“身份”(数据类型),到驱动它们互动的“原力”(运算符)。
- 学会了编排世界事件的“剧本”——用程序的基本结构搭建舞台,用控制流(
if
,while
,for
)导演剧情的起承转合,并用数组初步管理了你的“造物”集群。 - 最重要的是,你推开了面向对象这扇神秘的大门,学会了用“类”这张精密的“蓝图”去定义世间万物的“本质”,并用“对象”这一“咒语”将它们一一唤醒,赋予其独立的“灵魂”(属性)与“行动力”(方法)。你甚至让这些“造物”拥有了初步的“自我意识”(
this
),并开始思考如何为它们构建“秩序井然的家园”(封装、继承与多态)。
你手中的工具不再是零散的符号,它们已经组合成了有意义的“咒文”,能够初步响应你的意志,模拟出世界的雏形。你已经感受到了从抽象概念到具象创造的喜悦,这便是“造物”的初步魅力。
然而,造物主的征途是星辰大海。我们刚刚完成了地基的铺设,但这仅仅是开始。眼前的世界虽然初具规模,但要让它真正繁荣、稳固并充满无限可能,我们还需要更精妙的“法则”和更强大的“工具”。
在即将开启的《Java 创世手记 - 基础篇(下)》
中,我们将一同探索:
- 如何定义超越具体“物种”的 “能力契约”与“行为规范” (接口与抽象类),让你的世界拥有更高的灵活性与扩展性?
- 如何为你的无数“蓝图”建立有序的 “疆域与国度” (包),并更精细地掌控它们之间的“外交规则”(访问修饰符)?
- 如何发掘那些不依赖于个体、属于整个“种群”的 “共享魔力” (
static
),以及那些一旦设定便 “永恒不变”的誓言 (final
)? - 当你的世界遭遇“风暴”与“意外”,如何构建更完善的 “应急预案” (高级异常处理)?
- 以及,当“数组”这个“初级兵营”不足以承载你日益壮大的“造物大军”时,如何运用 Java 提供的 强大“后勤系统” (集合框架核心)来高效管理它们?
造物主,请稍作休憩,回顾我们已共同铸就的基石。然后,怀揣着这份初窥门径的喜悦与对未知的渴望,让我们一同迈向下一段更加精彩的创世旅程。你手中的锤子将更加得心应手,你眼前的世界也将因此而更加波澜壮阔!
让我们的造物之旅,在坚实的基础上,继续高歌猛进!