Java/条件语句图形递归之道
模运算符作用于整数(和整数表达式),并返回第一个操作数除以第二个操作数的余数。在 Java 中,模运算符是百分号 %。语法与其他运算符完全相同
int quotient = 7 / 3;
int remainder = 7 % 3;
第一个运算符,整数除法,返回 2。第二个运算符返回 1。因此,7 除以 3 等于 2,余数为 1。
模运算符非常有用。例如,你可以检查一个数字是否可以被另一个数字整除:如果 x%y 为零,那么 x 可以被 y 整除。
此外,你可以使用模运算符从数字中提取最右边的数字或数字。例如,x % 10 返回 x 最右边的数字(以 10 为基数)。类似地,x % 100 返回最后两位数字。
为了编写有用的程序,我们几乎总是需要能够检查某些条件并相应地改变程序的行为。条件语句给了我们这种能力。最简单的形式是 if 语句
if (x > 0)
System.out.println ("x is positive");
括号中的表达式称为条件。如果为真,则执行方括号中的语句。如果条件不为真,则不会发生任何事情。
条件可以包含任何比较运算符,有时称为关系运算符
x == y // x equals y
x != y // x is not equal to y
x > y // x is greater than y
x < y // x is less than y
x >= y // x is greater than or equal to y
x <= y // x is less than or equal to y
一个常见的错误是使用单个 = 代替双等号 ==。请记住,= 是赋值运算符,而 == 是比较运算符。此外,没有像 =< 或 => 这样的东西。
条件运算符两侧必须是相同类型。你只能比较 int 与 int,double 与 double。不幸的是,在这一点上,你根本无法比较字符串!有一种方法可以比较字符串,但我们将在接下来的几章中讨论它。
条件执行的第二种形式是选择性执行,其中有两个可能性,条件决定执行哪一个。语法如下
if (x%2==0)
System.out.println ("x is even");
else
System.out.println ("x is odd");
如果 x 除以 2 的余数为零,那么我们知道 x 是偶数,此代码会打印一条消息以表明这一点。如果条件为假,则执行第二个 print 语句。由于条件必须为真或假,因此两个选择中只有一个将被执行。
顺便说一下,如果你认为你可能经常需要检查数字的奇偶性,你可能想将此代码包装在一个方法中,如下所示
public static void printParity (int x)
if (x%2==0)
System.out.println ("x is even");
else
System.out.println ("x is odd");
现在,你有一个名为 printParity 的方法,它将为任何你提供的整数打印一条适当的消息。在 main 中,你将如下调用此方法
printParity (17);
始终记住,当你调用一个方法时,你无需声明提供参数的类型。Java 可以确定它们的类型。你应该抵制编写如下内容的诱惑
int number = 17;
printParity (int number); // output: "x is odd"
有时,你希望检查一系列相关的条件并选择其中一项操作。一种方法是将一系列 ifs 和 elses 连接起来
if (x > 0)
System.out.println ("x is positive");
else if (x < 0)
System.out.println ("x is negative");
else
System.out.println ("x is zero");
这些链可以根据你的需要无限延伸,但如果它们变得过于复杂,则可能难以阅读。一种使它们更容易阅读的方法是使用标准缩进,如这些示例所示。如果你保持所有语句和波浪号对齐,你犯语法错误的可能性更小,并且在发生错误时更容易找到它们。
除了链接之外,你还可以将一个条件语句嵌套在另一个条件语句中。我们可以将前面的示例写成
if (x == 0)
System.out.println ("x is zero");
else
if (x > 0)
System.out.println ("x is positive");
else
System.out.println ("x is negative");
现在有一个外部条件语句包含两个分支。第一个分支包含一个简单的 print 语句,但第二个分支包含另一个条件语句,它本身有两个分支。幸运的是,这两个分支都是 print 语句,虽然它们也可能都是条件语句。
再次注意,缩进有助于使结构清晰,但是,嵌套条件语句很快就会变得难以阅读。通常情况下,最好避免它们。
另一方面,这种 **嵌套结构** 很常见,我们将在后面再次看到它,所以你最好习惯它。
return 语句允许你在到达方法末尾之前终止方法的执行。使用它的一个原因是,如果你检测到错误情况
public static void printLogarithm (double x)
if (x <= 0.0)
System.out.println ("Positive numbers only, please.");
return;
double result = Math.log (x);
System.out.println ("The log of x is " + result);
这定义了一个名为 printLogarithm 的方法,它接受一个名为 x 的 double 作为参数。它首先检查 x 是否小于或等于零,如果是,则打印一条错误消息,然后使用 return 退出方法。执行流程立即返回调用方,方法的其余行不会被执行。
我在条件的右侧使用了浮点数,因为左侧有一个浮点数变量。
你可能想知道为什么你可以使用类似“x 的对数是”+ result 这样的表达式,因为其中一个操作数是字符串,另一个是 double。好吧,在这种情况下,Java 正在代表我们执行智能操作,在它执行字符串连接之前,它会自动将 double 转换为字符串。
这种功能是设计编程语言时遇到的一个常见问题的一个例子,即形式主义与便利性之间存在冲突,形式主义要求形式语言应该具有简单的规则,很少有例外,而便利性要求编程语言在实践中易于使用。
大多数情况下,便利性获胜,这对专家程序员来说通常是件好事(他们可以免受严格但笨拙的形式主义的困扰),但对初学者程序员来说是件坏事,他们经常对规则的复杂性和异常数量感到困惑。在这本书中,我试图通过强调规则并省略许多异常来简化问题。
但是,重要的是要知道,无论何时你尝试“添加”两个表达式,如果其中一个是字符串,则 Java 将将另一个转换为字符串,然后执行字符串连接。你认为将整数和浮点数进行运算会发生什么?
为了在屏幕上绘制图形,你需要两个对象,一个画板和一个图形对象。
- 画板:画板是一个窗口,它包含一个可以绘制图形的空白矩形。Slate 类不是标准 Java 库的一部分;它是我为本课程编写的。
- 图形:图形对象是我们用来绘制线条、圆圈等的对象。它是 Java 库的一部分,因此它的文档位于 Sun 网站上。
与 Graphics 对象相关的 方法定义在内置的 Graphics 类中。与 Slates 相关的 方法定义在 Slate 类中,如附录 slate 中所示。
Slate 类中的主要 方法是 makeSlate,它基本上做你预期的事情。它创建一个新的窗口,并返回一个 Slate 对象,你可以用它在程序的后面部分引用这个窗口。你可以在一个程序中创建多个 Slate。
Slate slate = Slate.makeSlate (500, 500);
makeSlate 接受两个参数,窗口的宽度和高度。因为它属于另一个类,所以我们必须使用 *点符号* 指定类的名称。
返回值被分配给一个名为 slate 的变量。类名(大写 *S*)和变量名(小写 *s*)之间没有冲突。
我们需要的下一个 方法是 getGraphics,它接受一个 Slate 对象,并创建一个 Graphics 对象,可以在上面进行绘制。你可以将 Graphics 对象看作一块粉笔。
Graphics g = Slate.getGraphics (slate);
使用名称 g 是惯例,但我们可以将其命名为任何东西。
为了在屏幕上绘制东西,你需要在 graphics 对象上调用 方法。我们已经调用了大量的 方法,但这是我们第一次 *在对象上调用 方法*。语法类似于从另一个类调用 方法
g.setColor (Color.black);
g.drawOval (x, y, width, height);
对象的名称出现在点号之前;方法的名称出现在点号之后,后面跟着该方法的参数。在本例中,该方法接受一个参数,它是一个颜色。
setColor 更改当前颜色,在本例中为黑色。所有被绘制的东西都将是黑色的,直到我们再次使用 setColor。
Color.black 是 Color 类提供的一个特殊值,就像 Math.PI 是 Math 类提供的一个特殊值一样。Color 提供了其他颜色的调色板,包括
black blue cyan darkGray gray lightGray magenta orange pink red white yellow
为了在 Slate 上进行绘制,我们可以调用 Graphics 对象上的 draw 方法。例如
g.drawOval (x, y, width, height);
drawOval 接受四个整数作为参数。这些参数指定一个边界框,即椭圆将被绘制在其中的矩形(如图所示)。边界框本身不会被绘制;只有椭圆会被绘制。边界框就像一个指南。边界框总是水平或垂直方向;它们从不处于奇怪的角度。
如果你仔细想想,有很多方法可以指定矩形的位置和大小。你可以给出中心或任何角点的位置,以及高度和宽度。或者,你可以给出相对角点的位置。选择是任意的,但在任何情况下,它都需要相同数量的参数:四个。
按照惯例,指定边界框的常用方法是给出左上角的位置以及宽度和高度。指定位置的常用方法是使用坐标系。
你可能熟悉二维笛卡尔坐标,其中每个位置由一个 x 坐标(沿 x 轴的距离)和一个 y 坐标标识。按照惯例,笛卡尔坐标向右和向上增加,如图所示。
令人讨厌的是,计算机图形系统通常使用笛卡尔坐标的变体,其中原点位于屏幕或窗口的左上角,正 y 轴的方向向下。Java 遵循此惯例。
度量单位称为像素;典型的屏幕大约有 1000 个像素宽。坐标始终是整数。如果你想将浮点数用作坐标,你必须将其四舍五入为整数(参见四舍五入部分)。
假设我们想要绘制米老鼠的图片。我们可以使用我们刚刚绘制的椭圆作为脸,然后添加耳朵。在这样做之前,最好将程序分解为两个 方法。main 将创建 Slate 和 Graphics 对象,然后调用 draw,draw 完成实际的绘制工作。
public static void main (String[] args)
int width = 500;
int height = 500;
Slate slate = Slate.makeSlate (width, height);
Graphics g = Slate.getGraphics (slate);
g.setColor (Color.black);
draw (g, 0, 0, width, height);
public static void draw (Graphics g, int x, int y, int width, int height)
g.drawOval (x, y, width, height);
g.drawOval (x, y, width/2, height/2);
g.drawOval (x+width/2, y, width/2, height/2);
draw 的参数是 Graphics 对象和一个边界框。draw 调用 drawOval 三次,以绘制米老鼠的脸和两个耳朵。下图显示了耳朵的边界框。
/-\ /-\ | | | | \ / \ / /---\ | | \___/
如图所示,左耳边界框左上角的坐标为 (x, y)。右耳的坐标为 (x+width/2, y)。在这两种情况下,耳朵的宽度和高度都是原始边界框宽度和高度的一半。
请注意,耳盒的坐标都是相对于原始边界框的位置 (x 和 y) 和大小 (宽度和高度) 的。因此,我们可以使用 draw 在屏幕上的任何位置以任何大小绘制米老鼠(尽管很蹩脚)。作为练习,修改传递给 draw 的参数,使米老鼠的高度和宽度为屏幕的一半,并且居中。
原型
drawLine drawRect fillOval fillRect prototype interface
另一个与 drawOval 参数相同的绘图命令是
drawRect (int x, int y, int width, int height)
在这里,我使用了一种标准格式来记录 方法的名称和参数。这些信息有时被称为 方法的接口或原型。查看此原型,你可以判断参数的类型以及(基于它们的名称)推断它们的功能。以下另一个示例
drawLine (int x1, int y1, int x2, int y2)
使用参数名称 x1、x2、y1 和 y2 意味着 drawLine 从点 (x1, y1) 到点 (x2, y2) 绘制一条线。
你可能想尝试的另一个命令是
drawRoundRect (int x, int y, int width, int height,
int arcWidth, int arcHeight)
前四个参数指定矩形的边界框;其余两个参数指示角点应圆角的程度,指定角点圆弧的水平和垂直直径。
这些命令还有 *填充* 版本,它们不仅绘制形状的轮廓,而且还会填充它。接口是相同的;只有名称被更改了
fillOval (int x, int y, int width, int height)
fillRect (int x, int y, int width, int height)
fillRoundRect (int x, int y, int width, int height,
int arcWidth, int arcHeight)
没有 fillLine 这样的东西——它根本没有意义,因为直线是一维的。
我在上一章中提到过,一个 方法调用另一个 方法是合法的,我们已经看到了几个这样的例子。我忘记提的是,一个 方法调用自身也是合法的。可能不明显为什么这是一件好事,但事实证明,这是程序可以做到的最神奇和最有趣的事情之一。
For example, look at the following method:
public static void countdown (int n)
if (n == 0)
System.out.println ("Blastoff!");
else
System.out.println (n);
countdown (n-1);
该方法名为 countdown,它接受一个整数作为参数。如果参数为零,它将打印单词Blastoff。否则,它将打印该数字,然后调用名为 countdown 的方法(自身),并将 n-1 作为参数传递。
如果我们在 main 中像这样调用此方法,会发生什么?
countdown (3);
- countdown 的执行从 n=3 开始,由于 n 不为零,它打印值 3,然后调用自身,并将 3-1 传递...
- countdown 的执行从 n=2 开始,由于 n 不为零,它打印值 2,然后调用自身,并将 2-1 传递...
- countdown 的执行从 n=1 开始,由于 n 不为零,它打印值 1,然后调用自身,并将 1-1 传递...
- countdown 的执行从 n=0 开始,由于 n 为零,它打印单词Blastoff!然后返回。
- 获得 n=1 的 countdown 返回。
- 获得 n=2 的 countdown 返回。
- 获得 n=3 的 countdown 返回。
然后你回到 main(多么奇妙的旅程)。因此,总的输出看起来像这样
3 2 1 Blastoff!
作为第二个例子,让我们再看看 newLine 和 threeLine 方法。
public static void newLine ()
System.out.println ("");
public static void threeLine ()
newLine (); newLine (); newLine ();
逐字
虽然这些方法有效,但如果我想打印 2 个或 106 个换行符,它们将帮不上忙。更好的选择是
public static void nLines (int n)
if (n > 0)
System.out.println ("");
nLines (n-1);
这个程序非常相似;只要 n 大于零,它就打印一个换行符,然后调用自身以打印 n-1 个额外的换行符。因此,打印的换行符总数为 1 + (n-1),通常约为 n。
方法调用自身的过程称为递归,这样的方法被称为递归方法。
递归方法的堆栈图
[edit | edit source]在上一章中,我们使用堆栈图来表示方法调用期间程序的状态。相同类型的图表可以更容易地解释递归方法。
请记住,每次调用方法时,它都会创建一个新的方法实例,该实例包含方法的局部变量和参数的新版本。
有一个 main 实例和四个 countdown 实例,每个实例都有不同的参数 n 值。堆栈的底部,n=0 的 countdown 是基本情况。它不会进行递归调用,因此没有更多的 countdown 实例。
main 的实例是空的,因为 main 没有参数或局部变量。作为练习,为 nLines 绘制一个堆栈图,调用参数 n=4。
约定和神圣法则
[edit | edit source]在过去的几个部分中,我多次使用短语按约定来表示设计决策,这些决策在某种意义上是任意的,因为没有明显的理由用一种方式而不是另一种方式,而是由约定决定的。
在这些情况下,熟悉约定并使用它对你来说是有利的,因为它将使你的程序更容易被其他人理解。同时,区分(至少)三种类型的规则很重要
- 神圣法则 这是我用来表示由于逻辑或数学的某些基本原理而成立的规则,并且在任何编程语言(或其他形式系统)中都成立。例如,不可能用少于四条信息来指定边界框的位置和大小。另一个例子是整数加法是可交换的。这是加法定义的一部分,与 Java 无关。
- Java 规则 这些是 Java 的语法和语义规则,你不能违反它们,因为生成的程序将无法编译或运行。有些是任意的;例如,+ 符号代表加法和字符串连接这一事实。其他规则反映了编译或执行过程的底层限制。例如,你必须指定参数的类型,但不是参数。
- 样式和约定 很多规则没有被编译器强制执行,但对于编写正确、可调试和可修改以及其他人可以阅读的程序来说至关重要。例如,缩进和花括号的位置,以及命名变量、方法和类的约定。
随着我们的学习,我将尝试指出各种事物属于哪一类,但你可能想不时地思考一下。
虽然我正在谈论这个话题,你可能已经发现,类的名称总是以大写字母开头,但变量和方法以小写字母开头。如果名称包含多个单词,你通常将每个单词的第一个字母大写,例如 newLine 和 printParity。这些规则属于哪一类?
词汇表
[edit | edit source]- 模数 对整数起作用的操作符,当一个数字除以另一个数字时产生余数。在 Java 中,它用百分号()表示。
- 条件语句 一块语句,这些语句是否执行取决于某个条件。
- 链式 一种将多个条件语句按顺序连接起来的方法。
- 嵌套 将一个条件语句放在另一个条件语句的一个或两个分支内。
- 坐标 指定二维图形窗口中位置的变量或值。
- 像素 测量坐标的单位。
- 边界框 指定矩形区域坐标的常用方法。
- 类型转换 将一种类型转换为另一种类型的操作符。在 Java 中,它显示为括号中的类型名称,如 (int)。
- 接口 对方法所需参数及其类型的描述。
- 原型 使用类似 Java 语法来描述方法接口的一种方法。
- 递归 调用正在执行的相同方法的过程。
- 无限递归 一种递归调用自身,但永远不会到达基本情况的方法。通常的结果是 StackOverflowException。
- 分形 一种递归定义的图像,因此图像的每个部分都是整体的较小版本。