嗨翻-Java-第三版-早期发布--全-

嗨翻 Java 第三版(早期发布)(全)

原文:zh.annas-archive.org/md5/61230db7f544ddbf7c504e73e6498107

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:打破表面:深入了解:快速入门

image

Java 带你去新的地方。从它谦逊的版本 1.02 发布到公众以来,Java 以其友好的语法、面向对象的特性、内存管理,以及最重要的——可移植性的承诺,吸引了程序员们。一次编写,到处运行的诱惑力太过强大。程序员们追随着这种承诺疯狂前行,抗击错误、限制,还有,哦对了,那个慢如蜗牛的事实。但那是很久以前的事了。如果你刚开始接触 Java,你很幸运。我们中的一些人不得不在雪地里徒步走五英里,双脚光着(上坡),才能使最微不足道的应用程序运行起来。但是,,为什么,可以骑着今天更流畅、更快速、更易读易写的 Java。

Java 的工作方式

目标是编写一个应用程序(在这个例子中是一个互动派对邀请),并使它能够在你的朋友所拥有的任何设备上运行。

image

你在 Java 中会做什么

你将输入源代码文件,使用 javac 编译器编译它,然后在 Java 虚拟机上运行编译后的字节码。

image

注意

(注意:这是教程……片刻后你将开始编写真正的代码,但现在,我们只是希望你能对所有内容如何结合有所感觉。

换句话说,这页上的代码并不完全真实;不要试图编译它。)

Java 的简史

Java 最初发布(有人说是“逃逸”)于 1996 年 1 月 23 日。它已经有超过 25 年的历史了!在前 25 年里,Java 语言不断发展,Java API 也显著增长。据我们最好的估计,过去 25 年里编写了超过 17 万亿行的 Java 代码。在编程过程中,你肯定会遇到一些相当古老的 Java 代码,也会碰到一些更新的代码。Java 以其向后兼容性而闻名,所以旧代码可以在新的 JVM 上完全正常运行。

在本书中,我们通常会从使用较旧的编码风格开始(记住,在“现实世界”中你可能会遇到这样的代码),然后我们会介绍新的编码风格。

同样地,我们有时会展示 Java API 中的旧类,然后再展示更新的替代方案。

image

速度和内存使用

当 Java 刚发布时,它很慢。但很快,HotSpot VM 和其他性能增强工具应运而生。虽然 Java 不是市面上最快的语言,但它被认为是一种非常快的语言——几乎和像 C 和 Rust 这样的语言一样快,比大多数其他语言都快得多

Java 有一个神奇的超级能力——JVM。Java 虚拟机可以在代码运行时优化你的代码,因此可以创建非常快速的应用程序,而无需编写专门的高性能代码。

但是——完全披露——与 C 和 Rust 相比,Java 使用了大量内存。

问:Java 版本的命名约定很令人困惑。有 JDK 1.0,1.2,1.3,1.4,然后跳到 J2SE 5.0,然后变成 Java 6,Java 7,上次我查看时,Java 已经到了 Java 18。发生了什么?

**答:过去 25 年来,版本号变化很大!我们可以忽略字母(J2SE/SE),因为现在基本上不再使用。数字稍微复杂一些。

从技术上讲,Java SE 5.0 实际上是 Java 1.5。对于 6(1.6)、7(1.7)和 8(1.8)也是如此。理论上,Java 仍然是 1.x 版本,因为新版本向后兼容,一直回溯到 1.0。

然而,版本号与每个人使用的名称不同有点令人困惑,因此从 Java 9 开始的官方版本号只是数字,没有“1”前缀;即,Java 9 实际上是版本 9,而不是版本 1.9。

在本书中,我们将使用常见的约定 1.0–1.4,然后从 5 开始,我们将去掉“1”前缀。

自 2017 年 9 月发布 Java 9 以来,每隔六个月就会发布一个 Java 版本,每个版本都有一个新的“主要”版本号,所以我们从 9 迅速跃升到 18!

Java 中的代码结构

image

在源文件中,放置一个类。

在一个类中,放置方法。

在一个方法中,放置语句。

源文件中应该放什么?

源代码文件(扩展名为.java)通常包含一个类定义。代表程序的一部分,尽管一个非常小的应用程序可能只需要一个类。类必须放在一对大括号内。

image

类中应该放什么?

一个类有一个或多个方法。在 Dog 类中,bark方法将包含狗应该吠叫的指令。您的方法必须在一个类内部声明(换句话说,在类的大括号内部)。

image

方法中应该放什么?

在方法的大括号内,编写指示该方法如何执行的指令。方法代码基本上是一组语句,目前您可以将方法视为一种函数或过程。

image

类的解剖

当 JVM 开始运行时,它会在命令行中给出的类中查找。然后它开始寻找一个特别编写的方法,看起来像:

  public static void main (String[] args) {
     // your code goes here
  }

接下来,JVM 运行主方法的大括号{ }之间的所有内容。每个 Java 应用程序至少必须有一个,至少必须有一个main方法(不是每个一个 main;只是每个应用程序一个 main)。

image

现在不用担心记住任何东西...本章只是为了让您入门。

编写带有main()的类

在 Java 中,所有内容都放在一个中。您将键入源代码文件(扩展名为.java),然后将其编译为一个新的类文件(扩展名为.class)。当您运行程序时,实际上是在运行一个类。

运行程序意味着告诉 Java 虚拟机(JVM)“加载 **MyFirstApp** 类,然后开始执行它的 **main()** 方法。一直运行,直到 main 中的所有代码都执行完毕。”

在 第二章 中,《前往对象之城》,我们将深入讨论整个 的事情,但目前,你需要问的唯一问题是,我如何编写 Java 代码以便运行? 一切都始于 main()

main() 方法是你的程序开始运行的地方。

无论你的程序有多大(换句话说,你的程序使用了多少 ),都必须有一个 main() 方法来启动整个过程。

imageimageimage

今晚的讲座:编译器与 JVM 就“谁更重要?”展开争斗

Java 虚拟机 编译器
什么,你在开玩笑? HELLO。我就是 Java。我才是真正让程序运行起来的人。编译器只是给你一个文件。仅此而已。你可以打印出来用作壁纸、点燃火柴、铺鸟笼,反正这个文件不会做任何事情,除非我在那里运行它。
我不喜欢那种语气。
还有一件事,编译器没有幽默感。不过,如果你整天都在检查瑕疵一点点的语法错误……
不好意思,但没有 ,你到底要运行什么?Java 被设计为使用字节码编译器是有原因的。如果 Java 是一种纯解释语言,在运行时虚拟机必须从文本编辑器源代码直接翻译,那么 Java 程序将运行得极其缓慢。
我并不是说你完全没用。但真的,你到底做了什么?说真的。我完全不知道。程序员可以手写字节码,我也能接受。伙计,你可能很快就失业了。
不好意思,但那是相当无知(更不用说 傲慢 了)的观点。虽然 理论上 —— 理论上 你可以运行任何正确格式的字节码,即使它不是由 Java 编译器生成的,但实际上这是荒谬的。手写字节码的程序员就像画度假照片一样,而不是拍照片 —— 当然,这是一门艺术,但大多数人更喜欢用他们的时间做其他事情。如果你能称呼我为“伙计”,我会很感激的。
(关于幽默感的事情我就此结束。)但你还是没有回答我的问题,你到底做什么?
记住,Java 是一种强类型语言,这意味着我不能允许变量保存错误类型的数据。这是一个关键的安全功能,我能够在它们传递给你之前阻止绝大多数违规行为。而且我也——
但有些问题仍然存在!我可以抛出 ClassCastException,有时人们会试图将错误类型的东西放入声明为保存其他内容的数组中,还有...
对不起,我还没说完呢。是的,运行时可能会出现一些数据类型异常,但其中一些必须允许以支持 Java 的另一个重要功能——动态绑定。在运行时,Java 程序可以包含原始程序员甚至不知道的新对象,因此我必须允许一定的灵活性。但我的工作是阻止任何在运行时永远不可能成功的事情。通常情况下,我可以判断某些事情是否会失败,例如,如果程序员意外地尝试将 Button 对象用作 Socket 连接,我会检测到并保护他们免受在运行时造成伤害。
好的。当然。但是安全性呢?看看我做的所有安全措施,而你只是在检查分号?哦哦大的安全风险!多亏有你啊!
对不起,但正如他们所说的,我是第一道防线。正如我之前描述的数据类型违规可能会在程序中造成严重后果。我也是防止访问违规的人,例如试图调用私有方法或更改从安全角度上讲绝不能更改的方法的人。我阻止人们触及他们不应看到的代码,包括试图访问另一个类的关键数据的代码。要描述我的工作的重要性可能需要数小时,甚至数天。
无论如何,我也必须做同样的事情,仅仅是为了确保在运行之前没有人偷偷改变了字节码。
当然,但正如我之前所说,如果我没有防止可能的问题中的 99%,你们会完全停摆。看起来我们时间不多了,所以我们得以后再聊这个问题。
哦,你可以指望它。伙计

主方法中可以说些什么?

一旦进入 main(或任何方法),就开始有趣了。你可以说大多数编程语言中正常说的话来让计算机做些什么

你的代码可以告诉 JVM:

图片

Images 做一些事情

语句: 声明、赋值、方法调用等。

int x = 3;
String name = "Dirk";
x = x * 17;
System.out.print("x is " + x);
double d = Math.random();
// this is a comment

Images 一遍又一遍地做某事

循环: forwhile

while (x > 12) {
  x = x - 1;
}

for (int i = 0; i < 10; i = i + 1) {
  System.out.print("i is now " + i);
}

Images 在这种条件下做一些事情

分支: if/else 测试

if (x == 10) {
  System.out.print("x must be 10");
} else {
  System.out.print("x isn't 10");
}
if ((x < 3) && (name.equals("Dirk"))) {
  System.out.println("Gently");
}
System.out.print("this line runs no matter what");

图片

循环和循环...

Java 有很多循环结构:while、do-while 和for,最古老的是for。你将在本书的后面章节中详细了解循环。现在让我们从 while 开始。

语法(更不用说逻辑)是如此简单,你可能已经睡着了。只要某个条件为真,你就在循环内执行所有操作。循环块由一对花括号界定,因此你想重复的任何内容都必须在该块内。

循环的关键在于条件测试。在 Java 中,条件测试是一个产生布尔值的表达式,换句话说,它要么是true,要么是false

如果你说类似于,“当冰淇淋在浴缸中为真时,继续舀取”,你有一个明确的布尔测试。浴缸里要么冰淇淋,要么没有。但如果你说,“当鲍勃继续舀取时”,你就没有真正的测试。要使其工作,你需要将其更改为类似于,“当鲍勃打呼噜时…”或“当鲍勃穿格子衬衫时…”

简单的布尔测试

你可以通过检查变量的值来进行简单的布尔测试,使用如下比较运算符:

< (小于)

> (大于)

==(相等)(是的,这是两个等号)

注意赋值运算符(单个等号)和等于运算符(两个等号)之间的区别。许多程序员在想要输入==时不小心输入了=。(但不包括你。)

int x = 4; // assign 4 to x
while (x > 3) {
  // loop code will run because
  // x is greater than 3
  x = x - 1; // or we’d loop forever
}
int z = 27; //
while (z == 17) {
  // loop code will not run because
  // z is not equal to 17
}

while 循环的示例

image

条件分支

在 Java 中,if测试基本上与while循环中的布尔测试相同——只不过你会说,“还有巧克力时…”而不是“如果还有巧克力…”

image

上述代码只有当条件(x等于 3)为真时才执行打印“x 必须是 3”的行。不管条件是否为真,“这将无论如何运行”都会运行。因此,根据x的值,要么打印一条语句,要么打印两条语句。

但是,我们可以在条件中添加else,这样我们可以说类似于,“如果还有巧克力,继续编码,否则(否则)获取更多巧克力,然后继续…”

image

编写一个严肃的业务应用程序

image

让我们把你新学到的 Java 技能用于实际。我们需要一个包含main()intString变量、一个while循环和一个if测试的类。稍加改进,你将很快能够构建业务后端。但是在查看本页上的代码之前,想一想如何编写经典的儿童最爱,“10 个绿色瓶子”。

public class BottleSong {
  public static void main(String[] args) {
    int bottlesNum = 10;
    String word = "bottles";

    while (bottlesNum > 0) {

      if (bottlesNum == 1) {
        word = "bottle"; // singular, as in ONE bottle.
      }

      System.out.println(bottlesNum + " green " + word + ", hanging on the wall");
      System.out.println(bottlesNum + " green " + word + ", hanging on the wall");
      System.out.println("And if one green bottle should accidentally fall,");
      bottlesNum = bottlesNum - 1;

      if  (bottlesNum > 0) {
         System.out.println("There'll be " + bottlesNum +
                            " green " + word + ", hanging on the wall");
      } else {
        System.out.println("There'll be no green bottles, hanging on the wall");
      } // end else
    } // end while loop
  } // end main method
} // end class

我们的代码还有一个小缺陷。它编译和运行,但输出并不完全完美。看看你能否发现这个缺陷并修复它。

周一早晨,鲍勃的启用 Java 的房子

image

周一早上 8:30,鲍勃的闹钟响了,就像每个工作日一样。但鲍勃度过了疯狂的周末,伸手按下了贪睡按钮。那时动作开始了,启用 Java 的设备开始运行…

图片

首先,闹钟向咖啡机发送消息“嘿,那个极客又睡过头了,咖啡延迟 12 分钟。”

咖啡机向 Motorola^(TM)烤面包机发送消息,“别烤面包了,鲍勃在睡觉。”

闹钟然后向鲍勃的安卓手机发送消息,“给鲍勃 9 点打电话,告诉他我们有点晚了。”

图片图片

最后,闹钟向山姆(山姆是狗)的无线项圈发送消息,带着太熟悉的信号,意味着“拿报纸,但别指望散步。”

图片

几分钟后,闹钟再次响起。鲍勃再次按下了贪睡按钮,家用电器开始交谈。最后,闹钟第三次响起。但就在鲍勃伸手按贪睡按钮时,时钟向山姆的项圈发送了“跳起来并吠叫”的信号。震惊地完全清醒,鲍勃起床了,感激他的 Java 技能和即兴的网购增强了他生活中的日常例行事务。

他的吐司被烤了。

他的咖啡冒着热气。

他的报纸在等待着。

图片

又是一个美好的Java 智能家居的早晨。

图片

好吧,瓶子歌曲并不是真正的严肃商业应用。还需要一些实际的东西来展示给老板吗?看看词组生成器的代码。

注意

注意:当你将这些内容输入编辑器时,让代码自己换行!在输入字符串(在“引号”之间的内容)时,永远不要按回车键,否则它无法编译。因此,你在本页看到的连字符是真实的,你可以输入它们,但在关闭字符串之后才按回车键。

public class PhraseOMatic {
  public static void main (String[] args) {

    ![Images](https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hd1st-java-3e/img/1circlea.png) // make three sets of words to choose from. Add your own!
    String[] wordListOne = {"agnostic", "opinionated",
   "voice activated", "haptically driven", "extensible",
   "reactive", "agent based", "functional", "AI enabled",
   "strongly typed"};

     String[] wordListTwo = {"loosely coupled", "six sigma",
   "asynchronous", "event driven", "pub-sub", "IoT", "cloud
   native", "service oriented", "containerized", "serverless",
   "microservices", "distributed ledger"};

     String[] wordListThree = {"framework", "library",
    "DSL", "REST API", "repository", "pipeline", "service
    mesh", "architecture", "perspective", "design",
    "orientation"};

    ![Images](https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hd1st-java-3e/img/1circleb.png) // find out how many words are in each list
    int oneLength = wordListOne.length;
    int twoLength = wordListTwo.length;
    int threeLength = wordListThree.length;

    ![Images](https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hd1st-java-3e/img/1circlec.png) // generate three random numbers
    java.util.Random randomGenerator = new java.util.Random();
    int rand1 = randomGenerator.nextInt(oneLength);
    int rand2 = randomGenerator.nextInt(twoLength);
    int rand3 = randomGenerator.nextInt(threeLength);

    ![Images](https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hd1st-java-3e/img/1circled.png) // now build a phrase
    String phrase = wordListOne[rand1] + " " +
    wordListTwo[rand2] + " " + wordListThree[rand3];

    ![Images](https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hd1st-java-3e/img/1circlee.png) // print out the phrase
    System.out.println("What we need is a " + phrase);
  }
}

词组生成器

工作原理

简而言之,该程序制作了三个单词列表,然后随机从每个列表中选择一个单词,并打印出结果。如果你不完全理解每一行发生了什么,不要担心。天哪,你还有整本书要读,所以放松点。这只是一个从 30,000 英尺外部瞄准杠杆范式的快速浏览。

1. 第一步是创建三个 String 数组——这些容器将保存所有单词。声明和创建数组很容易;这里是一个小的例子:

 String[] pets = {"Fido", "Zeus", "Bin"};

每个单词都用引号括起来(因为所有好的字符串都必须如此),并用逗号分隔。

2. 对于三个列表(数组)中的每一个,目标是选择一个随机单词,因此我们必须知道每个列表中有多少单词。如果列表中有 14 个单词,那么我们需要一个介于 0 和 13 之间的随机数(Java 数组是从零开始的,因此第一个单词位于位置 0,第二个单词位于位置 1,最后一个单词在一个 14 元素数组中的位置为 13)。非常方便的是,Java 数组很乐意告诉你它的长度。你只需要问。在宠物数组中,我们会说:

 int x = pets.length;

x现在将保存值 3。

3. 我们需要三个随机数。Java 提供了几种生成随机数的方式,包括 java.util.Random(稍后我们会看到为什么这个类名前面加了 java.util)。**nextInt()**方法返回一个介于 0 和我们指定的某个数之间的随机数,不包括我们给定的数。因此,我们将给它列表中的元素个数(数组长度)。然后将每个结果分配给一个新变量。我们也可以要求一个介于 0 和 5 之间的随机数,不包括 5:

 int x = randomGenerator.nextInt(5);

4. 现在我们来构建这个短语,从每个列表中选择一个单词并将它们组合在一起(同时在单词之间插入空格)。我们使用“+”运算符,它 连接(我们更喜欢更技术性的 组合)String 对象。要从数组中获取元素,您需要使用该数组的索引号(位置)来指定您想要的东西:

 String s = pets[0]; // s is now the String "Fido"
 s = s + " " + "is a dog"; // s is now "Fido is a dog"

5. 最后,我们将短语打印到命令行,然后...voil...!我们在市场营销中

练习

图片

码头磁铁

图片

冰箱上混乱地放置了一个工作中的 Java 程序。你能重新排列这些代码片段,使其成为一个产生下面列出的输出的工作 Java 程序吗?有些花括号掉在了地板上,它们太小了,无法捡起,所以你可以随意添加多少个!

图片

Output:

图片

图片 答案在“练习解答”中。

成为编译器

图片

每个 Java 文件都代表一个完整的源文件。你的任务是扮演编译器,确定这些文件是否会编译。如果它们不能编译,你会如何修复?

图片 答案在“练习解答”中。

A

class Exercise1a {
  public static void main(String[] args) {
    int x = 1;
    while (x < 10) {
      if (x > 3) {
        System.out.println("big x");
      }
    }
  }
}

B

public static void main(String [] args) {
  int x = 5;
  while ( x > 1 ) {
    x = x - 1;
    if ( x < 3) {
      System.out.println("small x");
    }
  }
}

C

class Exercise1c {
  int x = 5;
  while (x > 1) {
    x = x - 1;
    if (x < 3) {
      System.out.println("small x");
    }
  }
}

图片

JavaCross

让你的右脑也有事情做。

这是您的标准填字游戏,但几乎所有的解答单词都来自第一章。为了保持清醒,我们还添加了一些来自高科技世界的(非 Java)单词。

图片

Across

4. 命令行调用者

6. 再次回来?

8. 无法双向运行

9. 笔记本电脑电源的缩写

12. 数字变量类型

13. 芯片的缩写

14. 说一些话

18. 相当一群角色

19. 宣布一个新类或方法

21. 提示作用何在?

Down

1. 不是整数(或 ______ 你的船)

2. 空手而回

3. 公开房屋

5. '东西'持有者

7. 直到态度改善

10. 源代码消费者

11. 无法确定

13. 程序员和运营部门

15. 令人震惊的修改器

16. 只需要一个

17. 如何完成任务

20. 字节码消费者

图片 答案在“JavaCross”中。

混合信息

image

下面列出了一个简短的 Java 程序。程序中缺少一个代码块。你的挑战是将候选代码块(左侧)与插入后的输出进行匹配。不会使用所有输出行,某些输出行可能会重复使用。用线条连接代码块与它们匹配的命令行输出。

imageimage

注意

将每个候选者与可能的输出之一匹配

Images Answers in “Mixed Messages”。

image

Pool Puzzle

image

你的工作是从池中提取代码片段并将它们放入代码中的空行。每个片段只能使用一次,你不需要使用所有的片段。你的目标是创建一个能够编译、运行并输出所列结果的类。别被愚弄了——这比看起来要难。

Images Answers in “Pool Puzzle”。

输出

image

注意

注意:每个池中的片段只能使用一次!

