第2章 预备:学习Java 2 API
Java程序的结构
基本Java数据类型,包括String和数组
数值和条件运算符,以及它们的优先运算顺序
条件语句,包括控制语句和循环语句
使用throws语句和try-catch的异常处理
2.1 Game Over!程序
2.1.1 import语句
Java对象被组织到包中。为了方便组织,包中包括了相关的类。Java API包含75个包(总共大约2000个类,JDK1.4.0)
C++注解:Java中的import关键字与C++中的#include指示相似,仅有的不同是java中头文件和源文件之间没有差别。对类的声明和实现都包含在一个.java文件中。
2.1.2 给Java代码加注释
在工作中一定要写注释,但是也要避免过度的注释,过度的注释只能使代码混乱并在很大程度上干扰注意力。
注意C风格注释不要嵌套。第一个“*/”会使最近的“/*”失去作用。
2.1.3 Java类声明
2.1.4 Java方法声明
C++注解:Java中的main方法与控制台C或C++应用程序中的典型main函数相似。注意在Java中,它必须在一个类中定义,而在C和C++中必须全局定义。此外,Java中的main方法总是返回void,与C或C++的main函数不同,它们可以有多个返回类型。
2.1.5 Java中的代码块
2.1.6 Java程序组成部分的关键点回顾
? 使用import语句使编译器知道要用的库
? 注释能在很多方面帮助程序员,但是不正确使用它会弊大于利
? 用Java写的一切都被封装成类,即使程序的开始点即main方法也不例外
? 一个代码块是完成一个特定任务相关的语句。代码块保持程序的结构化并且容易读写
2.2 比特和字节:原始的Java类型
程序中应写些什么呢?答案是:数据,程序没有数据就不能生存,尤其是游戏。
Java是面向对象语言。但是,Java不是百分百的面向对象,因为Java支持8种原始类型。
2.2.1 基本的整数类型
通常而言,对于游戏开发,一般会使用int型表示整数数据,而忽略long,short和byte型。因为int型的取值范围大于40亿,通常使用它比较安全。
2.2.2 浮点类型
一个常量浮点值要能被真正看作float,它必须以f(或F)后缀结束;否则,会被当作double值。对于double值来说,d(或D)后缀是可选的。
至今,通常使用哪种类型来存储浮点数还没有定论。双精度在本质上给了我们相对于浮点数两倍的精度,但是,这是有代价的——通常是内存的消耗。Java API中大部分的方法返回类型和数据成员时使用double型。
2.2.3 Char类型
Java中一个字符(char)表示Unicode字符集中的一个元素。Unicode字符由16位组成,因此,有216(65535)个不同的字符可用,这与C++的标准128个ASCII字符大不相同。Unicode字符集给了我们很大的灵活性,它能包含所有不同语言的字符,以及数学、科学、文字中的常用符号。
字符由单引号括起来的单个字符表达,通常用16进制表示,范围从’\u0000’到’\uFFFF’(u告诉编译器你在用两个字节[16位]字符信息表示一个Unicode字符)。
C++注解:注意,与C++不同,Java中一个字符数组没有必要组成一个字符串。稍后将会看到,Java API定义了自己的String类型。一般只有敏感信息,如密码字段,才会作为字符数组处理。这是因为,Java对象在没有被Java虚拟机作为不被引用的对象清空之前一直驻留在内存中。如果有精明的黑客潜进系统,则把敏感数据留在内存中可能会造成危险的局面。
想得到更多的关于Unicode字符集的信息,以及字符的完整列表,可以到http://www.unicode.org网站上查看。
2.2.4 布尔型
Java中,任何一个boolean变量只有两个有效值:true和false。boolean型没有等价的数值赋值,也不允许类型转换。还有,注意所有的if和while声明都相当于一个boolean结果。
2.2.5 String类型
Java中的String类型并不是原始类型!它实际上是一个Java类。
复制数组:
如果需要将一个数组中的值复制给另一个数组,可以使用System类中预定义的arrayCopy方法。
System.arrayCopy(Object src,int src_position,Object dst,int dst_position,int length);
其中,src指的是从中复制的源数组,src_position指源数组的开始下标,dst指的是复制的目的数组,dst_option指的是目的数组的开始下标,length指定复制的数组元素的个数。
多维数组:
数组可以使问题变量很简单,但如果使用不恰当则会使问题变得异常复杂。如果在是否使用数组来解决某个问题上不确定,下面的提示可能会有一些帮助:
? 在纸上简写出要解决问题。如果数据不能以行和列的方式组织,那么使用数组可能不是最好的解决方法。
? 在写自己的数组工具,如排序和查询算法之前,查看一下Java API。通常从Java.util.Arrays包开始查看比较好。无疑Java包会节约你的时间,而且要写出比API中提供的算法更有效的代码是很难的。
? 虽然可以把数组定义为三、四、五或更多维,但是用这些取代二维数组后,事情会变得很复杂。如果你需要使用超过三维的数组,试着检测能否设计或找到另外一种数据结构来使用,比如树,这很可能会更简洁有效地解决问题。
2.2.6 强制转换变量类型
C++注解:与C++不同,Java boolean类型没有等价的数值赋值。因此没有可以直接地把一个boolean转换为int变量的方法。把boolean值转换到另外一种类型最快方式可能如下所示:
boolean gameStarted=true;
int game=gameStarted?1:0;
虽然这里没有强转,但是一行代码也可以实现转换。
最后,关于变量还有需要注意的地方。在Java中,没有typedef操作符,也没有与#define预编译指令等价的操作。虽然这可能要花费一些时间来适应,但是它会帮助排除错误并且使代码更清晰。因此,对于那些喜欢写很多“神秘”代码的人,再也不能定义一个如fa26b9这样的变量类型了。
2.2.7 Java数据类型、数组和标识符需要记忆的要点
? Java中的8种原始数据类型byte,short,int,long,float,double,boolean和char本身不是Java类,但它们是类中不可缺少的部分。
? Java中的数组可以与C和C++中大致一样地编写,但是Java的实现提供了访问数组元素总数的一个附加长属性。
? 强制转换是把变量从一种类型转换为另外一种类型的好方法,但是要小心这种方法中潜在的副作用。
2.3 Java中的运算符
2.3.1 赋值运算符
Java为还没有准备进行初始化或引用为空的对象提供了一个特殊的null值。null值指向内存零位置,并说明这个对象还没有创建。
2.3.2 比较运算符
一般来说,当需要判断一个给定的条件是否在对象之间存在时,如compare、compareTo、equals这样的方法会比较好。
2.3.3 算术运算符
记住一个整数被另外一个整数除后,结果还是整数,这就是100/3后结果为整数33的原因。
2.3.4 自增和自减运算符
2.3.5 更多的整数运算符
1.“位”运算符
下面来为一个简单的“妖怪”对象定义下面的属性:
public final static int ALIVE=1;
public final static int HUNGRY=2;
public final static int ANGRY=4;
public final static int HAIRY=8;
注意这里使用了独特的能力来描绘每一个属性。用这种方法,离右边最远的那一位表示妖怪是活是死,相邻的第二位表示妖怪是否饥饿……。这里无须对已定义的属性再定义可选择的属性,比如DEAD或NOT_HUNGRY。1即代表一个指定属性的真值,而0则相反。我们来创建一个int变量,名为attributes,初始值为0:
int attributes=0;
要为妖怪设置属性,比如ALIVE且HAIRY,使用位或运算符,如下:
attributes=ALIVE | HAIRY;
要使妖怪生气,则把attributes变量和ANGRY属性进行或运算即可:
attributes |=ANGRY;
要访问每一个属性,则使用位与运算符。下面的代码片段对每个属性的存在进行测验:
if(attributes & ALIVE>0)
System.out.println(“I am alive!”);
if(attributes & HUNGRY>0)
System.out.println(“I am hungry!”);
if(attributes & HAIRY>0)
System.out.println(“I am hairy!”);
属性设置好后,如果想重置一个特殊的属性应该怎么办呢?要实现这一点,使用位与和非运算符。下面显示了从attributes变量中重置HAIRY属性的做法:
attributes &= ~HAIRY
当然,如果只是想简单地重置妖怪的所有属性,只需把attributes变量设置为0即可。
attributes=0;
提示:位或、与、异或运算符不仅能够操作整数,也可以操作boolean变量。为简单起见,这里决定不作正式介绍,但是当有需求出现时,它们仍然是有效的操作。
2.3.6 使用点运算符
2.3.7 instanceof运算符
2.3.8 优先级顺序
2.3.9 关于运算符的记忆要点
? 运算符是使代码运行的齿轮。关键点是知道哪些运算符是可用的以及每个运算符的使用方法。
? 虽然使用位运算符对程序来说不是很“有机的”方法,但是它们足够快而简洁,会成为游戏程序员的最好朋友。
? 如果不想记住运算符优先级的14个级别,那么应使用圆括号或者把大表达式细分为小表达式。
2.4 条件语句
2.4.1 switch语句
C++注解:Java中布尔表达式求值的规则与C++中稍有不同。C++中没有真正的boolean类型,因此,这些表达式会转换成数值表达式,即,0代表false,其他的任何值都为真。Java中有真正的boolean变量,这样的表达式必定会得到一个boolean结果。所以,C++表达式while(1)在Java中必须变为while(true)。这样做是很好的,因为它迫使程序员“考虑布尔”,而这样会减少错误的发生。这也是从boolean型到任何整数类型的可允许转换不存在的原因。
2.4.2 Java中的循环语句
2.4.3 用break、continue和return提前退出循环
2.5 处理运行时异常
Java异常处理的目的在于:让我们的程序在出现运行时错误时不至于崩溃,仍然可以继续运行,直到正常结束,这样可以最大可能的避免出现内存泄漏。
一般说来,当有异常存在时,它被“扔给”应用程序,应用程序中恰当的“catch”代码块会处理这个错误。特别地,声明运行时错误种类以及位置的信息会打印在标准输出设备上。
作为程序员有两种可以使用异常处理的主要方法。第一是在方法声明中可以声明throws子句,这在不想自己另外写代码处理异常时非常有用。第二是使用try和catch结构来显式地解决异常。
2.5.1 使用try和catch块
2.5.2 使用throws子句
这样不是显式处理错误,而是把错误传给(退还给)调用的方法。错误一直向上传,直到有显式处理这个错误的方法为止。
2.5.3 关于流程控制语句的记忆要点
? 所有的Java代码都被封装成不同类型的块。像条件和循环语句这样的流程控制语句决定了程序的行为。
? 异常处理能够帮助处理那些不可避免的运行时错误。throws语句和try-catch块都会捕捉和报告这些错误。
2.6 总结
希望现在读者对构成Java程序的很多基础结构有好的感觉。虽然本章包括的东西非常多,但是C++程序员应当能够很快地抓住两种语言之间的异同点,其他的程序员,除了语法可能是潜在的障碍之外,也应当熟悉本章列出的概念。
试着做下面的练习,它们会解开读者对本章内容的迷惑。在第3章中,我们将通过设计和使用面向对象的类来进一步地研究Java语言。
2.7 练习
1. Java import语句的目的是什么?
2. 下面的哪个类可以在文件Gunship.java中定义?
a) public class Gunship(可以)
b) public class Starship(不可以)
c) class Gunship(可以)
d) class Spaghetti(可以)
e) public class Battleship extends Gunship(不可以)
要点:声明public的类名必须和文件名相同,根据这一点,得到以上答案
3. 下面哪个是声明java中main方法的正确方法?
a) public static void main(String[] args)
b) public static void main(String args[])
c) public static void main(String arg[])
d) public static void main(String[] arg)
e) 以上都是
要点:答案是(5)
4. 为下面的变量提出合适的原始数据类型
a) pi的值(double)
b) 用户名(String)
c) 弹球游戏的最高分(long)
d) 网站上点击电影名星Burt Reynolds的数目(long)。
e) 一个有40000个数字值元素的数组,每个值都在0~127之前(byte[40000])。
5. 根据用C++的经验,描述Java不允许从布尔值到任何整数类型进行转换的优势。
答:本人没怎么用过C++,所以也说不清楚。不过本人使用过C,所以对于不允许布尔值到任何整数类型进行转换的优势还是有所感悟。
在C中,由于不存在布尔值,所以,它是以0代表假,非0代表真。于是在下面的两循环中就会发生完全不同的效果。
(1)while(i==1){}(当i的值为1时才循环)
(2)while(i=1){}(死循环)
两者的不同在于:在(1)中,计算机首先计算逻辑表达式i==1的值,之后判断这个值是0还是1,显而易见,只有当i为1时,逻辑表达式i==1才会返回1,循环才会进行。
而(2)中的,计算机同样首先计算i=0的值,而i=1是一个赋值语句,这样,不论之前i的值是多少,第-:i会被修改为1,第二:表达式i=1将返回i的值,也就是整数1,计算机将1转换为逻辑值“真”看待,那么就将出现一个死循环。
而在java中,因为有了布尔值,并且布尔不能转换为逻辑值,于是(2)将会报数据类型错误,这样,可以大大避免我们程序员(特别是初学者)犯下无意中写出死循环的情况。
6. (稍难)写一个程序,使之创建一个“无规则”数组,包含帕斯卡三角形前5行的所有值,并把它打印到屏幕上。你可以很努力地编出这些值,但是对于奖励分值,试着从算法中得到。输出应当如下所示,不是必须居中。
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
7. 请解释比较两个String对象为什么与比较同一原始数据类型的两个值不同。写一个比较两个String对象并打印出结果的小测试程序。
8. 如果熟悉如DirectX的图形包,试着写一个用移位操作构造32位颜色的程序。颜色以ARGB规则显示,也就是说,每个alpha、红、绿、蓝均为8位(值为0~255)。使用Integer.toHexString方法把颜色值的结果以16进制输出。
9. 下面的程序用种子21创建一个随机对象,它会把产生器的下一个在[0,1000000)范围内的整数值保存到n。
import java.io.*;
import java.util.*;
public class RandomExercise
{
public static void main(String[] args)
{
Random r=new Random(21);
int n=r.nextInt(1000000);
System.out.println(“number to generate is ”+n);
//your code goes here
}
}
完成上面的程序,使之对用同一个随机数产生器、同一个传送到nextInt方法的参数而再次生成相同数字的n的次数进行计数,并把结果打印到屏幕上。(提示:程序每运行122963次总会再次找到相同的数字,为什么?)
10. 声明一个关于Point对象的二维数组(不要忘记引入java.awt.Point),如下:
Point[][] points=new Point[10][10];
现在写一个循环来填充每个Point元素,用下标(i,j)以及i和j的值。这里需要两个循环,一个为行一个为列。实际创建每个Point对象的代码应当如下所示:
points=new point(i,j);
11. 讨论从无限循环中跳出的一些方法,如下:
while(true)
{
//program code here
}
这种结构的固有的危险是什么?能否做防护使这种结构不会总是危险的?
12. 这章中没有讨论的主题之一是递归。递归算法用分解成更简单的一些问题的方法来解决复杂的问题。递归方法的工作方式是在到达一个基本状态之前多次调用自己。在找到基本状态后,向上返回到每个调用方法。这里不研究递归如何工作的“奇妙”,不过可以看一个简单的例子。
任何给定数字n的阶乘,如下定义:
n!=n*(n-1)*(n-2)*…*1
所以,5=5×4×3×2×1=120。读者可以写一个递归计算阶乘的方法,如下:
public int factorial(int n)
{
if(n==0||n==1)
{
return n;
}
return n*factorial(n-1);
}
注意这个方法在找到基本状态,即n是0或1之前,是如何重复调用自己的。每一个对factorial的调用能够把自己的答案传送给调用它的相应的factorial。最终,过程返回到最初的调用并得到正确的值。
试着写一个程序,使用上面的方法,看看是否清楚它的工作过程。打印出每一个对facorial的调用的值可能会使情况变得清晰。接下来试着用非递归(重复)的方式写相同的程序。进一步地,解释在哪种情况下,上面的代码会失败以及如何来修复它。
13. 设想你准备写一个计算器程序,可以计算普通的算法表达。下面是这个程序的运行例子,用户想对两个数进行除运算:
Enter the numerator: 67
Enter the denominator: 13
67/13=5.1538463
解释什么样的条件可能会引起要抛出的异常,假设用户的输入都是有效的数字。对于处理算术异常而言,可能比try-catch块更好的方法是什么?
14. 描述整数左移和右移与乘法和除法在对相同数字的操作上有怎样的关联。假设移位操作比乘除法快,描述移位运算可能派上用场的一些情况。
答:左移相当于乘二,右移相当于除二。能派上用场的地方很多,不过我一时想不起来了。