Android游戏开发详解

978-7-115-39185-8
作者: 【美】Jonathan S. Harbour
译者: 李强
编辑: 陈冀康

图书目录:

详情

本书教读者从头开始学习如何开发Android游戏。没有任何经验的初学者,或者有一定经验的Java开发者,都可以通过本书开始游戏开发。本书是一本友好的入门指南,教授读者如何构建自己的游戏。读者讲学习Android开发的基础知识,如何编写面向对象的Java应用,交互式的2D游戏,触控界面等。还将学习如何在游戏中整合全球领先的社交功能,以及发布游戏与众多的Android用户分享。

图书摘要

版权信息

书名:Android游戏开发详解

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美]Jonathan S. Harbour

  译    李 强

  责任编辑 陈冀康

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Simplified Chinese translation copyright ©2015 by Posts and Telecommunications Press

ALL RIGHTS RESERVED

The Beginner’s Guide to Android Game Development (ISBN: 978-1-908689-26-9) by James S. Cho

Originally published by Glasnevin Publishing.

Copyright © James S. Cho 2014

本书中文简体版由Glasnevin Publishing授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


Android游戏开发有很大的市场需求,但又容易给人以很简单的错觉。实际上,Android游戏开发涉及编程基础、Java编程语言、游戏开发、代码优化、Android应用程序开发等众多的知识和技能。

本书是一本面向初学者的优秀的Android游戏开发指南。全书共11章,分为4个部分,按部就班地介绍了Java语言和编写面向对象的应用程序等基本知识,带领读者尝试Android的构建模块,并创建有趣的、交互性的、支持触摸控制的2D游戏。本书还通过配套站点,提供了众多的示例Java和Android游戏项目库,可供你自己继续学习并成长为一名游戏程序员。

如果你已经或者想要开发Android游戏,但是却不知道从何下手,那么本书是为你量身定做的。不管你是没有任何编程经验的初学者,还是一名有经验的Java开发者,都可以通过阅读本书成长为一名Android游戏开发人员。


作为对编程知之甚少或者毫无所知的初学者,开始学习Android游戏开发,可能会觉得就像是穿越陌生的星际的旅程。有太多的事情要尝试,太多的知识要学习,令人遗憾的是,还有如此之多的方式令人陷入迷途。

究其原因之一,可能是Android游戏开发给人以很简单的错觉。这个术语给人的感觉是,只需要学习和掌握一个主题就够了,实际上,Android游戏开发包括各种不同的主题,其中的一些如下所示。

如果你不了解这些主题,也不必惊讶!这正是需要指南的地方。本书是为初学者而编写的,作者也曾经是初学者,不知道从何处开始学习。本书将引导你经历构建自己的Android游戏的每一个步骤。如果这正是你的学习目标,那么,这本书很适合你。

本书并不会对读者做太多假设。当然,我们假设你有基本的数学知识,并且知道如何在计算机上安装程序或应用,但是,并不会假设你之前编写程序,或者有物理学的学位。

如果你是第一次开始编写代码,肯定会遇到一些问题。这没事。实际上,当你遇到难处,请访问本书的配套网站并寻求帮助。无论是编辑、Kilobolt的工作人员或者是陌生人,都会乐意帮助你解答问题或解决问题。

学习本书过程中,你将会阅读和编写很多代码。一些章节的整个篇幅都是学习如何编写代码,并且很少讨论游戏开发。其背后的思路是,如果你能够脱离游戏开发的环境去理解和编写代码,那么,在创建图形和游戏的时候,你可以很容易地应用这些知识。

通过从头到尾依次阅读,你将会从本书中获益良多。尽管如此,如果你记得对某个主题非常熟悉的话,跳过它也没问题。周期性的知识点检查,允许你下载工作项目的最新版本,并且从一个部分或一章的中间开始工作。

此外,要力图保持积极。你的学习旅程不会像穿越未知的星际那样紧张、刺激,但是,我期望它同样能够令人兴奋。有本书作为你的指导,你立刻就可以创建自己的游戏。

尽管本书的编写尽量全面,但是,一本书恐怕不足以涵盖Android游戏开发的主题。尽管如此,本书会随着配套网站一起完善。如果你觉得某个概念的介绍不够全面,请通过jamescho7. com/book/feedback反馈给我们。作者很高兴能够更详细地介绍一些重要的概念。

我想要感谢Glasnevin出版社的Helen McGrath博士,她给了我编写本书的机会。在整个过程中,她给予了巨大的帮助,没有她的话,不可能有这本书。

接下来,我要感谢Dr. Bryan Mac Donald、Kyle Yu、Vignesh Sivashanmugam以及所有其他不厌其烦地编辑我的书稿,使其尽可能减少错误的人们。得益于他们的努力,本书才能够成为那些想要学习Android游戏开发的人们的合适的指南。

感谢Racheal Reeves为本书封面做出的精彩设计。由于Racheal的努力工作,本书才如此完美。最后感谢Ling Yang无尽的耐心。Ling的爱支持和激励我在很多不眠之夜努力工作以完成这本书,我希望Ling有一天能够读到它。

本书中给出的Java和Android代码,以及其他附加资源,都可以通过本书的配套站点获取:http://www.jamescho7.com

对本书的任何评论、建议和勘误,欢迎通过以下电子邮件联系:

info@glasnevinpublishing.com

James有多年的游戏开发经验。他最早在笔记本上开始了自己的游戏开发职业,最终创建了Kilobolt,这是一家位于美国的独立游戏工作室。此外,他还教授一系列流行的编程课程,并且在杜克大学学习计算机科学的同时担任助教。

除了编写代码,阅读科学研究相关的文献,James还是曼联球迷,并且不断探索新的美食。



无论是何种情况,你离成为一名程序员都还相去甚远。本章将为你打下很好的基础,以便你能够成长为一名善于思考的、成功的Java程序员,从而能够编写高效的代码并构建优秀的游戏。我们从第2章开始,才会真正地编写程序,因此,现在你还不需要计算机。

从最基本的层面看,编程是让计算机执行以代码(code)的形式给出的一系列的任务。让我们来看一些示例代码,看看程序员能够提供什么样的指令。现在,还不要关心每个符号和每行代码背后的含义。我们将在本书中详细介绍这些。现在,先尝试理解其逻辑。阅读每行代码前面的注释,尝试搞清楚后面的代码的意图。

程序清单1.1 程序员的指令

01 // Instruct the computer to create two integer variables called a and 
02 // b, and assign values 5 and 6, respectively.
03 int a = 5;
04 int b = 6;
05 // Create another integer variable called result using a + b. 
06 int result = a + b;
07 // Print the result (Outputs the value of result to the Console).
08 print("The value of a + b is " + result);

程序清单1.1展示了程序员输入到像Notepad(Windows)或TextEdit (Mac)这样的一个文本编辑器中的内容。计算机在控制台所产生的输出如下所示。

The value of a + b is 11

好了,我们看完了Java代码的一个小示例。在继续学习之前,这里有一些需要记住的关键知识点。

关键知识点

代码执行的基本规则

代码是从上到下一行接着一行地执行的。这是一个简化的说明,但是,现在很适合我们。稍后,我们会给这条规则添加内容。