class PoolPuzzleOne {
  public static void main(String [] args) {
    int x = 0;

    while ( __________ ) {

       _____________________________
       if ( x < 1 ) {
         ___________________________
       }
       _____________________________

       if ( __________ ) {
         ____________________________

         ___________
       }
       if ( x == 1 ) {

        ____________________________
       }
       if ( ___________ ) {

         ____________________________
       }
       System.out.println();

       ____________
    }
  }
}

image

练习解答

image

磨砺你的铅笔

(来自“there are no Dumb Questions”)

public class DooBee {
  public static void main(String[] args) {
    int x = 1;
    while (x < 3) {
      System.out.print("Doo");
      System.out.print("Bee");
      x = x + 1;
    }
    if (x == 3) {
      System.out.print("Do");
    }
  }
}

Code Magnets

(来自“Code Magnets”)

class Shuffle1 {
  public static void main(String[] args) {

    int x = 3;
    while (x > 0) {

      if (x > 2) {
        System.out.print("a");
      }

      x = x - 1;
      System.out.print("-");

      if (x == 2) {
       System.out.print("b c");
      }

      if (x == 1) {
        System.out.print("d");
        x = x - 1;
      }
    }
  }
}

image

BE the Compiler

(来自“BE the Compiler”)

A

image

B

image

C

image

Pool Puzzle

(来自“Pool Puzzle”)

image

class PoolPuzzleOne {
  public static void main(String [] args) {
    int x = 0;

  while ( x < 4 ) {

    System.out.print("a");
    if ( x < 1 ) {
       System.out.print(" ");
    }
    System.out.print("n");

    if ( x > 1 ) {
       System.out.print(" oyster");
       x = x + 2;
    }
    if ( x == 1 ) {
       System.out.print("noys");
    }
    if ( x < 1 ) {
       System.out.print("oise");
   }
   System.out.println();

   x = x + 1;
   }
 }
}

image

JavaCross

(来自“JavaCross”)

image

Mixed Messages

(来自“Mixed Messages”)

imageimage

第二章:去 Objectville 旅行:类和对象

图片

我听说过会有对象。 在第一章中,我们把所有的代码都放在了 main()方法里。那并不完全是面向对象的。事实上,那根本不是面向对象。好吧,我们确实使用了一些对象,比如短语生成器中的字符串数组,但我们并没有真正开发出自己的对象类型。所以现在我们必须离开那个过程式的世界,摆脱 main(),开始创造一些属于我们自己的对象。我们将看看是什么让 Java 中的面向对象开发如此有趣。我们将看看类和对象的区别。我们将看看对象如何能够改变你的生活(至少是你编程生活的一部分。对于你的时尚品味我们就没有什么办法了)。警告:一旦你到达对象之城,你可能再也不想回去了。给我们寄张明信片吧。

椅子战争

(或者说,对象如何改变你的生活)

图片

从前在一个软件公司里,两个程序员拿到了同样的规格说明书,并被告知“开始吧”。一个非常讨厌的项目经理强迫这两位程序员竞争,承诺谁先交付就能得到一把 Aeron™椅子和一个可调节高度的站立式桌子,就像硅谷的技术人员一样。劳拉,程序式程序员,和布拉德,面向对象开发者,都知道这将是小菜一碟。

坐在她的(不可调节的)桌子前,劳拉自言自语道,“这个程序需要什么?我们需要哪些过程?” 她自问自答,“rotateplaySound。” 于是她着手构建这些过程。毕竟,一个程序不就是一堆过程吗?

布拉德与此同时在咖啡店里踱步,心里想着,“这个程序中有哪些要素...主要的参与者是谁?” 他首先想到了形状。当然,还有其他他考虑到的东西,比如用户、声音和点击事件。但是他已经有了这些部分的代码库,所以他把注意力集中在构建形状上。继续阅读,看看布拉德和劳拉是如何构建他们的程序的,并且回答你燃起的问题的答案,“那么,谁得到了 Aeron 和桌子?”

图片

在劳拉的桌子上

就像以前做了无数次一样,劳拉开始编写她的重要程序。她很快就写出了rotateplaySound

  rotate(shapeNum) {
    // make the shape rotate 360°
  }
  playSound(shapeNum) {
    // use shapeNum to lookup which
    // AIF sound to play, and play it
  }

在咖啡店里的布拉德的笔记本电脑前

布拉德为每种形状写了一个

图片

劳拉觉得她已经做得很好了。她几乎可以感受到 Aeron 的铁框椅在她下面……

但等等!规格发生了变化。

“好吧,严格来说劳拉,你是第一个,”经理说,“但是我们只需要在程序中增加一件小事情。对像你们这样的优秀程序员来说,这不是问题。”

“如果我每次听到这种话都能得到一角钱,我早就富了,” Laura 想着,知道规格变更没问题是一个幻想。“然而 Brad 看起来却奇怪地宁静。怎么回事?” 尽管如此,Laura 仍然坚持她的核心信念,即 OO 的方式虽然可爱,但速度慢。如果你想改变她的想法,你得从她冰冷,死去的,腕管综合征的手中夺走。

image

Laura 的办公桌回来了。

旋转过程仍然可以工作;代码使用查找表将 shapeNum 匹配到实际形状图形。但是playSound 将不得不改变

   playSound(shapeNum) {
     // if the shape is not an amoeba,
       // use shapeNum to lookup which
       // AIF sound to play, and play it
     // else
       // play amoeba .mp3 sound
  }

事实证明这并不是什么大不了的事情,但是她依然觉得触碰之前测试过的代码有些不安作为一个人,应该知道,无论项目经理说什么,规格总是会变

在 Brad 的便携式电脑上,在海滩上

Brad 笑了笑,啜饮着他的水果冰沙,编写了一个新类。有时,他最喜欢的是 OO 的一点就是他不必再触碰他已经测试和交付过的代码。"灵活性,可扩展性,..." 他沉思着,回想起 OO 的好处。

image

Laura 在 Brad 之前仅仅几分钟递交了。

(哈!那些 OO 废话也就到此为止了。)但是当那位真正让人讨厌的项目经理带着失望的口吻说道:“哦,不,不是分子应该旋转的方式…”时,Laura 脸上的笑容消失了。

结果,两个程序员的旋转代码都写成了这样:

1. 确定围绕形状的矩形。

2. 计算该矩形的中心,并围绕该点旋转形状。

image

但是分子形状应该围绕一个端点旋转,就像时钟的指针一样。

“我完蛋了,” Laura 想着,想象着烤焦的 Wonderbread™。“虽然,嗯嗯嗯。我可以在旋转过程中再添加一个 if/else,然后仅仅为分子硬编码旋转点代码。那可能不会造成什么破坏。” 但是她脑后的小声音说,“大错特错。你真的认为规格不会再次变化?”

imageimage

Laura 的办公桌回来了。

她觉得她最好为旋转过程添加旋转点参数。很多代码都受到了影响。测试,重新编译,再从头来过。过去工作正常的事情,现在却不行了。

  rotate(shapeNum, xPt, yPt) {
    // if the shape is not an amoeba,
      // calculate the center point
      // based on a rectangle,
      // then rotate
    // else
      // use the xPt and yPt as
      // the rotation point offset
      // and then rotate
  }

在 Brad 的便携式电脑上,在 Telluride 蓝草音乐节的草坪椅上

Brad 只在分子类中修改了旋转方法,但从未触碰过经过测试、工作并编译过的其他部分的代码。为了给分子添加一个旋转点,他添加了一个属性,所有的分子都将拥有这个属性。他修改,测试并在一个 Bela Fleck 的表演期间通过免费的音乐节 WiFi 交付了修订后的程序。

image

所以,OO 男孩 Brad 得到了椅子和办公桌,对吧?

image

不要着急。Laura 发现了 Brad 方法中的一个缺陷。而且,由于她确信如果她得到椅子和桌子,她也将是下一个晋升的候选人,所以她必须扭转这种局面。

LAURA: 你这里有重复的代码!旋转过程在所有四个 Shape 事物中都有。

BRAD: 这是一个方法,不是过程。它们是,不是事物

LAURA: 随便吧。这是一个愚蠢的设计。你必须维护四个不同的旋转“方法”。这怎么可能是好的?

BRAD: 哦,我猜你没看到最终设计。让我向你展示面向对象的继承是如何工作的,Laura。

image

你可以理解为,“Square 继承自 Shape”,“Circle 继承自 Shape”,以此类推。我从其他形状中移除了 rotate() 和 playSound(),所以现在只需维护一个副本。

Shape 类被称为其他四个类的超类。其他四个类是 Shape 的子类。子类继承超类的方法。换句话说,如果 Shape 类具有某些功能,那么子类就会自动获得相同的功能

变形虫的 rotate() 怎么办?

LAURA: 这不就是问题所在吗?那个变形虫形状有完全不同的旋转和播放声音的过程?

BRAD: 方法。

LAURA: 随便吧。如果 Amoeba 从 Shape 类“继承”其功能,它怎么可能做出不同的事情?

BRAD: 那是最后一步。Amoeba 类覆盖了 Shape 类的方法。然后在运行时,JVM 知道当有人告诉 Amoeba 旋转时应该运行哪个 rotate() 方法。

imageimage

LAURA: 如何“告诉”变形虫做某事?难道你不必调用过程,对不起——方法,然后告诉它旋转的东西吗?

BRAD: 这就是面向对象的很酷的地方。当需要例如让三角形旋转时,程序调用(调用)三角形对象上的 rotate() 方法。程序的其余部分实际上并不知道或关心 三角形是如何做到的。而当你需要向程序中添加新内容时,只需为新对象类型编写一个新类,这样新对象将拥有它们自己的行为

image

悬念让我无法忍受。谁得到了椅子和桌子?

image

来自二楼的 Amy。

(众所周知,项目经理把规范交给了三个程序员。Amy 因为她在不与同事争论的情况下继续进行面向对象编程而更快地完成了项目)。

当你设计一个类时,考虑将从该类类型创建的对象。考虑:

  • 对象所知道的事情

  • 对象所做的事情

image

对象关于自身知道的事情称为

  • 实例变量

对象可以做的事情被称为

  • 方法

image

对象了解自身的内容称为实例变量。它们表示对象的状态(数据),并且可以为该类型的每个对象具有唯一值。

将实例视为另一种称呼对象。

对象能够的事情称为方法。当你设计一个类时,你会考虑对象需要了解的数据,并设计操作该数据的方法。对象通常具有读取或写入实例变量值的方法。例如,闹钟对象具有一个实例变量来存储闹钟时间,以及两个用于获取和设置闹钟时间的方法。

因此,对象具有实例变量和方法,但这些实例变量和方法作为类的一部分进行设计。

类和对象之间有什么区别?

image

类不是对象(但它们用于构建对象)

image

类是对象的蓝图。它告诉虚拟机如何创建该特定类型的对象。从该类创建的每个对象可以具有自己的实例变量值。例如,您可以使用 Button 类制作多个不同的按钮,每个按钮可能具有自己的颜色、大小、形状、标签等。这些不同的按钮中的每一个都将是一个按钮对象

制作你的第一个对象

那么创建和使用对象需要什么?你需要两个类。一个类用于你想要使用的对象类型(狗、闹钟、电视等),另一个类用于测试你的新类。测试器类是你放置 main 方法的地方,在 main()方法中,你创建和访问你的新类类型的对象。测试器类只有一个任务:尝试你的新对象的方法和变量。

从本书的这一点开始,你将在许多示例中看到两个类。其中一个将是真正的类——我们真正想要使用其对象的类,另一个类将是测试器类,我们称之为 **TestDrive**。例如,如果我们创建了一个**Bungee**类,我们也需要一个**BungeeTestDrive**类。只有 **TestDrive**类会有一个 main()方法,它的唯一目的是创建你的新类(非测试器类)的对象,并使用点运算符(.)访问新对象的方法和变量。以下示例将清楚地展示这一切。不,真的

image

如果你已经掌握了一些面向对象的知识,你会知道我们没有使用封装。我们将在第四章《对象的行为》中讨论这一点。

制作和测试电影对象

image

class Movie {
  String title;
  String genre;
  int rating;

  void playIt() {
    System.out.println("Playing the movie");
  }
}

public class MovieTestDrive {
  public static void main(String[] args) {
    Movie one = new Movie();
    one.title = "Gone with the Stock";
    one.genre = "Tragic";
    one.rating = -2;
    Movie two = new Movie();
    two.title = "Lost in Cubicle Space";
    two.genre = "Comedy";
    two.rating = 5;
    two.playIt();
    Movie three = new Movie();
    three.title = "Byte Club";
    three.genre = "Tragic but ultimately uplifting";
    three.rating = 127;
  }
}

快!离开主方法!

只要你在 main()方法中,你就不是真正的 Objectville。测试程序在 main 方法中运行是可以的,但在真正的面向对象应用程序中,你需要对象之间的交互,而不是一个静态的 main()方法创建和测试对象。

main 方法的两种用途:

  • 为了测试你的真实类

  • 启动/开始你的Java 应用程序

一个真正的 Java 应用程序只是对象与其他对象的交流。在这种情况下,“交流”意味着对象相互调用方法。在前一页和第四章中,我们讨论了如何使用一个独立的 TestDrive 类中的 main()方法来创建和测试另一个类的方法和变量。在第六章中,我们讨论了如何使用一个带有 main()方法的类来启动一个真正的Java 应用程序(通过创建对象,然后释放这些对象与其他对象交互等)。

作为一个“窥视”,这是一个真正的 Java 应用程序可能的行为示例。因为我们仍然处于学习 Java 的最初阶段,所以我们使用了一个小工具包,因此你可能会觉得这个程序有些笨拙和低效。你可以考虑如何改进它,在后面的章节中我们会具体讨论这个问题。如果有些代码让你感到困惑,不要担心;这个示例的关键是对象之间的交互。

猜谜游戏

总结:

猜谜游戏包括一个游戏对象和三个玩家对象。游戏生成一个介于 0 和 9 之间的随机数,三个玩家对象尝试猜测它。(我们并没有说这是一个非常激动人心的游戏。)

类:

  GuessGame.class   Player.class   GameLauncher.class

逻辑:

1. GameLauncher 类是应用程序启动的地方;它有 main()方法。

2. 在 main()方法中创建了一个 GuessGame 对象,并调用了它的 startGame()方法。

3. GuessGame 对象的 startGame()方法是整个游戏进行的地方。它创建三个玩家,然后“想出”一个随机数(玩家要猜测的目标)。然后它要求每个玩家猜测,检查结果,并打印出关于获胜玩家的信息或要求他们再次猜测。

imageimage

运行猜谜游戏

输出(每次运行时都会有所不同)

imageimage

练习

image

成为编译器

image

这一页上的每个 Java 文件都代表一个完整的源文件。你的任务是扮演编译器,确定这些文件是否会编译通过。如果它们不能编译,你会如何修复它们?如果可以编译,它们的输出会是什么?

A

class StreamingSong {

  String title;
  String artist;
  int duration;

  void play() {
    System.out.println("Playing song");
  }

  void printDetails() {
    System.out.println("This is " + title +
                       " by " + artist);
  }
}

class StreamingSongTestDrive {
  public static void main(String[] args) {

    song.artist = "The Beatles";
    song.title = "Come Together";
    song.play();
    song.printDetails();
  }
}

B

class Episode {

  int seriesNumber;
  int episodeNumber;

  void skipIntro() {
    System.out.println("Skipping intro...");
  }

  void skipToNext() {
    System.out.println("Loading next episode...");
  }
}

class EpisodeTestDrive {
  public static void main(String[] args) {

    Episode episode = new Episode();
    episode.seriesNumber = 4;
    episode.play();
    episode.skipIntro();
  }
}

Images 答案见“BE the Compiler”。

代码磁铁

image

一个 Java 程序在冰箱上被弄得一团糟。你能重组代码片段,使其成为一个可以编译和运行,并且产生下面列出的输出的工作 Java 程序吗?一些花括号掉到了地板上,它们太小了,没法捡起来,所以你可以自由地添加需要的花括号。

Images 答案见 “代码磁铁”.

imageimage

池子谜题

imageimage

你的 任务 是从池中取出代码片段,放置到代码的空白行中。你可以多次使用同一个片段,并且不需要使用所有的片段。你的 目标 是创建能够编译和运行,并且产生下面列出的输出的类。本书中的一些练习和谜题可能有多个正确答案。如果你找到了另一个正确答案,请给自己加分!

输出

image

奖励问题!

如果输出的最后一行是 24 而不是 10,你将如何完成这个谜题?

imageimage

注意

注:可以多次使用池中的每个片段!

image

Images 答案见 “谜题解决方案”.

image

我是谁?

image

一群穿着全副武装的 Java 组件正在玩一个派对游戏,“我是谁?”他们给你一个线索,你根据他们说的话猜猜他们是谁。假设他们总是诚实地说出自己的情况。如果他们碰巧说的话对多个人都适用,你可以选择所有适用于这句话的人。在句子旁边的空白处填上一个或多个与句子相对应的名字。第一个给你提示了。

今晚的参与者:

类 方法 对象 实例变量

我是从 .java 文件编译而来。 class
我的实例变量值可以与我的朋友的值不同。 ________________________________
我像一个模板一样表现。 ________________________________
我喜欢做事情。 ________________________________
我可以有很多方法。 ________________________________
我代表“状态”。 ________________________________
我有行为。 ________________________________
我位于对象中。 ________________________________
我存在于堆上。 ________________________________
我用来创建对象实例。 ________________________________
我的状态可以改变。 ________________________________
我声明方法。 ________________________________
我可以在运行时改变。 ________________________________

Images 答案见 “我是谁?”.

练习解决方案

图片

代码磁铁

(来自“代码磁铁”)

class DrumKit {
  boolean topHat = true;
  boolean snare = true;

  void playTopHat() {
    System.out.println("ding ding da-ding");
  }

  void playSnare() {
    System.out.println("bang bang ba-bang");
  }
}

class DrumKitTestDrive {
  public static void main(String[] args) {
    DrumKit d = new DrumKit();
    d.playSnare();
    d.snare = false;
    d.playTopHat();
    if (d.snare == true) {
      d.playSnare();
    }
  }
}

图片

成为编译器

(来自“成为编译器”)

A

图片

B

图片

拼图解答

图片

游泳池谜题

(来自“游泳池谜题”)

public class EchoTestDrive {
  public static void main(String[]
args) {
    Echo  e1 = new Echo();
    Echo e2 = new Echo(); // correct answer
      - or -
    Echo e2 = e1;  // bonus "24" answer
    int x = 0;
    while (x < 4) {
      e1.hello();
      e1.count = e1.count + 1;
      if (x == 3) {
        e2.count = e2.count + 1;
      }
      if (x > 0) {
        e2.count = e2.count + e1.count;
      }
      x = x + 1;
    }
    System.out.println(e2.count);
  }
}
------------------------------------------
class Echo {
  int count = 0;

  void hello() {
    System.out.println("helloooo... ");
  }
}

图片

我是谁?

(来自“我是谁?”)

我从 .java 文件编译而来。
我的实例变量值可以与我的伙伴的值不同。 对象
我表现为模板。
我喜欢做事情。 对象,方法
我可以有很多方法。 类,对象
我代表“状态”。 实例变量
我有行为。 对象,类
我位于对象中。 方法,实例变量
我存在于堆中。 对象
我用于创建对象实例。
我的状态可以改变。 对象,实例变量
我声明方法。
我可以在运行时改变。 对象,实例变量
注意

注意:类和对象都被认为有状态和行为。它们在类中定义,但是对象也被认为“拥有”它们。现在我们不关心它们技术上属于哪里。

第三章:了解你的变量:原始类型和引用

图片

变量可以存储两种类型的东西:原始类型和引用。到目前为止,你已经在两个地方使用变量——作为对象状态(实例变量)和作为局部变量(在方法中声明的变量)。稍后,我们将使用变量作为参数(由调用代码发送给方法的值),以及作为返回类型(发送回方法调用者的值)。你已经看到变量声明为简单的原始整数值(类型int)。你已经看到变量声明为更复杂的东西,比如一个 String 或一个数组。但生活肯定还有更多,不仅仅是整数、字符串和数组。如果你有一个 PetOwner 对象,其中有一个 Dog 实例变量呢?或者一个 Car 有一个 Engine?在本章中,我们将揭开 Java 类型的神秘之处(比如原始类型和引用之间的区别),看看你可以声明为变量的内容,你可以放入变量的内容,以及你可以变量做什么。最后,我们将看到在可回收堆上生活是真正的样子。

声明一个变量

图片

Java 关心类型。它不会让你做一些奇怪和危险的事情,比如把长颈鹿引用塞进兔子变量中——当有人试图要求所谓的兔子hop()时会发生什么?它也不会让你把浮点数放入整数变量中,除非你告诉编译器你知道可能会失去精度(比如,小数点后的一切)。

编译器可以发现大多数问题:

Rabbit hopper = new Giraffe();

不要指望它能编译。幸好

要使所有这些类型安全起作用,你必须声明变量的类型。它是一个整数吗?一只狗?一个单个字符?变量有两种类型:原始对象引用。原始类型保存基本值(想想:简单的位模式),包括整数、布尔值和浮点数。对象引用保存,嗯,对象引用(哎呀,这难道不是解释清楚了吗)。

我们先看原始类型,然后再讨论对象引用的真正含义。但无论类型如何,你必须遵循两个声明规则:

