Java 之道/方法
在上一章中,我们在处理非整数时遇到了一些问题。我们通过测量百分比而不是分数来解决问题,但更通用的解决方案是使用浮点数,它们可以表示分数和整数。在 Java 中,浮点类型称为 double。
您可以创建浮点变量并使用与其他类型相同的语法为它们赋值。例如
double pi;
pi = 3.14159;
同时声明变量并为其赋值也是合法的
int x = 1;
String empty = "";
double pi = 3.14159;
实际上,这种语法非常常见。组合的声明和赋值有时被称为初始化。
虽然浮点数很有用,但它们通常是混淆的根源,因为整数和浮点数之间似乎存在重叠。例如,如果你有值 1,它是整数、浮点数还是两者兼而有之?
严格来说,Java 区分整数 1 和浮点数 1.0,即使它们看起来是同一个数字。它们属于不同的类型,严格来说,不允许在类型之间进行赋值。例如,以下操作是非法的
int x = 1.1;
因为左侧的变量是 int,而右侧的值是 float。但很容易忘记这条规则,尤其是因为在某些情况下,Java 会自动从一种类型转换为另一种类型。例如
double y = 1;
理论上不应该合法,但 Java 通过自动将 int 转换为 double 来允许它。这种宽松的处理很方便,但它会导致问题;例如
double y = 1 / 3;
你可能会期望变量 y 被赋予值 0.333333,这是一个合法的浮点值,但实际上它将得到值 0.0。原因是右侧的表达式似乎是两个整数的比率,因此 Java 执行整数除法,这会产生整数 0。转换为浮点,结果为 0.0。
解决此问题(一旦你弄清楚它是什么)的一种方法是使右侧成为浮点表达式
double y = 1.0 / 3.0;
这将 y 设置为 0.333333,如预期的那样。
我们已经看到的所有运算——加法、减法、乘法和除法——也适用于浮点值,尽管你可能想知道底层机制是完全不同的。实际上,大多数处理器都有专门的硬件用于执行浮点运算。
正如我提到的,Java 会在必要时自动将 int 转换为 double,因为转换过程中不会丢失任何信息。另一方面,从 double 到 int 的转换需要舍入。Java 不会自动执行此操作,以确保你作为程序员意识到数字小数部分的丢失。
将浮点值转换为整数最简单的方法是使用类型转换。类型转换之所以得名,是因为它允许你取属于一种类型的值并将其转换为另一种类型(在塑造或重塑的意义上,而不是抛出)。
不幸的是,类型转换的语法很丑陋:你将类型名称放在括号中,并将其用作运算符。例如
int x = (int) Math.PI;
(int) 运算符的作用是将后续的内容转换为整数,因此 x 接收的值为 3。
类型转换优先于算术运算,因此在以下示例中,PI 的值首先被转换为整数,因此 Java 发现了一个错误,因为 PI 的转换值在乘以 20.0 时被转换为 double,它不是 int 类型。
int x = (int) Math.PI * 20.0;
转换为整数总是向下舍入,即使小数部分为 0.99999999。
这两个属性(优先级和舍入)会使类型转换变得很麻烦。
Java 提供了一组内置函数,包括你能想到的大多数数学运算。这些函数称为方法。大多数数学方法对 double 进行操作。
数学方法使用与我们已经看到的打印命令类似的语法调用
double root = Math.sqrt (17.0);
double angle = 1.5;
double height = Math.sin (angle);
第一个示例将 root 设置为 17 的平方根。第二个示例找到 1.5 的正弦,即变量 angle 的值。Java 假设你与 sin 及其他三角函数(cos、tan)一起使用的值以弧度表示。如果你需要将度数转换为弧度
double degrees = 90;
double angle = degrees * 2 * Math.PI / 360.0;
注意 PI 全部是大写字母。Java 不识别 Pi、pi 或 pie。
Math 类中另一个有用的方法是 round,它将浮点值舍入到最接近的整数并返回一个 int。
int x = (int) Math.round (Math.PI * 20.0);
在这种情况下,乘法先发生,然后调用方法。结果是 63(从 62.8319 上舍入)。
就像数学函数一样,Java 方法可以组合,这意味着你可以将一个表达式用作另一个表达式的一部分。例如,你可以使用任何表达式作为方法的参数
double x = Math.cos (angle + Math.PI/2);
此语句获取 Math.PI 的值,将其除以二并将结果添加到变量 angle 的值。然后将总和作为参数传递给 cos 方法。(注意 PI 是变量名,而不是方法名,因此没有参数,甚至没有空参数 ())。
你也可以获取一个方法的结果并将它作为参数传递给另一个方法
double x = Math.exp (Math.log (10.0)):
在 Java 中,log 函数始终使用以 10 为底,因此此语句找到 10 的以 10 为底的对数。结果被分配给 x;我希望你知道它是什么。
到目前为止,我们只使用内置于 Java 的方法,但也可以添加新方法。实际上,我们已经看到一个方法定义:main。名为 main 的方法很特殊,因为它指示程序执行从哪里开始,但 main 的语法与其他方法定义相同
public static void NAME ( LIST OF PARAMETERS )
STATEMENTS
你可以为你的方法起任何你喜欢的名字,但不能将其命名为 main 或任何其他 Java 关键字。参数列表指定为了使用(或调用)新函数需要提供什么信息(如果有的话)。
main 的唯一参数是 String[] args,它表明任何调用 main 的人都必须提供一个字符串数组(我们将在数组章节中讨论数组)。我们将编写的第一个方法没有参数,因此语法如下
public static void newLine ()
System.out.println ("");
此方法名为 newLine,空括号表示它不接受任何参数。它只包含一条语句,该语句打印一个空字符串,用 "" 表示。打印没有字母的字符串可能看起来不是那么有用,但请记住,println 在打印后跳到下一行,因此此语句的作用是跳到下一行。
在 main 中,我们可以使用类似于调用内置 Java 命令的方式来调用此新方法
public static void main (String[] args)
System.out.println ("First line.");
newLine ();
System.out.println ("Second line.");
此程序的输出为
First line. Second line.
注意两行之间的额外空格。如果我们想要在行之间有更多空格呢?我们可以重复调用相同的方法
public static void main (String[] args)
System.out.println ("First line.");
newLine ();
newLine ();
newLine ();
System.out.println ("Second line.");
或者我们可以编写一个名为 threeLine 的新方法,它打印三行新行
public static void threeLine ()
newLine (); newLine (); newLine ();
public static void main (String[] args)
System.out.println ("First line.");
threeLine ();
System.out.println ("Second line.");
您应该注意有关此程序的几件事
- 您可以重复调用相同的过程。实际上,这样做很常见也很有用。
- 您可以让一个方法调用另一个方法。在这种情况下,main 调用 threeLine,threeLine 调用 newLine。同样,这也是很常见且有用的。
- 在 threeLine 中,我将三条语句都写在一行上,这在语法上是合法的(请记住,空格和新行通常不会改变程序的含义)。
另一方面,通常将每个语句放在单独的一行上是一个更好的主意,以便使您的程序易于阅读。我在这本书中有时会违反这条规则以节省空间。
到目前为止,可能还不清楚为什么创建所有这些新方法值得如此麻烦。实际上,有很多原因,但此示例只演示了两个
- 创建新方法为您提供了一个机会,可以为一组语句命名。方法可以通过将复杂的计算隐藏在一个命令后面,以及使用英语单词代替神秘的代码来简化程序。哪个更清楚,newLine 还是 System.out.println ("")?
- 创建新方法可以通过消除重复代码来使程序更小。例如,您将如何打印九个连续的新行?您可以只调用 threeLine 三次。
类
[edit | edit source]将上一节中的所有代码片段整合在一起,整个类定义如下
class NewLine
public static void newLine ()
System.out.println ("");
public static void threeLine ()
newLine (); newLine (); newLine ();
public static void main (String[] args)
System.out.println ("First line.");
threeLine ();
System.out.println ("Second line.");
第一行表示这是名为 NewLine 的新类的类定义。类是相关方法的集合。在本例中,名为 NewLine 的类包含三个方法,分别名为 newLine、threeLine 和 main。
我们见过的另一个类是 Math 类。它包含名为 sqrt、sin 以及许多其他方法。当我们调用数学函数时,我们必须指定类名(Math)和函数名。这就是为什么内置方法和我们编写的代码的语法略有不同的原因
Math.pow (2.0, 10.0);
newLine ();
第一个语句调用 Math 类中的 pow 方法(将第一个参数提升到第二个参数的幂)。第二个语句调用 newLine 方法,Java 假设(正确地)它在 NewLine 类中,这就是我们正在编写的。
如果您尝试从错误的类调用方法,编译器将生成错误。例如,如果您输入
pow (2.0, 10.0);
编译器会说类似“在 NewLine 类中找不到名为 pow 的方法”。如果您看到了此消息,您可能想知道为什么它在您的类定义中查找 pow。现在你知道为什么了。
具有多个方法的程序
[edit | edit source]当您查看包含多个方法的类定义时,您可能会想从上到下阅读它,但这很可能令人困惑,因为这不是程序执行的顺序。
执行始终从 main 的第一条语句开始,无论它在程序中的什么位置(在本例中,我故意将它放在底部)。语句按顺序一次执行一条,直到遇到方法调用。方法调用就像执行流中的一个岔路。您不会转到下一条语句,而是转到被调用方法的第一行,执行那里的所有语句,然后返回并从您离开的地方继续执行。
听起来很简单,除了您必须记住一个方法可以调用另一个方法。因此,当我们在 main 的中间时,我们可能必须离开并执行 threeLine 中的语句。但是当我们执行 threeLine 时,我们会被中断三次以离开并执行 newLine。
newLine 本身调用内置方法 println,这会导致又一次岔路。幸运的是,Java 非常擅长跟踪它在哪里,因此当 println 完成后,它会在 newLine 中从它离开的地方继续执行,然后回到 threeLine,最后回到 main,以便程序可以终止。
实际上,从技术上讲,程序不会在 main 结束时终止。相反,执行从调用 main 的程序中它离开的地方继续执行,该程序是 Java 解释器。Java 解释器负责处理删除窗口和一般清理等事情,然后程序终止。
这个可悲故事的寓意是什么?当您阅读程序时,不要从上到下阅读。相反,请遵循执行流程。
参数和参数
[edit | edit source]我们使用过的一些内置方法有参数,这些参数是您提供的值,让方法可以完成它的工作。例如,如果您想找到一个数字的正弦,您必须指示该数字是什么。因此,sin 以 double 值作为参数。要打印一个字符串,您必须提供该字符串,这就是 println 以 String 作为参数的原因。
有些方法不止一个参数,例如 pow,它接受两个 double 值,底数和指数。
请注意,在每种情况下,我们不仅必须指定参数的数量,还要指定它们的类型。因此,当您编写类定义时,参数列表指示每个参数的类型,这并不奇怪。例如
public static void printTwice (String phil)
System.out.println (phil);
System.out.println (phil);
此方法接受一个名为 phil 的参数,该参数的类型为 String。无论该参数是什么(目前我们不知道是什么),它都会被打印两次。我选择 phil 这个名字是为了表明您可以随意命名参数,但一般情况下,您希望选择比 phil 更具说明性的名称。
为了调用此方法,我们必须提供一个 String。例如,我们可能有一个像这样的 main 方法
public static void main (String[] args)
printTwice ("Don't make me say this twice!");
您提供的字符串称为参数,我们说参数被传递给方法。在本例中,我们正在创建一个包含文本“不要让我重复说这句话!”的字符串值,并将该字符串作为参数传递给 printTwice,在那里,与它的愿望相反,它将被打印两次。
或者,如果我们有一个 String 变量,我们可以使用它作为参数
public static void main (String[] args)
String argument = "Never say never.";
printTwice (argument);
请注意这里非常重要的一点:我们作为参数传递的变量的名称(argument)与参数的名称(phil)无关。再说一遍
我们作为参数传递的变量的名称与参数的名称无关。
它们可以相同,也可以不同,但重要的是要意识到它们不是一回事,除了它们碰巧具有相同的值(在本例中是字符串“永远不要说永远”)。
您作为参数提供的值必须与您调用的方法的参数具有相同的类型。这条规则非常重要,但它在 Java 中经常变得复杂,原因有两个
- 有些方法可以接受具有许多不同类型的参数。例如,您可以将任何类型发送到 print 和 println,它会根据需要做正确的事情,无论是什么。不过,这类情况是例外。
- 如果您违反了这条规则,编译器通常会生成一条令人困惑的错误消息。它不会说类似“您正在将错误类型的参数传递给此方法”的话,而是可能会说类似它找不到具有该名称并且可以接受该类型参数的方法。不过,一旦您看到此错误消息几次,您就会弄清楚如何解释它。
堆栈图
[edit | edit source]
参数和其他变量只存在于它们自己的方法中。在 main 的范围内,没有 phil 这样的东西。如果您尝试使用它,编译器会抱怨。同样,在 printTwice 内部,没有 argument 这样的东西。
对于每个方法,都有一个称为帧的灰色框,其中包含该方法的参数和局部变量。方法的名称出现在框架之外。像往常一样,每个变量的值都绘制在一个框中,变量名称位于该框旁边。
具有多个参数的方法
[edit | edit source]声明和调用具有多个参数的方法的语法是错误的常见来源。首先,请记住您必须声明每个参数的类型。例如
public static void printTime (int hour, int minute)
System.out.print (hour);
System.out.print (":");
System.out.println (minute);
写成 int hour, minute 可能很诱人,但这种格式只适用于变量声明,不适用于参数。
另一个常见的困惑是,您不必声明参数的类型。以下代码是错误的!
int hour = 11;
int minute = 59;
printTime (int hour, int minute); // WRONG!
在这种情况下,Java 可以通过查看 hour 和 minute 的声明来判断它们的类型。当您将它们作为参数传递时,包含类型是不必要的,也是非法的。正确的语法是:printTime (hour, minute)。
作为练习,绘制一个使用参数 11 和 59 调用的 printTime 的堆栈帧。
您可能已经注意到,我们使用的一些方法(例如 Math 方法)会产生结果。其他方法(例如 println 和 newLine)会执行某些操作,但不会返回值。这会引发一些问题。
- 如果您调用一个方法但没有对结果做任何操作(即没有将其分配给变量或用作更大表达式的一部分),会发生什么?
- 如果您将打印方法用作表达式的部分,例如 System.out.println ("boo!") + 7,会发生什么?
- 我们可以编写产生结果的方法,还是只能使用 newLine 和 printTwice 之类的方法?
第三个问题的答案是“是的,您可以编写返回结果的方法”,我们将在接下来的几章中进行介绍。我会让您尝试回答另外两个问题。实际上,如果您对 Java 中什么合法或非法有任何疑问,一个好的方法是询问编译器。
- 浮点数:一种可以包含分数和整数的变量(或值)类型。在 Java 中,这种类型称为 double。
- 类 一组命名的方法。到目前为止,我们已经使用了 Math 类和 System 类,并且编写了名为 Hello 和 NewLine 的类。
- 方法 一系列命名语句,用于执行某些有用的功能。方法可以接受参数,也可以不接受参数,可以产生结果,也可以不产生结果。
- 参数 用于调用方法的信息。参数类似于变量,因为它们包含值并具有类型。
- 参数 调用方法时提供的数值。此值必须与相应的参数具有相同的类型。
- 调用 导致执行一个方法。