注释( // )

在Java中,两条斜杠后面的内容是注释。注释是为人类而编写的(在这里是我向你描述代码的方式),因此,Java虚拟机(Java Virtual Machine,稍后详细介绍Java虚拟机)不会执行注释。

行号

我们可以通过行号来引用代码。在确定行号的时候,必须把注释和空行都算在内。例如,在程序清单1.1中,如下的代码出现在第3行。

int a = 5;

正如程序清单1.1所示,我们可以让计算机把值存储为变量,并且我们可以对这些值执行数学计算和连接(连接是将文本和整数组合起来,参见程序清单1.1第8行)。我们甚至可以在控制台显示这些运算的结果。这只是冰山一角。稍后,我们可以绘制一个视频游戏角色,并且实现它在屏幕上移动的动画,它每走一步还会发出脚步声。看上去如下所示(注意,下面只是一个示例。在学习完本书的几章之后,你将能够编写自己的代码)。

程序清单1.2 更复杂的指令的示例

while (mainCharacter.isAlive()) {
  mainCharacter.updatePosition();
  mainCharacter.animate(time); 
  if (mainCharacter.getFoot().collidesWith(ground)) {
  footstepSound.play(volume); 
  }
  screen.render(mainCharacter); 
}

在前面的示例中,我们看到了数据类型(data type)的例子。例如,在程序清单1.1中,我们使用了整数值(integer value)5和6,这两个都是数值数据的例子。我们来看看其他的数据类型,先介绍其他的数值类型。

这些是Java中主要的基础数据类型,我们称之为基本数据类型(primitive type)。在后面各章中,我们将会看到很多使用基本数据类型的例子。

字符串(String)指的是一系列的字符。正如其名称所示,我们可以使用字符串将多个字符保存到一起(而基本类型char只能够保存一个字符)。

char firstInitial= 'J';
char lastInitial = 'C';
String name = "James";

注意,这里关键字String的首字母大写了,而基本数据类型char的首字母没有大写。这是因为,字符串属于对象(object)一类,而不属于基本数据类型。我们稍后要花很多时间讨论这些对象,它们在Java编程中扮演重要的角色。现在,我们只需要将字符串当作基本数据类型一样对待就行了。

所有的基本数据类型(和字符串)都可以表示为变量。它们都是使用相同的基本语法来声明(创建)的。

创建一个新的变量的时候,我们总是要声明两件事情:变量的数据类型(data type)和变量的名称(variable name)。在大多数情况下,我们还使用赋值运算符(assignment operator,即=)给变量指定一个初始值。有两种方法做到这点。第一种方法是指定一个字面值(literal value),例如,图1-1所示的‘J’。第二种方法是,指定一个计算值的表达式(expression),例如,图1-1所示的35 + 52(这个表达式在赋值之前计算)。

图1-1 变量声明的示例

赋值运算符(=)不是在声明相等性。这一点很重要。正如其名称所示,我们使用赋值运算符把一个值(在等号的右边)赋给(assign)一个变量(在等号的左边)。例如,考虑如下两行代码。

int a = 5;
a = a + 10;

在这个例子中,我们不是要表示a和a + 10的对等关系。我们直接将a + 10的值赋值给一个已有的变量a。进行区分的一种较好的方法是,将等号读作“获得”。因此,图1-1应该读作,“int类型的num获得了表达式35 + 52的结果”。

作为一个练习,浏览一下程序清单1.3的6行代码中的每一行,并且尝试将其读出声,说出每行的含义。记住,要区分字面值(literal value)和表达式(expression)(如果忘记了,再回顾一下图1-1)。第1行应该读作“short类型的num获得了15”。记住,这意味着,“声明了一个类型为short、名为num的变量,并且将字面值15赋值给它”。

程序清单1.3 声明各种变量

1  short numberOfLives = 15;
2  long highScore = 21135315431 - 21542156; // uses an expression;
3  float pi = 3.14159f;
4  char letter = 'J';
5  String J = "James";
6  boolean characterIsAlive = true;

注意,当我们讨论字符和字符串的时候,使用了‘ ‘和“ ”将字面值和具有相同名称的变量(variable)区分开来。例如,在程序清单1.3中,变量名J引用“James”,而字面值‘J’指的是它自己。

关键知识点

声明变量

当创建一个新的变量的时候,我们把一个值存储到计算机的内存中以便随后使用。我们可以使用该变量的名称来引用它。

打个比方,可以把变量看作一个盒子。当我们输入int a = 5的时候,是告诉Java虚拟机创建一个相应大小的盒子,并且把我们的值放进去。

引用变量

一旦创建了变量,在引用它的时候,我们不应该声明其类型。提供变量的名称就足够了。

复制值

考虑如下所示的代码。

你能告诉我,在程序结束的时候,x和z分别是什么值吗?如果你的回答是5和10,那么你答对了!

如果不是,也不必担心。很多初学者都不能正确地理解第2行代码。在第2行代码中,我们不是说int x和int z引用相同的盒子(变量)。相反,我们创建了一个名为int z的新盒子,并且将int x的内容复制后赋给它。

这对我们来说意味着什么呢?这意味着,当我们在第3行将z和5相加的时候,z变成了10,而x仍然是5。

在以上的每个示例中,我们在声明过程中都使用了一个初始值来初始化(initialized)变量。然而,正如我在本节开始所介绍的,声明一个变量的时候初始化它(分配一个初始值),这本身并不是必须要做的。例如,我们可以这么做。

int a, b, c;
a = 5;
b = 6;
c = 7;

上面的第1行代码声明了3个整数类型,分别名为a、b和c。没有明确地赋给其初始值。接下来的代码行分别用值5、6和7初始化了这3个整数。

尽管这么做是允许的,我们通常也将声明变量并初始化它们,就像在前面程序清单1.3中所做的那样。

int x = 5; // declare a new integer called x
int z = x; // assign the value of x to a new integer z
z = z + 5; // increment z by 5
[End of Program]

在我们继续深入之前,值得先细致地介绍如何具体把值存储到变量中。我前面提到,不同的基本数据类型具有不同的位大小。例如,一个int有32位而一个long有64位。你可能会问,那么,到底什么是位?

位(bit)是一个二进制位的简称。换句话说,如果你有一个只有0和1的二进制数,每个数字就是1位。达到8位的时候,例如,(10101001),你就有了1字节。

对于位,你需要记住的一点是:拥有的位越多,所能表示的数值也越多。为了说明这一点,让我们问一个问题。十进制的1位能够表示多少个数字?当然是10个(0,1,2,3,4,5,6,7,8和9)。两位呢?100个(00,01……99)。我们看到,每增加一个位数,都会使得我们所能表示的数值增多到原来的10倍。对于二进制数字来说,也是如此,只不过每次增加一位,所能表示的数值的数量是原来的两倍。

在计算中,位是很重要的,因为我们所操作的机器是由细小的电路组成,而这些电路要么是开,要么是关。数据表示的挑战,完全由此而引发。我们不能使用这些电路来直接表示“hello”这样的单词。我们必须使用任意某种系统将单词“hello”和某些电路的开关组合联系起来。

这就是我们应该了解的和变量相关的知识。通过声明一个新的变量,我们在内存中分配了特定数目的位(根据声明的类型),并且将某些数据的一个二进制表示存储起来,以便随后使用。

在Java中,可以将一种数据类型转换为另一种类型。例如,我们可以接受一个int值并且将其存储到一个long变量中。之所以能这样,是因为long变量保存了64位,可以很容易地容纳来自较小的类型int(32位)中的数据而不会遇到麻烦。但是,如果我们接受一个64位的long数字,并且试图将其放入到一个32位的int的“容器”中,将会发生什么呢?会有丧失精度的风险。这64位中的32位,必须删除掉,然后我们才能将数字放置到int变量中。

规则是这样的:如果从一个较小的类型转换为一个较大的类型,这是安全的。如果要从一个较大的类型转换为一个较小的类型,应该小心以避免丢失重要的数据。稍后,我们将详细介绍如何从一种类型转换为另一种类型。

我们前面看到了,变量可以用来存储值,并且变量可以在运算中用作运算数,如图1-2所示。

图1-2 变量可以用来存储值,也可以用作运算数

表1-1所列内容是你必须知道的5种算术运算。在了解示例的过程中,请记住如下两条规则。

规则 #1  涉及两个整数的一个运算,总是会得到一个整数的结果(整型变量中不允许有小数值)。

规则 #2  至少涉及一个浮点数(小数值)的运算,其结果总是浮点数。

表1-1  必须知道的5种运算

运算符

说明

示例/值

+(加法)

将加号两边的运算数相加

3 + 10 = 13 4.0f + 10 = 14.0f

−(减法)

从第一个运算数中减去第二个运算数

16 − 256 = -240

*(乘法)

将乘号两边的运算数相乘

4.0f * 3 = 12.0f

/(除法)

用第一个运算数除以第二个运算数 前面提到的两条规则在这里特别重要

6/4 = 1 记住规则#1,我们将其向下舍入到最近的整数 6.0f/4 = 1.5f 记住规则#2

%(求余数、模除)

计算除法运算的余数

10 % 2 = 0 (执行10/2,然后计算余数,也就是0) 7 % 4 = 3

在执行运算的时候,使用标准的运算顺序。计算机将会按照如下的顺序执行运算。

1.圆括号(或方括号)。

2.指数。

3.乘法/除法/余数。

4.加法/减法。

如下的示例说明了运算顺序的重要性。

print(2 + 5 % 3 * 4);——输出“10”。
print((2 + 5) % 3 * 4);——输出“4”。
true
false
true
true
false
false

现在来看看在两个值之间进行比较的关系运算符,如表1-2所示。注意,在下面的示例中,算术运算在关系运算之前执行。如下所有的计算,都得到一个true或false值(布尔)。

表1-2  关系运算符用来确定一个值与另一个值进行比较的结果

运算符

说明

示例/值

== (等于)

检查运算符两边的两个值是否相等

(3 + 10 == 13)  = true (true == false)  = false

!=(不等于)

检查运算符两边的两个值是否不相等

(6/4 != 6.0f/4)  = true

>(大于)

判断第一个运算数是否比第二个运算数大

6 > 5 = true

<(小于)

判断第二个运算数是否比第一个运算数大

6 < 5 = false

>=(大于或等于)

含义明显

6 >= 6 = true

<=(小于或等于)

含义明显

10 <= 9 + 1 = true

!(取反)

将一个布尔值取反。这是一个一元运算符(只需要一个运算数)

!true = false !false = true

关键知识点

赋值和比较

注意,==运算符和=运算符不同。前者(==)用来比较两个值,并且输出一个true或false值。后者(=)用来将一个值赋值给一个变量。

下面的程序清单1.4展示了使用这些关系运算符的另外两个示例。我已经给每一条print语句加上了标签,以便你可以看到相应的输出。

程序清单1.4 关系运算符

01  print(1 == 2); // #1 (equal to)
02  print(!(1 == 2)); // #2 (inverse of print # 1)
03
04  int num = 5;
05  print(num < 5); // #3 (less than)
06
07  boolean hungry = true;
08  print(hungry); // #4 
09  print(hungry == true); // #5 (equivalent to print #4)
10  print(hungry == false); // #6 
11  print(!hungry); // #7 (equivalent to print #6)

程序清单1.4的输出如下所示。

下面几个小节将会假设你理解关系运算符如何工作,因此,确保你理解每条打印代码行中发生了什么。仔细看一下程序清单1.4中的示例#5和示例#6,理解为什么我们要省略==运算符。

两个主要的条件运算符是|| (OR)和&& (AND)。如果|| (OR)运算符任意一边的布尔值为真,该运算符将求得真。只有&& (AND)运算符两边的布尔值都为真时,该运算符才会求得真。

我们假设你想要判断一个给定的数字是否是正的偶数。要做到这一点,必须检查两个条件。首先,我们必须确定该数字是正的。其次,我们必须检查该数字是否能够被2整除。程序清单1.5给出了我们可能为此而编写的代码的一个示例。

程序清单1.5 条件运算符

1  // Remember to evaluate the RIGHT side of the = operator before 
2  // assigning the result to the variable.
3  int number = 1353; 
4  boolean isPositive = number > 0; // evaluates to true
5  boolean isEven = number % 2 == 0; // evaluates to false
6  print(isPositive && isEven); // prints false
7  print(isPositive || isEven); // prints true

让我们将目前为止所学到的所有内容组合起来,并且讨论编程的一个重要方面,即函数。

函数是一组规则。特别地,函数应该接受一个值并且输出一个相应的结果。以一个数学函数为例。

f(x)=3x +2

输入是任意的数值x,输出是3x +2的结果

例如,f(1)=3(1)+2=5

在Java中,我们可以定义一个非常类似的函数。如下的函数将接受一个float类型的输入,并且输出计算3x+2的结果。

程序清单1.6 Java函数

1  float firstFunction (float x) {
2    return 3*+ 2; 
3  }

现在,我们来进一步看看如何编写一个Java函数(也叫作方法,具体原因我们在下一章中介绍)。要编写一个Java函数,首先声明返回值的类型。还要给函数一个名称,例如,firstFunction。在函数名称后面的圆括号中,列出所有必需的输入。

开始花括号和结束花括号,表明函数从哪里开始以及函数在哪里结束。如果这还不够形象化,这么做会有所帮助:想象一下,以花括号作为对角线形成一个矩形,将函数包围起来,如图1-3所示。这有助于你确定每个函数从哪里开始以及从哪里结束。

图1-3 深入了解如何编写函数

程序清单1.7展示了如何在代码中使用函数。注意,我们假设在代码中某处定义了一个名为firstFunction的函数,并且其行为就像程序清单1.6所描述的那样。

程序清单1.7 使用函数

1  // 1. declare a new float called input
2  float input = 3f; 
3  // 2. declare a new float called result and initialize it with the 
4  // value returned from firstFunction(input);
5  float result = firstFunction(input); 
6  // 3. print the result
7  print(result);

程序清单1.7的输出如下。

11.0

程序清单1.7的第5行有着某种魔力。让我们具体讨论这里发生了什么。通常,我们总是必须先计算赋值操作符的右边。计算这个表达式,涉及调用程序清单1.6中所定义的函数。当调用firstFunction的时候,程序将会进入到程序清单1.6中的函数定义,传入参数input。在firstFunction中,接受input的值并且将其复制到一个名为x的临时的局域变量(local variable)中,并且该函数向调用者(caller)返回3x + 2的值(在第5行)。这个返回值可以存储为一个变量,这正是我们使用result所做的事情。然后程序继续进行,打印出该返回值。

函数可能接受多个输入,甚至是没有输入。在函数定义中,我们必须列出想要让函数接受的所有的输入,通过为每个想要的输入声明一个临时的局域变量来做到这一点。这些必需的输入,每一个都可以称为参数(parameter),其示例参见程序清单1.8。

程序清单1.8 函数声明

1  // Requires three integer inputs.
2  int getScore(int rawScore, int multiplier, int bonusScore) {
3   return rawScore * multiplier + bonusScore;
4  }
5
6  // Requires no inputs.
7  float getPi() {
8   return 3.141592f;
9  }

无论何时调用一个函数,你都必须传入在圆括号之间列出的所有的参数。例如,在程序清单1.8中,函数getScore声明了3个整型变量。你必须传入相应的值,否则的话,程序将无法运行。类似地,只有当你不传入任何参数的时候,函数getPi才会工作。

如前面所述,当我们把一个变量当作参数传递给函数的时候,只有其值(value)可以供函数使用(这个值是复制的)。这意味着,下面的程序清单1.9和程序清单1.10都将打印出相同的值15 700(根据程序清单1.8第3行所给出的公式)。

程序清单1.9 使用变量来调用getScore

1  int num1 = 5000;
2  int num2 = 3;
3  int num3 = 700;
4  print(getScore(num1, num2, num3));

程序清单1.10 使用直接编码的值来调用getScore

1  print(getScore(5000, 3, 700));

在程序清单1.9中,我们使用变量调用了getScore函数。注意,由于我们通过值来传递参数,参数的变量名无关紧要。它们不一定必须要和函数定义中的局域变量的名称一致。程序清单1.10没有使用变量,而是传递了直接编码(hardcoded)的值。

当然,在我们编写的大多数程序中,像getScore这样的函数,其参数都会根据用户执行和使用的习惯而改变,因此,我们通常要避免直接编码字面值。

总的来说,要使用一个函数,我们必须做两件事情:首先,必须声明函数定义(如程序清单1.6所示);其次,必须调用该函数(如程序清单1.7所示)。如果想要让函数访问某些外部的值,我们会传递参数。函数返回的值拥有某种类型,这在声明函数的时候必须明确地声明,并且,可以使用相应的变量类型和赋值运算符来存储该值。

让我们再来看一个函数。

程序清单1.11 还活着吗?

1  boolean isAlive (int characterHealth) {
2    return characterHealth > 0;
3  }

作为练习,请尝试回答如下的问题(答案在后面给出)。

Q1:  程序清单1.11中的函数的名称是什么?         

Q2:  程序清单1.11中的函数返回一个什么类型的值?          

Q3:  程序清单1.11中的函数接受几个输入?          

Q4:  列出该函数的所有的输入的名称:          

Q5:  isAlive(5)的结果是true还是false?          

Q6  isAlive(-5) 的结果是true还是false?          

Q7:  isAlive(0) 的结果是true还是false?          

如果你感到迷惑,不要失望!需要花一些时间,才能够完全理解函数。如果你对函数还不是完全清楚,随着在本章中看到更多的示例,以及在第2章中开始编写自己的函数,你会对函数有更深的认识。

上述问题的答案是:Q1: isAlive,Q2: boolean,Q3: 一个,Q4: characterHealth,Q5: true,Q6: false,Q7: false。

我们现在把注意力转向控制流程(control flow 也称为流程控制,flow control),这指的是代码行将要按照什么样的顺序执行。还记得代码执行的基本规则吧,它是说代码要从上到下地执行。在最简单的程序中,代码真的是按照线性方式从上向下执行的。然而,在任何有用的程序中,我们可能会看到,根据某些条件,会跳过一些代码行甚至重复执行一些代码行。让我们来看一些例子。

if-else语句块用来在代码中创建分支或多条路径。例如,我们可以检查如characterLevel > 10这样的条件来判断一个字符串内容,如图1-6所示。根据characterLevel的值,游戏可以执行不同的指令。你可以看到图1-4中有3条路径。

图1-4 一个if-else语句块包含了一条if语句、一条else-if语句和一条else语句

我们可以创建比上面的例子具有更多或更少分支的if-else语句块。实际上,我们甚至可以把if语句嵌套在其他的if语句中,以允许“内嵌的”分支。

无论何时,当你写下关键字if的时候,就开始了一个新的if-else语句块,如图1-6所示。你可以编写一个没有任何else-if或else语句的if语句块。这绝对没问题。

在你开始一个新的if-else语句块之后,每一个额外的else-if都表示一个新的分支。else语句是表示“我放弃”的分支,并且它将会为你处理所有的剩下的情况。

在给定的if-else语句块中,你只能选取一个分支。注意,在图1-6中,如果character Level是11,if和else-if语句中的条件似乎都满足。你可能会认为,这将会导致characterTitle变成“King”,随后很快又变成“Knight”。然而,不会发生这种情况,因为在if-else语句块中,你的代码只能选取一个分支,如图1-5所示。

图1-5 if-else语句块包含一条if语句、一条else-if语句和一条else语句

再回来看看函数。实际上,我们可以通过if-else语句块使得函数更为强大。if-else语句块还是像前面所介绍的那样工作,但是现在,我们将其包含到函数中,这意味着,我们要留意更多的花括号。看看下面的示例函数,看能否确定哪个开始花括号对应哪个结束花括号。第一个示例中已经为你标识清楚了。

示例1

String theUltimateAnswer(boolean inBinary) {
  String prefix = “The answer to life the universe and everything:”;
  if (inBinary) {
  return prefix + 101010;
  } else {
  return prefix + 42;
  }
}

示例2

boolean isLessThanTen(int num) {
  if (num < 10) {
  return true;
  } else {
  return false;
  }
}

示例3

boolean isEven(int num) {
  if (num % 2 == 0) {
  return true;
  } else {
  return false;
  }
}

示例4

String desertSecurity(boolean hasGun, boolean hasRobots) {
  if (hasGun) {
  return "I've got a bad feeling about this.";
  } else if (hasRobots) {
  return "These are NOT the droids we are looking for."
  } else {
  return "Move along."
  }
}

现在,我们必须掌握通过读取花括号来判断每个代码块从哪里开始以及从哪里结束的方法,让我们采取一些步骤。假设我们想要编写一个函数,它告诉我们一个人是否能够看一部限制级的电影(我们将根据资格返回true和false)。我们将设置如下所示的条件。

因此,我们必须将if-else语句嵌套(nest)到一个更为通用的条件之中,才能够处理没有伪造的ID或没有父母陪伴的人的情况。让我们来看看代码,从3个主要分支开始。

程序清单1.12 我能看电影吗(不完整版本)

1 boolean canWatch(int age, int minimumAge, boolean fakeID, boolean withParent) {
2  if (fakeID) {
3     return true; 
4  } else if (withParent) {
5     return true; 
6  } else {
7     // Nested if statements go here.
8  }
9 }

现在,在第3个分支中(else语句)添加两种特定的情况。

程序清单1.13 内部分支

if (age >= minimumAge) {
  return true; 
} else {
  return false; 
}

现在,我们可以将程序清单1.12和程序清单1.13放到一起,组成程序清单1.14。

程序清单1.14 我能看电影吗(完整版)

01 boolean canWatch(int age, int minimumAge, boolean fakeID, boolean withParent) {
02    if (fakeID) { 
03     return true; 
04    } else if (withParent) {
05     return true; 
06    } else {
07     if (age >= minimumAge) {
08      return true; 
09     } else {
10      return false; 
11     }  
12    }
13 }

尽管程序清单1.14中的代码能够很好地运行,我们还是可以进行一些优化,如程序清单1.15所示。

程序清单1.15 我能看电影吗(简化版#1)

01 boolean canWatch(int age, int minimumAge, boolean fakeID, boolean withParent) {
02   if (fakeID || withParent) { // Two cases were combined into one if statement.
03    return true; 
04   } else {
05    if (age >= minimumAge) {
06       return true; 
07    } else {
08       return false; 
09    }  
10   }
11 }

注意,在程序清单1.15中,我们在第2行使用“OR”运算符||将两种情况组合到一条if语句中。我们将所有的“true”的情况组合起来,以继续简化该函数,如程序清单1.16所示。

程序清单1.16 我能看电影吗(简化版#2)

01 boolean canWatch(int age, int minimumAge, boolean fakeID, boolean withParent) {
02   if (fakeID || withParent || age >= minimumAge) { 
03    return true; 
04   } else {
05    return false; 
06   }
07 }

不管你是否相信,我们可以完全去除掉if-else语句块而只是返回(fakeID || withParent || age >= minimumAge)的值,从而更进一步简化,参见程序清单1.17。

程序清单1.17 我能看电影吗(简化版#3)

1 boolean canWatch(int age, int minimumAge, boolean fakeID, boolean withParent) {
2    return (fakeID || withParent || age >= minimumAge); 
3 }

编写这样整洁的代码,就使得你(以及你的同事)能够更加高效地工作,而不需要使用诸如程序清单1.14那样复杂的逻辑。在整本书中,我们将学习到更多编写整洁代码的技巧。

在前面的小节中,我们介绍了使用if和else语句块来产生代码分支。现在,我们来介绍两种类型的循环:while循环和for循环。循环允许我们执行重复性的任务。循环特别重要,没有它们,游戏将无法运行。

Initiate counting!
1
2
3
4
5
Counting finished!

假设你想要编写一个函数打印出所有的正整数,直到达到给定的输入n。解决这个问题的策略(算法)如下。

1.  创建一个新的整型,将其值初始化为1。

2.  如果该整数小于或等于给定的输入n,打印其值。

3.  将该整数增加1。

4.  重复步骤2和步骤3。

我们已经学习了如何执行该算法的前3步。让我们写下已经知道的内容。

程序清单1.18 计数器(非完整版)

1 ????? countToN(int n) {
2  int counter = 1; // 1. Create a new integer, initialize it at 0.
3  if (counter <= n) { // 2. If this integer is less than or equal to the input
4  print(counter); // Print the value
5  counter = counter + 1; // 3. Increment the integer by 1
6  }
7 }

我们必须在代码中解决两个问题。首先,函数应该返回什么类型(通过程序清单1.18的第1行中的问号来表示)?它应该是一个int类型吗?实际上,在我们的例子中,甚至没有一条return语句;该函数并不会产生任何可供我们使用的结果。当没有返回任何值的时候,就像前面的函数那样,我们说返回类型是void。

其次,如何让这段代码重复步骤2和步骤3?这实际上很简单。我们使用一个while循环,只要某个条件能够满足,就会让这个循环保持运行。在我们的例子中,所需要做的只是用关键字“while”替代关键字“if”。完整的函数如程序清单1.19所示(修改的代码突出显示)。

程序清单1.19 计数器(完整版)

1 void countToN(int n) {
2  int counter = 1; // 1. Create a new integer, initialize it at 0.
3  while (counter <= n) { // 2. If this integer is less than or equal to the input
4  print(counter); // Print the value
5  counter = counter + 1; // 3. Increment the integer by 1
6  }
7 }

让我们一行一行地来看看该函数(参见程序清单1.19)。

第1行声明了函数的返回类型(void)、函数名称(countToN)和输入(n)。

第2行声明了一个名为counter的新的整型,并且将其值初始化为1。

第3行开始一个while循环,只要条件(counter <= n)满足,它就会运行。

第4行打印出counter变量的当前值。

第5行将counter增加1。

当到达第5行的时候(第6行的花括号表示循环结束),代码将再次执行第3行。这里会重复,直到counter变得比n大,此时,会跳出while循环。要看看这是如何工作的,让我们在代码中的任意地方调用该函数。

print(“Initiate counting!”); 
countToN(5); // Call our countToN() function with the argument of 5.
print(“Counting finished!”);

相应的输出如下所示。

这就是while循环。只是编写一条if语句,并且将关键字“while”放到那里,代码就可以重复一项任务了。

关键知识点

while循环

只要给定的条件计算为true,while循环就将继续迭代。如果我们有一个条件总是为true,例如,while (5 > 3) …,while循环将不会结束。这就是一个无限循环。

程序清单1.19中的计数逻辑的使用如此频繁,以至于人们为此专门设计了一个循环。它叫作for循环。for循环的语法考虑到了各种问题的较为整洁的解决方案,使得我们能够节省代码行。如图1-6所示。

图1-6 for循环有3个主要组成部分:初始化、终止条件和自增

for循环需要做3件事情。必须初始化计数器变量,设置终止条件,然后定义一个自增表达式。该循环将持续迭代(重复),直到终止条件计算为假(在上面的示例中,就是直到i大于6)。每次迭代之后,i都会按照自增表达式中给出的规则来递增。

在程序清单1.19中使用一个for循环来计数重新编写代码的话,可以得到程序清单1.20。

程序清单1.20 计数器(for循环版)

1 void countToN(int n) {
2  for (int i=1; i<=n; i++) {
3   print(i); 
4  }
5 }

一旦掌握了语法,编写for循环比编写while循环要快很多。for循环很快将会变为我们的无价之宝,并用来干从移动精灵到渲染动画的每一件事情。

如果你已经学到了这里,恭喜你!你已经完成了进入美丽的、复杂的并且偶尔令人沮丧的编程世界的第一步。但是,在编写一些Java代码之前,你还不能自称为一名Java程序员。因此,快打开你的计算机并且开始学习第2章,在那里,我们要构建一些Java程序了。


第1章内容完全是成为Java程序员的准备工作。在本章中,你将编写自己的第一个Java程序(包括一款简单的游戏),并学习如何把游戏的角色、加血(power-up)以及其他实体表示为Java对象。

Java是一种面向对象编程语言。在面向对象的范型中,我们以对象的形式来表示数据,以帮助我们形成概念并沟通思路。例如,在构建视频共享Web应用程序的时候,我们可能要创建一个User对象来表示每个用户账户(及其所有的数据,例如,用户名、密码、上传的视频等)。使用一个Video对象来表示每一个上传的视频,其中的很多视频都组织到一个Playlist对象中。

考虑到整洁、健壮的代码更容易阅读和理解,面向对象编程允许我们将相关的数据组织到一起。为了了解这一思路,我们来编写自己的第一个Java程序。

关键知识点

访问本书的配套站点

本书中的所有代码示例、勘误文档,以及额外的补充内容,都可以通过本书的配套站点jamescho7.com来获取。

Java的安装可能颇有些技巧。如果在本章的任何地方,你有不明白之处,请访问配套站点,那里有视频指南可以帮助你开始安装Java。

在开始编写简单点的Java程序和构建令人兴奋的游戏之前,我们必须在自己的机器上安装一些软件。然而,这个过程有点枯燥且颇费时间,但是,为了让第一个程序开始运行,这些代价都是值得的。

我们将利用一个集成开发环境(Integrated Development Environment,IDE)来编写Java/Android应用程序。IDE是一种工具的名称,它能够帮助我们轻松地编写、构建和运行程序。

我们将要使用的IDE叫作Eclipse,这是一款强大的开源软件。然而,我们将下载Google改进版的Eclipse,即Android Developer Tools (ADT) Bundle,而不是安装单纯的Eclipse。我们稍后再介绍所有这些术语的含义。

要构建Android应用程序,必须先安装Android SDK(软件开发工具包)。通常,你需要单独下载它(和下载Eclipse的过程分开),并且用一个插件(为Eclipse提供额外功能的一个插件)来集成它;然而,Google允许你下载包含了Eclipse和Android SDK的一个包(即ADT包),从而使得这个过程更加容易。

按照如下的步骤来准备用于Java/Android开发的机器。

① 下载ADT包,请访问如下的站点。

http://developer.android.com/sdk/index.html

② 应该会看到图2-1所示的页面。

图2-1 Android SDK下载页面

一旦看到了这个页面,点击“Download Eclipse ADT”按钮。该站点将会自动检测你的操作系统,以便你能够下载正确的版本。

③ 你将会看到图2-2所示的界面。

根据你的操作系统的类型,下载32位或者64位的版本。不确定应该选择哪个版本?可以通过如下方式搞清楚。

图2-2 32位还是64位

在Windows上查看操作系统类型

在Windows上,鼠标右键点击“我的电脑”(My Computer)并且点击“属性” (Properties)。或者,可以导航到“控制面板”(Control Panel)并查找“系统”(System)。将会看到图2-3所示的窗口。

图2-3 Windows系统信息

如果你的机器是32位的,将会看到32-bit Operating System或x86-based processor。否则,你应该会看到64-bit Operating System。记住这个版本,并且下载相应的ADT。

在Mac OS X上查看操作系统类型

要查看使用的是32位还是64位的操作系统,必须检查处理器的类型。如下所示的页面告诉你如何判断以及解释这些信息。

http://support.apple.com/kb/HT3696

记住操作系统的版本,并且下载相应的ADT版本。

④下载的是一个很大的.zip文件(在编写本书的时候,文件大概有350MB)。直接将这个文件解压缩到一个便于使用的目录中。不必安装它。

解压之后,应该会看到两个文件夹和一个名为SDK Manager的文件。现在,你只需要关心Eclipse文件夹,因为在本章后面我们才会用到Android。

Eclipse是用Java构建的。这意味着,你需要在自己的机器上安装一个Java运行时环境(Java Runtime Environment,JRE),才能运行Eclipse。由于我们将运行Java程序并且会开发Java程序,我们将安装JDK(Java Development Kit),其中包含了一个JRE和各种开发工具。

要安装JDK,导航到如下所示的页面。

http://www.oracle.com/technetwork/java/javase/downloads/index.html

在编写本书的时候,JDK的最新版本是JDK 8。考虑到兼容性,我们将使用JDK 7,以便在Android开发中不会遇到问题。

向下滚动页面,直到看到Java SE 7uNN,其中,NN是Java 7最近的两位更新编号。如图2-4所示,当前的版本是Java SE 7u55。根据你阅读本书的时间,最新版本会有所   不同。

② 点击JDK下方的DOWNLOAD按钮。应该会打开图2-5所示的对话框。

③ 选中“Accept License Agreement”并且下载与你的操作系统对应的JDK版本。这里,x86指的是32位,而x64指的是64位。如果你忘记了这一信息,请参考上一小节的步骤3。

④ 一旦下载完成,使用默认的设置安装该文件。

图2-4 Java SE 7下载包

图2-5 JDK 7下载页面

既然已经下载了所有必需的文件,导航到解压开的ADT Bundle文件夹,并且打开Eclipse文件夹。一旦进入该文件夹,启动Eclipse应用程序(在Windows上名为eclipse.exe)。

如果你看到了一条关于未定义的PATH变量的错误,这意味着,Eclipse不能找到JRE。要解决这一问题,访问如下页面。

http://docs.oracle.com/javase/tutorial/essential/environment/paths.html

如果没有错误,那么,应该会看到图2-6所示的一个对话框。

图2-6 ADT工作区启动程序

图2-6展示了一个对话框,它要求你设置一个工作区,也就是在其中创建自己的Java项目的文件夹。在这里,你可以选择并创建想要的任何文件夹,并且Eclipse将会使用它来管理你的Java项目。

在选择了工作区之后,Eclipse将会打开,并且你将会看到图2-7所示的欢迎界面。

图2-7 Android IDE欢迎界面

现在,我们已经准备好了IDE,可以开始编写第一个Java程序了。由于还没有构建任何的Android应用程序,我们可以安全地退出这个标签页。如图2-8所示。

图2-8 退出欢迎界面

完成之后,我们将可以访问几个不同的视图。现在,只需要关心其中的2个视图:Package Explorer 和Editor Window。如图2-9所示。

图2-9 Package Explorer 和Editor Window

我们终于开始编写第一个Java程序了。Eclipse中的Java程序都是组织成项目的。要创建一个新的项目,在Package Explorer上点击鼠标右键(在Mac上是Control +点击),点击New,然后选择Java Project,如图2-10所示。

图2-10 创建一个新的Java项目

将会打开图2-11所示的对话框,要求你分配一个项目名称。我们把这个项目叫作“Beginning Java”。现在,可以离开这个对话框了。

在Eclipse中创建的每个Java项目,都有两个重要的组成部分,如图2-12所示。

(1)src文件夹是放置所有源代码(Java类)的地方。我们将要编写的所有代码,都放在这个src文件夹中。

(2)第二部分是JRE System Library,它包含了我们可以在自己的Java代码中使用的所有重要的Java库。

在指定了项目名称之后,点击Finish按钮。

图2-11 New Java Project对话框

图2-12 Java项目的结构

Java要求我们在Java类中编写代码。可以在一个文本编辑器(如Notepad和TextEdit)中创建并修改类,或者可以像我们一样,使用Eclipse这样的一款集成开发环境。

要编写第一个程序,必须创建自己的第一个Java类。在src文件夹上点击鼠标右键(在Mac上是Control +点击),并且选择New > Class

将会打开New Java Class对话框。我们将只提供类名FirstProgram,其他的设置保留不动,然后点击Finish按钮,忽略关于默认包的警告。如图2-13所示。

图2-13 New Java Class对话框

FirstProgram类将会在编辑器窗口中自动打开。如果没有,在左边的Package Explorer中双击FirstProgram.java文件,如图2-14所示。

图2-14 第一个类

Eclipse将会为我们自动生成一些基本的代码,如程序清单2.1所示。注意,我已经给这段代码添加了一些额外的注释,以说明每一行代码在做什么。除非你手动添加,这些注释不会出现在代码中。

程序清单2.1 FirstProgram.java

1    public class FirstProgram { // Denotes beginning of the class
2            // methods go here!
3    } // Denotes the end of the class

注意开始的花括号和结束的花括号:{和}。前一个花括号表明FirstProgram类从哪里开始,后一个花括号表明该类在哪里结束。我们将在这些花括号之间编写自己的代码。对于Java程序员新手来说,花括号导致了很多令人头疼的问题,因此,在后面几章中,我将通过标记花括号来帮助你。你应该留意花括号,并且习惯于查看开始花括号结束花括号之间的关系。

Java程序从main方法开始。main方法由此也称为一个Java程序的起点。当我们构建并执行一个程序的时候,在main方法中提供的任何指令,都将是要执行的第一行代码。在FirstProgram类中(两个花括号之间),添加如下的代码段。

程序清单2.2 main方法

    public static void main(String[] args) {
          // This is the starting point of your program.
    } // End of main

对于Java程序员新手来说,关键字public、static、String[]和args会引起很多混淆。我们很快将回过头来介绍所有这些关键字。现在,关注一下我们已经知道的3件事情:方法的名称、方法的参数(输入)和返回类型。

参见程序清单2.2,你可能已经猜到了,方法的名称是main。它接受一个参数,这是一组String对象,我们将这组参数命名为args(这个名称遵从于惯例。如果你愿意的话,可以将它命名为rabbits)。正如关键字viod所示,返回类型是无返回值;我们在这个main方法中不用提供任何结果或输出。

现在,程序在Eclipse中如图2-15所示。

如果你此刻遇到麻烦,我建议访问本书的配套网站jamescho7.com。那里有视频指南帮助你顺利地设置和运行。

图2-15 添加main方法

System.out.println("Hello, world! I am now a Java programmer");

学习一种新的编程语言,要做的一件传统的事情是,就是在控制台打印出“Hello, world”。有两点原因使得这件事情很重要。首先,如果你能够成功地做到这一点,你知道机器已经正确地设置好并能够进行开发了(即IDE和Java安装在后台都进行得很顺利)。其次,这意味着,你已经在新环境中执行了第一行代码,并且已经准备好进行下一步。

在第1章中,我们介绍过可以使用一个print()函数来打印内容。遗憾的是,由于Java的面向对象设计(我们将很快介绍这一点),它并没有这样一个简单的打印函数。相反,我们必须使用System.out.println(),其中最后两个字母是LN的小写字母,是单词line的缩写。

现在,完整的类应该如程序清单2.3所示。

程序清单2.3 FirstProgram.java – Hello World!

1 public class FirstProgram { // Denotes beginning of the class
2    
3    public static void main(String[] args) { // Beginning of Main
4        System.out.println("Hello, world! I am now a Java programmer");
5    } // End of Main
6
7 } // Denotes the end of the class

注意,我们使用缩进来表示不同的层级。FirstProgram类包含了一个main方法,main方法那里缩进了一次。反过来,main方法包含了println语句,它缩进两次。这样的格式使得我们能够快速判断有多少行代码形成结构,以及每一个这样的代码部分是从哪里开始到哪里结束。

要执行一个程序,我们直接在项目的src文件夹(或FirstProgram类)上点击鼠标右键(在Mac上是Control +点击),如图2-16所示。

图2-16 执行Java应用程序

当执行该程序的时候,会弹出Console(如图2-17所示),并且显示消息“Hello, world! I am now a Java programmer”。如果由于任何原因,控制台没有出现,那么可以点击工具栏(Eclipse窗口顶部)上的Windows菜单,然后选择Show View > Console来打开它。

图2-17 控制台显示了FirstProgram.java的输出

成功了!如果你能够得到这一输出,恭喜你!你已经成功地编写了第一个Java程序。

如果你遇到麻烦,没有得到所示的消息,请访问本书的配套站点jamescho7.com。那里的视频将会指导你如何通过这些步骤,并确保毫无问题地做到这一点。

在我们点击运行按钮和出现“Hello, world…?”之间,发生了什么事情。不管你是否相信,所有的事情都是在幕后进行的。当我们编写源代码的时候,Java编译器会编译它,这意味着,它会检查代码潜在的错误并将其转换为只有机器能够理解的语言。这个机器,就是执行代码并把想要的文本打印到控制台的Java虚拟机(Java Virtual Machine,JVM)。如图2-18所示。

图2-18 Java的魔术

JVM是一个虚拟的机器。它运行于操作系统之上,并且能够执行Java指令。使用这样一个虚拟机的好处在于,你可以在一种操作系统(如Windows或Mac)上编写跨平台的Java代码,而代码会在另外一种操作系统上运行。

现在,我们已经尝到了甜头,让我们回过头来看看第1章介绍过的一些概念,并且构建一个简单的计算器程序。让我们给出一些动手实践的指导,来构建一个新的Java程序。请记住如下的主要步骤。

① 创建一个新的Java项目(将其命名为SecondProject)。

② 在src文件夹中创建一个新的类(将其命名为SimpleCalculator)。

③ 创建一个main方法。

如果任何时候你碰到困难,应该参考前面的小节。一旦按照上面的步骤进行,应该会看到程序清单2.4所示的内容。

程序清单2.4 SimpleCalcualtor类

public class SimpleCalculator {

    public static void main(String[] args) {

    }
}

计算器应用程序背后的思路很简单。我们将创建两个float变量,表示两个运算数。我们将创建第3个变量来表示想要执行的计算。

我们将使用一个整数来表示计算,规则如下。

1:加法。

2:减法。

3:乘法。

4:除法。

源代码将检查3个变量中的值,并且使用它们来产生所请求的算术计算的结果。给类SimpleCalculator添加如下代码。新的代码如程序清单2.5的第4行到第31行所示。

程序清单2.5 带有逻辑的SimpleCalcualtor类

01 public class SimpleCalculator {
02  
03  public static void main(String[] args) {
04    float operand1 = 5;
05    float operand2 = 10;
06    int operation = 1;
07
08    if (operation == 1) { 
09  
10       // Addition
11       System.out.println(operand1 + " + " + operand2 + " =");
12       System.out.println(operand1 + operand2);
13
14    } else if (operation == 2) {
15
16       // Subtraction
17       System.out.println(operand1 + " - " + operand2 + " =");
18       System.out.println(operand1 - operand2);
19
20    } else if (operation == 3) {
21
22       // Multiplication
23       System.out.println(operand1 + " * " + operand2 + " =");
24       System.out.println(operand1 * operand2);
25
26    } else {
27
28       // Division
29       System.out.println(operand1 + " / " + operand2 + " =");
30       System.out.println(operand1 / operand2);
31    }
32
33  }
34 }

运行该程序!应该会得到如下所示的输出。

5.0 + 10.0 =

15.0

花时间看一下这段代码。确保可以一行一行地浏览代码,并搞清楚发生了什么。

我们首先是声明了两个新的float变量,名为operand1和operand2,并使用值5和10来初始化它们。我们声明的第3个变量名为operation,将值1赋给了它。

然后,是一系列的if语句,它们测试operation变量的值并确定执行正确的计算。当一条if语句满足的时候,执行两条System.out.println()语句,打印出了我们所看到的结果。注意,在这里我们使用加法运算,将两个带有float值的字符串连接(组合)起来。

那么,如果我们想要计算25×17的值的话,该如何修改呢?我们直接将operand1的值改为25,将operand2的值改为17,将operation的值改为3,如程序清单2.6所示。

程序清单2.6 修改后的SimpleCalcualtor类

public class SimpleCalculator {

    public static void main(String[] args) {
        float  operand1 = 5;
        float  operand2 = 10;
        int  operation = 1;
        float operand1 = 25;
        float operand2 = 17;
        int operation = 3;
        if (operation == 1) { 
            // Addition
            System.out.println(operand1 + " + " + operand2 + " =");
            System.out.println(operand1 + operand2); 
        } else if (operation == 2) {
            // Subtraction
            System.out.println(operand1 + " - " + operand2 + " =");
            System.out.println(operand1 - operand2);
        } else if (operation == 3) {
             // Multiplication
             System.out.println(operand1 + " * " + operand2 + " =");
             System.out.println(operand1 * operand2);
        } else {
             // Division
             System.out.println(operand1 + " / " + operand2 + " =");
             System.out.println(operand1 / operand2);
        }
    }
}

再次运行该程序,应该看到如下结果。

25.0 * 17.0 =

425.0

SimpleCalculator现在还不是非常有用。每次要执行一个简单的计算的时候,它都要求我们修改代码。最好的解决方案是,要求程序的用户为我们的operand1、operand2和operation提供想要的值。实际上,Java提供了一种方法可以做到这一点,但是,需要我们先理解如何使用对象,因此,我们现在先不讨论这种方法。

5 is an odd number.
6 is an even number.
7 is an odd number.
8 is an even number.
9 is an odd number.
10 is an even number.
11 is an odd number.
12 is an even number.

在下一个示例中,我们将利用第1章中介绍过的for循环来打印出数字5到12之间的每一个偶数。这是一个简单的游戏示例,但是,掌握for循环语法的技巧很重要。

创建一个名为CountingProject的新的Java项目,并且创建一个名为EvenFinder的新类,添加程序清单2.7所示的main方法。

程序清单2.7 EvenFinder类

01 public class EvenFinder {
02
03    public static void main(String[] args) { 
04        int startingNum = 5;
05        int endingNum = 12;
06      
07        for (int i = startingNum; i < endingNum + 1; i++) {
08      
09            // Execute following code if i < endingNum + 1
10    
11            if (i % 2 == 0) {
12                 System.out.println(i + " is an even number.");
13            } else {
14                 System.out.println(i + " is an odd number.");
15            }
16      
17            // Repeat for loop
18        }
19    }
20 }

运行该程序,应该会看到如下所示的输出。

还记得吧,for循环有3个组成部分。我们首先初始化一个计数器变量i。然后,提供了一个终止条件,该条件说“运行这个循环直到不再满足这个条件”。最后,我们提供了计数器变量自增的规则。

在前面的示例中,计数器从值5开始,并且只要其值小于endingNum + 1就会自增。当i的值变得和endingNum + 1相等的时候,循环终止(不再执行循环体),并且程序结束。

尝试自己一行一行地执行这些代码,每次“循环”运行的时候,手动增加i值。确保你理解for循环何时终止,以及为何终止。如果这对你来说有些困难,回顾一下第1章中介绍循环的部分可能会有所帮助。

我们已经应用了第1章中介绍过的概念来编写和运行一些非常简单的Java程序。接下来,我们将把注意力转向对象,它使得我们能够编写更加复杂和强大的程序。

什么是对象?以你看待现实世界中的物体的方式来思考Java对象,这么做是有帮助的。对象所拥有的属性,我们称之为状态(state)和行为(behavior)。

让我们以手机为例子。你的手机拥有状态,它可能是黑色的,并且可能打开了电源开关。这些属性可以帮助我们描述手机以形成其状态。手机还会有行为。它可能能够播放音乐,或者对触摸做出响应。通常,这些行为都独立于手机的状态(但并不总是如此)。例如,如果你的手机是关机的(这是其状态的一个特性),手机不再能够执行任何这些行为。

Java对象也大同小异。它们也有状态和属性。实际上,你将在这整本书中学习状态和属性。变量(v ariable)通常用来描述一个对象的状态。函数(function),我们也称之为方法(method),描述一个对象的行为。

图2-19给出了一个示例,展示了我们如何使用变量和方法来设计一个Java的Phone对象。

图2-19 一个Phone对象的框架

关键知识点

快速介绍命名惯例

你可能注意到了,我们在命令类、变量和方法的时候,遵从相同的惯例。这些是应该了解和遵守的共同规则。让我们详细介绍一下。

类名、变量名和方法名应该是一个单词(多个单词的话,要组合到一个单词中)。在命名类的时候,我们使用所谓的UpperCamelCase方法,其中每个单词的第一个字母大写。在本书中,类的名称显示为等宽粗体的形式。如下是恰当的类名(注意,它们都是名词)。

在命名变量和方法的时候,我们使用camelCase方法。将名称的首字母小写,并且将每个后续的单词的首字母大写。在本书中,变量和方法名称都以常规的等宽字体显示。如下是恰当的变量名和方法名(注意,变量名称是名词,方法名是动词)。

图2-19所示的一个对象框架,如何将其转换为Java代码呢?使用类(class)。我们已经创建了很多类,但是,还没有介绍什么是类。

类提供了一个模板,以供创建Java对象。常用的类比把类描述为一个蓝图。如下是一个Phone类的样子。

程序清单2.8 Phone类的一个示例

01 public class Phone {
02
03    // These variables describe the Phone object's state
04    boolean poweredOn;
05    boolean playingMusic;
06    String phoneManufacturer;
07    double androidVersionNumber;
08
09    // These methods are the Phone object's behaviors
10    void togglePower() {
11        if (poweredOn) {
12            System.out.println("Powering off!");
13            poweredOn = false;
14            playingMusic = false;
15        } else {
16             System.out.println("Powering on!");
17             poweredOn = true;
18        }
19    } // ends togglePower method
20
21    void playMusic() {
22        if (poweredOn) {
23            System.out.println("Playing music!");
24            playingMusic = true;
25        }
26    } // ends playMusic method
27  
28    void upgrade(double newVersion) {
29        if (newVersion > androidVersionNumber) {
30            androidVersionNumber = newVersion;
31      } else {
32            System.out.println("Upgrade failed!");
33      }
34    } // ends upgrade method
35
36 } // ends class
Game  DragonKnight  SimpleCalculator  MathHelper

程序清单2.8所示的这个Phone类,是创建单个的Phone对象的一个蓝图。它告诉我们一个对象要成为一个Phone对象,需要哪些属性(状态和行为)。我们将使用代码来探究其含义,并且在随后的小节中讨论类和对象之间的隐含意义。

versionNumber  drawCharacter()     addNum()  failingStudent

我们现在开始真正地使用对象。创建一个名为BasicObjects的新的Java对象。然后,创建一个名为World的新类,并且给它一个简单的“Hello, world!” 的main方法,如程序清单2.9所示。

程序清单2.9 World.java

 public class World {

    public static void main(String[] args) {
        System.out.println(“Hello, world!”);
    }
 }

World类将表示一个小型的虚拟世界,我们可以用对象填充这个世界。它将是我们的程序的入口点(我们从这个类开始运行程序),因此,它需要main方法。

在相同的src目录中,创建另一个名为Phone的类,如图2-20所示。

图2-20 BasicObjects的类结构

在Eclipse中,将程序清单2.8中的Phone类复制到Phone.java中。Phone类不应该有main方法。Phone类的主要作用是简化一个虚拟设备的相关信息的保存;它是一个想象的手机的一种表示,仅此而已。Phone类和World类一起构成了一个程序,并且在本书中,我们的程序通常只有一个main方法,这意味着,只有一条路径启动程序。

如果我们要运行两个类程序的话,你能够预计到将会发生什么吗?World类中的代码还会运行吗?Phone类中的代码还会运行吗?只有一种方法能够搞清楚这一点。在src目录上点击鼠标右键(在Mac上是Control+点击),以启动程序,并且将该项目当作一个Java应用程序运行。应该会看到如下所示的输出。

Hello, world!

这个项目有两个类,但是Eclipse能够找到包含main方法的类(World.java)并且运行它。尽管Phone类中有很多的代码,但没有任何代码会对输出产生影响,因为我们没有要求main方法使用Phone类来执行任何行为。让我们做一些修改。

我们想要使用Phone类作为蓝图,创建一个新的Phone对象。为了做到这点,我们使用如下所示的语法。

Phone myPhone = new Phone();

使用我们前面用来创建基本类型变量相同的方式,来创建一个对象变量。首先声明对象变量的类型(Phone),然后指定一个名称(myPhone),最后赋值。

语法的不同之处在于最后一步。要创建一个新的Phone对象,我们必须使用Java的内建关键字new,并且声明我们想要用来创建Phone对象的蓝图,即Phone类。让我们将上面的代码添加到main方法中,如程序清单2.10的第5行所示。

程序清单2.10 World.java—更新后的版本

1    public class World {
2    
3        public static void main(String[] args) {
4            System.out.println("Hello, world!");
5            Phone myPhone = new Phone();
6        }
7    }

在本书后面,我们将会讨论new关键字的作用,以及声明new Phone()的时候到底发生了什么。

Hello, world!
myPhone's state:
Powered on: true
Playing music: false
Manufacturer: Samsung
Version: 4.4

现在,我们可以访问一个Phone对象了。myPhone表示使用Phone类(class)创建的一个单个的Phone对象(object)。它是一个独立的实体,独立于我们将来可能使用蓝图(Phone类)创建的任何其他Phone对象。我们使用实例(instance)这个术语来描述这种现象。

为了更加具体地说明,让我们考虑一下,在工厂中批量生产智能手机的时候会发生什么情况。我们使用相同的蓝图来生产数以千计的设备,而且它们都是彼此独立的。它们可以有自己的属性和行为,这意味着,关闭一个设备不会影响到使用相同的蓝图生产的其他设备。与此非常相似,由单个的类而创建的每一个对象,都是该类的一个独立的实例(instance),并且接受各个变量的自己的副本,来描述对象的状态。这些变量叫作实例变量(instance variable)。

我们现在可以开始修改myPhone的状态并且调用其行为了。让我们先来为单个的Phone对象的状态指定一些初始值,如程序清单2.11所示(从第6行到第9行)。

程序清单2.11 World.java——更新后的版本

01 public class World {
02    
03    public static void main(String[] args) {
04        System.out.println("Hello, world!");
05        Phone myPhone = new Phone();
06        myPhone.poweredOn = true;
07        myPhone.playingMusic = false;
08        myPhone.phoneManufacturer = "Samsung";
09        myPhone.androidVersionNumber = 4.4;
10    }
11 }

注意一下,我们是如何访问属于Phone对象的实例变量的。要获取一个对象的特定的变量,使用点运算符。点运算符用来表示所有权。例如,myPhone.poweredOn表示属于myPhone对象的poweredOn变量。

现在已经为Phone对象的变量指定了一些初始值,myPhone是描述性数据的一个集合。如果某人访问了我们的myPhone对象,他通过打印myPhone的当前状态的值,就能够完全知道其状态了,如程序清单2.11所示(从第11行到第15行)。

程序清单2.12 World.java—更新后的版本

01 public class World {
02    
03    public static void main(String[] args) {
04        System.out.println("Hello, world!");
05        Phone myPhone = new Phone();
06        myPhone.poweredOn = true;
07        myPhone.playingMusic = false;
08        myPhone.phoneManufacturer = "Samsung";
09        myPhone.androidVersionNumber = 4.4;
10    
11        System.out.println("myPhone's state:");
12        System.out.println("Powered on: " + myPhone.poweredOn);
13        System.out.println("Playing music: " + myPhone.playingMusic);
14        System.out.println("Manufacturer: " + myPhone.phoneManufacturer);
15        System.out.println("Version: " + myPhone.androidVersionNumber);
16    }
17 }

再次运行该程序。应该会看到如下所示的输出。

正如你所看到的,我们能够将有意义的数据组织到一个集合中,即一个Phone对象中,这个对象的名称是myPhone。myPhone现在是一个复杂的信息集合。我们将在后面的小节中介绍这一点如何有用。

Hello, world!
myPhone's state:
Powered on: true
Playing music: false
Manufacturer: Samsung
Version: 4.4
Powering off!
myPhone's NEW state:
Powered on: false
Playing music: false
Manufacturer: Samsung
Version: 4.5

在前面的小节中,我们介绍了如何赋值和访问所创建的对象的状态。接下来,我们讨论方法,并且学习如何调用对象的行为。

调用方法也需要使用点运算符。我们使用点运算符来引用属于一个特定对象的具体的方法。在程序清单2.12的main方法的底部,添加如下所示的两行代码。

myPhone.togglePower();
myPhone.upgrade(4.5);

如果我们再回头来看看Phone类,会看到togglePower方法检查boolean poweredOn的当前值,并且对其取反(将ture变为false,将false变为true)。由于创建对象的时候,myPhone最初是打开的,我们期望myPhone现在关闭。我们还预测了myPhone的androidVersionNumber从4.4变为4.5。

为了测试这些,我们又一次打印出myPhone对象的状态,在main方法的底部添加一些打印语句,如程序清单2.13所示。

程序清单2.13 打印出myPhone的状态

01 public class World {
02    
03    public static void main(String[] args) {
04      System.out.println("Hello, world!");
05      Phone myPhone = new Phone();
06      myPhone.poweredOn = true;
07      myPhone.playingMusic = false;
08      myPhone.phoneManufacturer = "Samsung";
09      myPhone.androidVersionNumber = 4.4;
10      
11      System.out.println("myPhone's state:");
12      System.out.println("Powered on: " + myPhone.poweredOn);
13      System.out.println("Playing music: " + myPhone.playingMusic);
14      System.out.println("Manufacturer: " + myPhone.phoneManufacturer);
15      System.out.println("Version: " + myPhone.androidVersionNumber);
16      
17      myPhone.togglePower();
18      myPhone.upgrade(4.5);
19      
20      // include “\n” to skip a line when printing.
21      System.out.println("\nmyPhone's NEW state:");
22      System.out.println("Powered on: " + myPhone.poweredOn);
23      System.out.println("Playing music: " + myPhone.playingMusic);
24      System.out.println("Manufacturer: " + myPhone.phoneManufacturer);
25      System.out.println("Version: " + myPhone.androidVersionNumber);
26    }
27 }

相应的输出如下所示。

正如所预测那样,手机关闭了,并且其Android版本现在是4.5。我们能够调用myPhone行为来执行特定的操作,以修改myPhone的状态了。

注意,到目前位置,我们能够以两种不同的方式来修改Phone对象的状态。我们能够使用点运算符直接访问其变量,并且分配显式的值;还能够使用Phone对象提供的行为来间接地修改Phone对象的状态。

如果能够直接深入到myPhone对象,取出其信息并修改,我们说对象的变量是暴露的。从现在开始,我们将禁止暴露变量,基于很多原因,暴露变量可能会有问题。

例如,如果某人试图给一个变量分配一个非法的(或者不符合逻辑的)值,会怎么样呢?如下的代码对Java程序来说可能是可以接受的,但是,随后如果我们想要扩展这一程序的话,它可能会引发问题,并且这些值真的可能会影响到一些其他的功能。

myPhone.androidVersionNumber = -10;   // Version should be positive
myPhone.poweredOn = false;       // This is fine
myPhone.playingMusic = true;       // Shouldn’t play music while phone is off

暴露变量可能会引发问题,另一个原因在于,我们可能需要处理敏感信息。如果要运行一个本章开头所讨论的视频共享站点,我们可能不想让用户访问User对象的password变量,它应该总是隐藏起来的。在这里,安全性是一个问题。

我们想要隐藏变量的第三个原因,是为了可维护性和可扩展性。当我们随后有更加复杂的程序和游戏,它们带有众多不同类型的、彼此交互的对象,这时我们想要尽可能地减少依赖性(即那些严重依赖于特定交互的功能)。我们需要记住,程序和游戏可能会改变。你可能选择删除类并创建新的类,但是,你不想让这样的情况发生:即不得不重新编写整个应用程序来处理一处小小的修改。

例如,我们假设你有一个Enemy类,它与Player类和GameLevel类交互得很好。稍后,你决定要删除Enemy类,并且用一个SuperZombieOrangutan类来替代它。如果在Enemy、Player和GameLevel类之间有太多的依赖性,你可能需要重写这些类以处理新的敌人类型,你将要创建3个新的类而不是一个。这可能会变成一种恶意的模式。如果这需要花费很多的时间,你可能确定这一修改并不值得,这意味着你的游戏将会少一种僵尸怪兽。这绝不是好事情。

简而言之,你想要能够为游戏添加想要的功能,又不必担心修改已有的代码会成为可怕的梦魇。这意味着,我们想要让类尽可能地保持独立,而隐藏变量是朝着正确方向迈进的一步。我们将在后面的一章中更深入地讨论这一概念。

Hello, world!
Phone's state:
Powered on: false
Playing music: false
Manufacturer: Samsung
Version: 4.4
Powering on!
Phone's state:
Powered on: true
Playing music: false
Manufacturer: Samsung
Version: 4.5

让我们记住上面的原理,并且努力地改进程序。首先,添加一个内建的Java关键字private作为所有Phone对象的变量的修饰符,如程序清单2.14第4行到第7行所示。

程序清单2.14 隐藏Phone类中的变量

01 public class Phone {
02
03    // These variables describe the Phone object's state
04    private boolean poweredOn;
05    private boolean playingMusic;
06    private String phoneManufacturer;
07    private double androidVersionNumber;
08
09    // These methods are the Phone object's behaviors
10    void togglePower() {
11        if (poweredOn) {
12            System.out.println("Powering off!");
13            poweredOn = false;
14            playingMusic = false;
15        } else {
16            System.out.println("Powering on!");
17            poweredOn = true;
18        }
19    } // ends togglePower method
20
21    void playMusic() {
22        if (poweredOn) {
23            System.out.println("Playing music!");
24        }
25    } // ends playMusic method
26  
27    void upgrade(double newVersion) {
28        if (newVersion > androidVersionNumber) {
29            androidVersionNumber = newVersion;
30      } else {
31            System.out.println("Upgrade failed!");
32      }
33    } // ends upgrade method
34
35 } // ends class

让变量成为private的,意味着其他的类不再能够直接访问它们,也意味着这些变量不再是暴露的了。因此,你将会看到World类中出现错误,如图2-21所示(不能直接引用不同的类中的一个private的变量)。

程序目前有所谓的编译时错误(发生在代码编译过程中的错误,参见图2-18以及后续的介绍)。有编译时错误的程序无法运行。JVM甚至不会接受这种程序。让我们删除引发错误的所有代码行,如程序清单2.15所示(在删除的所有代码行上,都有一条删除线)。

图2-21 一个严重错误

程序清单2.15 World.java——删除错误代码

01 public class World {
02    
03    public static void main(String[] args) {
04        System.out.println("Hello, world!");
05        Phone myPhone = new Phone();
06        myPhone.poweredOn = true;
07        myPhone.playingMusic = false;
08        myPhone.phoneManufacturer = "Samsung";
09        myPhone.androidVersionNumber = 4.4;
10      
11        System.out.println("myPhone's state:");
12        System.out.println("Powered on: " + myPhone.poweredOn);
13        System.out.println("Playing music: " + myPhone.playingMusic);
14        System.out.println("Manufacturer: " + myPhone.phoneManufacturer);
15        System.out.println("Version: " + myPhone.androidVersionNumber);
16      
17        myPhone.togglePower();
18        myPhone.upgrade(4.5);
19    
20        // include “\n” to skip a line when printing.
21        System.out.println("\nmyPhone's NEW state:");
22        System.out.println("Powered on: " + myPhone.poweredOn);
23        System.out.println("Playing music: " + myPhone.playingMusic);
24        System.out.println("Manufacturer: " + myPhone.phoneManufacturer);
25        System.out.println("Version: " + myPhone.androidVersionNumber);
26    }
27 }

要执行这一清理工作,我们必须删除程序的两项功能。我们不再能够为Phone对象的变量赋初始值,并且不再能够访问这些变量以打印出它们。我们可以通过在Phone类中提供方法来执行这些任务,从而以更高效的方式来实现这些功能。

让我们给Phone类添加两个新的方法:initialize()和describe(),如程序清单2.16所示,并且为playingMusic和androidVersionNumber变量提供初始值(如程序清单2.16的第5行和第7行所示)。

程序清单2.16 Phone.java——更新版本(新的代码行突出显示)

01 public class Phone {
02
03    // These variables describe the Phone object's state
04    private boolean poweredOn;
05    private boolean playingMusic = false;
06    private String phoneManufacturer;
07    private double androidVersionNumber = 4.4;
08
09    // These methods are the Phone object's behaviors
10    void initialize(boolean poweredOn, String phoneManufacturer) {
11        this.poweredOn = poweredOn;
12        this.phoneManufacturer = phoneManufacturer;
13    }
14
15    void togglePower() {
16        if (poweredOn) {
17            System.out.println("Powering off!");
18            poweredOn = false;
19            playingMusic = false;
20        } else {
21            System.out.println("Powering on!");
22            poweredOn = true;
23        }
24    } 
25
26    void playMusic() {
27        if (poweredOn) {
28            System.out.println("Playing music!");
29        }
30    } 
31    
32    void upgrade(double newVersion) {
33        if (newVersion > androidVersionNumber) {
34            androidVersionNumber = newVersion;
35        } else {
36            System.out.println("Upgrade failed!");
37        }
38    } 
39    
40    void describe() {
41        System.out.println("\nPhone's state:");
42        System.out.println("Powered on: " + poweredOn);
43        System.out.println("Playing music: " + playingMusic);
44        System.out.println("Manufacturer: " + phoneManufacturer);
45        System.out.println("Version: " + androidVersionNumber);
46    }
47
48 } // ends class

让我们讨论一下describe()方法(程序清单2.16的第40行到第46行)。你注意到,它执行了我们前面在World类中所执行的相同的打印行为。这一次,我们不必使用点运算符,因为可以从同一个类中访问这些变量。

然而,在某些情况下,你确实需要使用点运算符。来进一步看一下initialize()方法(程序清单2.16的第10行到第13行)。

void initialize(boolean poweredOn, String phoneManufacturer) {
      this.poweredOn = poweredOn;
      this.phoneManufacturer = phoneManufacturer;
}

initialize()方法直接接受两个输入:一个名为poweredOn的boolean值,以及一个名为phoneManufacturer的字符串。这个方法唯一的功能就是,将我们没有为其提供默认值的两个变量poweredOn和phoneManufacturer(还记得吧,我们已经为另外两个变量提供了初始值)初始化。如图2-22所示。

注意,我们在这里确实使用了点运算符。使用this关键字让程序知道,我们引用的是对象的这个实例,即我们在其上调用initialize()方法的当前Phone对象。这就是我们如何区分属于对象的poweredOn变量和属于方法(通过参数而接受)的poweredOn变量。

既然已经创建了两个方法,我们就能够访问Phone对象的私有变量,让我们来修改World类,以便它可以调用这些方法,参见程序清单2.17中高亮显示的第6行、第7行和第10行。

图2-22 相同名称,不同所有者

程序清单2.17 World.java——调用新的方法

01 public class World {
02
03    public static void main(String[] args) {
04        System.out.println("Hello, world!");
05        Phone myPhone = new Phone();
06        myPhone.initialize(false, "Samsung");
07        myPhone.describe();
08        myPhone.togglePower();
09      myPhone.upgrade(4.5);
10      myPhone.describe();
11    }
12 }

相应的输出如下所示。

对我们来说,理解一个类和一个对象之间的区别是很重要的,因此,来看看这部分内容。对象只是数据的集合,它们包含了描述变量和方法的关系的一组数据。类是用来创建这些对象的蓝图。

为了说明这一点,我们假设你在玩乐高积木(你的年龄并不大,可以玩乐高)。你找到一个说明手册并且开始构建太空飞船。说明手册包含了你构建太空飞船所需的所有信息:需要构建的机翼的数目,需要添加的大炮的数目等等。使用这个手册构建的每一个乐高模型,都是太空飞船,但是,手册本身不是飞船,它只是蓝图。

类和对象之间也有类似的关系。尽管类描述了对象的状态和行为是什么(即要让一个对象具备该类型,它需要哪些属性),而类本身不是对象。

让我们来看一下实例和对象独立性的概念。使用一个类,我们可以创建想要的任意多个对象。例如,可以创建一个Spaceship类并且使用它来实例化(创建实例)50个Spaceship对象。这些Spaceship对象中的每一个,都叫作Spaceship类的实例。实例是更为“泛化”的类的“具体的”表示,这就好像乐高组合是其各个说明手册的具体化的表示。如图2-23所示。

图2-23 Spaceship:类VS.实例

就像现实生活中的对象一样,同一个类的不同实例是彼此独立的。还是以50个Spaceships为例,你可以修改Spaceships类的一个实例(一个单个的Spaceships对象),而其他的49个实例并不会受到影响。

现在,让我们暂时从创建自己的类告一段落,来享受一下Java自带的现成的类。使用已有的编程语言,而不是自己创造一种编程语言,其好处在于你可以获取已有的代码,并且在自己的项目中实现它们。好在对于我们来说,Java类配备了内容广泛的文档,涉及它们所包含的变量、如何初始化这些变量,以及它们执行哪些行为,从而我们可以将这些类用于自己的程序,并且只关注它们特定于我们的项目的重要问题。

可以通过如下的链接访问Java SE7的完整文档:http://docs.oracle.com/javase/7/docs/api/

让我们通过使用熟悉的一个类String,来练习一下如何使用Java文档。创建一个名为FunWithStrings的新的Java项目,并且创建一个名为StringTester的、带有main方法的新的类,如程序清单2.18所示。

程序清单2.18 StringTester.java——空的版本

01 public class StringTester {
02
03    public static void main(String[] args) {
04    
05    }
06
07 }

String类(它隐藏于Java库之中)允许我们在自己的代码中创建String对象。让我们使用用于初始化对象的new关键字,来初始化一个String对象。在main方法中添加如下代码。

String s = new String(“this is a string”);

字符串是如此的常用,以至于Java提供了一种特殊的方法来初始化它们。再添加如下的一行代码。

String s2 = “this is also a string”;

程序清单2.19给出了更新后的类。

程序清单2.19 StringTester.java——更新的版本

01 public class StringTester {
02
03    public static void main(String[] args) {
04        String s = new String("this is a string");
05        String s2 = "this is also a string"; 
06
07    }
08
09 }

像其他的Java对象一样,Strings也有状态和行为。在本书中,我们将只关注Strings的行为,其状态对于我们来说没有用。

让我们现在来使用Java文档。搜索String 类,并且向下滚动到Method Summary。你会发现,这里给出了String对象可用的方法的一个列表。如图2-24所示。

图2-24 String类的部分方法概览

这个表中的单个条目,告诉我们每个方法的返回类型,以及方法名、所需的参数(输入)和方法概览。

String有一个方法,可以从一个指定的位置(称为索引)获取一个单个的字符(类型为char)。这个方法名为charAt(),它接受一个整数值,表示想要的字符的索引。

Java中的索引值是基于0的,这意味着,第一个字符的索引为0。让我们看一下这在代码中意味着什么。我们将调用charAt()方法,并且查看String s中的第3个字符(索引2),如程序清单2.20中第7行代码所示。

程序清单2.20 打印出一个字符串中的字符

01 public class StringTester {
02
03    public static void main(String[] args) {
04        String s = new String("this is a string");
05        String s2 = "this is also a string"; 
06
07        char ch = s.charAt(2);
08        System.out.println("The third character is " + ch);
09    }
10
11 }

相应的输出如下所示。

The third character is i

让我们来看看使用Java文档的另一个例子。查找Method Summary。能否找到一个方法,它返回给定的String的长度。浏览Method Summary,会找到图2-25所示的内容。

图2-25 length()方法的概览

这张图告诉我们要使用length()方法所需的所有信息。我们知道,它返回一个整数来表示调用该方法的String的长度。该方法没有参数。让我们尝试得到s和s2的长度,并且判定哪一个更长。修改StringTester类,使其如程序清单2.21所示;新的代码在第10行到第19行。

程序清单2.21 StringTester.java(更新版本)

01 public class StringTester {
02
03    public static void main(String[] args) {
04        String s = new String("this is a string");
05        String s2 = "this is also a string"; 
06
07        char ch = s.charAt(2);
08        System.out.println("The third character is " + ch);
09      
10        int sLen = s.length();
11        int s2Len = s2.length();
12
13        if (sLen > s2Len) {
14            System.out.println("s is longer than s2.");
15        } else if (sLen == s2Len) {
16              System.out.println("They have the same length.");  
17            } else {
18              System.out.println("s2 is longer than s");
19          }
20
21    }
22
23 }

运行该代码,将会得到如下所示的结果。

The third character is i

s2 is longer than s

我鼓励你尝试一下Java文档中列出的其他方法。能够使用Java文档,这是一项重要的技能。和其他所有值得做的事情一样,只有通过练习才能较好地掌握。记住如下几件事情。

① 返回类型:(这决定了需要在结果中存储什么类型的变量)。

② 方法名称:(必须完全像显示的那样拼写。方法名称是区分大小写的)。

③ 输入:(必须总是提供为了让方法工作而所需的参数。这包括提供正确的参数个数和正确的类型)。

④ 一些方法要求CharSequence类型的输入。当你遇到这样的方法的时候,你可能要提供一个String。这是因为有一种有趣的特性叫作多态(polymorphism,即一个对象能够采取多种形式的能力),我们将会在下一章中详细讨论它。

You have rolled a 1
You have rolled a 2
You have rolled a 3
You have rolled a 4
You have rolled a 5
You have rolled a 6

在我们的下一个项目中,将模拟一个六面色子的滚动。色子会出现在许多现代的桌上游戏中,因为它们增添了不可预期的因素,如图2-26所示。在本节中,我们将展示在Java程序中如何模拟这种随机性。

图2-26 一个标准的色子

我们首先创建一个名为“DiceProject”的新的Java项目。其中,创建一个名为DiceMaker的新的类,并且像通常一样给它一个main方法。

要产生一个随机数,我们必须使用Java库中名为Random的一个内建类。我们使用熟悉的对象创建语法来创建一个Random对象,如程序清单2.22的第4行所示。

程序清单2.22 DiceMaker.java

1  public class DiceMaker {
2
3    public static void main(String[] args) {
4        Random r = new Random();
5    }
6
7 }

应该注意到,Eclipse告诉你在创建Random对象的代码行中有一个错误,如图2.27所示。

一旦将鼠标移动到关键字Random上,将会出现如下所示的信息。

Random cannot be resolved to a type

这只是告诉你,编译器不能创建一个Random类型的对象,因为它不知道Random类在哪里。

图2-27 Random不能被解析为一个类型

要修正这个问题,必须提供完整的地址,让编译器知道在哪里找到Random。想要的Random类可以在java.util.Random中找到(这是UnitedKingdom.London.221BBakerSt形式的地址)。让我们导入这个类,如程序清单2.23第1行所示。

程序清单2.23 导入java.util.Random

1 import java.util.Random;
2
3 public class DiceMaker {
4
5   public static void main(String[] args) {
6       Random r = new Random();
7   }
8
9 }

既然已经告诉计算机在哪里找到Random,我们就能够调用这个方法了。我们感兴趣的是nextInt()方法,它接受一个整数,并且返回0(包括)到所接受的整数(不包括)之间的一个值。

例如,r.nextInt(6)将会随机地产生如下所示的数字之一。

0, 1, 2, 3, 4, 5

如果我们想要生成1到6之间的数字,直接给结果加上1就行了,如程序清单2.24中的第7行和第8行所示。

程序清单2.24 模拟色子滚动

01 import java.util.Random;
02
03 public class DiceMaker {
04
05    public static void main(String[] args) {
06        Random r = new Random();
07        int randNum = r.nextInt(6) + 1;
08        System.out.println("You have rolled a " + randNum);
09    }
10
11 }

运行该程序的时候,将会看到如下所示的结果之一。

Random类有哪些应用呢?当英雄杀死怪兽的时候,你可以选择实现一个随机数生成器来决定丢下什么物品。当在类似Minecraft这样的游戏中生成地图的时候,也可以使用随机数生成器。真的有无数种可能性。

我们已经使用了Random类来模拟随机性,但是,它并没有实现真正的随机性。尽管它似乎产生了随机数,但实际上它遵循了一个公式,该公式生成理论上可以预测的结果。我们将这种现象叫作伪随机数(pseudo-random)。这对我们编写游戏不可能有任何影响,但是它引发了一个有趣的讨论。长期来讲,你可以肯定地期待这个随机数生成器将会生成所有可能的数字的一个一致的分布。如果你想要学习有关真正的随机性的更多知识,请访问Random.org网站。

在上面的示例中,我们必须导入java.util.Random。这是我们将要从Java库导入的Random类的全名。

Java库组织成包的形式,其中包含了可以在代码中使用的各种类。无论何时,当你想要使用Java库中的一个类的时候,都必须告诉程序,在哪里可以找到这个类所在的包(完整名称)。

并不是所有的对象都需要导入。例如,属于java.lang包的String,由于它如此常用,实际上会自动导入。下一小节所要介绍的数组,也是不用导入就可以创建。

Java允许我们把对象和基本类型组织到一起。我们常见的有两种对象,可以用来进行分组,它们是数组和列表。

要表示某种类型的一个数组(或组),我们使用方括号。例如,如果想要整数的一个数组,可以像下面这样声明。

int[] numbers = new int[5];

上面例子中的数字5,表示名为numbers的数组应该有多大。正如上面所声明的,numbers将能够容纳5个整数值。要描述数组的样子,我们可以画一个表,如图2-28所示。

numbers[0]

numbers[1]

numbers[2]

numbers[3]

numbers[4]

0

0

0

0

0

图2-28 一个整数数组(默认值)

一开始,数组将会有默认值(创建整数数组的时候,默认值是0)。Java允许我们直接为每个索引(或位置)分配数值。数组索引是基于0的,就像字符串中的字符一样。数组的赋值语法如下所示。

numbers[0] = 5;
numbers[1] = 10;
numbers[2] = 15;
numbers[3] = 20;
numbers[4] = 25;

numbers数组将会如图2-29所示。

numbers[0]

numbers[1]

numbers[2]

numbers[3]

numbers[4]

5

10

15

20

25

图2-29 一个整数数组(赋值之后)

我们可以使用完全相同的语法来获取这些值。举例如下。

int sum = numbers[0] + numbers[1] + numbers[2] + numbers[3] + numbers[4];
System.out.println(sum)     // will print 75

数组也有缺点。一旦创建了数组,就不能改变其大小。为什么会有这个问题呢?想象一下,你在开发一个射击游戏,其中每次玩家点击鼠标左键,就会有一个Bullet对象添加到一个数组中(表示所有已经发射的子弹)。我们事先不知道需要多少个Bullet。某些玩家可能使用42颗子弹。其他的玩家可能使用刀和手榴弹,甚至不开一枪就完成了关卡。在这种情况下,使用ArrayList通常会更好,它允许我们动态地调整大小以放入更多的对象。

ArrayLists比数组更加常用,并且你应该知道如何使用它们(以及如何用好它们)。要使用ArrayList,必须首先导入它。

import java.util.ArrayList

创建ArrayLists,就像创建任何其他的对象一样。

ArrayList playerNames = new ArrayList();

我们使用add()方法向一个ArrayList对象中插入对象。

playerNames.add(“Mario”); 
playerNames.add(“Luigi”); 
... 
playerNames.add(“Yoshi”);

你可以看到,这是String对象的一个ArrayList。你可以调用get()方法,使用基于0的索引(注意,用于数组的[]对于ArrayLists无效)从一个ArrayList获取一个对象(在这个例子中,是一个String对象)。

playerNames.get(2);     // will retrieve “Luigi” (kind-of)

理论上讲,我们可以在一个单个的ArrayList中,放置所有的各种类型的对象,而不管其类型是什么;然而,这不是很有用,因为一旦你这么做了,可能不知道某个位置(如索引152)具体是什么类型的对象。如果不知道它是什么类型的对象,你就不知道它有什么方法。看如下所示的例子。

someArrayList.get(152);     // What kind of object is this?

我们从someArrayList提取出第153个对象(记住,索引是基于0的)。问题在于,我们对这个对象一无所知。它可能是一个可口的Sushi对象,又或者甚至是一个危险的Bomb。如果我们这样编写代码,想象一下后果。

Monster hungryOne = new Monster();
Object unknown = someArrayList.get(152);   // The Object is actually a Bomb
hungryOne.eat(unknown);           // hungryOne thinks it’s Sushi
// Boom!

实际上,Java允许我们通过添加<Type>标志,来限制ArrayLists只保存某一种类型的对象。

ArrayList<String> playerNames = new ArrayList<String>();
playerNames.add(“Mario”);     // Works! 
Bomb b = new Bomb();
playerNames.add(b);       // Gives type-mismatch error

现在,我们知道从playerNames获取的任何对象都是一个String,并且我们可以在其上调用String方法。

// Any object from playerNames will always be a String
String nameZero = playerNames.get(0); 
System.out.println(nameZero.length());

不能直接将基本数据类型插入到一个ArrayList中。实际上,如下所示的代码是不允许的。

ArrayList<int> numbers = new ArrayList<int>(); // not allowed

要绕开这个限制,可以直接使用一个内建的包装类,即每种基本数据类型的对象版本。这包括int所对应的Integer,char所对应的Character,等等。要做到这点,直接创建该ArrayList并声明包装类作为其类型。

ArrayList<Integer> numbers = new ArrayList<Integer>();

这个ArrayList最初的大小为0。

System.out.println(numbers.size());   // Prints zero

接下来,直接调用add()方法,并且传入想要放到ArrayList中的int值。这些值将会自动地包装到一个Integer对象中。

numbers.add(2); 
numbers.add(3); 
numbers.add(1);

此时,ArrayList看上去如图2-30所示(注意,其长度动态地增长了)。

index 0

index 1

index 2

2

3

1

图2-30 numbers: 整数的一个ArrayList

你可以调用get()方法,传入想要的值的索引,从而获取基本类型值。例如,要取回数字3,让ArrayList给出位于索引1的值。这个值会自动转换为一个int(从包装的Integer对象),因此,你可以将它存储到一个int变量中。

int myNum = numbers.get(1); 
System.out.println(myNum);   // Prints 3
My name is Marty
I am 40 years old
My name is Person #0
I am 29 years old
My name is Person #1
I am 1 years old
My name is Person #2
I am 4 years old
My name is Person #3
I am 21 years old
My name is Person #4
I am 47 years old
My name is Person #0
I am 27 years old
My name is Person #1
I am 27 years old
My name is Person #2
I am 20 years old
My name is Person #3
I am 28 years old
My name is Person #4
I am 5 years old
My name is Person #5
I am 49 years old
My name is Person #6
I am 2 years old
My name is Person #7
I am 26 years old

在亲眼见到ArrayList的应用之前,你很难认识到它有多么强大,因此,让我们来尝试一个例子。

我们将编写包含了2个类的一个简单的程序。第一个类是我们的进入点,其中,我们存储了main方法并且创建了ArrayList。第二个类将是表示人的一个定制类。

首先,创建一个名为Groups的、新的Java项目。其中,创建一个名为ListTester的新的类,并且给其一个main方法,如程序清单2.25所示。

程序清单2.25 ListTester.java

01 public class ListTester {
02
03    public static void main(String[] args) {
04    
05    }
06
07 }

现在,在同一项目中创建第二个类并将其命名为Person。添加如下所示的变量和方法(参见程序清单2.26)。

程序清单2.26 Person.java

01 public class Person {
02    
03    private String name;
04    private int age;
05
06    public void initialize(String name, int age) {
07        this.name = name;
08        this.age = age;
09    }
10
11    public void describe() {
12        System.out.println("My name is " + name);
13        System.out.println("I am " + age + " years old"); 
14    } 
15
16 }

Person类描述了一个新的Person对象的蓝图。特别是,它表明了一个Person对象的状态将由两个实例变量来描述,即name和age。我们没有给name和age默认值,并且,必须调用initialize()方法来提供这些值。一旦Person对象有了一个name和age,我们就可以调用describe()方法,以易于理解、可读的形式打印出这些信息。让我们回到ListTester并且确保可以做到这一点。

程序清单2.27 ListTester.java(更新版本)

1 public class ListTester {
2
3    public static void main(String[] args) {
4        Person p = new Person();
5        p.initialize("Marty", 40);  
6        p.describe();
7    }
8
9 }

我们来一行一行地看一看程序清单2.27:首先创建了Person类的一个名为p的新实例。此时,p有两个实例变量:name和age。这些变量还没有初始化。

接下来,我们调用了initialize()方法,它接受两个值:一个String和一个整数。initialize()方法将会接受这两个值,并且将其赋值给实例变量。

现在,两个实例变量已经初始化了,我们可以通过调用describe()来要求Person对象描述自己。结果如下所示。

现在,我们创建多个Person对象并且将它们组织到一个ArrayList中。修改ListTester类,使其如程序清单2.28所示。

程序清单2.28 创建ArrayList并添加第一个循环

01 import java.util.ArrayList;
02 import java.util.Random;
03
04 public class ListTester {
05
06    public static void main(String[] args) {
07      
08        ArrayList<Person> people = new ArrayList<Person>();
09        Random r = new Random();
10      
11        for (int i = 0; i < 5; i++) {
12            Person p = new Person();
13            p.initialize("Person #" + i, r.nextInt(50));
14            people.add(p);
15        }
16    }
17
18 }

在程序清单2.28中,我们创建了一个新的、名为people的ArrayList,以及一个新的名为r的Random对象。然后,开始了一个for循环,它将运行5次。循环每迭代(重复)一次,我们就创建一个名为p的新的Person对象。用相应的名称(Person #i,其中i从0到4)和一个随机生成的年龄值,来为该Person初始化实例变量。最后,我们把新创建的Person对象添加到ArrayList中(第14行)。循环重复,创建了一个全新的Person,初始化它并且再次添加它。

注意如下所示的代码行。

Person p = new Person();

在循环中创建的任何变量,都只在其相同的迭代中有效,这意味着,该变量仅限于在循环的当前迭代中存在。因此,我们可以在循环的每一次重复中复用变量名p。

每次调用上面的代码,我们都用变量名p创建了一个新的Person。然后,将临时变量p中保存的值,存储到较为持久的、名为people的ArrayList中,以便随后在代码中可以引用每一个新创建的Person对象,而不需要为它们中的每一个分配一个唯一的变量名。

为了看到这是如何工作的,我们可以尝试再次迭代循环,并且调用describe()方法,如程序清单2.29所示(第17行到第20行)。

程序清单2.29 添加第2个循环

01 import java.util.ArrayList;
02 import java.util.Random;
03
04 public class ListTester {
05
06    public static void main(String[] args) {
07      
08        ArrayList<Person> people = new ArrayList<Person>();
09        Random r = new Random();
10      
11        for (int i = 0; i < 5; i++) {
12            Person p = new Person();
13            p.initialize("Person #" + i, r.nextInt(50));
14            people.add(p);
15        }
16      
17        for (int i = 0; i < people.size(); i++) {
18            Person p = people.get(i);
19            p.describe();
20        }
21
22    }
23
24 }

最终的输出如下所示(年龄可能不同,因为是随机生成的)。

你可能会问,为什么要将第17行到第20行的循环运行people.size()次而不是5次?两个值是相同的,并且任何一个解决方案都会产生相同的输出;然而,上面的例子是一个更加灵活的循环,因为它并不需要把循环运行的次数直接编码。根据ArrayList people的大小,第二个for循环将运行相应的次数。这意味着,我们可能需要将上一个循环(即向ArrayList添加对象的那个循环)运行的次数从5修改为8,而下面的循环则不需要修改,因为people.size()也会增加为8。

程序清单2.30 迭代8次

01    import java.util.ArrayList;
02    import java.util.Random;
03
04    public class ListTester {
05
06        public static void main(String[] args) {
07      
08            ArrayList<Person> people = new ArrayList<Person>();
09            Random r = new Random();
10      
11            //for (int i = 0; i < 5; i++) {
12            for (int i = 0; i < 8; i++) {  
13                Person p = new Person();
14                p.initialize("Person #" + i, r.nextInt(50));
15                people.add(p);
16            }
17            // people.size() is now 8!
18            for (int i = 0; i < people.size(); i++) {
19                Person p = people.get(i);
20                p.describe();
21            }
22
23        }
24
25    }

最终输出如下所示(年龄可能不同,因为是随机生成的)。

上面的示例展示了如何使用循环快速地创建多个对象,并将它们组织到一个ArrayList中。我们还学习到,可以通过一个for循环快速遍历一个ArrayList的所有成员并调用其方法。

在前面的示例中,我们的程序包含1个或2个较小的类。随着学习本书,我们将要编写拥有更多类的程序。实际上,有些游戏很容易拥有10个以上的类,而且每个类都满足游戏架构中的某些角色。仔细研究前面的例子,如果有任何的不解或问题,请访问本书的配套网站jamescho7.com。在那里贴出你关于本书的问题,我将尽力解答它们。

我们已经在本章中介绍了很多的内容,并且所有这些概念都会在本书中再次出现。要记住Java这门新的语言的语法很难,但是关键在于练习。现在,花一点时间研究本章中示例的源代码(可通过jamescho7.com获取),运行该程序,进行创新试验;并且最重要的是,努力理解我们所讨论过的话题。如果你通过这种方法理解了核心概念,你将从本章获益匪浅。如果你遇到困难,请到我们的论坛上发帖。我们将主动监控帖子,并且解答你可能遇到的任何问题。

如果你准备好了,请继续阅读第3章,我们将在其中介绍一些更高级的Java话题,包括构造方法、继承、接口、图形和线程,要开始编写Java游戏,你需要了解所有这些内容。


我们已经学习了面向对象编程的基础知识,并且学习了Java创建和使用对象的基本语法。在本章中,我们将介绍一些重要的对象设计概念,以便能够创建有意义的类并且以直观的方式来组织它们。

本章的内容会很密集,用较少的篇幅介绍了很多比较难的内容。实际上,你会发现在读完一次之后,自己没有记住各种概念背后的语法细节,当然这也完全没有问题。重要的是,你读懂了说明,并且理解了相应的程序清单。稍后,在需要参考和回顾的时候,我们还会回到这些页面。

通过回顾第2章中的重要概念并且做一些小的修改,我们可以更容易地进入较为复杂的主题。首先创建一个名为Constructors的项目,并且创建一个World类,如程序清单3.1所示。

程序清单3.1 World.java

1 public class World {
2
3    public static void main(String[] args) {
4    
5    
6    }
7
8 }

我们还将创建一个名为Coder的类,如程序清单3.2所示。

程序清单3.2 Coder.java

1 public class Coder {
2    private String name;
3    private int age;
4 
5     public void initialize(String name, int age) {
6         this.name = name;
7       this.age = age;
8     }
9 
10    public void writeCode() {
11        System.out.println(name + " is coding!");
12    }
13
14    public void describe() {
15        System.out.println("I am a coder");
16        System.out.println("My name is " + name);
17        System.out.println("I am " + age + " years old");
18    }
19
20 }

现在,你的项目应该如图3-1所示。

图3-1 Constructor项目

在继续学习之前,要确保理解Coder类。Coder.java是创建Coder对象的蓝图。在这个蓝图中,我们已经声明了一个Coder对象,它应该有2个变量描述其状态:表示name的一个String,以及表示age的一个整数。

和其他对象一样,我们的Coder对象也有行为。initialize()方法允许我们使用所提供的值来初始化Coder对象的实例变量。writeCode()方法将打印出文本,表明Coder对象正在编码。describe()方法将直接以容易理解的形式列出所有实例变量的值。

I am a coder
My name is null
I am 0 years old

回到World类,创建Coder对象的一个实例,并且让它描述自己。代码应该如程序清单3.3所示。

程序清单3.3 World.java (更新版)

1 public class World {
2
3    public static void main(String[] args) {
4        Coder c = new Coder();
5        c.describe();
6    }
7
8 }

当我们初次声明新的Coder对象的时候,其实例变量还没有初始化(意味着,它们为每个变量类型都保留了默认值)。运行该World类,将会得到如下所示的输出。

正如上面的结果所示,int的默认值是0。一个空的对象引用变量(指向一个对象的一个变量)的默认值为null,这意味着“没有内容”。这直接意味着对象引用变量没有包含任何值。如图3-2所示。

图3-2 空的对象引用变量

在继续学习之前,我们想要指出导致很多Java程序意外终止的一个常见错误,即NullPointerException。当你试图调用属于一个null对象变量的一个方法的时候,就会发生这种运行时错误(在程序执行的时候发生的错误)。看看下面的例子。

String a;   // Equivalent to String a = null;
a.length();

如果你要在main方法中运行这段代码,将会得到如下所示的错误(带有出错的地方的行号)。

Exception in thread "main" java.lang.NullPointerException

无论何时,当你遇到这条错误消息,解决方法是找到所有的对象变量,并且使用null值来初始化它们。

I am a coder
My name is null
I am 0 years old
I am a coder
My name is Bill
I am 59 years old

要避免任何潜在的NullPointerExceptions,我们现在使用initialize()方法来初始化新的Coder类的实例变量(如程序清单3.4的第7行所示)。

程序清单3.4 初始化Coder及其实例变量

1 public class World {
2 
3     public static void main(String[] args) {
4         Coder c = new Coder(); // Initializes the variable c
5         c.describe();
6         System.out.println("");   // insert empty line for readability
7         c.initialize("Bill", 59); // Initializes c’s instance variables
8         c.describe();
9     }
10
11 }

当我们运行程序清单3.4的时候,将会得到如下所示的输出。

在前面的小节中,我们已经学习了使用如下所示的语法来创建对象。

Coder c = new Coder();

上面代码的new Coder()部分,展示了如何调用所谓的默认构造(default constructor)方法,该方法直接创建了Coder对象的一个实例,以供我们在变量c中使用它。

Java还允许使用定制的构造方法,它就像普通方法一样,可以接受供该对象使用的值。为了看看这是如何起作用的,我们先关注如下两行代码。

Coder c = new Coder(); // Uses the default constructor  
...
c.initialize(“Bill”, 59);

定制的构造方法允许我们将代码简化成如下所示的形式。

Coder c = new Coder(“Bill”, 59);

为了做到这一点,我们必须先在Coder类中声明想要的定制构造方法,如下所示。

public Coder(String name, int age) {
     this.name = name; 
     this.age = age;
}

构造方法看上去和方法类似,但是实际上有很大的不同。首先,构造方法没有返回类型(甚至不是void)。其次,构造方法的名称必须和包含它的类相同。

尽管有这些不同,注意我们的构造方法接受了参数,并且像initialize()方法那样将它们分配给Coder对象的实例变量。

现在,我们可以将这个构造方法添加到Coder类中,并且删除initialize()方法,如下所示。

public class Coder {

     private String name; 
     private int age; 

     public Coder(String name, int age) {
         this.name = name; 
         this.age = age; 
     }




publicvoid initialize(String name, int age) {
this.name = name;
this.age = age;
}


     public void writeCode() {
         System.out.println(name + " is coding!");
     }



     public void describe() {
         System.out.println("I am a coder");
         System.out.println("My name is " + name);
         System.out.println("I am " + age + " years old");
     }




}

可以认为构造方法是必需的,这是创建对象的一条规则。这好像是在说:“如果想要创建我的对象,你必须传递我所要求的输入。”

在创建自己的构造方法的时候,你只是明确了当没有提供Coder对象的name和age的时候,是不能够创建它的。因此,不能使用下面的语法创建一个Coder对象。

Coder c = new Coder(); // no longer works!

让我们对World类做一些修改,以反映这些变化,如程序清单3.5所示。

程序清单3.5 调用定制的构造方法

public class World {
    public static void main(String[] args) {
Coder c = new Coder();
c.describe();
System.out.println(""); // insert empty line for readability
c.initialize("Bill", 59);
        Coder c = new Coder("Bill", 59);
        c.describe();
    }
}

运行这段代码,将会得到如下所示的输出。

I am a coder
My name is Bill
I am 59 years old

关键知识点

对象构造方法

  • 构造方法提供了一种方法,在创建对象的过程中初始化对象中的实例变量。
  • 构造方法和关键字new一起使用。
  • 如果你选择不创建构造方法的话,Java会提供一个默认的构造方法。
  • 所有的构造方法必须以类的名称来命名。
  • 可以有任意多个构造方法,但是,每个构造方法必须有不同的一组参数。
I am a coder
My name is Bill
I am 59 years old
Bill, 59
I am a coder
My name is Steve
Invalid age provided

构造方法允许你在创建对象的时候初始化对象的实例变量,但是,它对于随后访问或修改这些值就帮不上什么忙了。此外,由于使用了private修饰符来隐藏变量,我们没有办法来直接完成这两项任务。实际上,如下所示的代码将会导致错误。

... 
// somewhere inside the World class...
Coder c3 = new Coder(“Mark”, 30);
String c3Name = c3.name; // cannot reference private variable from another class
c3.age = 25; // cannot modify private variable from another class
...

怎样才能绕开这些限制呢?我们可以将Coder类的实例变量标记为public的,但是,由于第2章所介绍的原因,我们不想这么做。相反,可以在Coder类中创建访问器(accessor)方法。我们将讨论两种类型的访问器方法。

1.  getter方法返回了所请求的隐藏变量的值的一个副本(但是,保留该隐藏变量不动)。通过这么做,我们可以使得隐藏变量避免未经授权的修改,同时还允许访问该变量的值。

2.  setter方法允许其他的类修改一个隐藏变量的值,只要这些类遵守我们在该setter方法中描述的规则。

我们来看看这些访问器方法的应用。向Coder类添加如下所示的getter和setter方法:getAge()、 setAge()、getName()和setName()(参见程序清单3.6的第26行到第28行)。

程序清单3.6 向Coder.java添加getter和setter方法

01 public class Coder {
02 
03     private String name;
04     private int age;
05 
06     public Coder(String name, int age) {
07         this.name = name;
08         this.age = age;
09     }
10
11    public void writeCode() {
12        System.out.println(name + " is coding!");
13    }
14
15    public void describe() {
16        System.out.println("I am a coder");
17        System.out.println("My name is " + name);
18        System.out.println("I am " + age + " years old");
19    }
20
21    public String getName() {
22        return name;
23    }
24    
25    public int getAge() {
26        return age;
27    }
28    
29    public void setName(String newName) {
30        if (newName != null) {
31            name = newName;
32        } else {
33            System.out.println("Invalid name provided!");
34        }
35    }
36    
37    public void setAge(int newAge) {
38        if (newAge > 0) {
39            age = newAge;
40        } else {
41            System.out.println("Invalid age provided");
42        }
43    }
44 }

我们的两个getter方法返回了该方法的调用者的name和age变量。这意味着,能够访问(或引用)Coder对象的任何类,都可以调用其getter方法,并且看到Coder的实例变量的值。这里,值是关键字。我们并没有允许访问实例变量最初的版本,而是允许访问存储在其中的值。

两个setter方法允许其他的类修改Coder对象的实例变量,但是,我们可以提供一组规则,以确保这些实例变量不会被非法或无效地修改。在程序清单3.6中,我们的setters拒绝了非正值的age值和null的name值。

让我们在World类中调用getters和setters以测试它们,如程序清单3.7的第8行和第9行所示。

程序清单3.7 在World.java中调用getters和setters

01 public class World {
02    public static void main(String[] args) {
03      
04        Coder c = new Coder("Bill", 59);
05        c.describe();
06        System.out.println(""); // empty line for readability
07  
08        String cName = c.getName();
09        int cAge = c.getAge();
10
11        System.out.println(cName + ", " + cAge);
12        System.out.println(""); // empty line for readability
13        c.setName("Steve");
14        c.setAge(-5); // This will be rejected by our setter method
15
16        c.describe(); 
17    }
18
19 }

输出如下所示的结果。

在前面的例子中,我们能够创建一种方法,来保持Coder对象的实例变量私有,同时允许外界通过公有的访问器方法,来获取(get)和修改(set)这些隐藏的变量。这允许我们保持安全地获取和使用私有变量,同时允许我们访问和修改需要的值。注意,我们的setter方法可以拒绝不合法的参数,因此,我们能够防止World类将Coder对象的年龄修改为−5。

接下来,我们介绍一种方法,使用所谓的接口(interface),将对象分组为不同的类别。接口是一个抽象(abstract)的类别,它描述了属于该类别的对象的基本组成部分。为了更好地理解这一点,我们来学习一个实例。

接口和类相似,但是,它有一些显著的区别。如下所示是一个Human接口的样子。

程序清单3.8 Human接口

public interface Human {
    public void eat();

    public void walk();

    public void urinate(int duration); 
}

正如程序清单3.8所示,接口包含了各种抽象(abstract)的方法,它们没有方法体。这些没有方法体的抽象方法告诉我们,一个Human的对象分类必须能够做什么,但它们没有指定必须要如何实现这些操作。

为了说明接口实际上是什么,让我们先不要看代码。在你的脑海里,想象一下作为一个人类意味着什么(你不必变得太富有哲理)。接下来,我们看看如下的列表,告诉我是否每个人都满足你对于人类的看法:你的邻居、你最好的朋友和你自己。

对于所有这些人,你可能会回答是的。这是因为,当我们让你想一下对人类的看法的时候,你不会认为这是某一个个别的人。相反,你会形成某种规则,即一个人如何与他的世界交互,并且使用这种思路来判定不同人的人类特性。

接口大体上也是如此。程序清单3.8中的Human接口,不是用来创建单个的Human对象的。相反,它定义了一个交互的模式,阐述了一个Human对象在你的程序中应该具有什么样的行为。它提供了一组基本的要求,如果要创建Human类型的更多的具体版本(如King类)的话,必须要满足这些需求,如程序清单3.9所示。

程序清单3.9 King类

public class King implements Human {
 public void eat() {
    System.out.println("The King eats."); 
 }
 public void walk() {
    System.out.println("The King walks."); 
 }
 public void urinate(int duration) {
    System.out.println("The King urinates for " + duration + " minutes."); 
 }

 public void rule() {
     System.out.println("The King reigns."); 
 }


}

研究一下King类和Human接口之间的关系,你会注意到一些事情。首先,King类声明了它实现了Human接口,作为程序员,我们就是这样指定想要让King类属于Human这个分类的。其次,King类声明了程序清单3.8中给出的Human接口中的所有3个方法,并且这个类为这些前面的抽象方法中的每一个都提供了一个具体的方法体。第三,King类有一个额外的名为rule()的方法,这将其与泛型的Human区分开来。

接口是一系列的协议。如果选取了一个对象来实现一个接口,该对象同意实现接口中的每一个抽象方法。这意味着什么呢?这意味着,一个King对象,不管他想要保持多么神秘,都必须实现所有的Human接口的抽象方法,包括urinate()方法,因为国王毕竟也是人。如果不满足这一需求,愤怒的JVM将会向他显示红色的错误消息。

你可能会问,为什么我们必须创建一个接口和一个类,来定义一个单个的King类呢?你可能会告诉自己,现在Human接口还真的做不了太多事情,你说的绝对没错。

使用接口允许我们创建一类对象,但是,在学习多态之前,我们很难意识到这对程序来说意味着什么。

来看一下如下所示的方法。

public void feed(Human h) {
      System.out.println("Feeding Human!"); 
      h.eat();
}

该方法可以接受一个单个的Human类型的参数。实际上,它可以接受实现了Human接口的一个类的任何对象实例。这很有用,因为在单个的程序中,我们可能创建多个类,例如,Villain、Professor和SushiChef,而它们都扩展了Human接口。

这意味着如下所示的示例都能够工作。

// Elsewhere in same program
King kong = new King();
Villain baddie = new Villain();
Professor x = new Professor();
SushiChef chef = new SushiChef();

// Any Human can be fed:
feed(kong); // A King is Human
feed(baddie); // A Villain is Human
feed(x); // A Professor is Human
feed(chef); // A SushiChef is Human

这只是关于多态能够做什么的一个小例子,它是一种有趣的方式,描述了与多种类型的对象交互的一个通用方法。在后面的各章中,我们将以一个更加实用的方式介绍接口和多态。

在设计对象的分类的时候,你可能会发现另一种叫作继承(inheritance)的模式,它给了我们更多的控制权。继承描述了这样一种现象,一个类继承了另一个类中的变量和方法。在这种情况下,继承者称为子类(subclass,或孩子类),而祖先称作超类(superclass,或者父类)。

使用继承比使用接口的优点在于,可以具备复用代码的能力。还记得吧,实现了一个接口的每一个类,都必须针对接口中声明的每一个抽象方法提供一个完整的实现。使用前面小节的例子,这意味着,King、Villain、Professor和SushiChef,都必须拥有它们自己的eat()、walk()和urinate()方法。在这种情况下,继承很强大,因为它允许相似的类共享方法和变量。我们将使用一个假想的角色扮演游戏的例子来说明这一点。

在创建一款角色扮演游戏的时候,你可能有一个名为Hero的类来表示玩家角色,如程序清单3.10所示。

程序清单3.10 Hero类

01 public class Hero {
02    protected int health = 10; // We will discuss ‘protected’ later in this section
03    protected int power = 5; 
04    protected int armor = 3; 
05
06    public void drinkPotion(Potion p) {
07     health += p.volume(); // Equivalent to health = health + p.volume();
08    }
09    
10    public void takeDamage(int damage) {
11    int realDamage = damage - armor; 
12    if (realDamage > 0) {
13      health -= realDamage; // Equivalent to health = health – realDamage.
14    }
15    }
16    
17    // ... more methods
18
19 }

在创建了Hero之后,你随后决定要让自己的RPG和竞争者有所区分,那就实现一个独特的类系统,其中玩家能够在此前没有见过的Warrior、Mage和Rogue类之间做出选择。

接下来,和任何值得尊敬的面向对象程序员会做的一样,你为每一种角色类型创建了一个单独的Java类,因为Warrior、Mage和Rogue中的每一个都应该具有无法想象的强大而独特的能力。你还决定,既然所有的角色类都是泛型的Hero类的第一个和最重要的扩展,它们每一个都应该拥有程序清单3.10中的Hero类的所有变量和方法。这就是继承的用武之地。

来看一下程序清单3.11到程序清单3.13。

程序清单3.11 Warrior类

public class Warrior extends Hero {

    //    ... other variables and methods
    public void shieldBash() {
        ...
    }
}

程序清单3.12 Mage类

public class Mage extends Hero {

    //  ... other variables and methods
    public void useMagic() {
      ...
    }
}

程序清单3.13 Rogue类

public class Rogue extends Hero {

    //    ... other variables and methods
    public void pickPocket() {
        ...
    }
}

注意,我们使用关键字extends表示继承。这是合适的,因为所有这3个类都是超类Hero的扩展。在继承中,每个子类都针对超类中的所有非私有的变量和方法,接受它们自己的版本(程序清单3.10中的protected变量,类似于private变量,因为外部类是无法访问它们的;然而,和private变量不同,在继承中,子类是可以访问它们的)。

在应用多态的时候,继承的好处最明显,多态允许我们在如下所示的一个方法中使用Hero的任何子类。

// Will attack any Hero regardless of Class
public void attackHero(Hero h, int monsterDamage) {
      h.takeDamage(monsterDamage); 
}

基于文本的程序很容易构建,但是基于文本的游戏已经过时了。在本节中,我们将介绍如何使用Java类库中的类(尤其是javax.swing包中的类),来创建一个图形用户界面(Graphical User Interface ,GUI)。你会发现,尽管添加一个简单的用户界面很直接,但GUI是一个很大的主题。我将只是提供一个快速的介绍,完全只是创建一个窗口和显示一个基于Java的游戏所需要的基础知识。如果你想要学习Swing的更多知识,并且要创建专业的应用程序,请访问如下所示的教程:[http://docs.oracle.com/javase/tutorial/ uiswing/TOC.html](http://docs.oracle.com/javase/tutorial/ uiswing/TOC.html)。

当在Java中开发一款图形化应用程序的时候,我们首先要创建一个叫作JFrame对象(从javax.swing.JFrame导入)的窗口。这个窗口中是一个内容面板(想象一下窗口面板),我们可以向其中添加各种UI元素,例如,按钮、滚动条和文本区域。

内容面板的默认布局叫作BorderLayout。它允许我们将UI元素放置到5个区域中的一个,如图3-3所示。

图3-3 Windows 8上的一个JFrame及其内容面板

图3-3所示的这5个区域中的每一个,都只能容纳一个UI元素,这意味着,BorderLayout只支持5个元素;然而,这对我们来说不是问题,因为我们只需要一个名为JPanel的元素。

JPanel对象是一个简单的、空的容器,我们可以将其添加到一个BorderLayout的某个区域中。我们可以在单个的JPanel对象上绘制想要让玩家看到的任何内容,就像是在画布上绘图一样。例如,考虑一下图3-4所示的屏幕截图。这个屏幕截图取自TUMBL游戏的一个正在开发的版本,这款游戏是我所开发的第一款游戏。你所看到的一切,从玩家的分数、暂停按钮到角色以及加血,都绘制到一个单个的JPanel上。

图3-4 刚刚开始构建的TUMBL的屏幕截图

当我们在计算机上处理图形的时候,使用一个基于像素的xy坐标系统。此外,我们还把左上角的像素当作原点(0, 0)。这意味着,如果屏幕上的分辨率是1 920像素×1 080像素的话,右下角的像素的坐标是(1 919, 1 079)。

现在,我们已经讨论了构建图形化应用程序所需了解的一切内容。开始动手吧。

创建一个名为FirstGraphics的Java项目,并且创建一个名为FirstFrame的类,它带有一个完整的main方法。然后,通过给main方法添加如下所示的代码行(确保导入了javax.swing.JFrame),我们创建了一个JFrame对象。

JFrame frame = new JFrame("My First Window");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(480, 270);
frame.setVisible(true);

此时,你的FirstFrame类应该如程序清单3.14所示。

程序清单3.14 FirstFrame类

01 import javax.swing.JFrame; 
02
03 public class FirstFrame { 
04    
05    public static void main(String[] args) {
06        JFrame frame = new JFrame("My First Window");
07        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
08        frame.setSize(480, 270); 
09        frame.setVisible(true); 
10    }
11
12 }

运行这个FirstFrame类,应该会看到图3-5所示的结果。

图3-5 我的第一个窗口

注意,所出现的窗口带有一个“My First Window”标题。显然,这个内容面板(图3-5中的灰色区域)目前是空的。

在前面小节的非图形化的示例中,只要JVM执行了程序的最后一行代码,程序就结束了。但图形化应用程序并不是这样的。JFrame甚至会在最后一行代码之后持续,就像这个窗口所展示的那样。通过点击退出按钮来结束该程序。

让我们确保理解在定义JFrame的4行代码(程序清单3.14的第6行到第9行)中发生了什么。在第6行,我们使用定制的构造方法,创建了一个名为frame的新的JFrame对象。这允许我们为自己的窗口设置标题。

接下来,在第7行,我们指定了当窗口关闭的时候应该发生什么情况。当用户关闭窗口的时候,我们想要让整个程序结束,因此,我们从JFrame类向setDefaultCloseOperation()方法传入了一个名为EXIT_ON_CLOSE的公有的int(还记得吧,点运算符用来访问另一个类中的公有方法和变量)。

第8行直接告诉窗口调整其大小,以便成为480像素宽、270像素高。一旦完成了这一步,在第9行调用setVisible()方法就使得该帧出现在屏幕上。

现在,我们有了一个JFrame,是时候添加其内容面板了。要做到这一点,我们将创建一个名为MyPanel的新类。该类将会是使用继承创建的JPanel的一个定制版本,因此,我们必须扩展JPanel,先导入java.swing.JPanel。

将程序清单3.15所示的代码复制到你的MyPanel类中。一旦运行了该程序,我们将讨论它。别忘了如第1行、第2行和第4行所示,添加适当的导入。

程序清单3.15 MyPanel类

01 import java.awt.Color;
02 import java.awt.Graphics;
03
04 import javax.swing.JPanel;
05
06 public class MyPanel extends JPanel {
07
08    @Override
09    public void paintComponent(Graphics g){
10        g.setColor(Color.BLUE);
11        g.fillRect(0, 0, 100, 100);
12        
13        g.setColor(Color.GREEN);
14        g.drawRect(50, 50, 100, 100);
15      
16        g.setColor(Color.RED);
17        g.drawString("Hello, World of GUI", 200, 200);
18      
19        g.setColor(Color.BLACK);
20        g.fillOval(250, 40, 100, 30);
21    }
22  
23 }

现在必须回头看看FirstFrame类,构造MyPanel的一个实例,并且将其添加到内容面板的一个区域中。通过在main方法的底部添加如下所示的代码行,来做到这一点。

MyPanel panel = new MyPanel(); // Creates new MyPanel object.
frame.add(BorderLayout.CENTER, panel); // Adds panel to CENTER region.

更新后的FirstFrame类应该如程序清单3.16所示(注意第1行的import语句)。

程序清单3.16 更新后的FirstFrame类

01 import java.awt.BorderLayout;
02
03 import javax.swing.JFrame;
04
05 public class FirstFrame {
06    
07    public static void main(String[] args) {
08        JFrame frame = new JFrame("My First Window");
09        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
10        frame.setSize(480, 270);
11        frame.setVisible(true);
12      
13        MyPanel panel = new MyPanel();  // Creates new MyPanel Object
14        frame.add(BorderLayout.CENTER, panel);  // adds panel to CENTER region
15    }
16
17 }

运行FirstFrame,将会看到图3-6所示的界面。

图3-6 更新后的FirstFrame类的输出

在讨论发生了什么之前,我们需要先解释一些术语。还记得吧,我们可以在JFrame的内容面板区域添加各种图形化的元素。这些图形化的元素叫作微件(widget),它们属于一类叫作JComponent的泛型对象。这意味着,JPanel以及在基于Swing的图形化应用程序中使用的其他的图形化元素,都是一种类型的部件(component)。

我现在要解释当运行程序的时候发生了什么,首先来说明MyPanel类。回头去看看程序清单3.15,你还会记得MyPanel扩展了JPanel(程序清单3.15的第6行)。这就是说,MyPanel是JPanel的一个子类;换句话说,它是泛型的JPanel类的一个更加具体的版本。也就是说,MyPanel继承了属于JPanel的所有共有的方法(由此,MyPanel通过继承成为JPanel的一个特定类型)。

有一个继承的方法名为paintComponent()。它是描述部件应该如何渲染的一个方法。我们想要控制这个方法,以便可以告诉程序一个MyPanel对象应该如何显示。为了做到这点,我们在自己的MyPanel类中声明了paintComponent()方法,并且添加了一个@Override修饰符(在第8行),通过这种方法,让编译器知道我们要使用自己的方法来替代已有的paintComponent()方法。

在这个方法paintComponent()内部,我们使用所提供的Graphic对象g,调用了8个方法(程序清单3.15中的第10行到第20行)。

Graphics对象每次可以绘制一项内容,并且它像笔刷一样工作。首先使用setColor()方法选择一种颜色,并且告诉Graphics对象,使用几个绘制和填充方法之一来绘制什么。

setColor()方法接受一个Color对象,我们可以通过Color类来获取该对象(这个类保存了很多Color对象作为公有变量,我们可以使用点运算符来引用它)。注意,对于每种颜色有两个变量,一个全部都是大写,一个全部都是小写。它们总是为你返回相同的 颜色。

作为一般性的规则,以单词draw开头的方法只是绘制想要的形状的框架。另一方面,以单词fill开头的方法,将会绘制整个形状。例如,g.drawRect(50, 50, 100, 100)将会绘制一个方形的边框,其左上角位于(50, 50),并且其边长为100个像素。

请通过如下的链接,访问Graphics类的Method Summary,以了解在paintComponent()方法中调用的方法的更多的信息,以及参数的具体含义:[http://docs.oracle.com/javase/7/ docs/api/java/ awt/ Graphics.html](http://docs.oracle.com/javase/7/ docs/api/java/ awt/ Graphics.html)。

现在,我们来解释在MyPanel中发生了什么,让我们先来讨论在程序清单3.16中添加到FirstFrame类中的两行代码。

MyPanel panel = new MyPanel(); // Creates new MyPanel object.
frame.add(BorderLayout.CENTER, panel); // Adds panel to CENTER region.

第一行代码使用熟悉的语法创建了一个新的MyPanel对象。然后,第二行将其添加到图3-3所示的中心区域(Center)。注意,空的区域并不占用空间。

一旦MyPanel对象添加到了JFrame,其paintComponent方法就会自动调用。这意味着,我们不必明确地要求panel来绘制自己。这就是为什么我们能够看到图3-6所示的各种图形。

确保自己再次回顾代码,以理解如何能够得到甚至让毕加索感到骄傲的惊人的艺术品。

介绍完这个示例,我们也就结束了本书的第3章和第1部分。如果你一直在学习,那么应该已经了解了编程的基础知识,掌握了Java基础并且学习了高级的面向对象设计概念。Java游戏开发就在前面,它肯定是一个巨大的挑战甚至会带来更多的兴奋。

在我们继续学习之前,我想要提醒你,Java是一种庞大的编程语言。尽管我已经试图尽可能地向你介绍在开发Java和Android游戏的时候可能会遇到的所有概念,但我还是没办法公正地对待这门语言。如果你有兴趣学习Java的更多知识以及更详细地了解所有这些概念,应该单独找一本不错的图书来学习Java。我喜欢的是如Kathy Sierra和Bert Bates编写的《Head First Java》这样的图书。

其次,我想要提醒你,你不是一个人在战斗。如果你难以理解任何的概念,想要学习更多内容,或者只是想到处逛逛,你可以访问本书的配套站点jamescho7.com。我很高兴能够讨论你的任何问题或关注点。

现在,让我们深呼一口气。深入Java游戏开发世界的时刻到了。


相关图书

Python面向对象编程:构建游戏和GUI
Python面向对象编程:构建游戏和GUI
精通游戏测试(第3版)
精通游戏测试(第3版)
罗布乐思开发官方指南 从入门到实践
罗布乐思开发官方指南 从入门到实践
游戏引擎原理与实践 卷2 高级技术
游戏引擎原理与实践 卷2 高级技术
游戏数值设计
游戏数值设计
游戏引擎原理与实践 卷1 基础框架
游戏引擎原理与实践 卷1 基础框架

相关文章

相关课程