除了类型,变量需要一个名称,这样你可以在代码中使用该名称。


变量必须有一个类型


除了类型,变量需要一个名称,这样你可以在代码中使用该名称。


变量必须有一个名称


图片

注意:当你看到类似于:“一个类型为 X 的对象”这样的陈述时,将类型视为同义词。(我们将在后面的章节中进一步细化这一点。)

“我想要一杯双份摩卡,不,把它变成一个整数。”

当你想到 Java 变量时,想象杯子。咖啡杯,茶杯,能装下很多你喜欢的饮料的巨大杯子,电影院里装爆米花的大杯子,带有美妙触感把手的杯子,以及你学会永远不能把金属装饰放进微波炉的杯子。

一个变量只是一个杯子。一个容器。它 容纳 一些东西。

它有一个大小和一个类型。在这一章中,我们首先看看原始类型的变量(杯子),稍后我们将看看持有对象引用的杯子。请跟随我们继续使用整个杯子类比——尽管现在它很简单,但当讨论变得更复杂时,它会给我们提供一个共同的看待事物的方式。这很快就会发生。

基本数据类型就像咖啡店里的杯子一样。如果你去过星巴克,你就知道我们在这里说什么。它们有不同的大小,并且每个都有一个名称,如“short”、“tall”,以及,“我想要一个‘grande’摩卡半加因为加了奶油。”

在柜台上展示的杯子可能会让你可以适当地下订单:

image

而在 Java 中,基本数据类型有不同的大小,这些大小有名称。当你在 Java 中声明任何变量时,必须使用特定的类型进行声明。这里的四个容器是 Java 中的四种整数基本类型。

image

每个杯子都包含一个值,所以对于 Java 的基本数据类型,与其说,“我想要一杯高法式浓缩”,不如对编译器说,“我想要一个带有数字 90 的 int 变量”。除了一个微小的区别……在 Java 中,你还需要给你的杯子起个 名字。所以实际上是,“请给我一个 int,值为 2486,并且给这个变量起名为 height。” 每个基本数据类型变量都有固定数量的位(杯子大小)。Java 中六种数值基本类型的大小如下所示:

imageimage

原始类型

类型 位深度 值范围

布尔和 char

boolean (特定于 JVM) truefalse
char 16 0 到 65535

数值(所有都是有符号的)

整数

byte 8 位 -128 到 127
short 16 位 -32768 到 32767
int 32 位 -2147483648 到 2147483647
long 64 位 -huge 到 huge

浮点数

float 32 位 变化
double 64 位 变化

具有赋值的原始声明:

image

你真的不想把那个……

确保值能够适合变量。

image

你不能把一个大的值放进一个小杯子里。

好吧,可以,但会有一些损失。你会得到一些,正如我们所说的,溢出。如果编译器能够从你的代码中判断出某些东西不适合放在你正在使用的容器(变量/杯子)中,它会尽力帮助防止这种情况。

例如,你不能把一个 int 的东西倒进一个 byte 大小的容器中,如下所示:

int x = 24;
byte b = x;
//won’t work!!

为什么这不起作用,你会问?毕竟,x的值是 24,而 24 绝对小得足以放入一个字节中。知道,我们也知道,但编译器关心的只是你试图把大东西放到小东西里,并且可能溢出。不要指望编译器知道x的值,即使你在代码中能看到它的确切值。

  • 你可以通过多种方式给变量赋值,包括:
  • 在等号后面输入一个文字值(x=12,isGood = true,等等)

  • 将一个变量的值赋给另一个变量(x = y)

  • 使用结合两者的表达式(x = y + 43

在下面的例子中,文字值以粗体斜体显示:

int size = 32; 声明一个名为size的整数,将其赋值为32
char initial = ***'j';*** 声明一个名为initial的字符,将其赋值为‘j’
double d = ***456.709;*** 声明一个名为d的双精度数,将其赋值为456.709
boolean isLearning; 声明一个名为isCrazy的布尔型(未赋值)
isLearning = ***true;*** 将值true赋给之前声明的isCrazy
int y = x + ***456***; 声明一个名为y的整数,将其赋值为当前x的值加456

远离关键字!

图片

你知道你的变量需要一个名称和一个类型。

你已经了解了基本类型。

那么你可以用什么作为名称呢? 规则很简单。你可以根据以下规则为类、方法或变量命名(实际规则略微灵活,但这些规则能保证你的安全):

  • 它必须以字母、下划线(_)或美元符号($)开头。你不能以数字开头。

    • 在第一个字符之后,你也可以使用数字。只是别以数字开头。
  • 它可以是任何你喜欢的东西,只要遵守这两条规则,只要不是 Java 的保留字。

保留字是编译器识别的关键字(和其他东西)。如果你真的想搞乱编译器,那就着用保留字作为名称。

你已经看到了一些保留字:

图片

但还有很多我们还没有讨论过的。即使你不需要知道它们的含义,你仍然需要知道自己不能自行使用它们。绝对不要——任何情况下——试图现在就记住这些。为了在你的脑袋中为它们腾出空间,你可能得把其他东西忘了。比如你停车的地方。别担心,到这本书结束时,你会对它们大部分了如指掌的。

图片

此表格保留

_ 捕获 双精度 浮点 整数 私有 超级
抽象 字符 否则 接口 受保护 开关 尝试
断言 枚举 跳转到 公共 同步
布尔 常量 扩展 如果 本地 返回 易挥发性
break continue false implements new short throw while
byte default final import null static throws
case do finally instanceof package strictfp transient

Java 的关键字、保留字和特殊标识符。如果你用这些作为名称,编译器可能会非常,非常不高兴。

控制你的 Dog 对象

image

你知道如何声明一个原始变量并给它赋值。但是现在怎么处理非原始变量呢?换句话说,对象呢?


  • 实际上并不存在对象变量。

  • 只有一个对象引用变量。

  • 一个对象引用变量保存代表访问对象的位。

  • 它不保存对象本身,而是保存类似指针或地址的东西。不过,在 Java 中,我们并不真正知道引用变量里面是什么。我们知道它代表一个且仅一个对象。而 JVM 知道如何使用引用来访问对象。


你不能把一个对象塞进一个变量里。我们经常这样想...我们会说,“我把 String 传递给 System.out.println()方法。”或者,“该方法返回一个 Dog”,或者,“我把一个新的 Foo 对象放入名为 myFoo 的变量中。”

但事实并非如此。并不存在可以随着对象大小增长的巨大可扩展杯子。对象只存在于一个地方——可垃圾回收的堆中!(你将在本章后面了解更多。)

虽然原始变量充满了代表变量实际的位,但对象引用变量充满了代表访问对象的方式的位。

你可以在引用变量上使用点运算符(.)来表示,“使用点之前的东西获取点之后的东西。”例如:

myDog.bark();

意思是,“使用变量 myDog 引用的对象来调用 bark()方法。”当你在对象引用变量上使用点运算符时,可以将其视为按下该对象的遥控器上的按钮。

image

一个对象引用只是另一个变量值

放入杯子里的东西。

只是这一次,这个值是一个遥控器。


原始变量

byte x = 7;

代表 7 的位进入变量(00000111)。

image


引用变量

Dog myDog = new Dog();

代表访问 Dog 对象的位进入变量。

Dog 对象本身并不进入变量!

image

我们不关心引用变量中有多少个 1 和 0。这取决于每个 JVM 和月相。

对象声明、创建和赋值的 3 个步骤

image

Images 声明一个引用变量

image

Dog myDog = new Dog();

告诉 JVM 为一个引用变量分配空间,并将该变量命名为myDog。这个引用变量永远是 Dog 类型。换句话说,这是一个有按钮可以控制一只狗的遥控器,但不能是猫、按钮或插座。

Images 创建一个对象

image

Dog myDog = new Dog();

告诉 JVM 在堆上为一个新的狗对象分配空间(我们会在第九章详细学习这个过程,对象的生与死)。

Images 链接对象和引用

image

Dog myDog = new Dog();

分配一个新的狗给引用变量myDog。换句话说,编程这个遥控器

Java 暴露

image

本周的采访:对象引用

HeadFirst: 那么告诉我们,作为一个对象引用,生活是什么样子的?

Reference: 相当简单,实际上。我是一个遥控器,可以被编程来控制不同的对象。

HeadFirst: 你是说在运行时即使你在引用不同的对象?比如,你可以先引用一个狗,然后五分钟后引用一个车吗?

Reference: 当然不行。一旦我声明了,就是这样了。如果我是一个狗的遥控器,那么我永远不能指向(哦,不好意思,我们不应该说指向),我的意思是,引用除了狗以外的任何东西。

HeadFirst: 这意味着你只能参考一个狗吗?

Reference: 不是的。我可以参考一个狗,然后五分钟后参考另一个不同的狗。只要它是狗,我就可以被重定向(就像重新编程你的遥控器换一个不同的电视)。除非...不,算了。

HeadFirst: 不,告诉我。你刚才想说什么?

Reference: 我觉得现在你不想深入讨论这个,但我简单告诉你——如果我被标记为final,那么一旦我被分配了一个狗,我就永远不能被重新编程为其他东西,只能永远是那个狗。换句话说,没有其他对象可以分配给我。

HeadFirst: 你说得对,现在我们不想谈论这个。好的,除非你是final,那么你可以先引用一个狗,然后后来引用一个不同的狗。你能否完全不引用任何东西?是不是可能什么都不被编程?

Reference: 是的,但是谈到这个让我感到不安。

HeadFirst: 为什么会这样?

Reference: 因为这意味着我是null,而这让我感到不安。

HeadFirst: 你是说,因为这样你就没有价值了?

参考: 哦,null 一个值。我仍然是一个遥控器,但这就像你带回了一个新的通用遥控器,却没有电视。我没有被程序化去控制任何东西。他们可以整天按我的按钮,但什么好事都不会发生。我感觉如此……无用。浪费位。虽然不是那么多位,但仍然如此。而且这不是最糟糕的部分。如果我是对特定对象唯一的引用,然后我被设置为null(去程序化),这意味着现在没有人能够访问我曾经引用的那个对象。

HeadFirst: 这是不好的因为……

参考: 你必须吗?在这里,我已经与这个对象建立了关系,一种亲密的连接,然后这种联系突然残酷地被切断了。我再也见不到那个对象,因为现在它有资格进行[制作人,请奏悲伤的音乐]垃圾回收。哭泣。但程序员是否曾经考虑过?哭泣。为什么,为什么我不能成为原始类型?我讨厌成为一个引用。 责任,所有破碎的附件……

垃圾收集堆上的生活

image

Book b = new Book();
Book c = new Book();

声明两个 Book 引用变量。创建两个新的 Book 对象。将 Book 对象分配给引用变量。

这两个 Book 对象现在存活在堆上。

参考文献:2

对象:2


image


Book d = c;

声明一个新的 Book 引用变量。而不是创建一个新的第三个 Book 对象,将变量c的值分配给变量d。但这是什么意思?这就像说“拿 c 中的位,复制它们,然后把那个副本放入 d 中。”

变量cd都指向同一个对象。

变量cd持有同一个值的两个不同副本。两个遥控器程序控制同一台电视。

参考文献:3

对象:2


image

c = b;

将变量b的值分配给变量c。到现在为止,你知道这意味着什么。变量b内的位被复制,然后这个新的副本被放入变量c中。

b 和 c 都指向同一个对象。

变量c不再引用其旧的 Book 对象。

参考文献:3

对象:2

生命与死亡在堆上

image

Book b = new Book();
Book c = new Book();

声明两个 Book 引用变量。创建两个新的 Book 对象。

这两个 Book 对象现在存活在堆上。

活跃引用:2

可达对象:2


image

b = c;

将变量c的值分配给变量b。变量c内的位被复制,然后这个新的副本被放入变量b中。两个变量持有相同的值。

b 和 c 都指向同一个对象。对象 1 被抛弃,可以进行垃圾回收(GC)。

活跃引用:2

可达对象:1

被抛弃的对象:1

变量b引用的第一个对象,对象 1,再没有引用。它是不可达的。

image


c = null;

将值 null 赋给变量 c。这使得 c 成为一个null 引用,意味着它不引用任何东西。但它仍然是一个引用变量,另一个 Book 对象仍然可以分配给它。

对象 2 仍然有一个活动引用(b),只要它有引用,对象就不会被垃圾回收。

活动引用:1

null 引用:1

可达对象:1

废弃对象:1

数组就像一个杯盘

Java 标准库包含许多复杂的数据结构,包括映射、树和集合(请参见附录 B),但在您只想要一个快速、有序、高效的事物列表时,数组是非常好的选择。通过使用索引位置,数组使您能够快速随机访问任何元素。

数组中的每个元素都只是一个变量。换句话说,八种原始变量类型中的一种(考虑:大毛狗)或引用变量。您可以将该类型的 变量 中的任何内容分配给该类型的 数组元素。因此,在 int 类型的数组(int[])中,每个元素可以保存一个 int。在 Dog 数组(Dog[])中,每个元素可以保存……一个 Dog 吗?不,记住引用变量只保存一个引用(一个遥控器),而不是对象本身。因此,在 Dog 数组中,每个元素可以保存指向 Dog 的遥控器。当然,我们仍然需要创建 Dog 对象……您将在下一页看到所有这些内容。

确保在图片中注意一件重要的事情——数组是一个对象,即使它是一个原始类型的数组。

Images 声明一个 int 数组变量。数组变量是数组对象的遥控器。

int[] nums;

Images 创建一个长度为 7 的新 int 数组,并将其分配给先前声明的 int[] 变量 nums

nums = new int[7];

Images 为数组中的每个元素赋予一些整数值。记住,整数数组中的元素只是整数 变量

image

无论它们声明为持有原始类型还是对象引用,数组始终是对象。

image

数组也是对象

您可以有一个声明为持有基本值的数组对象。换句话说,数组对象可以有原始类型元素,但数组本身永远不是原始类型。无论数组包含什么,数组本身始终是一个对象!

制作一组 Dogs

Images 声明一个 Dog 数组变量

Dog[] pets;

Images 创建一个长度为 7 的新 Dog 数组,并将其分配给先前声明的 Dog[] 变量 pets

pets = new Dog[7];

什么是缺失的?

image

Dogs!我们有一个 Dog 引用数组,但没有实际的 Dog 对象


Images 创建新的 Dog 对象,并将它们分配给数组元素。

记住,Dog 数组中的元素只是 Dog 引用 变量。我们仍然需要 Dogs!

pets[0] = new Dog();
pets[1] = new Dog();

image

控制你的狗(使用引用变量)

image

Dog fido = new Dog();
fido.name = "Fido";

我们创建了一个狗对象,并使用字符串点运算符在引用变量fido上访问名称变量。

我们可以使用fido引用来让狗叫()或吃()或追猫()。

image

fido.bark();

fido.chaseCat();

如果狗在一个狗数组中会发生什么?

我们知道可以使用点运算符访问狗的实例变量和方法,但是在什么上面呢?

当狗在数组中时,我们没有实际的变量名(比如fido)。相反,我们使用数组表示法,并在数组中特定索引(位置)上的对象上按下遥控器按钮(点运算符):

Dog[] myDogs = new Dog[3];

myDogs[0] = new Dog();

myDogs[0].name = "Fido";

myDogs[0].bark();

*是的,我们知道我们这里没有展示封装,但我们试图保持简单。暂时而言。我们将在第四章中进行封装。

一个狗的例子

imageimage

输出

image

练习

image

成为编译器

image

这页上的每个 Java 文件代表一个完整的源文件。你的任务是扮演编译器的角色,确定这些文件是否会编译和运行而不出现异常。如果不能,你将如何修复它们?

A

class Books {
  String title;
  String author;
}

class BooksTestDrive {
  public static void main(String[] args) {
    Books[] myBooks = new Books[3];
    int x = 0;
    myBooks[0].title = "The Grapes of Java";
    myBooks[1].title = "The Java Gatsby";
    myBooks[2].title = "The Java Cookbook";
    myBooks[0].author = "bob";
    myBooks[1].author = "sue";
    myBooks[2].author = "ian";

    while (x < 3) {
      System.out.print(myBooks[x].title);
      System.out.print(" by ");
      System.out.println(myBooks[x].author);
      x = x + 1;
    }
  }
}

B

class Hobbits {
  String name;

  public static void main(String[] args) {
    Hobbits[] h = new Hobbits[3];
    int z = 0;

    while (z < 4) {
      z = z + 1;
      h[z] = new Hobbits();
      h[z].name = "bilbo";
      if (z == 1) {
        h[z].name = "frodo";
      }
      if (z == 2) {
        h[z].name = "sam";
      }
      System.out.print(h[z].name + " is a ");
      System.out.println("good Hobbit name");
    }
  }
}

Images 答案在“练习解答”。

代码磁铁

image

一个工作的 Java 程序在冰箱上搅乱了。你能重构代码片段,使其成为一个能产生以下输出的工作的 Java 程序吗?一些花括号掉到地板上了,太小了,没法捡起来,所以你可以随意添加你需要的数量!

imageimage

Images 答案在“代码磁铁”。

池谜题

imageimage

你的工作是从池中取出代码片段,并将它们放入代码的空白行中。你可以多次使用相同的片段,并且你不需要使用所有的片段。你的目标是制作一个能编译和运行并产生列出的输出的类。

输出

image

额外问题!

为了额外的奖励分数,使用池中的片段填补上面缺失的输出。

image

注意:池中的每个片段都可以多次使用!

image

Images 答案在“池谜题”。

一堆麻烦

image

右侧列出了一个简短的 Java 程序。当达到“//做一些事情”时,一些对象和一些引用变量将被创建。你的任务是确定哪些引用变量指向哪些对象。并非所有引用变量都会被使用,有些对象可能会被多次引用。画线连接引用变量与其匹配的对象。

提示: 除非你比我们聪明得多,否则你可能需要画出“垃圾回收堆上的生活”这一章中的图表–本章的 60 页。使用铅笔,这样你就可以画出然后擦除参考链接(从参考遥控器指向对象的箭头)。

图片

引用变量: HeapQuiz 对象:
图片 图片
注意

将每个引用变量与匹配的对象进行匹配。

你可能不需要使用每个参考。

图片 答案在“一堆麻烦”中。

被盗参考案

图片

那是一个漆黑而风雨交加的夜晚。Tawny 悠然走进程序员的牛棚,就像她拥有这个地方一样。她知道所有程序员仍在努力工作,她需要帮助。她需要在关键类中添加一个新方法,该类将被加载到客户的新秘密 Java 手机中。手机内存中的堆空间很紧张,每个人都知道。牛棚里通常喧闹的嗡嗡声突然安静下来,当 Tawny 缓缓走向白板时。她草草勾画了新方法功能的简要概述,然后缓慢地扫视了房间。“好了,伙计们,现在是紧要关头,”她轻声说道。“谁能创建出最节省内存的版本这个方法,明天就跟我一起去客户的发布派对上,帮我安装新软件。”

五分钟推理

图片

第二天早上,Tawny 滑入牛棚。“女士们先生们,”她微笑着说,“飞机几个小时后就要起飞了,让我看看你们有什么!” Bob 第一个上场;当他开始在白板上勾画设计时,Tawny 说,“Bob,让我们直奔主题,告诉我你是如何处理更新联系对象列表的。” Bob 迅速在白板上画了一段代码片段:

    Contact [] contacts = new Contact[10];
    while (x < 10 ) {   // make 10 contact objects
      contacts[x] = new Contact();
      x = x + 1;
    }
    // do complicated Contact list updating with contacts

“Tawny,我知道我们的内存很紧张,但你的规格书说我们必须能够访问所有十个可允许的联系人的个人信息;这是我能想到的最好的方案,”Bob 说。接下来是 Kate,已经想象着派对上的椰子鸡尾酒,“Bob,”她说,“你的解决方案有点笨拙,你不觉得吗?” Kate 咧嘴笑道,“看看这个宝贝”:

    Contact contactRef;
    while ( x < 10 ) {   // make 10 contact objects
      contactRef = new Contact();
      x = x + 1;
    }
    // do complicated Contact list updating with contactRef

“我节省了一堆值得记忆的引用变量,Bob-o-rino,所以收起你的防晒霜吧,”Kate 嘲笑道。“不要那么快,Kate!”Tawny 说,“你节省了一点内存,但 Bob 跟我走。”

为什么 Tawny 在 Kate 的方法使用更少内存时选择了 Bob 的方法?

图片 答案在 “五分钟推理” 中。

练习解答

图片

锻炼你的铅笔

(来源于 “锻炼你的铅笔”)

图片

代码磁铁

(来源于 “代码磁铁”)

class TestArrays {
  public static void main(String[] args) {
    int[] index = new int[4];
    index[0] = 1;
    index[1] = 3;
    index[2] = 0;
    index[3] = 2;
    String[] islands = new String[4];
    islands[0] = "Bermuda";
    islands[1] = "Fiji";
    islands[2] = "Azores";
    islands[3] = "Cozumel";
    int y = 0;
    int ref;
    while (y < 4) {
      ref = index[y];
      System.out.print("island = ");
      System.out.println(islands[ref]);
      y = y + 1;
    }
  }
}

图片

成为编译器

(来源于 “成为编译器”)

A

图片

B

图片

拼图解答

图片

池谜题

(来源于 “池谜题”)

class Triangle {
  double area;
  int height;
  int length;

  public static void main(String[] args) {
    int x = 0;
    Triangle[] ta = new Triangle[4];
    while (x < 4) {
      ta[x] = new Triangle();
      ta[x].height = (x + 1) * 2;
      ta[x].length = x + 4;
      ta[x].setArea();
      System.out.print("triangle " + x + 
                       ", area");
      System.out.println(" = " + ta[x].area);
      x = x + 1;
    }
    int y = x;
    x = 27;
    Triangle t5 = ta[2];
    ta[2].area = 343;
    System.out.print("y = " + y);
    System.out.println(", t5 area = " + 
                       t5.area);
  }

  void setArea() {
    area = (height * length) / 2;
  }
}

图片

五分钟推理

(来源于 “五分钟推理”)

被偷窃的引用案件

Tawny 发现 Kate 的方法有一个严重缺陷。虽然她没有像 Bob 那样使用许多引用变量,但无法访问她方法创建的除最后一个 Contact 对象以外的任何对象。每次循环结束时,她都会将一个新对象赋给唯一的引用变量,因此之前引用的对象会被遗弃在堆中——无法访问。由于无法访问创建的十个对象中的九个,Kate 的方法变得毫无用处。

(这款软件大获成功,客户给了 Tawny 和 Bob 额外的一周夏威夷的时间。我们想告诉你,通过完成这本书,你也会得到这样的好处。)

一堆麻烦

(来源于 “一堆麻烦”)

引用变量: 堆谜题对象:
图片

第四章:对象的行为:方法使用实例变量

image

状态影响行为,行为影响状态。我们知道对象具有状态行为,由实例变量方法表示。但直到现在,我们还没有探讨状态和行为之间的关系。我们已经知道类的每个实例(特定类型的每个对象)可以为其实例变量拥有自己独特的值。狗 A 可以有一个名为“Fido”的名字,体重为 70 磅。狗 B 是“Killer”,体重为 9 磅。如果狗类有一个叫做 makeNoise()的方法,那么你认为一只 70 磅的狗会比那只小 9 磅的狗叫得更深吗?(假设那烦人的尖叫声可以被视为叫声。)幸运的是,这就是对象的全部意义——它具有行为,可以作用于其状态。换句话说,方法使用实例变量值。比如,“如果狗的体重小于 14 磅,则发出尖叫声,否则...”或“增加体重 5 磅。”让我们去改变一些状态吧。

记住:一个类描述了一个对象知道什么和做什么

image

一个类是对象的蓝图。当你编写一个类时,你描述的是 JVM 应该如何制造该类型的对象。你已经知道该类型的每个对象可以有不同的实例变量值。但方法呢?

该类型的每个对象的方法行为都可能不同吗?

image

嗯...有点像。*

特定类的每个实例具有相同的方法,但这些方法可以根据实例变量的值表现不同。

Song 类有两个实例变量,titleartist。当你在一个实例上调用 play()方法时,它将播放由titleartist实例变量值表示的歌曲。因此,如果你在一个实例上调用 play()方法,你将听到卡贝洛的“Havana”,而另一个实例则播放特拉维斯的“Sing”。但方法的代码是相同的。

void play() {
    soundPlayer.playSound(title, artist);
}
Song song1 = new Song();
song1.setArtist("Travis");
song1.setTitle("Sing");
Song song2 = new Song();
song2.setArtist("Sex Pistols");
song2.setTitle("My Way");

大小影响叫声

小狗的叫声不同于大狗的叫声。

image

Dog 类有一个实例变量size,bark()方法使用它来决定发出什么样的叫声。

image

class Dog {
  int size;
  String name;

  void bark() {
    if (size > 60) {
      System.out.println("Wooof! Wooof!");
    } else if (size > 14) {
      System.out.println("Ruff!  Ruff!");
    } else {
      System.out.println("Yip! Yip!");
    }
  }
}

class DogTestDrive {

  public static void main(String[] args) {
    Dog one = new Dog();
    one.size = 70;
    Dog two = new Dog();
    two.size = 8;
    Dog three = new Dog();
    three.size = 35;

    one.bark();
    two.bark();
    three.bark();
  }
}

image

你可以把东西送给一个方法

正如你从任何编程语言中期望的那样,你可以将值传递给你的方法。例如,你可能想通过调用一个 Dog 对象的方法告诉它叫多少次:

d.bark(3);

根据你的编程背景和个人偏好,可能会使用术语参数或者参数来表示传递给方法的值。尽管有正式的计算机科学区别,穿实验室白大褂的人(几乎肯定不会读这本书)可能会用,但我们在这本书中有更重要的事情要做。所以可以随意称呼它们(参数、甜甜圈、毛球等等),但我们按照这样做:

调用者传递参数。方法接收参数。

参数是你传递给方法的东西。一个参数(像 2、Foo 或指向狗的引用)被面朝下地投入到...等等...参数中。而参数只不过是一个本地变量。一个带有类型和名称的变量,可以在方法体内使用。

但是这里的重点是:如果一个方法接收一个参数,你必须在调用时传递一个值。而且那个值必须是适当类型的值。

image

你可以从一个方法中得到东西

方法也可以返回值。每个方法都声明了一个返回类型,但到目前为止,我们所有的方法都是使用void返回类型,这意味着它们不返回任何内容。

void go() {
}

image

但是我们可以声明一个方法,向调用者返回特定类型的值,比如:

int giveSecret() {
  return 42;
}

如果你声明一个方法返回一个值,你必须返回与声明类型兼容的值!(或者与声明类型兼容的值。在我们讨论第七章和第八章时,我们会更详细地讨论多态性。)

无论你说你会返回什么,你一定要返回!

编译器不会让你返回错误类型的东西。

image

你可以将多个东西发送到一个方法

方法可以有多个参数。在声明它们时用逗号分隔,在传递它们时也用逗号分隔。最重要的是,如果一个方法有参数,你必须传递正确类型和顺序的参数。

调用一个有两个参数的方法,并向其发送两个参数

image

你可以将变量传递给一个方法,只要变量类型与参数类型匹配。

image

Java 是按值传递的。这意味着按照拷贝传递。

imageimageimage

提醒:Java 关心类型!

image

当返回类型声明为兔子时,你不能返回长颈鹿。参数也是一样。你不能将长颈鹿传递给需要兔子的方法。

你可以用参数和返回类型做一些很酷的事情

image

现在我们已经看到了参数和返回类型的工作原理,是时候将它们用于正途了:让我们创建GettersSetters。如果你喜欢正式些,也许你更愿意称它们为AccessorsMutators。但这样会浪费很多好音节。而且,Getters 和 Setters 符合常见的 Java 命名约定,所以我们就这样称呼它们。

Getters 和 Setters 让你可以获取和设置东西。通常是实例变量的值。Getter 的唯一目的是作为返回值将那个特定 Getter 应该获取的内容的值返回。到现在为止,Setter 生来就为了有机会接受一个参数值并将其用于设置实例变量的值。

image

class ElectricGuitar {
  String brand;
  int numOfPickups;
  boolean rockStarUsesIt;

  String getBrand() {
    return brand;
  }

  void setBrand(String aBrand) {
    brand = aBrand;
  }

  int getNumOfPickups() {
    return numOfPickups;
  }

  void setNumOfPickups(int num) {
    numOfPickups = num;
  }

  boolean getRockStarUsesIt() {
    return rockStarUsesIt;
  }

  void setRockStarUsesIt(boolean yesOrNo) {
    rockStarUsesIt = yesOrNo;
  }
}

封装

做或者冒着被羞辱和嘲笑的风险。

image

直到这个最重要的时刻,我们一直在犯着最严重的面向对象的错误(我们不是说像没有“B”在 BYOB 这样的小违规)。不,我们说的是带着大写“F”的 Faux Pas。还有“P”。

我们的可耻过失?

曝露我们的数据!

在这里,我们就像漫不经心一样,毫不在乎地把我们的数据留给任何人看,甚至触摸。

你可能已经经历过那种略显不安的感觉,即离开你的实例变量暴露在外的感觉。

暴露意味着可通过点操作符访问,就像:

 theCat.height = 27;

想想这个使用我们的遥控器直接改变 Cat 对象的大小实例变量的想法。在错误的人手中,引用变量(遥控器)是一种非常危险的武器。因为有什么能阻止:

image

这将是一个坏事。我们需要为所有的实例变量构建 setter 方法,并找到一种方法来强制其他代码调用 setter 而不是直接访问数据。

image

隐藏数据

是的,从一个请求坏数据的实现到一个保护你的数据保护你后续修改实现权利的实现,是如此简单。

好吧,那么如何隐藏数据?通过publicprivate访问修饰符。你对public很熟悉——我们在每个主方法中都使用它。

这里有一个封装的入门法则(所有关于法则的免责声明都有效):将你的实例变量标记为private,并提供public的 getters 和 setters 来进行访问控制。当你在 Java 中拥有更多的设计和编码技巧时,你可能会以稍有不同的方式进行,但现在,这种方法会保护你的安全。

“不幸的是,比尔忘记封装他的 Cat 类,结果得到了一只扁平的猫。”

(在饮水机旁听到)

Java 暴露

image

本周的采访:一个对象公开谈论封装。

HeadFirst: 封装有什么了不起的地方?

Object: 好的,你知道那个梦境吗,你在给 500 人讲话,突然意识到你赤裸了?

HeadFirst: 是的,我们听说过那个。它和那个关于普拉提机的故事差不多……不,我们不谈这个。好了,所以你感觉很裸露。但除了有点暴露外,还有什么危险吗?

Object: 有危险吗?有危险吗?[开始笑] 嘿,你们其他实例都听到了吗,“有危险吗?”他问?[笑倒在地上]

HeadFirst: 这有什么好笑的?似乎是个合理的问题。

Object: 好的,我来解释一下。它是[再次爆笑,无法控制]

HeadFirst: 我能给你什么吗?水?

Object: 哎呀!哦天啊。不用了,真的。我会认真的。深呼吸。好了,继续吧。

HeadFirst: 那么封装能保护你免受什么?

Object: 封装在我的实例变量周围放了一个力场,所以没有人可以将它们设置为,比如说,不合适的东西。

HeadFirst: 能举个例子吗?

Object: 很高兴为你解释。大多数实例变量的值都编码有关它们边界的某些假设。比如,想想如果允许负数会破坏什么。办公室的浴室数量。飞机的速度。生日。杠铃的重量。电话号码。微波炉功率。

HeadFirst: 我明白你的意思了。那么封装如何让你设定边界?

Object: 强制其他代码通过 setter 方法。这样,setter 方法可以验证参数并决定是否可行。也许方法会拒绝它并什么也不做,或者可能会抛出异常(比如如果是信用卡申请中的空社会安全号码),或者方法可能会将发送的参数四舍五入到最接受的值。关键在于,在 setter 方法中可以做任何你想做的事情,而如果你的实例变量是公共的,你什么也做不了。

HeadFirst: 但有时我看到 setter 方法只是简单地设置值而没有检查任何东西。如果你有一个实例变量没有边界,那么这个 setter 方法会不会造成不必要的开销?会有性能损失吗?

Object: setters(以及 getters)的关键在于你随时可以改变主意,而不会破坏其他人的代码! 想象一下,如果你公司里一半的人都在使用你的类和公共实例变量,突然有一天你突然意识到,“哎呀——对这个值我没考虑到的地方,我得改成 setter 方法了。” 你会破坏所有人的代码。封装的酷之处在于你可以改变主意。没人会受伤。直接使用变量的性能收益微乎其微,几乎——或者说从未——值得。

封装 GoodDog 类

imageimage

数组中的对象如何表现?

就像任何其他对象一样。唯一的区别在于获得它们的方式。换句话说,如何获得遥控器。让我们尝试在数组中调用 Dog 对象的方法。

  • Images 声明并创建一个 Dog 数组以保存七个 Dog 引用。

    Dog[] pets;
    pets = new Dog[7];
    

    image


  • Images 创建两个新的 Dog 对象,并将它们分配给数组的前两个元素。

    pets[0] = new Dog();
    pets[1] = new Dog();
    
  • Images 在两个 Dog 对象上调用方法。

    pets[0].setSize(30);
    int x = pets[0].getSize();
    pets[1].setSize(8);
    

    image

声明和初始化实例变量

你已经知道变量声明至少需要一个名称和类型:

int size;
String name;

你知道你可以在同一时间初始化(赋值给)变量:

int size = 420;
String name = "Donny";

但是当你不初始化实例变量时,调用 getter 方法会发生什么?换句话说,在初始化之前实例变量的是多少?

imageimage

你不必初始化实例变量,因为它们总是有一个默认值。数字类型的基本类型(包括 char)得到 0,布尔类型得到 false,对象引用变量得到 null。

(记住,null 只是意味着一个没有控制/未编程到任何东西的遥控器。一个引用,但没有实际对象。)

实例变量与局部变量的区别

  • Images 实例变量声明在类内部但不在方法内部。

    class Horse {
      private double height = 15.2;
      private String breed;
      // more code...
    }
    
  • Images 局部变量在方法内声明。

    image

  • Images 局部变量必须在使用之前初始化!

    imageimage

局部变量不会有默认值!如果在变量初始化之前尝试使用局部变量,编译器会报错。

比较变量(基本类型或引用类型)

有时候你想知道两个基本类型是否相同;例如,你可能想检查一个 int 结果与某个预期的整数值是否相等。这很简单:只需使用 == 运算符。有时候你想知道两个引用变量是否引用堆上的同一个对象;例如,这个 Dog 对象是否确实是我最初创建的那个 Dog 对象?这也很简单:只需使用 == 运算符。但有时候你想知道两个对象是否相等。为此,你需要使用 .equals() 方法。

对象的相等概念取决于对象的类型。例如,如果两个不同的 String 对象具有相同的字符(比如,“my name”),它们在语义上是等价的,不管它们是否是堆上的两个不同对象。但是对于狗呢?如果两只狗的大小和体重相同,你是否希望将它们视为相等?可能不。因此,两个不同对象是否应被视为相等取决于该特定对象类型的逻辑。我们将在后面的章节中再次探讨对象的相等概念,但现在我们需要明白,== 运算符仅用于比较两个变量的比特位。无论这些比特位代表什么,它们要么相同,要么不同。

用 == 运算符来比较两个原始类型

== 运算符可以用来比较任何类型的两个变量,它简单地比较比特位。

if (a == b) {...} 检查 a 和 b 的比特位,并在比特模式相同时返回 true(尽管左端所有额外的零无关紧要)。

   int a = 3;
   byte b = 3;
   if (a == b) { // true }

image

用 == 来比较两个原始类型或者查看两个引用是否指向同一个对象。

用 equals() 方法来检查两个不同的对象是否相等。

(例如,两个不同的 String 对象,它们都包含字符“Fred”)

用 == 运算符来查看两个引用是否相同(这意味着它们在堆上引用同一个对象)

记住,== 运算符只关心变量中的比特模式。无论变量是引用还是原始类型,规则都是一样的。因此,如果两个引用变量指向同一个对象,== 运算符返回 true!在这种情况下,我们不知道比特模式是什么(因为它依赖于 JVM 并对我们隐藏),但我们知道,无论它看起来如何,对于指向单个对象的两个引用,它将是相同的。

image

Foo a = new Foo();
Foo b = new Foo();
Foo c = a;
if (a == b) { } // false
if (a == c) { } // true
if (b == c) { } // false

image

练习

image

成为编译器

image

此页面上的每个 Java 文件都代表一个完整的源文件。你的任务是扮演编译器,确定这些文件是否会编译。如果它们不能编译,你将如何修复它们?如果它们可以编译,它们的输出将会是什么?

A

class XCopy {

  public static void main(String[] args) {
    int orig = 42;
    XCopy x = new XCopy();
    int y = x.go(orig);
    System.out.println(orig + " " + y);
  }

  int go(int arg) {
    arg = arg * 2;
    return arg;
  }
}

B

class Clock {
  String time;

  void setTime(String t) {
    time = t;
  }

  void getTime() {
    return time;
  }
}

class ClockTestDrive {
  public static void main(String[] args) {
    Clock c = new Clock();

    c.setTime("1245");
    String tod = c.getTime();
    System.out.println("time: "+tod);
  }
}

Images 答案在“BE the Compiler”中。

一群穿着全套服装的 Java 组件正在玩一个派对游戏,“我是谁?” 他们给你一个提示,你根据他们说的内容来猜他们是谁。假设他们总是对自己说实话。如果他们碰巧说了一些对多个参与者都可能是真实的话,那么请写下所有这些话对应的参与者名字。在每个句子旁边填上一个或多个参与者的名字。

谁是我?

image

今晚的参与者:

实例变量,参数,返回,getter,setter,封装,公共,私有,按值传递,方法

一个类可以有任意数量的这些。 __________________________________
一个方法只能有一个这样的。 __________________________________
这可以被隐式提升。 __________________________________
我更喜欢我的实例变量是私有的。 __________________________________
它实际上意味着“制作一个副本”。 __________________________________
只有 setter 应该更新这些。 __________________________________
一个方法可以有很多这样的。 __________________________________
根据定义,我会返回一些东西。 __________________________________
不应该与实例变量一起使用。 __________________________________
我可以有很多参数。 __________________________________
根据定义,我接受一个参数。 __________________________________
这些有助于创建封装。 __________________________________
我总是独自飞行。 __________________________________

图片 答案在“我是谁?”。

混合信息

图片

右侧列出了一个简短的 Java 程序。程序中有两个代码块缺失。你的挑战是将候选代码块(下方)与插入后看到的输出进行匹配。

并非所有输出行都会被使用,有些输出行可能会被多次使用。画线连接候选代码块与它们匹配的命令行输出。

图片图片

图片 答案在“混合信息”。

池谜题

图片图片

你的工作是从代码池中提取代码片段并将它们放入代码中的空白行中。你不能多次使用相同的代码片段,也不需要使用所有的代码片段。你的目标是创建一个能够编译、运行并产生列出的输出的类。

图片 答案在“池谜题”。

输出

图片

public class Puzzle4 {
  public static void main(String [] args) {
    ___________________________________
    int number = 1;
    int i = 0;
    while (i < 6) {
      ___________________________
      ___________________________
      number = number * 10;
      _________________
    }

    int result = 0;
    i = 6;
    while (i > 0) {
      _________________
      result = result + ___________________
    }
    System.out.println("result " + result);
  }
}

class ___________ {
  int intValue;
  ________  ______ doStuff(int _________) {
    if (intValue > 100) {
      return _________________________
    } else {
      return _________________________
    }
  }
}
注意

注意:每个代码片段只能使用一次!

图片

刺激城市的快速时光

图片

五分钟的谜题

当 Buchanan 粗鲁地从后面抓住 Jai 的胳膊时,Jai 呆住了。Jai 知道 Buchanan 既愚蠢又丑陋,他不想让这个大个子受惊。Buchanan 命令 Jai 进入他老板 Leveler 的办公室,但 Jai 最近没有做错什么,所以他觉得与 Leveler 聊聊应该不会太坏。他最近在西区卖了很多神经兴奋剂,他觉得 Leveler 会很高兴。黑市的兴奋剂不是最好的赚钱方式,但它们相当无害。他见过的大多数兴奋剂君子过一段时间就戒掉了,回归正常生活,也许只是比以前少了点专注力。

Leveler 的“办公室”看起来像是一个陈旧的滑板车,但是当 Buchanan 把他推进去后,Jai 发现它被改装过,提供了所有地方老大 Leveler 可能期望的额外速度和装甲。“Jai,我的孩子,” Leveler 嘶嘶地说,“很高兴再次见到你。” “彼此彼此……” Jai 感受到 Leveler 问候背后的恶意,说道,“我们应该已经不欠不欠了,Leveler,我错过了什么吗?” “哈!你看起来很不错,Jai。你的销量不错,但我最近经历了,咱们说,一点‘漏洞’,” Leveler 说道。

Jai 不由自主地皱了皱眉头;他曾经是一位顶级的街头黑客。每当有人找出如何突破街头插孔的安全措施时,不良注意就会转向 Jai。“绝对不可能是我,老兄,” Jai 说道,“不值得冒险。我已经退出黑客界,我只是转移我的东西,管好自己的事。” “是啊,是啊,” Leveler 笑了笑,“我相信你这一次没事,但在这件事上我会损失很多,直到这个新的街头黑客被拒之门外!” “好吧,祝你好运,Leveler。也许你可以就把我丢在这儿,我再为你多运输几‘单位’,然后今天就收工了。” Jai 说道。

图片

“对不起,Jai,这并不那么容易。” Leveler 暗示道。“Buchanan 告诉我,你对 Java NE 37.3.2 相当了解。” “神经版?没错,我有点玩过。” Jai 回答道,感到有点不安。“神经版是我让刺激物瘾君子们知道下一个投放点的方式。” Leveler 解释道。“问题是,有些刺激物瘾君子竟然保持清醒长到足够搞懂如何入侵我的仓储数据库。” “我需要像你这样思维敏捷的人,Jai,来看看我的 StimDrop Java NE 类;方法、实例变量,整个大餐,看看他们是如何进来的。应该……” “喂!” Buchanan 叫道,“我可不希望像 Jai 这样的垃圾黑客到我的代码里窥探!” “冷静点大哥,” Jai 看到机会,“我相信你对你的访问权限做得很好……” “别告诉我,比特位狗!” Buchanan 咆哮道,“我把那些君子级的方法全部设为公开,以便他们能访问投放点数据,但所有关键的仓储方法我都标记为私有的。外面没人能访问那些方法,伙计,没人!”

“我想我能找到你的漏洞,Leveler。我们把 Buchanan 放在拐角处,然后绕街区巡游怎么样?” Jai 建议道。Buchanan 握紧拳头朝 Jai 走去,但 Leveler 的麻痹器已经在 Buchanan 的脖子上,“放开吧,Buchanan,”Leveler 嘲笑道,“把手放在我能看到的地方,走到外面。我想 Jai 和我有一些计划要制定。”

Jai 怀疑什么?

他会完整地从 Leveler 的滑板车中走出来吗?

图片 “五分钟谜题”中的答案。

练习解答

图片

磨砺你的铅笔

(来自“磨砺你的铅笔”)

图片

成为编译器

(来自“成为编译器”)

‘XCopy’类编译并运行正常!输出结果为:‘42 84’。记住,Java 是按值传递的(也就是按拷贝传递),变量‘orig’不会被 go( )方法改变。

图片

我是谁?

(来自“我是谁?”)

一个类可以有任意数量的这些。 实例变量,getter,setter,方法
一个方法只能有一个这个。 返回
这个可以被隐式提升。 返回,参数
我更喜欢我的实例变量是私有的。 封装
它实际上意味着“制作一份拷贝”。 按值传递
只有 setter 应该更新这些。 实例变量
一个方法可以有很多这些。 参数
根据定义,我会返回一些东西。 getter
不应该与实例变量一起使用。 public
我可以有很多参数。 方法
根据定义,我接受一个参数。 setter
这些有助于创建封装。 getter, setter, public, private
我总是独自飞行。 返回

谜题解答

游泳池谜题

(来自“游泳池谜题”)

public class Puzzle4 {
  public static void main(String[] args) {
    Value[] values = new Value[6];
    int number = 1;
    int i = 0;
    while (i < 6) {
      values[i] = new Value();
      values[i].intValue = number;
      number = number * 10;
      i = i + 1;
    }

    int result = 0;
    i = 6;
    while (i > 0) {
      i = i - 1;
      result = result + values[i].doStuff(i);
    }
    System.out.println("result " + result);
  }
}

class Value {
  int intValue;
  public int doStuff(int factor) {
    if (intValue > 100) {
      return intValue * factor;
    } else {
      return intValue * (5 - factor);
    }
  }
}

输出

图片

五分钟谜题

(来自“五分钟谜题”)

Jai 怀疑什么?

Jai 知道 Buchanan 不是最聪明的人。当 Jai 听到 Buchanan 谈论他的代码时,Buchanan 从未提到他的实例变量。Jai 怀疑,虽然 Buchanan 确实正确处理了他的方法,但他没有将他的实例变量标记为private。这个疏忽很容易让 Leveler 损失成千上万。

混合信息

(来自“混合信息”)

图片

第五章:超强方法:编写一个程序

image

让我们为我们的方法加点力气。 我们尝试了变量,玩了一些对象,并写了一些代码。但我们还不够强大。我们需要更多的工具。像运算符。我们需要更多的运算符,这样我们可以做一些比如说更有趣的事情。还有循环。我们需要循环,但这种弱弱的while循环是怎么回事?如果我们真的认真的话,我们需要for循环。也许学习生成随机数也会有用。还有为什么我们不通过构建一个真实的东西来学习所有这些,看看从头编写(和测试)程序是什么感觉。也许是一个游戏,像战舰。这是一个需要大力完成的任务,所以需要章来完成。我们将在本章中构建一个简单版本,然后在第六章中构建一个更强大的豪华版本。

让我们来建立一个类似战舰游戏的游戏:“击沉一个创业公司”

这是你对抗计算机的游戏,但与真正的战舰游戏不同,你不需要放置自己的船只。相反,你的任务是在尽可能少的猜测中击沉计算机的船只。

哦,我们不是在击沉船只。我们要消灭不明智的硅谷创业公司(从而建立业务相关性,以便你可以报销这本书的费用)。

目标: 在尽可能少的猜测中击沉所有计算机的创业公司。根据你的表现,你将得到一个评级或等级。

设置: 游戏程序启动时,计算机在虚拟的 7 x 7 网格上放置了三个创业公司。完成后,游戏会询问你的第一个猜测。

游戏玩法: 我们还没有学会构建 GUI,所以这个版本在命令行上运行。计算机会提示你输入一个猜测(一个单元格),你将在命令行上输入“A3”,“C5”等)。作为对你猜测的回应,你会在命令行上看到一个结果,要么是“命中”,“错过”,或“你击沉了 poniez”(或者是今天幸运的创业公司名称)。当你将所有三个创业公司送上天堂的 404 时,游戏将打印出你的评级。

image

你将要建立击沉一个创业公司的游戏,拥有一个 7 x 7 的网格和三个创业公司。每个创业公司占据三个单元格。

游戏互动的一部分

image

首先,一个高级设计。

我们知道我们将需要类和方法,但它们应该是什么?为了回答这个问题,我们需要更多关于游戏应该做什么的信息。

首先,我们需要弄清楚游戏的一般流程。这里是基本想法:

image

图 5-1. 哇。一个真正的流程图。
  • Images 用户开始游戏。

    • Images 游戏创建三个创业公司。

    • Images 游戏将三个创业公司放置在虚拟网格上。

  • Images 游戏开始。

    重复以下步骤,直到没有更多的创业公司:

    图片

  • 图片 游戏结束。

    基于猜测次数给用户评级。

现在我们对程序需要做的事情有了一个概念。下一步是弄清楚我们需要哪种对象来完成工作。记住,首先要像 Brad 一样思考,而不是像 Laura(我们在第二章中遇到的 A Trip to Objectville);首先专注于程序中的事物而不是过程

“简单启动游戏”是一个更温和的介绍

看起来我们至少需要两个类,一个是游戏类,一个是启动类。但在构建完整的击沉一家初创公司游戏之前,我们将从简化版本简单启动游戏开始。我们将在章节构建简单版本,然后在一章节构建豪华版本。

这个游戏中一切都更简单。不使用 2D 网格,我们只隐藏启动在单个中。而不是个启动,我们使用一个

尽管目标相同,但游戏仍需要创建一个启动实例,将其放置在行中的某个位置,获取用户输入,当所有启动单元格都被击中时,游戏结束。这个简化版本的游戏为我们构建完整游戏奠定了良好的基础。如果我们能够使这个小版本工作,稍后可以将其扩展到更复杂的版本。

在这个简单版本中,游戏类没有实例变量,所有游戏代码都在 main()方法中。换句话说,当程序启动并且 main()开始运行时,它将创建一个且唯一的启动实例,为其选择一个位置(在单个虚拟七单元格行上的三个连续单元格),询问用户进行猜测,检查猜测并重复,直到所有三个单元格都被击中。

记住虚拟行是...虚拟的。换句话说,它在程序中不存在。只要游戏和用户都知道启动隐藏在七个可能单元格(从零开始)中的三个连续单元格中,行本身就不需要在代码中表示。您可能会想要构建一个包含七个整数的数组,然后将启动分配给数组中的三个元素,但您不需要这样做。我们只需要一个数组,其中包含启动占据的三个单元格。

图片

  • 图片 游戏开始 并创建一个启动,将其放置在七个单元格中的一个单行上的三个单元格上。

    而不是“A2”,“C4”等等,位置只是整数(例如:1,2,3 是图片中的单元格位置):

    图片

  • 图片 游戏开始。 提示用户进行猜测;然后检查是否击中任何启动的三个单元格。如果击中,增加 numOfHits 变量。

  • 图片 游戏完成当所有三个单元都被击中时(numOfHits 变量的值为 3),并告诉用户击沉 Startup 花了多少次猜测。

完整的游戏交互

图片

开发一个类

作为程序员,您可能有一套编写代码的方法论/过程/方法。我们也是如此。我们的顺序旨在帮助您看到(和学习)我们在编写类的过程中的思维方式。在真实世界中,这不一定是我们(或)编写代码的方式。当然,在真实世界中,您将遵循个人喜好、项目或雇主指定的方法。但是,我们可以做几乎任何我们想做的事情。当我们创建一个“学习体验”的 Java 类时,我们通常是这样做的:

  • 图片 弄清楚这个类应该什么。

  • 图片 列出实例变量和方法。

  • 图片 为方法编写准备代码。(您很快就会看到这一点。)

  • 图片 为方法编写测试代码

  • 图片 实现这个类。

  • 图片 测试方法。

  • 图片 根据需要调试重新实现

  • 图片 表达感激之情,因为我们不必在真实的用户上测试我们所谓的学习体验应用程序。

图片

我们为每个类写三件事:

图片

此条显示在下一组页面上,告诉您正在处理的部分。例如,如果您在页面顶部看到这张图片,那么意味着您正在处理 SimpleStartup 类的准备代码。

图片

准备代码

一种伪代码形式,帮助您专注于逻辑,而不用担心语法。

测试代码

一个类或方法将测试真实代码并验证它是否做正确的事情。

真实代码

类的实际实现。这是我们编写真正的 Java 代码的地方。

图片

阅读这个例子后,您将会理解到准备代码(我们版本的伪代码)是如何工作的概念。它在真实的 Java 代码和类的简单英文描述之间处于中间状态。大多数准备代码包括三个部分:实例变量声明,方法声明,方法逻辑。准备代码最重要的部分是方法逻辑,因为它定义了需要发生的事情,这些事情在我们实际编写方法代码时会被翻译成如何发生。

图片

编写方法实现

现在让我们写真正的方法代码并让这个程序正常工作。

图片

在我们开始编写方法之前,让我们回顾一下并编写一些代码来测试这些方法。是的,在有任何需要测试的东西之前,我们先编写测试代码!

先写测试代码的概念是测试驱动开发(TDD)的实践之一,它可以让您更轻松(更快地)编写代码。我们不一定建议您使用 TDD,但我们确实喜欢先写测试的部分。而 TDD 听起来就很酷。

为 SimpleStartup 类编写测试代码

我们需要编写测试代码,可以创建 SimpleStartup 对象并运行其方法。对于 SimpleStartup 类,我们真正关心的只有 checkYourself() 方法,尽管我们 不得不实现 setLocationCells() 方法以确保 checkYourself() 方法正确运行。

仔细看看下面为 checkYourself() 方法准备的准备代码(setLocationCells() 方法是一个简单的 setter 方法,所以我们不担心,但在一个“真实”的应用程序中,我们可能需要一个更健壮的“setter”方法来测试)。

然后问自己,“如果 checkYourself() 方法被实现了,我能写什么测试代码来证明该方法工作正常?”

根据这些准备代码:

METHOD String checkYourself(int userGuess)
 GET the user guess as an int parameter
 REPEAT with each of the location cells in the int array
   // *COMPARE* the user guess to the location cell
   IF the user guess matches
      INCREMENT the number of hits
      *// FIND OUT* if it was the last location cell:
      IF number of hits is 3, RETURN “Kill” as the result
      ELSE it was not a kill, so RETURN “Hit”
      END IF
   ELSE the user guess did not match, so RETURN “Miss”
   END IF
  END REPEAT
END METHOD

这里是我们应该测试的内容:

  1. 实例化一个 SimpleStartup 对象。

  2. 分配一个位置(一个包含 3 个整数的数组,如 {2, 3, 4})。

  3. 创建一个整数表示用户的猜测(2、0 等)。

  4. 调用 checkYourself() 方法并传递一个虚假的用户猜测。

  5. 打印结果以查看是否正确(“通过”或“失败”)。

测试 SimpleStartup 类的代码

image

Images 请解决。

checkYourself() 方法

从准备代码到 Java 代码没有完美的映射;我们会做一些调整。准备代码让我们更清楚代码需要做什么,现在我们必须弄清楚如何编写可以执行 how 的 Java 代码。

在心里想想你可能想要(或需要)改进的代码部分。数字 image 是你以前没见过的东西(语法和语言特性)。它们在对面页面有解释。

image

只是新的东西

我们以前没见过的东西都在这一页上。别担心!本章后面还有更多细节。这些内容足以让你开始。

image

SimpleStartup 和 SimpleStartupTestDrive 的最终代码

public class SimpleStartupTestDrive {
  public static void main(String[] args) {
    SimpleStartup dot = new SimpleStartup();
    int[] locations = {2, 3, 4};
    dot.setLocationCells(locations);
    int userGuess = 2;
    String result = dot.checkYourself(userGuess);
    String testResult = "failed";
    if (result.equals("hit")) {
      testResult = "passed";
    }
    System.out.println(testResult);
  }
}

class SimpleStartup {
  private int[] locationCells;
  private int numOfHits = 0;

  public void setLocationCells(int[] locs) {
    locationCells = locs;
  }

  public String checkYourself(int guess) {
    String result = "miss";
    for (int cell : locationCells) {
      if (guess == cell) {
        result = "hit";
        numOfHits++;
        break;
      } // end if
    } // end for
    if (numOfHits ==
        locationCells.length) {
      result = "kill";
    } // end if
    System.out.println(result);
    return result;
  } // end method
} // close class

这里潜藏着一个小 bug。它可以编译和运行,但...现在不用担心,但稍后我们 不得不面对它。

SimpleStartupGame 类的准备代码,所有操作都在 main() 中完成。

有一些事情你必须相信。例如,我们有一行准备代码说“从命令行获取用户输入。”让我告诉你,这比我们现在想要从头开始实现的要多一点。但幸运的是,我们正在使用 OO。这意味着你可以请求其他类/对象为你做一些事情,而不用担心它是如何做到的。当你编写准备代码时,你应该假设某种方式你将能够做任何你需要做的事情,这样你就可以将所有的脑力投入到解决逻辑问题上。

*public static void main (String [] args)*
   DECLARE an int variable to hold the number of user guesses, named numOfGuesses, and set it to 0
   MAKE a new SimpleStartup instance
   COMPUTE a random number between 0 and 4 that will be the starting location cell position
   MAKE an int array with 3 ints using the randomly generated number, that number incremented by 1, and that number incremented by 2 (example: 3,4,5)
   INVOKE the setLocationCells() method on the SimpleStartup instance
   DECLARE a boolean variable representing the state of the game, named *isAlive.* SET it to true

   WHILE the Startup is still alive (isAlive == true):
     GET user input from the command line
     *// CHECK* the user guess
     INVOKE the *checkYourself()* method on the SimpleStartup instance
     INCREMENT *numOfGuesses* variable
     ***// CHECK*** for Startup death
    IF result is “kill”
          SET isAlive to false (which means we won’t enter the loop again)
          PRINT the number of user guesses
    END IF
   END WHILE
END METHOD

图片

游戏的 main()方法

就像你对 SimpleStartup 类所做的那样,考虑一下你可能想要(或需要)改进的代码部分。编号的东西图片是我们想要指出的内容。它们在对面的页面上有解释。哦,如果你想知道为什么我们跳过了这个类的测试代码阶段,我们不需要为游戏编写测试类。它只有一个方法,那么在你的测试代码中你会做什么?创建一个单独的类来调用这个类的 main()吗?我们没费心,我们只需运行这个来测试它。

图片

random()和 getUserInput()

这页上有两件需要更详细解释的事情。这只是一个快速的查看,以便让你继续前进;有关 GameHelper 类的更多细节在本章末尾。

图片图片

最后一个类:GameHelper

我们制作了Startup类。

我们制作了game类。

剩下的只有helper—具有 getUserInput()方法的类。获取命令行输入的代码超出了我们现在想要解释的范围。这会引出最好留到以后讨论的话题。(以后,即第十六章,保存对象。)

只需复制下面的代码并将其编译成一个名为 GameHelper 的类。将所有三个类文件(SimpleStartup、SimpleStartupGame、GameHelper)放入同一个目录,并将其设置为工作目录。

图片

import java.util.Scanner;

public class GameHelper {
  public int getUserInput(String prompt) {
    System.out.print(prompt + ": ");
    Scanner scanner = new Scanner(System.in);
    return scanner.nextInt();
  }
}

图片

我们知道你有多喜欢打字,但在那些你更愿意做其他事情的罕见时刻,我们已经将 Ready-Bake Code 提供在oreil.ly/hfJava_3e_examples上。

让我们玩吧

当我们运行它并输入数字 1,2,3,4,5,6 时会发生什么。看起来不错。

一个完整的游戏交互

(你的情况可能有所不同)

图片

这是什么?一个 bug?

哇!

当我们输入 1,1,1 时会发生什么。

另一个游戏交互(天啊)

图片

更多关于 for 循环的内容

我们已经涵盖了章节的所有游戏代码(但我们将在下一章节中继续完成游戏的高级版本)。我们不想用一些细节和背景信息打断你的工作,所以我们把它放在这里。我们将从 for 循环的细节开始,如果你在另一种编程语言中看到了这种语法,只需浏览一下最后几页...

常规(非增强型)for 循环

imageimage

用简单的英语说: “重复 100 次。”

编译器如何看待它:

  • 创建一个变量 i 并将其设置为 0。

  • i 小于 100 时重复。

  • 在每次循环迭代结束时,将 i 加 1。

第一部分:初始化

使用此部分声明和初始化一个变量,在循环体内使用。你通常会将此变量用作计数器。实际上,你可以在这里初始化多个变量,但更常见的是使用单个变量。

第二部分:布尔测试

这是条件测试的地方。无论里面放了什么,它必须解析为布尔值(你知道的,truefalse)。你可以有一个测试,比如 (x >= 4),或者甚至可以调用一个返回布尔值的方法。

第三部分:迭代表达式

在此部分,放入你希望每次循环都发生的一个或多个事情。请记住,这些内容发生在每次循环的末尾

重复 100 次:

循环的步骤

for (int i = 0; i < 8; i++) {
   System.out.println(i);
}
System.out.println("done");

image

for 和 while 的区别

while 循环只有布尔测试;它没有内置的初始化或迭代表达式。当你不知道要循环多少次,只想在某些条件为真时继续时,while 循环很好用。但如果你知道要循环多少次(例如,数组的长度,7 次等),用 for 循环更清晰。以下是上面的循环使用 while 重写的示例:

image

输出:

image

增强型 for 循环

Java 语言在 Java 5 中添加了另一种称为增强型 forfor 循环。这使得在数组或其他类型的集合中迭代所有元素更加简单(下一章节将介绍其他类型的集合)。增强型 for 循环仅仅提供了一个更简单的方式来遍历集合中的所有元素。我们也会在下一章中看到增强型 for 循环,当我们讨论不是数组的集合时。

image

用简单的英语说: “对于 nameArray 中的每个元素,将该元素赋给 'name' 变量,并运行循环体。”

编译器如何看待它:

  • 创建一个名为 name 的 String 变量并将其设置为 null。

  • nameArray 中的第一个值分配给 name。

  • 运行循环体(由花括号限定的代码块)。

  • nameArray 中的下一个值分配给 name。

  • 当数组中仍然有元素时重复。

注意

注意:根据他们过去使用的编程语言不同,有些人称增强型 for 循环为“for each”或“for in”循环,因为这就是它的读法:“for EACH thing IN the collection…”

第一部分:迭代变量声明

使用此部分来声明和初始化一个变量,在循环体内使用。每次循环迭代时,这个变量将保存来自集合的不同元素。这个变量的类型必须与数组中的元素兼容!例如,你不能声明一个int类型的迭代变量并用于String[]数组

第二部分:实际集合

这必须是一个数组或其他集合的引用。同样,暂时不用担心其他非数组类型的集合——你将在下一章中看到它们。

原始数据类型的类型转换

在我们结束本章之前,我们想解决一个悬而未决的问题。当我们使用 Math.random()时,我们必须将结果转换为 int。将一个数值类型转换为另一个可以改变值本身。理解这些规则非常重要,这样你就不会对此感到意外。

image

在第三章,了解你的变量中,我们讨论了各种原始数据类型的大小以及你不能直接将大数据放入小数据中的问题:

    long y = 42;
    int x = y;       // won’t compile

long 比 int 大,编译器不能确定 long 的值可能是多少。它可能已经与其他 long 一起参加聚会,并且具有非常大的值。为了强制编译器将较大的原始变量值装入较小的原始变量中,你可以使用类型转换操作符。它看起来像这样:

    long y = 42;     // so far so good
    int x = (int) y; // x = 42 cool!

加入类型转换会告诉编译器将 y 的值截断为 int 大小,并将 x 设置为剩余的值。如果 y 的值大于 x 的最大值,那么剩下的将是一个奇怪(但可计算的*)数字:

    long y = 40002;       // 40002 exceeds the 16-bit limit of a short
    short x = (short) y;  // x now equals -25534!

不过,重点是编译器允许你这样做。假设你有一个浮点数,你只想获取它的整数(int)部分:

    float f = 3.14f;
    int x = (int) f;   //  x will equal 3

别想把任何东西转换成布尔型或反之——赶紧离开。

*它涉及符号位、二进制、“二进制补码”和其他极客技术。

练习

image

BE the JVM

image

本页面上的 Java 文件表示一个完整的源文件。你的任务是扮演 JVM 并确定程序运行时的输出结果。

class Output {
  public static void main(String[] args) {
    Output output = new Output();
    output.go();
  }

  void go() {
    int value = 7;
    for (int i = 1; i < 8; i++) {
      value++;
      if (i > 4) {
        System.out.print(++value + " ");
      }
      if (value > 14) {
        System.out.println(" i = " + i);
        break;
      }
    }
  }
}

image

-或-

image

-或-

image

Images 答案在“Be the JVM”中。

代码磁铁

image

一个工作中的 Java 程序被混在冰箱上。你能重组这些代码片段,使其成为一个能够产生下面列出的输出的工作中的 Java 程序吗?一些大括号掉到了地板上,而它们太小了,没法捡起来,所以随意添加你需要的大括号!

图片图片

图片 答案在“代码磁铁”中。

JavaCross

图片图片

如何通过填字游戏学习 Java?嗯,所有的单词与 Java 相关。此外,提示提供了隐喻、双关语等,这些心理转折直接刻印在你的大脑中,通向 Java 知识的替代路径!

横向

1. 构建的花哨计算机术语

4. 多部分循环

6. 先测试

7. 32 位

10. 方法的答案

11. 准备代码般

13. Change

15. 大工具包

17. 一个数组单元

18. 实例或本地

20. 自动工具包

22. 看起来像原始类型,但是..

25. 无法转换

26. 数学方法

28. 迭代我

29. 早退

向下

2. 增量类型

3. 类的主力工作

5. Pre 是一种 _____

6. For 循环的迭代 ______

7. 确定第一个值

8. While 或 For

9. 更新实例变量

12. 走向发射

14. 一个周期

16. 健谈的包

19. 方法信使(简称)

21. 好像

23. 在之后添加

24. Pi 之家

26. 编译并 ____

27. ++ 数量

图片 答案在“JavaCross”中。

混合信息

图片

下面列出了一个简短的 Java 程序。程序中有一个代码块缺失。你的挑战是将候选代码块(左侧)与插入后看到的输出匹配。不会使用所有输出行,某些输出行可能会重复使用。用线条连接候选代码块和它们匹配的命令行输出。

图片 答案在“混合信息”中。

图片图片

将每个候选项与可能的输出匹配

练习解决方案

图片

成为 JVM

(来自“成为 JVM”)

class Output {

  public static void main(String[] args) {
    Output output = new Output();
    output.go();
  }

void go() {
  int value = 7;
  for (int i = 1; i < 8; i++) {
    value++;
    if (i > 4) {
      System.out.print(++value + " ");
    }
    if (value > 14) {
      System.out.println(" i = " + i);
      break;
    }
   }
  }
}
注意

你记得考虑 break 语句了吗?它如何影响输出?

图片

代码磁铁

(来自“代码磁铁”)

class MultiFor {

  public static void main(String[] args) {
    for (int i = 0; i < 4; i++) {

     for (int j = 4; j > 2; j--) {
       System.out.println(i + " " + j);
     }

     if (i == 1) {
       i++;
     }
   }
  }
}
注意

如果这段代码块在‘j’循环之前会发生什么?

图片

谜题解决方案

图片

JavaCross

(来自“JavaCross”)

图片

混合信息

(来自“混合信息”)

图片

第六章:使用 Java 库:了解 Java API

image

Java 自带数百个预先构建的类。 如果你知道如何在 Java 库中找到所需的内容,就无需重复造轮子,这就是Java API的用途。 你有更重要的事情要做。 如果你要编写代码,最好只编写对你的应用程序真正定制的部分。 你知道每天下午 5 点准时下班的程序员吗?那些早上 10 点才露面的?他们在使用 Java API。 大约再过八页,你也将会如此。 Java 核心库是一个巨大的类堆积,等待你像积木一样使用,从中组合出自己的程序。 本书中使用的现成 Java 代码不需要从头开始创建,但你仍然需要输入它。 Java API 充满了你甚至不需要输入的代码。 你只需学会如何使用它。

在我们的上一章中,我们给你留下了一个悬念:一个错误

应该是什么样子的

当我们运行它并输入数字 1, 2, 3, 4, 5, 6 时会发生什么。看起来不错。

完整的游戏交互(结果可能有所不同)

image

这个 bug 的外观

当我们输入 2, 2, 2 时会发生什么

另一种游戏交互方式(哎呀)

image

在当前版本中,一旦你击中,你可以简单地再重复两次来击毁目标!

那么到底发生了什么?

image

我们如何修复它?

我们需要一种方法来知道单元格是否已经被命中。让我们先看看我们目前所知道的...

我们有一个虚拟的七个单元格的行,并且一个启动程序将占据该行的三个连续单元格中的某个位置。 这个虚拟行显示一个在单元格位置 4、5 和 6 放置的启动程序。

image

启动程序有一个实例变量 —— 一个 int 数组 —— 用于保存该启动程序对象的单元格位置。

image

选项一太笨重了

选项一似乎比你预期的要多些工作。这意味着每当用户进行一次命中时,你都必须更改第二个数组(命中单元格数组)的状态,噢——但首先你必须检查命中单元格数组,看看那个单元格是否已经被命中过。它可以工作,但肯定还有更好的方法...

选项二稍微好一些,但仍然相当笨重

选项二比选项一少一些笨重,但效率不是很高。 即使一个或多个位置已经无效(因为它们已经被“击中”并有一个 -1 的值),你仍然必须循环遍历数组中的所有三个插槽(索引位置)。必须有更好的方法...

用于 checkYourself() 方法的原始准备代码: 如果我们能把它改成:
image

image

醒来,闻闻图书馆的味道

仿佛魔术一般,真的有这样的事情。

但它不是一个数组,而是一个 ArrayList。

核心 Java 库中的一个类(API)。

Java 平台标准版(Java SE)附带数百个预构建的类。就像我们的 Ready-Bake Code 一样。不同之处在于这些内置类已经编译过了。

这意味着不需要打字。

只需使用它们。

image

一些你可以用 ArrayList 做的事情

image

Java 暴露

image

本周的采访对象:ArrayList,关于数组

HeadFirst: 所以,ArrayList 就像数组,对吧?

ArrayList: 在他们的梦中!是一个对象,非常感谢。

HeadFirst: 如果我没有弄错,数组也是对象。它们和其他对象一样都存放在堆上。

ArrayList: 当然,数组放在堆上,,但是数组仍然是一个想成为 ArrayList 的东西。一个冒充者。对象有状态行为,对吧?这一点我们很清楚。但是你真的尝试过在数组上调用方法吗?

HeadFirst: 既然你提到了,我不能说我试过。但是我要调用什么方法呢?我只关心在数组中放入东西和取出东西时调用的方法。而且我可以在想要放入和取出数组时使用数组语法。

ArrayList: 是这样吗?你的意思是你真的从数组中移除了某些东西吗?(天哪,他们是在哪里培训你们的?)

HeadFirst: 当然我从数组中取出东西。我说 Dog d = dogArray[1],然后我从数组中的索引 1 处得到 Dog 对象。

ArrayList: 好吧,我会尽量说得慢些,这样你就能跟上了。你并没有,我重申没有,从数组中移除那只 Dog。你所做的只是复制了对 Dog 的引用,并将其赋给另一个 Dog 变量。

HeadFirst: 哦,我明白你的意思了。不,我确实没有从数组中移除 Dog 对象。它仍然在那里。但我可以将其引用设置为 null,我猜。

ArrayList: 但我是一个一流的对象,所以我有方法,我实际上可以做事情,比如从自己身上移除 Dog 的引用,而不只是将其设置为 null。而且我可以动态地改变我的大小(查一下)。试试看如何让一个数组做到这一点!

HeadFirst: 哎呀,真不想提起这个,但谣言说你只不过是一个被夸大但效率低下的数组。实际上,你只是数组的包装器,为像我自己这样的人添加了额外的方法,比如调整大小,我本来要自己写。而且说到这里,你甚至不能容纳原始类型!这不是一个很大的限制吗?

ArrayList: 我简直不敢相信你相信这种城市传说。不,我不仅仅是一个效率低下的数组。我承认可能有极其罕见的情况,数组在某些特定事情上可能会稍微,我重申,稍微快一点点。但是放弃所有这些强大功能来换取微小的性能提升值得吗?再看看这些灵活性。至于基本类型,当然可以将基本类型放入 ArrayList 中,只要它被包装在基本类型包装类中(在 第十章 中会有更多介绍)。如果你使用的是 Java 5 或以上版本,这个包装(以及再次取出基本类型时的解包)会自动发生。好吧,我承认,是的,如果你使用的是基本类型的 ArrayList,可能使用数组会更快,因为涉及到所有的包装和解包,但是还是...这些谁真的还使用基本类型?

哦,看看时间!我要迟到上普拉提课了。 我们下次再做。

解决方案

磨练你的铅笔

(来自 “磨练你的铅笔”)

图片图片

使用 ArrayList 时,你在操作一个类型为 ArrayList 的对象,所以你只是在一个普通的对象上调用普通的老方法,使用普通的点运算符。

对于 数组,你使用 特殊的数组语法(例如 myList[0] = foo),这种语法除了数组之外你不会在其他地方使用。尽管数组是一个对象,它生活在自己的特殊世界中,你不能对其调用任何方法,尽管可以访问它唯一的实例变量 length

将 ArrayList 与普通数组进行比较

  • 图片 一个普通的数组在创建时必须知道其大小。

    但是对于 ArrayList,你只需创建一个类型为 ArrayList 的对象。每次都是如此。它永远不需要知道它应该有多大,因为它会随着对象的添加或删除而增长或缩小。

    图片

  • 图片 要将对象放入普通数组中,必须将其分配给特定位置。

    (索引从 0 到数组长度减 1。)

    图片

    如果索引超出数组的边界(例如数组声明大小为 2,现在尝试对索引 3 赋值),它会在运行时崩溃。

    使用 ArrayList,你可以使用 add(anInt, anObject) 方法指定索引,或者只需不断使用 add(anObject),ArrayList 会自动增长以腾出空间来存放新元素。

    图片

  • 图片 数组使用 Java 中其他地方不使用的数组语法。

    但是 ArrayList 是普通的 Java 对象,因此它们没有特殊的语法。

    图片

  • 图片 ArrayList 是参数化的。

    我们刚刚说过,与数组不同,ArrayList 没有特殊的语法。但是它们确实使用了一些特殊的东西——参数化类型

    图像

使用 语法,我们可以声明并创建一个 ArrayList,它知道(并限制)可以保存的对象类型。我们将在第十一章的数据结构中详细讨论 ArrayLists 中参数化类型的细节,所以现在不要太过深思熟虑看到尖括号<>语法时,只需知道这是一种强制编译器只允许 ArrayList 中特定类型对象的方法即可。

Java 5 中添加了参数化类型,这是很久以前的事情,你几乎肯定在使用支持它们的版本!

图像

让我们修复启动代码

记住,这就是有错误版本的样子:

图像

新的改进的 Startup 类

图像

让我们构建真正的游戏:“击沉一个启动”

我们一直在工作的是“简化”版本,但现在让我们构建真正的版本。我们将不再使用单行,而是使用一个网格。而不是一个启动,我们将使用三个。

目标: 以尽可能少的猜测数击沉计算机的所有启动。根据您的表现,您将获得一个评级水平。

设置: 当游戏程序启动时,计算机会将三个启动随机放置在 虚拟的 7 x 7 网格 上。完成后,游戏会询问您的第一个猜测。

如何游戏: 我们还没有学习如何构建 GUI,因此这个版本在命令行上工作。计算机将提示您输入一个猜测(一个单元格),您将在命令行中输入它(如“A3”,“C5”等)。作为对您的猜测的响应,您将在命令行看到一个结果,要么是“命中”,要么是“未命中”,要么是“您击沉了 poniez”(或者今天的幸运启动)。当您把所有三个启动送上天堂大 404 时,游戏将打印出您的评级结束。

图像

您将要建立的是“击沉一个启动”的游戏,使用 7 x 7 的网格和三个启动。每个启动占据三个单元格。

游戏互动的一部分

图像

需要做出什么改变?

我们需要更改三个类:启动类(现在称为 Startup 而不是 SimpleStartup)、游戏类(StartupBust)和游戏助手类(我们现在不担心)。

  • 图像 Startup 类

    • 添加一个 名称 变量 来保存启动的名称(“poniez”,“cabista”等),这样每个启动在被击败时都可以打印出其名称(请参见对面屏幕上的输出)。
  • 图像 StartupBust 类(游戏)

    • 创建 三个 启动而不是一个。

    • 给这三个启动每个一个 名称 在每个启动实例上调用一个 setter 方法,以便启动可以将名称分配给其名称实例变量。

    • 将启动放在网格上,而不仅仅是一行,对所有三个启动都是如此。

      如果我们要随机放置 Startups,那么现在这一步比以前复杂得多。因为我们不想搞砸数学,所以我们将为给 Startups 分配位置的算法放入 GameHelper(即食代码)类中。

    • 检查每个用户猜测与所有三个 Startups,而不仅仅是一个。

    • 继续玩游戏(即接受用户的猜测并与剩余的 Startups 进行检查)直到没有更多的活跃 Startups 为止。

    • 退出主程序。 我们保留了主程序中的简单代码,只是为了…保持简单。但这不是我们想要的真正游戏。

3 个类:

图片

5 个对象:

图片

StartupBust 游戏中的参与者及其活动时机

  • 图片

    图片

  • 图片

    图片

  • 图片

    图片

  • 图片

    图片

  • 图片

    图片

  • 图片

    图片

准备好实际的 StartupBust 类的代码

图片

StartupBust 类有三个主要任务:设置游戏,玩游戏直到所有 Startups 死亡,并结束游戏。尽管我们可以直接将这三个任务映射为三个方法,但我们将中间任务(玩游戏)分成个方法,以保持粒度较小。较小的方法(意味着更小的功能块)有助于我们更轻松地测试、调试和修改代码。

变量声明

声明并实例化GameHelper实例变量,命名为helper

声明并实例化一个ArrayList来保存 Startup 的列表(最初为三个)。命名为startups

声明一个 int 变量来保存用户猜测的次数(这样我们可以在游戏结束时给用户一个得分)。将其命名为numOfGuesses并将其设置为 0。


方法声明

声明一个setUpGame()方法来创建并初始化具有名称和位置的 Startup 对象。向用户显示简要说明。

声明一个startPlaying()方法,询问玩家猜测并调用 checkUserGuess()方法,直到所有 Startup 对象从游戏中移除。

声明一个checkUserGuess()方法,该方法循环遍历所有剩余的 Startup 对象,并调用每个 Startup 对象的 checkYourself()方法。

声明一个finishGame()方法,根据用户击沉所有 Startup 对象所需的猜测次数打印用户表现的消息。


方法实现

方法:void setUpGame()

// 创建三个 Startup 对象并为它们命名

创建三个 Startup 对象。

设置每个 Startup 的名称。

添加Startups 到startups(ArrayList)中。

对于startups列表中的每个 Startup 对象重复以下操作:

调用helper 对象上的placeStartup()方法,以获取随机选择的

在 7 X 7 的网格上,为该 Startup 设置位置(垂直或水平三个单元格)。

根据placeStartup()调用的结果为每个 Startup 设置位置。

结束重复

结束方法

方法:void startPlaying()

在任何 Startups 存在时重复

通过调用助手getUserInput()方法获取用户输入。

通过checkUserGuess()方法评估用户的猜测。

结束重复

结束方法

方法:void checkUserGuess(String userGuess)

// 查找是否有任何 Startup 上的击中(和击毁)

numOfGuesses变量中增加用户猜测的次数。

将本地result变量(一个String)设置为“miss”,假设用户的猜测会失败。

startups列表中重复每个 Startup 对象。

通过调用 Startup 对象的checkYourself()方法评估用户的猜测。

根据情况将结果变量设置为“hit”或“kill”。

如果结果是“kill”,则从startups列表中移除该 Startup。

结束重复

显示结果值给用户。

结束方法

方法:void finishGame()

显示一个通用的“游戏结束”消息,然后:

如果用户猜测的次数很少,

显示祝贺消息。

否则

显示一个侮辱性的消息。

结束如果

结束方法

图片 由您解决。

图片图片图片图片图片图片

Startup 类的最终版本

图片

超级强大的布尔表达式

到目前为止,当我们在循环或if测试中使用布尔表达式时,它们通常很简单。在接下来要看到的一些现成的代码中,我们将使用更强大的布尔表达式,尽管我们知道您不会偷看,但现在正是讨论如何激活您的表达式的好时机。

“与”和“或”运算符 ( &&, || )

假设你正在编写一个 chooseCamera()方法,其中有很多关于选择相机的规则。也许你可以选择价格在$50 到$1000 之间的相机,但在某些情况下,你想更精确地限制价格范围。你想表达的可能是:

“如果价格范围在$300 $400 之间,则选择 X。”

if (price >= 300 && price < 400) {
  camera = "X";
}

假设在十个可用的相机品牌中,你有一些逻辑只适用于列表中的少数品牌:

if (brand.equals("A") || brand.equals("B")) {
  // do stuff for only brand A or brand B
}

布尔表达式可能会变得非常庞大和复杂:

if ((zoomType.equals("optical") &&
     (zoomDegree >= 3 && zoomDegree <= 8)) ||
   (zoomType.equals("digital") &&
    (zoomDegree >= 5 && zoomDegree <= 12))) {
   // do appropriate zoom stuff
}

如果你想真正技术性地去探讨,你可能会对这些运算符的优先级感到疑惑。与其成为优先级神秘世界的专家,我们建议您使用括号来使您的代码更清晰。

不等于(!=和!)

假设你有这样一个逻辑:“在十种可用的相机型号中,某个特定的事情对除了一种之外的所有相机型号都成立。”

if (model != 2000) {
  //  do non-model 2000 stuff
}

或者用于比较对象如字符串...

if (!brand.equals("X")) {
 // do non-brand X stuff
}

短路操作符(&&,||)

到目前为止,我们所看过的操作符 && 和 || 被称为短路操作符。对于 &&,只有在 && 的两边都为真时,表达式才为真。所以,如果 JVM 发现 && 表达式的左侧为假,它就会立即停止!甚至不去看右侧。

类似地,对于 ||,如果任一侧为真,表达式就为真,所以如果 JVM 发现左侧为真,它就宣布整个语句为真,并且不会去检查右侧。

为什么这很重要?假设您有一个引用变量,但不确定它是否已分配给一个对象。如果您尝试使用这个空引用变量调用方法(即未分配对象),您将会得到一个 NullPointerException。所以,请尝试这样做:

if (refVar != null &&
    refVar.isValidType()) {
  // do ‘got a valid type’ stuff
}

非短路操作符( &,| )

在布尔表达式中使用时,& 和 | 操作符的作用类似于它们的 && 和 || 对应物,但它们强制 JVM 始终检查表达式边。通常情况下,& 和 | 用于另一个上下文,用于位操作。

现成代码

image

这是游戏的辅助类。除了用户输入方法(提示用户并从命令行读取输入)之外,辅助类的大服务是为 StartupBust 创建单元位置。我们尽量保持它相对较小,这样您就不必输入太多内容。请记住,在您拥有这个类之前,您将无法编译 StartupBust 游戏类。

imageimage

使用库(Java API)

在 ArrayList 的帮助下,您成功完成了 StartupBust 游戏的全部过程。现在,正如承诺的那样,是时候学习如何在 Java 库中操作了。

在 Java API 中,类被分组到包中。

image

要使用 API 中的类,您必须知道类属于哪个包。

Java 库中的每个类都属于一个包。包有一个名称,例如**javax.swing**(包含一些即将学习的 Swing GUI 类的包)。ArrayList 位于名为**java.util**的包中,这个包意外地包含了一堆实用类。您将在附录 B 中学到更多关于包的内容,包括如何将您自己的类放入您自己的包中。不过,目前我们只是希望使用Java 提供的一些类。

在您自己的代码中使用 API 中的类很简单。您只需将这个类视为您自己编写的一样……就像您已经编译了它,它就坐在那里,等待您使用。只有一个很大的不同:在您的代码中的某处,您必须指示您要使用的库类的完整名称,这意味着包名 + 类名。

即使你不知道,你已经在使用来自包的类。 System(System.out.println)、String 和 Math(Math.random())都属于**java.lang**包。

你必须知道你想在代码中使用的类的完整名称。

ArrayList 不是 ArrayList 的全名,就像 Kathy 不是全名(除非像麦当娜或雪儿那样,但我们不去那里)。ArrayList 的全名实际上是:

图片

你必须告诉 Java 你想使用哪个 ArrayList。你有两个选择:

  • 图片 导入

    在你的源代码文件顶部放置一个导入语句:

    import java.util.ArrayList;
    public class MyClass {... }
    

    或者

  • 图片 类型

    在你的代码中到处输入完整的名称。每次使用它。无处不在你使用它。

    当你声明和/或实例化它时:

    java.util.ArrayList<Dog> list = new java.util.ArrayList<Dog>();
    

    当你将其用作参数类型时:

    public void go(java.util.ArrayList<Dog> list) { }
    

    当你将其用作返回类型时:

    public java.util.ArrayList<Dog> foo() {...}
    

    *除非该类在 java.lang 包中。

再说一次,如果你还没有完全掌握:

图片

“知道有 java.util 包中的 ArrayList 很好。但是自己,我怎么可能找出这一点呢?”

  • 朱莉娅,31 岁,手模特

图片

如何发现 API

你想知道的两件事情:

  • 图片 图书馆中有哪些功能?(哪些类?)

  • 图片 你如何使用这些功能?(一旦找到一个类,你如何知道它能做什么?)

  • 图片 浏览一本书

    图片

  • 图片 使用 HTML API 文档

    图片

docs.oracle.com/en/java/javase/17/docs/api/index.html

图片 浏览一本书

图片

翻阅参考书是发现 Java 库中内容的好方法。通过浏览页面,你可以轻松地找到看起来有用的包或类。

图片

图片 使用 HTML API 文档

Java 自带了一个名为 Java API 的绝妙在线文档集。你(或你的 IDE)也可以下载这些文档以备不时之需,以防你的互联网连接在最糟糕的时刻失效。

API 文档是获取有关包中内容更多细节的最佳参考,并且包括包中的类和接口提供的方法和功能。

文档的外观取决于你使用的 Java 版本。确保你查看的是你 Java 版本的文档!

Java 8 及更早版本

docs.oracle.com/javase/8/docs/api/index.html

图片

你可以浏览这些文档:

  • 自上而下: 从左上角的列表中找到你感兴趣的包,并深入研究。

  • 以类为首: 在左下方的列表中找到你想要了解更多的类,并点击它。

主面板会展示你正在查看的内容的详细信息。如果你选择了一个包,它会给出关于该包的摘要信息以及类和接口的列表。

如果你选择一个类,它会显示该类的描述以及所有方法的详细信息,包括它们的功能和使用方法。

Java 9 及更高版本

Java 9 引入了 Java 模块系统,我们在本书中不会涉及。但你需要了解的是,为了理解文档,JDK 现在被划分为 模块。这些模块将相关的包组合在一起。这样做可以更轻松地找到你感兴趣的类,因为它们按功能分组。到目前为止,我们在本书中讨论过的所有类都在 java.base 模块中;这包括核心 Java 包如 java.lang 和 java.util。

image

你可以浏览这些文档:

  • 自顶向下: 找到一个看起来涵盖你想要的功能的模块,查看其包,并从包中的类中进一步深入。

  • 搜索: 使用右上角的搜索功能直接跳转到你想了解的方法、类、包或模块。

image

使用类文档

无论你使用的是哪个版本的 Java 文档,它们都有类似的布局来显示关于特定类的信息。这里有详细的细节信息。

假设你正在浏览参考书,并找到了一个名为 ArrayList 的类,在 java.util 包中。书上介绍了一些信息,足够让你知道这正是你想要使用的,但你仍然需要了解更多关于方法的信息。在参考书中,你会找到 indexOf() 方法。但如果你只知道有一个叫做 indexOf() 的方法,它接受一个对象并返回该对象的索引(一个 int 值),那么你仍然需要知道一个关键信息:如果该对象不在 ArrayList 中会发生什么?仅仅看方法签名是无法告诉你这个问题的。但 API 文档会告诉你(大部分情况下)。API 文档告诉你,如果对象参数不在 ArrayList 中,indexOf() 方法会返回 -1。所以现在我们知道,它既可以用作检查对象是否在 ArrayList 中的方法,也可以同时获取其索引(如果对象存在的话)。但如果没有 API 文档,我们可能会认为 indexOf() 方法会在对象不在 ArrayList 中时出现错误。

image

在 第十一章 和 第十二章 中,你会看到我们如何使用 API 文档来学习如何使用 Java 库。

练习

image

代码磁铁

image

你能重新构建代码片段以创建一个能生成以下输出的工作 Java 程序吗? 注意: 要完成这个练习,你需要一条新的信息——如果你查看 ArrayList 的 API,你会发现第二个接受两个参数的 add 方法:

add(int index, Object o)

它让你指定向 ArrayList 放置正在添加的对象的位置。

图片

图片 答案在“Code Magnets”.

JavaCross

图片

这个填字游戏如何帮助你学习 Java?嗯,所有的词都 Java 相关(除了一个误导性的)。

提示: 当你怀疑时,记住 ArrayList。

图片

横向

1. 我不能表现得像

6. 或者,在法庭上

7. 它在哪,宝贝

9. 叉子的起源

12. 增长一个 ArrayList

13. 完全巨大

14. 值复制

16. 不是一个对象

17. 一个增强版的数组

19. 范围

21. 19 的对应物

22. 西班牙极客小吃(注:这与 Java 没有关系。)

23. 给懒惰的手指

24. 包的漫游地

下降

2. Java 行动的地方

3. 可寻址的单位

4. 第二小的

5. 分数默认

8. 图书馆的最伟大

10. 必须是低密度的

11. 他就在那里

15. 如同

16. 匮乏方法

18. 购物和数组有什么共同点

20. 图书馆的缩写

21. 事物循环

更多提示:

横向

1. 8 种类

7. 想想 ArrayList

16. 常见的原始类型

21. 数组的范围

22. 不是关于 Java 的——西班牙开胃小吃

下降

2. 可重写的是什么?

3. 想想 ArrayList

4. & 10. 原始类型

16. 想想 ArrayList

18. 他在制造一个 ______

图片 答案在“JavaCross”.

练习解决方案

图片图片

Code Magnets

(来自“Code Magnets”)

import java.util.ArrayList;

public class ArrayListMagnet {
  public static void main(String[] args) {
    ArrayList<String> a = new ArrayList<String>();
    a.add(0, "zero");
    a.add(1, "one");
    a.add(2, "two");
    a.add(3, "three");
    printList(a);

    if (a.contains("three")) {
      a.add("four");
    }
    a.remove(2);
    printList(a);

    if (a.indexOf("four") != 4) {
      a.add(4, "4.2");
    }
    printList(a);

    if (a.contains("two")) {
      a.add("2.2");
    }
    printList(a);
  }

  public static void printList(ArrayList<String> list) {
    for (String element : list) {
      System.out.print(element + "  ");
    }
    System.out.println();
  }
}

JavaCross

(来自“JavaCross”)

图片图片

第七章:在 Objectville 中过上更好的生活:继承和多态

image

为未来规划您的程序。 如果有一种方法可以编写 Java 代码,让您可以更多地度假,那对您来说价值多少?如果您可以编写其他人可以轻松扩展的代码呢?如果您可以编写灵活的代码,以应对那些令人讨厌的临时规范更改,这是否是您感兴趣的?那么今天是您的幸运日。只需轻松的三次 60 分钟的时间投入,您就可以拥有这一切。当您加入多态计划时,您将学会 5 步更好的类设计,3 个多态技巧,8 种编写灵活代码的方法,如果您现在行动——还有一个额外的关于利用继承的 4 个技巧的奖励课程。不要拖延,这么好的优惠将为您提供您应得的设计自由和编程灵活性。快速,简单,现在就可以获得。今天开始,我们将为您额外增加一个抽象层次!

椅子之战重温……

还记得在第二章里,当劳拉(过程式程序员)和布拉德(面向对象开发者)为 Aeron 椅子争斗的时候吗?让我们回顾一下那个故事的几个片段,复习继承的基础知识。

  • LAURA: 你有重复的代码!旋转过程在所有四个形状中都有。这是一个愚蠢的设计。你必须维护四个不同的旋转“方法”。这样的设计怎么能好?

BRAD: 哦,我猜你还没看到最终的设计。让我展示一下面向对象的继承是如何工作的,劳拉。

image

你可以把它理解为“Square 继承自 Shape”,“Circle 继承自 Shape”等等。我从其他形状中移除了 rotate()和 playSound(),现在只需维护一个副本。

Shape 类被称为其他四个类的超类。其他四个是 Shape 的子类。子类继承超类的方法。换句话说,如果 Shape 类具有功能,则子类自动获取相同的功能。

阿米巴的 rotate()呢?

image

LAURA: 这不正是问题的关键吗——阿米巴形状有完全不同的 rotate 和 playSound 过程?

如果阿米巴从 Shape 类继承其功能,它如何执行不同的操作呢?

BRAD: 这是最后一步。Amoeba 类覆盖了 Shape 类需要特定的阿米巴行为的任何方法。然后在运行时,JVM 知道当有人告诉 Amoeba 旋转时应该运行哪个 rotate()方法。

image

理解继承

当您使用继承进行设计时,您将通用代码放入一个类中,然后告诉其他更具体的类通用(更抽象)类是它们的超类。当一个类从另一个类继承时,子类从超类继承。

在 Java 中,我们说子类 extends 父类。继承关系意味着子类继承了父类的成员,包括实例变量和方法。例如,如果 PantherMan 是 SuperHero 的子类,PantherMan 类自动继承了所有超级英雄共有的实例变量和方法,包括 suit, tights, specialPower, useSpecialPower() 等等。但是 PantherMan 子类可以添加自己的新方法和实例变量,并且可以覆盖它从父类 SuperHero 继承的方法。

image

FriedEggMan 不需要任何独特的行为,因此他不会覆盖任何方法。SuperHero 类中的方法和实例变量已经足够了。然而,PantherMan 对他的服装和特殊能力有特定的要求,因此在 PantherMan 类中 useSpecialPower()putOnSuit() 都被覆盖了。

实例变量不会被覆盖,因为它们没有这个必要。它们不定义任何特殊行为,所以子类可以为继承的实例变量赋予任何值。PantherMan 可以将他继承的tights设为紫色,而 FriedEggMan 则将其设为白色。

一个继承的例子:

image

public class Doctor {

  boolean worksAtHospital;

  void treatPatient() {

    // perform a checkup
  }
}

public class FamilyDoctor extends Doctor {

  boolean makesHouseCalls;

  void giveAdvice() {
    // give homespun advice
  }
}

public class Surgeon extends Doctor {

  void treatPatient() {
    // perform surgery
  }

  void makeIncision() {
    // make incision (yikes!)
  }
}

让我们为动物模拟程序设计继承树

想象一下,你被要求设计一个模拟程序,让用户将各种不同的动物扔到环境中看看会发生什么。我们现在不必编写这个程序;我们主要关心的是设计。

我们已经得到了将在程序中出现的一些动物的列表,但不是全部。我们知道每种动物将由一个对象表示,并且这些对象将在环境中移动,执行每个特定类型被编程执行的操作。

而且我们希望其他程序员能够随时向程序中添加新类型的动物。

首先,我们必须找出所有动物共有的抽象特征,并将这些特征构建到一个所有动物类都可以扩展的类中。

  • Images 寻找具有共同属性和行为的对象。

    这六种类型有什么共同点?这可以帮助您抽象出行为。 (步骤 2)

    这些类型有什么共同点?这有助于定义继承树关系(步骤 4-5)

    image

使用继承来避免在子类中重复编写代码

我们有五个实例变量:

picture – 表示这种动物的 JPEG 文件名。

food – 这种动物食用的食物类型。目前只能有两个值:meatgrass

hunger – 表示动物饥饿水平的整数。它会根据动物吃饭的时间(和数量)而变化。

boundaries – 表示动物将漫游的“空间”的高度和宽度的值(例如 640 x 480)。

location – 动物在空间中所处位置的 X 和 Y 坐标。

我们有四个方法:

makeNoise() – 动物应该发出噪音时的行为。

eat() – 动物遇到其首选食物源()时的行为。

sleep() – 动物被认为处于睡眠状态时的行为。

roam() – 动物不吃或睡觉时的行为(可能只是四处游荡等待碰到食物源或边界)。

  • Images 设计一个代表共同状态和行为的类。

这些对象都是动物,因此我们将创建一个名为 Animal 的共同超类。

我们将放入所有动物可能需要的方法和实例变量。

image

所有动物都吃同样的方式吗?

假设我们都同意一件事:实例变量将适用于所有动物类型。狮子会有自己的 picture、food(我们考虑)、hunger、boundaries 和 location 的值。河马将有不同的作为他的实例变量,但他仍将拥有其他动物类型拥有的相同变量。狗、老虎等也是如此。但是行为呢?

我们应该重写哪些方法?

狮子和狗发出相同的声音吗?猫和河马是一样东西吗?也许在的版本中是这样,但在我们的版本中,进食和发出声音是特定于动物类型的。我们无法想出如何编写这些方法,以使它们适用于任何动物。好吧,这不是真的。例如,我们可以编写 makeNoise()方法,使其只是播放由该类型的实例变量定义的声音文件,但这并不是非常专业化。有些动物在不同情况下可能会发出不同的声音(比如吃东西时一个声音,遇到敌人时另一个声音等)。

因此,就像变形虫重写 Shape 类的 rotate()方法以获得更多变形虫特定(换句话说,独特)的行为一样,我们将不得不为我们的动物子类做同样的事情。

  • Images 决定子类是否需要特定于该特定子类类型的行为(方法实现)。

    查看 Animal 类后,我们决定应该由各个子类重写 eat()和 makeNoise()。

imageimage

寻找更多的继承机会

类层次结构开始形成。我们让每个子类重写makeNoise()eat()方法,以便狗的吠声不会被误认为是猫的喵声(对双方都是相当侮辱)。河马也不会像狮子一样吃东西。

但也许我们还可以做得更多。我们必须查看 Animal 的子类,并看看是否可以以某种方式将两个或更多子类分组,并为只属于那个新组的代码提供。

  • Images 寻找更多使用抽象的机会,通过找到两个或更多子类可能需要共同行为的地方。

    我们查看我们的类并看到 Wolf 和 Dog 可能有一些共同的行为,同样适用于 Lion、Tiger 和 Cat。

image

  • Images 完成类层次结构

    由于动物已经有了组织层次(整个界、门、纲的事情),我们可以使用最合适的级别来进行类设计。我们将使用生物学上的“家族”来组织动物,创建一个 Feline 类和一个 Canine 类。

    我们决定犬科动物可以使用一个通用的 roam()方法,因为它们倾向于成群结队移动。我们也看到猫科动物可以使用一个通用的 roam()方法,因为它们倾向于避开同类。我们将让河马继续使用其继承的 roam()方法——从 Animal 类那里继承的通用方法。

    所以我们现在完成了设计;我们将在本章稍后回到它。

image

调用哪个方法?

Wolf 类有四个方法。一个继承自 Animal,一个继承自 Canine(实际上是 Animal 类中一个方法的重写版本),以及两个在 Wolf 类中重写的方法。当你创建一个 Wolf 对象并将其分配给一个变量时,你可以在该引用变量上使用点运算符来调用所有四个方法。但是哪个版本的这些方法会被调用?

image

当你在对象引用上调用一个方法时,你实际上是调用该对象类型的最具体版本的方法。

换句话说,最低的那个获胜!

“最低”指继承树上的最底层。Canine 低于 Animal,Wolf 低于 Canine,因此在对 Wolf 对象的引用上调用方法时,JVM 首先在 Wolf 类中查找。如果 JVM 在 Wolf 类中找不到方法的版本,它会沿着继承层次向上查找,直到找到匹配的方法。

设计一个继承树

imageimage

使用 IS-A 和 HAS-A

image

记住,当一个类继承另一个类时,我们说子类扩展了超类。当你想知道一个东西是否应该扩展另一个东西时,请应用 IS-A 测试。

Triangle IS-A Shape,是的,这可以。

Cat IS-A Feline,这也行。

Surgeon IS-A Doctor,仍然适用。

Tub extends Bathroom,听起来合理。

直到应用 IS-A 测试。

要知道你是否设计了正确的类型,请问:“说类型 X 是类型 Y 有意义吗?”如果不是,你就知道设计有问题了,所以如果我们应用 IS-A 测试,浴缸是浴室显然是不对的。

如果我们反过来,让浴室扩展浴缸呢?那也不行,浴室是浴缸不行。

浴缸和浴室相关的,但不是通过继承。浴缸和浴室通过 HAS-A 关系连接。说“浴室有一个浴缸”有意义吗?如果是,那么意味着浴室有一个浴缸的实例变量。换句话说,浴室对浴缸有一个引用,但浴室不扩展浴缸,反之亦然。

image

浴室有一个浴缸,浴缸有泡泡。

但没有人从其他人那里继承(扩展)。

但等等!还有更多!

IS-A 测试在继承树的任何地方都适用。如果你的继承树设计良好,当你询问任何子类是否是任何其超类型时,IS-A 测试应该是有意义的。

如果 B 类扩展 A 类,则 B 类是 A 类。

这在继承树的任何地方都是正确的。如果类 C 扩展类 B,那么类 C 对于类 B 类 A 都通过了 IS-A 测试。

犬科动物扩展动物

狼扩展犬科动物

狼扩展动物

犬科动物是动物

狼是一种犬科动物

狼是动物

image

有了像这里显示的继承树,你总是可以说“狼扩展动物”或“狼是动物”。如果动物是狼的超类的超类,这并没有任何区别。事实上,只要动物在狼的继承层次结构的某处之上,狼是动物就总是正确的。

动物继承树的结构向世界表明:

“狼是犬科动物,所以狼可以做任何犬科动物可以做的事情。而且狼是动物,所以狼可以做任何动物可以做的事情。”

狼是否覆盖了动物或犬科动物中的某些方法并不重要。对于其他代码的世界来说,狼可以执行这四种方法。他们如何执行这些方法或这些方法在哪个类中被覆盖都不重要。狼可以叫声、吃饭、睡觉和漫步,因为狼扩展自动物类。

你如何知道你的继承是正确的?

到目前为止,我们所涵盖的内容显然还不足,但我们将在下一章中查看更多的面向对象问题(在那里我们最终会对本章的一些设计工作进行精炼和改进)。

然而,现在一个很好的指导原则是使用 IS-A 测试。如果“X 是 Y”有意义,那么这两个类(X 和 Y)很可能应该在同一个继承层次结构中。它们很可能具有相同或重叠的行为。

请记住,继承的 IS-A 关系只能在一个方向上工作!

三角形是一种形状有意义,所以你可以让三角形扩展形状。

但反过来——形状 IS-A 三角形——是有意义的,所以形状不应扩展三角形。请记住,IS-A 关系意味着如果 X 是 Y 的一种,则 X 可以做任何 Y 可以做的事情(可能更多)。

图片 你来解决。

提示:应用 IS-A 测试

谁得到保时捷,谁得到瓷器?(如何知道一个子类可以从其超类继承什么)

图片

子类继承超类的成员。成员包括实例变量和方法,尽管在本书的后面我们将会看到其他继承的成员。超类可以选择是否希望子类继承特定成员,这取决于特定成员的访问级别。

在本书中,我们将涵盖四种访问级别。从最严格到最宽松,这四种访问级别分别是:

图片

访问级别控制谁看见什么,对于拥有设计良好、健壮的 Java 代码至关重要。目前我们将只关注公共和私有两种。对于这两者的规则很简单:

public members *are* inherited
private members are *not* inherited

当子类继承一个成员时,就好像子类自己定义了该成员。在形状的例子中,正方形继承了rotate()playSound()方法,对于外部世界(其他代码),正方形类简单地具有rotate()playSound()方法。

一个类的成员包括在类中定义的变量和方法以及从超类继承的任何内容。

注意

注意:获取有关默认和受保护内容的更多详细信息,请参阅附录 B。

在设计时使用继承,您是在使用还是滥用?

尽管这些规则背后的一些原因直到本书的后面才会被揭示,但现在,简单地了解一些规则将有助于您构建更好的继承设计。

在一个类超类的更具体类型时使用继承。例如:柳树是树的更具体类型,因此柳树扩展树是有意义的。

在您有应该被多个相同类型的类共享的行为(实现的代码)时考虑继承。例如:正方形、圆形和三角形都需要旋转和播放声音,因此将这些功能放在一个超类形状(Shape)中可能是有意义的,也更容易维护和扩展。但是,请注意,虽然继承是面向对象编程的关键特性之一,但并不一定是实现行为重用的最佳方法。它可以让您入门,并且通常是正确的设计选择,但是设计模式将帮助您看到其他更微妙和灵活的选项。如果您不了解设计模式,那么这本书的一个好的后续阅读选择将是《Head First Design Patterns》

不要仅仅为了从另一个类中重用代码而使用继承,如果超类和子类之间的关系违反了上述两条规则中的任何一条。例如,想象一下,在 Animal 类中编写了特殊的打印代码,现在需要在 Potato 类中使用打印代码。你可能会考虑让 Potato 扩展 Animal 以便 Potato 继承打印代码。这毫无意义!Potato 不是 Animal!(因此,打印代码应该在一个 Printer 类中,所有可打印对象都可以通过 HAS-A 关系利用它。)

不要在子类和超类不通过 IS-A 测试时使用继承。始终要问自己子类是否是超类的更具体类型。例如:Tea IS-A Beverage 是有意义的。Beverage IS-A Tea 是没有意义的。

那么,所有这些继承到底带来了什么好处?

通过使用继承进行设计,你可以获得很多面向对象的好处。通过将一组类共有的行为抽象出来,放入一个超类中,你可以消除重复的代码。这样,当你需要修改时,只需更新一个地方,这种变更会神奇地反映在继承了该行为的所有类中。好吧,并没有魔法,但确实很简单:做出改变,然后重新编译该类。就是这样。你不必触及子类!

只需交付新变更的超类,所有扩展它的类将自动使用新版本。

Java 程序无非就是一堆类,因此在使用新版本的超类时,不需要重新编译子类。只要超类不会对子类造成破坏,一切都好。(我们将在本书的后面讨论在这个上下文中“破坏”一词的含义。现在,可以将其理解为修改了超类中某个方法的参数、返回类型、方法名等,而子类依赖于这些内容。)

  • Images 你避免了重复代码。

    将共有代码放在一个地方,并让子类从超类中继承该代码。当你想要改变这种行为时,你只需在一个地方进行修改,所有其他人(即所有子类)都会看到这一变更。

  • Images 你为一组类定义了一个通用协议。

    image

继承能确保所有归于某个特定超类型的类都具有超类型拥有的所有方法*

换句话说,你为通过继承关联的一组类定义了一个通用协议。

当你在超类中定义可以被子类继承的方法时,你在向其他代码宣告一种协议,说:“我的所有子类型(即子类)都可以做这些事情,使用这些看起来像这样的方法…”

换句话说,你建立了一个协议

类 Animal 为所有 Animal 子类型建立了一个通用协议:

image

要记住,当我们说任何 Animal 时,我们指的是 Animal 及其任何从 Animal 继承的类。这也意味着,任何一个在继承层次结构中 Animal 上方的类

但我们甚至还没有到真正酷的部分,因为我们把最好的一部分——多态性——留到了最后。

当你为一组类定义一个超类型时,该超类型的任何子类都可以替换为预期的超类型

嗯,什么?

别担心,我们还远没有解释完。再过两页,你就会成为专家。

我关心的原因是...

你可以利用多态性。

对我很重要的原因是...

你可以使用声明为超类型的引用来引用子类对象。

对我来说意味着...

你可以编写非常灵活的代码。更清洁的代码(更高效、更简单)。不仅更容易开发,而且在编写代码时从未想象过的方式中,也更容易扩展

这意味着你可以在你的同事更新程序时去度假,而且你的同事甚至可能不需要你的源代码。

你将在下一页看到它是如何工作的。

我们不知道你,但就个人而言,我们觉得整个热带假期的事情尤其具有动力。

图片

  • 当我们说“所有方法”时,我们指的是“所有可继承方法”,目前实际上意味着“所有公共方法”,尽管稍后我们会进一步细化这个定义。

要看看多态性是如何工作的,我们必须退后一步,看看我们通常如何声明一个引用并创建一个对象...

对象声明和赋值的三个步骤

图片

  • 图片 声明一个引用变量

    Dog myDog = new Dog();
    

    告诉 JVM 为一个引用变量分配空间。该引用变量永远是 Dog 类型。换句话说,一个遥控器,上面有控制 Dog 的按钮,但没有控制 Cat、Button 或 Socket 的按钮。

    图片

  • 图片 创建一个对象

    Dog myDog = new Dog();
    

    告诉 JVM 在可回收堆上为一个新的 Dog 对象分配空间。

    图片

  • 图片 链接对象和引用

    Dog myDog = new Dog();
    

    将新的 Dog 分配给引用变量 myDog。换句话说,编程遥控器。

    图片

重要的是,引用类型和对象类型是相同的。

在这个例子中,两者都是 Dog。

图片

但是在多态性中,引用类型和对象类型可以是不同的

Animal myDog = new Dog();

图片

使用多态性时,引用类型可以是实际对象类型的超类。

图片

当您声明一个引用变量时,通过“IS-A”测试与引用类型相同的任何对象都可以分配给该变量。换句话说,任何扩展声明的引用变量类型的对象都可以分配给引用变量。这使您能够做像制作多态数组这样的事情。

好的,好的,也许一个例子会有所帮助。

image

但等等!还有更多!

你可以有多态参数和返回类型。

如果您可以声明一个超类型的引用变量,比如 Animal,并将一个子类对象,比如 Dog,分配给它,请想想当引用是方法的参数时可能如何工作...

imageimage

通过多态性,您可以编写在引入新的子类类型到程序时无需更改的代码。

记得那个 Vet 类吗?如果你使用声明为类型Animal的参数编写该 Vet 类,你的代码可以处理任何 Animal 的子类。这意味着,如果其他人想利用你的 Vet 类,他们只需确保他们的新 Animal 类型扩展了 Animal 类。即使 Vet 类在编写时对将要处理的新 Animal 子类型一无所知,Vet 方法仍将正常工作。

保持合约:覆盖规则

当您从超类覆盖方法时,您同意履行合约。合约即说,“我不带参数,返回一个布尔值。”换句话说,您覆盖的方法的参数和返回类型必须在外部世界看起来完全像超类中被覆盖的方法。

方法 就是 合约。

如果多态性要起作用,Appliance 中 Toaster 覆盖方法的版本必须在运行时起作用。请记住,编译器查看引用类型来决定是否可以在该引用上调用特定方法。

image

使用对 Toaster 的 Appliance 引用时,编译器仅关心类Appliance是否具有您在 Appliance 引用上调用的方法。但在运行时,JVM 并不关注引用类型(Appliance),而是堆上实际的Toaster 对象

因此,如果编译器已经批准了方法调用,它能够工作的唯一方式是覆盖方法具有相同的参数和返回类型。否则,具有 Appliance 引用的人将调用 turnOn()作为无参方法,即使 Toaster 中有一个接受 int 参数的版本。在运行时调用哪个方法?是 Appliance 中的方法。换句话说,Toaster 中的 turnOn(int level)方法不是覆盖!

image

  • Images 参数必须相同,返回类型必须兼容。

    超类的合同定义了其他代码如何使用方法。无论超类接受什么样的参数,覆盖方法的子类必须使用相同的参数。并且无论超类声明什么返回类型,覆盖方法必须声明相同类型或子类类型。记住,子类对象保证能够执行其超类声明的任何操作,因此可以安全地返回子类,而超类预期返回的是安全的。

  • 图片 方法不能更少可访问。

    这意味着访问级别必须相同,或者更友好。例如,你不能覆盖一个公共方法并将其变为私有。如果在运行时调用的代码突然因为覆盖版本在运行时是私有的而被 JVM 关闭,那会是多么震惊的事情!

    到目前为止,我们学习了两种访问级别:私有和公共。另外两种在附录 B 中。关于与异常处理相关的重写还有另一条规则,但我们将等到第 13 章—— 风险行为 中再讨论。

    图片

对方法进行重载

方法重载只不过是具有相同名称但不同参数列表的两个方法。没有重载方法涉及多态性!

重载让你可以创建多个版本的方法,具有不同的参数列表,为调用者提供方便。例如,如果你有一个仅接受 int 的方法,调用代码必须在调用你的方法之前将 double 转换为 int。但如果你重载该方法并提供另一个接受 double 的版本,那么你为调用者简化了操作。当我们在第 9 章—— 对象的生死:构造函数 中查看构造函数时,你会看到更多这样的情况。

由于重载方法并不试图满足其超类定义的多态性约定,重载方法具有更大的灵活性。

重载方法只是一个具有相同方法名称的不同方法。它与继承和多态无关。重载的方法不同于重写的方法。

  • 图片 返回类型可以不同。

    你不能只改变返回类型。

  • 图片 你不能只改变返回类型。

    如果只有返回类型不同,则这不是有效的重 —— 编译器会假定你试图重写该方法。即使 也不合法,除非返回类型是超类声明的返回类型的子类型。要重载一个方法,你必须改变参数列表,尽管你 可以 将返回类型更改为任何类型。

  • 图片 可以 在任何方向上变化访问级别。

    你可以自由地使用一个更为严格的方法重载一个方法。这无关紧要,因为新方法无需履行重载方法的约定。

    合法的方法重载示例:

    public class Overloads {
      String uniqueID;
    
      public int addNums(int a, int b) {
        return a + b;
      }
    
      public double addNums(double a, double b) {
        return a + b;
      }
    
      public void setUniqueID(String theID) {
        // lots of validation code, and then:
        uniqueID = theID;
      }
    
      public void setUniqueID(int ssNumber) {
        String numString = "" + ssNumber;
        setUniqueID(numString);
      }
    }
    

练习

image

Mixed Messages

image

下面列出了一个简短的 Java 程序。程序中有一个代码块缺失!你的挑战是将候选的代码块(在左侧)与插入后看到的匹配命令行输出进行配对。并非所有输出行都会被使用,有些输出行可能会被多次使用。画线连接代码块与匹配的命令行输出。

程序:

image

代码候选人:

image

输出:

A’s m1, A’s m2, C’s m3, 6

B’s m1, A’s m2, A’s m3,

A’s m1, B’s m2, A’s m3,

B’s m1, A’s m2, C’s m3, 13

B’s m1, C’s m2, A’s m3,

B’s m1, A’s m2, C’s m3, 6

A’s m1, A’s m2, C’s m3, 13

Images 在 “Mixed Messages” 中的答案。

BE the Compiler

**如果将右侧列出的 A-B 方法对插入到左侧的类中,则会编译并产生显示的输出。 (将 A 方法插入到 Monster 类中,将 B 方法插入到 Vampire 类中。)

public class MonsterTestDrive {

  public static void main(String[] args) {
    Monster[] monsters = new Monster[3];
    monsters[0] = new Vampire();
    monsters[1] = new Dragon();
    monsters[2] = new Monster();
    for (int i = 0; i < monsters.length; i++) {
      monsters[i].frighten(i);
    }
  }
}

class Monster {
  ![Images](https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hd1st-java-3e/img/a.png)
}

class Vampire extends Monster {
  ![Images](https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hd1st-java-3e/img/b.png)
}

class Dragon extends Monster {
  boolean frighten(int degree) {
    System.out.println("breathe fire");
    return true;
  }
}

imageimage

Images 在 “BE the Compiler” 中的答案。

image** **# 练习解决方案

imageimage

BE the Compiler

(来自 “BE the Compiler”)

集合 1 可以工作。

集合 2 不会 编译,因为 Vampire 的返回类型是 int。

Vampire 的 frighten() 方法(B)不是 Monster 的 frighten() 方法的合法重写或重载。仅更改返回类型是不足以使其成为有效重载的,因为 int 类型与 boolean 类型不兼容。请记住,如果只更改返回类型,则必须将其更改为与超类版本兼容的返回类型,那么它就是一个重写。

集合 3 和 4 编译但会产生:

arrrgh

breathe fire

arrrgh

记住,Vampire 类没有重写 Monster 类的 frighten() 方法。(Vampire 类中集合 4 的 frighten() 方法使用的是 byte,而不是 int。)

代码候选人:

image

Mixed Messages

(来自 “Mixed Messages”)

image

Pool Puzzle

(来自 “Pool Puzzle”)

image

public class Rowboat extends Boat {
   public void rowTheBoat() {
      System.out.print("stroke natasha");
   }
}
public class Boat {
   private int length ;
   public void setLength ( int len ) {
         length = len;
   }
   public int getLength() {
      return length ;
   }
   public void move() {
      System.out.print("drift ");
   }
}

public class TestBoats {
   public static void main(String[] args){
      Boat b1 = new Boat();
      Sailboat b2 = new Sailboat();
      Rowboat b3 = new Rowboat();
      b2.setLength(32);
      b1.move();
      b3.move();
      b2.move();
   }
}
public class Sailboat extends Boat {
   public void move() {
      System.out.print("hoist sail ");
   }
}

输出:

image**

第八章:严肃的多态性:接口和抽象类

image

继承仅仅是开始。 要利用多态性,我们需要接口(而不是 GUI 类型)。我们需要超越简单的继承,达到只有通过设计和编码接口规范才能达到的灵活性和可扩展性的水平。Java 的一些最酷的部分甚至没有接口是不可能实现的,所以即使你自己不设计它们,你仍然必须使用它们。但你会 想要 设计它们。你会 需要 设计它们。会想知道你以前怎么活过的。什么是接口?它是一个 100%抽象的类。什么是抽象类?它是一个不能被实例化的类。这对你有什么好处?你马上就会看到。但是如果你想一想上一章的结尾,以及我们如何使用多态参数,使得单个 Vet 方法可以接受所有类型的 Animal 子类,那只是揭开表面。接口是多态性中的poly。抽象类中的ab。Java 中的咖啡因

我们在设计这个时有没有遗漏什么?

image

类结构并不算太糟糕。我们设计它以使重复代码最小化,并重写了我们认为应该具有特定子类实现的方法。从多态的角度来看,我们使其变得灵活,因为我们可以用 Animal 参数(和数组声明)设计使用 Animal 的程序,以便在运行时可以传入和使用任何 Animal 子类型—包括我们在编写代码时从未想象过的那些。我们将所有动物的共同协议(我们希望全世界都知道的四种方法)放在 Animal 超类中,我们准备开始制作新的 Lion、Tiger 和 Hippo。

我们知道我们可以说:

Wolf aWolf = new Wolf();

image

而我们知道我们可以说:

Animal aHippo = new Hippo();

image

但事情变得奇怪的地方在这里:

Animal anim = new Animal();

image

一个新的 Animal()对象 什么样子?

image

实例变量的值是什么?

有些类根本不应该被实例化!

创建一个 Wolf 对象或 Hippo 对象或 Tiger 对象是有意义的,但是究竟什么是Animal 对象?它是什么形状?什么颜色、大小、几条腿……

尝试创建 Animal 类型的对象就像是噩梦般的星际迷航™传输事故。在“我要上去”的过程中,缓冲区发生了不良反应。

但是我们如何处理这个问题呢?我们 需要 一个 Animal 类,用于继承和多态。但我们希望程序员只实例化类 Animal 的较少抽象的子类,而不是 Animal 本身。我们希望 Tiger 对象和 Lion 对象,而不是 Animal 对象

幸运的是,有一种简单的方法可以阻止一个类被实例化。换句话说,阻止任何人对该类型使用“**new**”。通过将类标记为**abstract**,编译器将阻止任何地方的代码创建该类型的实例。

你仍然可以将该抽象类型用作引用类型。事实上,这是你首次创建抽象类的一个重要原因(将其用作多态参数或返回类型,或者创建一个多态数组)。

在设计类的继承结构时,你必须决定哪些类是抽象的,哪些是具体的。具体类是那些具体到可以实例化的类。具体类意味着可以创建该类型的对象。

将一个类声明为抽象很容易——在类声明前加上关键字**abstract**

abstract class Canine extends Animal {
   public void roam() { }
}

编译器不允许你实例化一个抽象类。

抽象类意味着任何人都无法创建该类的新实例。你仍然可以将该抽象类用作声明的引用类型,以实现多态,但你不必担心有人创建该类型的对象。编译器保证这一点。

imageimage

抽象类在没有被扩展的情况下几乎没有用途、价值或生存的目的。

使用抽象类时,是你的抽象类的子类的实例在运行时执行工作。

但也有例外情况——抽象类可以拥有静态成员(见第十章)。

抽象 vs. 具体

image

非抽象的类称为具体类。在动物继承树中,如果我们将 Animal、Canine 和 Feline 设为抽象类,则 Hippo、Wolf、Dog、Tiger、Lion 和 Cat 将作为具体的子类。

翻阅 Java API,你会发现很多抽象类,特别是在 GUI 库中。GUI 组件是什么样子?Component 类是与 GUI 相关的类的超类,用于按钮、文本区域、滚动条、对话框等。你不会实例化一个通用的Component并将其放在屏幕上;你会创建一个 JButton。换句话说,你只实例化 Component 的一个具体子类,但从不实例化 Component 本身。

抽象方法

image

除了类外,你也可以将方法标记为抽象。抽象类意味着该类必须扩展;抽象方法意味着该方法必须重写。你可能会决定某些(或全部)抽象类中的行为在没有更具体的子类实现时没有任何意义。换句话说,你不能想象出任何通用的方法实现对子类可能有用。一个通用的 eat()方法会是什么样子?

抽象方法没有方法体!

因为你已经决定在抽象方法中没有任何有意义的代码,所以你不会加入方法体。所以没有花括号——声明只以分号结束。

image

如果你声明了一个抽象方法,你必须标记为抽象。你不能在非抽象类中拥有抽象方法。

如果你在一个类中放入了一个抽象方法,那么你必须将这个类标记为抽象。但是你可以在抽象类中混合使用抽象和非抽象方法。

必须实现所有抽象方法

image

实现一个抽象方法就像重写一个方法一样。**

抽象方法没有方法体;它们存在只为了多态性。这意味着继承树中的第一个具体类必须实现所有抽象方法。

然而,你可以通过自己成为抽象来推卸责任。例如,如果 Animal 和 Canine 都是抽象的,并且都有抽象方法,类 Canine 就不必实现 Animal 的抽象方法。但是一旦我们到达第一个具体的子类,比如 Dog,那么这个子类必须实现 Animal 和 Canine 的所有抽象方法。

但请记住,抽象类既可以有抽象方法,也可以有抽象方法,因此 Canine 可以实现 Animal 的抽象方法,这样 Dog 就不需要实现它。但是如果 Canine 对 Animal 的抽象方法一言不发,Dog 就必须实现 Animal 的所有抽象方法。

当我们说“你必须实现抽象方法”,这意味着你必须提供一个方法体。这意味着你必须在你的类中创建一个与抽象方法具有相同方法签名(名称和参数)且返回类型与声明的返回类型兼容的非抽象方法。你要在方法中放什么是由你决定的。Java 只关心的是这个方法存在,在你的具体子类中。

多态的实现

假设我们想写一个自己的列表类,这个类将保存 Dog 对象,但是暂且假设我们不知道 ArrayList 类。首先,我们只给它一个 add()方法。我们将使用一个简单的 Dog 数组(Dog[])来保存添加的 Dog 对象,并将其长度设置为 5。当我们达到 5 个 Dog 对象的限制时,你仍然可以调用 add()方法,但它不会做任何事情。如果我们还没有达到限制,add()方法会将 Dog 放入数组中的下一个可用索引位置,然后增加该索引(nextIndex)。

构建我们自己的针对 Dog 的特定列表

(也许是世界上最糟糕的尝试之一,从头开始制作我们自己的 ArrayList 类。)

image

哎呀,现在我们还需要保存 Cats

我们在这里有几个选择:

1. 创建一个单独的类 MyCatList,用来保存 Cat 对象。相当笨重。

2. 创建一个单一的类 DogAndCatList,将两个不同的数组作为实例变量,并且有两个不同的 add()方法:addCat(Cat c)和 addDog(Dog d)。另一个笨拙的解决方案。

3. 创建一个异构的 AnimalList 类,接受任何动物子类(因为我们知道如果规格变更以添加猫,迟早还会添加其他种类的动物)。我们最喜欢这个选项,所以让我们更改我们的类,使其更通用,接受动物而不仅仅是狗。我们已经突出显示了关键更改(逻辑当然是一样的,但代码中的类型从 Dog 到 Animal 的变化)。

构建我们自己的动物特定列表

imageimage

那么非动物类呢?为什么不创建一个足够通用以接受任何东西的类?

image

你知道这将引向何方。我们想要改变数组的类型,还有 add()方法的参数,改成某种比 Animal 更上层、更通用、更抽象的东西。但是我们该如何做呢?我们没有 Animal 的超类。

但是话又说回来,也许我们有……

Java 中的每个类都扩展自类 Object。

Object 类是所有类的母类;它是一切的超类。

即使你利用多态性,你仍然必须创建一个带有接受和返回你的多态类型的方法的类。如果在 Java 中没有一种通用的超类,Java 的开发人员就无法创建可以接受自定义类型的方法的类……在编写库类时他们根本不知道这些类型

所以从一开始你就在为类 Object 的子类编写代码,而你甚至都不知道。你编写的每个类都扩展自 Object, 而无需你说它。但你可以把你编写的类想象成是这样的:

  public class Dog extends Object { }

等等,但是 Dog 已经扩展了某个东西,Canine。没问题。编译器将会使Canine扩展 Object。虽然Canine扩展了 Animal。那也没问题,编译器会直接使 Animal 扩展 Object。

任何没有显式扩展其他类的类,默认都会扩展 Object。

所以,由于 Dog 扩展了 Canine,它并没有直接扩展 Object(尽管它间接扩展了它),Canine 也是如此,但是 Animal 确实直接扩展了 Object。

那么这个超级超级超级大类 Object 里面都有什么?

image

如果你是 Java,你希望每个对象具有什么行为呢?嗯……让我们看看……要不要一个方法来判断一个对象是否等于另一个对象?还要不要一个方法来告诉你该对象的实际类类型?或许还要一个方法,为对象生成一个哈希码,这样你就可以在哈希表中使用该对象(稍后我们会讨论 Java 的哈希表)。哦,这里还有一个好方法——一个方法,打印出该对象的字符串消息。

你猜怎么着?就像魔术一样,Object 类确实拥有这四种方法。不过,这还不是全部,但我们确实关心这些。

Images equals(Object o)

image

Images getClass()

image

Images hashCode()

image

Images toString()

image

使用类型为 Object 的多态引用是有代价的…

在你忙着为所有的超灵活的参数和返回类型使用类型 Object 之前,你需要考虑一个小问题:使用类型 Object 作为引用可能会带来一些问题。记住,我们不是在讨论如何制作类型 Object 的实例;我们讨论的是制作某些其他类型的实例,但使用类型 Object 的引用。

当你把一个对象放入 ArrayList 中时,它以 Dog 形式放进去,并以 Dog 形式出来:

image

但是当你将其声明为 ArrayList

posted @ 2025-11-21 09:08  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报