16. 静态成员、主方法与异常处理¶
16.1. 主方法¶
传统上,学习新编程语言时编写的第一个程序称为 hello world 程序。它只是在屏幕上显示"Hello, World!"字样。在 Java 中,它看起来像这样:
public class HelloWorld
{
public static void main(String[] args)
{
// generate some simple output
System.out.println("Hello, World!");
}
}
我们从之前的模块中得知,此方法内的单行代码将打印出短语"Hello, World!"
该程序定义了一个名为 main() 的方法,其签名如下:
public static void main(String[] args)
具有此确切签名的声明方法很特殊:它可用作执行独立 Java 程序的入口点。 HelloWorld 程序是 Java 应用程序 (简称 Java 应用)的一个示例。应用程序是一种独立程序,意味着其运行不依赖任何其他程序。在 Java 中,只要你编写的类提供了具有此特殊签名的 main() 方法声明,该类就可以包含自己的独立程序。
每个 Java 应用程序都必须有一个 main() 方法(具有完全相同的方法签名),该方法在程序运行时是开始执行的位置。对于包含多个类的程序,由程序员决定哪个类应包含 main() 方法。此外,当你“启动”或“执行”一个 Java 应用程序时,执行从 main() 方法开始,而不是通过创建对象——然后由 main() 负责创建程序所需的所有对象。
如上所述,该方法的方法签名非常具体。这是因为该方法在程序中具有非常特殊的作用。方法签名必须 完全 为:
public static void main(String[] args)
你在这里看到的许多关键字都会很眼熟。
public表示该方法可被程序中的其他类访问。void表示该方法不返回任何值。main是该方法的名称。String[] args表示该方法接收一个名为args的字符串数组。
字符串数组 args 实际上是一项功能,允许用户在程序启动时向其传递字符串参数。在大多数情况下,我们在本课程中无需担心如何使用此数组。然而,它仍然是方法签名的一部分,缺少它就不能被视为 main 方法。当使用命令行以独立方式执行 Java 程序时,命令行上给出的参数将用于填充此数组,因此它们在 main() 内部是可访问的。
16.2. static 关键字¶
在英语中,当我们说某物是 static 时,意味着它不会移动或改变。这在 Java 中也是如此。相比之下,计算机程序是 dynamic 的。它会变化。它会做事并执行某些操作。
一旦确定,我们就知道带有 static 关键字的任何内容都意味着它不会改变。static 关键字是一个修饰符,您可以将其放在类中的声明上,它意味着相应的声明将属于类本身,而不是特定的对象(类的实例)。
让我们看看这是什么意思:
public class Ball
{
private String color;
public Ball()
{
this.color = "Red";
}
public void setColor(String newColor)
{
this.color = newColor;
}
public String getColor()
{
return this.color;
}
}
这段代码声明了一个 Ball 类。初始化时,所有 Ball 对象的 color 字段都被设置为 "Red",但提供了一个 setter 方法,以便我们可以将其更改为其他颜色。字段 color “属于”该对象,因为 Ball 的每个实例都有自己的 color 字段,并且每个实例都可以在自己的 color 字段中存储不同的值,而与其他所有 Ball 分开。不过,要访问或更改此字段,我们需要创建一个 Ball 对象来操作。
Ball b1 = new Ball(); // creates a red Ball
b1.setColor("Blue"); // changes the color to Blue
System.out.println(b1.getColor()); // prints out the string "Blue"
现在,让我们假设我们想要的所有 Ball 对象必须具有完全相同的直径——15 厘米。这可能是使用 static 关键字的一个好应用。
public class Ball
{
private String color;
private static int diameter = 15;
// ...
}
使用 static 关键字的主要原因是,当你希望定义整个类的共有属性,而不仅仅是单个对象的属性时。这里,因为 diameter 被声明为 static ,所以类本身只存储一个 diameter 字段,并且该单一副本由从该类创建的所有对象共享。不是每个对象都存储自己的直径,而是只有一个地方存储这个值——即在类本身中。当你希望单个资源(或方法)被类中的所有对象共享时,会使用 static 修饰符。
然后,在访问静态方法或字段时,你的表述方式与存储在对象本身中的普通字段不同。例如,考虑 toString() 类的一个 Ball 方法:
public String toString()
{
return this.color + " ball with diameter " + Ball.diameter;
}
在此,当引用 color 字段时,我们使用 this.color ,其中 this 指代 当前对象,该对象拥有 color 字段。我们讨论的是存储在当前对象中的 color 值。然而,当引用 diameter 时,它不属于当前对象,而是属于类。因此,我们在点号 (.) 之前使用类名来引用它: Ball.diameter 。
同样地,如果我们有一个包含 main 方法的类,则无需创建该类的对象即可运行 main() ,因为 main() 是一个(由于其为 static)属于类本身而非在单个对象上调用的方法:
public class HelloWorld
{
public static void main(String[] args)
{
// generate some simple output
System.out.println("Hello, World!");
}
}
通过此声明,我们可以在不创建 HelloWorld 类型对象的情况下调用 main() 方法。
HelloWorld.main(null);
简言之,声明为 static 的方法或字段属于类本身,而非由该类创建的任何对象,并且由该类创建的所有对象共享同一份 static 字段或方法。此外,在引用静态字段或方法时,应使用类名后跟点号(.),而不是使用 this ,因为它们属于类而非类的任何实例。
16.2.1. 一个常见陷阱¶
需要牢记的一点是,静态方法在某些方面与非静态方法是分开的。让我们更仔细地查看 Ball 示例。
public class Ball
{
private String color;
private static int diameter = 15;
public static void printData()
{
// This line will not work!
String message = "This ball is " + color + " and has a diameter of "
+ diameter + " cm" ;
System.out.println(message);
}
}
方法 printData() 是静态的。它可以在不创建对象的情况下被调用。因此,我们无法引用字段 color ,因为该字段属于 Ball 的实例(并且每个球都有自己的颜色)。更一般地说,在静态方法中,我们不能引用任何非静态的内容。
在 main() 方法中常会犯这个错误。让我们再次查看“Hello World”代码的另一种实现:
public class HelloWorld
{
public void print()
{
System.out.println("Hello, World!");
}
public static void main(String[] args)
{
// This will not work!
print();
}
}
在这种情况下,方法 print() 是非静态的——这意味着你必须拥有 HelloWorld 类的一个实例才能调用该方法。它不能从静态方法中调用!有两种方法可以解决这个问题。我们可以向 print() 方法添加 static 关键字。这是最简单的解决方案,但通常良好的设计实践是仅在绝对必要时才使用 static 关键字。
相反,我们可以在 main() 方法中创建 HelloWorld 类的新对象,并使用该对象调用 print() 方法。
public class Hello
{
public void print()
{
System.out.println("Hello, World!");
}
public static void main(String[] args)
{
// This will work!
HelloWorld hObject = new HelloWorld();
hObject.print();
}
}
事实上,这种模式在 main() 方法中非常常见,其中 main() 方法的核心目的就是创建程序所需的初始对象,并通过调用这些对象的一个或多个方法来“启动”它们。
然而,请记住,静态方法在没有可用类实例对象的情况下无法调用非静态方法。简而言之,在静态方法中编写代码时,请注意你所引用的内容!
16.3. 错误¶
程序中可能发生两种主要错误:编译时错误和运行时错误。区分它们有助于更快地定位问题。
16.3.1. 编译时错误¶
编译时错误发生在你违反 Java 语言语法规则时。例如,圆括号和大括号必须成对出现。因此 int x = (1 + 2); 是合法的,但 int x = 8); 不是。在后一种情况下,程序无法被编译,编译器会显示一个错误。
在 BlueJ 中,编译时错误会在你按下“编译”之前就以红色下划线的形式出现在出错代码下方。
编译器发出的错误消息通常会指出程序中发生错误的位置,有时还能确切地告诉您错误是什么。例如,让我们回到 hello world 程序:
public class HelloWorld
{
public static void main(String[] args)
{
System.out.println("Hello, World!") // there's a missing semi-colon here!
}
}
如果您忘记在 print 语句末尾添加分号,可能会收到如下错误消息:
File: Hello.java [line: 5]
Error: ';' expected
这相当不错:错误位置正确,且错误消息说明了问题所在。
但是(正如你可能在本课程中见过的),错误消息并不总是容易理解。有时编译器报告的是程序中检测到错误的位置,而非错误实际发生的位置。有时,对问题的描述反而比帮助更令人困惑。
public class HelloWorld
{
public static void main(String[] args)
{
System.out.println("Hello, World!");
// there's a missing curly brace here!
}
您可能会收到如下消息:
File: Hello.java [line: 7]
Error: reached end of file while parsing
这里有两个问题。首先,错误消息是从编译器的角度编写的,而不是从你的角度。 解析 是在翻译之前读取程序的过程;如果编译器在解析过程中到达文件末尾,那就意味着遗漏了某些内容。但编译器不知道遗漏了什么。它也不知道遗漏在哪里。编译器在程序末尾(第 7 行)发现了错误,但缺失的大括号应该在前一行。
错误消息包含有用信息,因此您应努力阅读并理解它们。但重要的是要知道,它们并未讲述全部情况。
16.3.2. 运行时错误¶
在编程生涯的最初几周,你可能会花费大量时间追踪编译时错误(有时称为语法错误)。但随着经验的积累,你犯的错误会越来越少,发现错误的速度也会越来越快。
第二类错误是运行时错误,之所以这样称呼,是因为它直到程序开始运行后才会出现。在 Java 中,这类错误发生在解释器执行字节码且出现问题时。这些错误也称为 异常,因为它们通常表明发生了某些异常(且糟糕)的情况。
当发生运行时错误时,解释器会显示一条错误消息,说明发生了什么以及发生的位置。例如,如果您不小心除以零,您将收到如下消息:
Exception in thread "main" java.lang.ArithmeticException: / by zero at HelloWorld.main(HelloWorld.java:5)
此输出的某些部分可用于调试。第一行包含异常名称 java.lang.ArithmeticException 以及一条更具体地说明发生情况的消息 / by zero 。
下一行显示了发生错误的方法; HelloWorld.main 表示类 HelloWorld 中的方法 main() 。它还报告了定义该方法的文件 HelloWorld.java,以及发生错误的行号 5。
16.4. 抛出异常¶
在 Java 中,错误和其他异常情况通过 抛出 和 捕获 异常来处理。当检测到错误或异常情况时,您可以 抛出异常。这就像拉响火警警报。
假设我们有一个方法,用于将一个参数除以另一个参数:
public double div(double num, double denom)
{
return num / denom;
}
然而,如果我们无法保证变量 denom 不为零怎么办?我们可以添加一个 if 语句,在 denom 等于 0 时抛出异常。
public double div(double num, double denom)
throws Exception
{
if (denom == 0.0)
{
throw new Exception("Tried to divide by 0!");
}
return num / denom;
}
运行 div(1.0, 0.0) 会导致以下文本打印到控制台:
java.lang.Exception: Tried to divide by 0!
我们的消息就是被打印出来的内容。
同样,抛出异常就像拉响火警警报。它会让一切停止运行。正如 return 语句一样,当抛出 Exception 时,代码将在该行停止执行。
我们可以在上方看到创建新 Exception 的语法。 throw 是 Java 中用于抛出异常的关键字。
你会注意到 throw 之后的语法看起来很像我们创建对象的方式——因为它确实就是在做这件事。Exception 是 Java 中内置的一种对象。事实上, Exception 是一个父类,拥有许多不同的子类。在这种情况下,使用名为 IllegalArgumentException 的子类或许更为准确。
public double div(double num, double denom)
throws IllegalArgumentException
{
if (denom == 0.0)
{
throw new IllegalArgumentException("Tried to divide by 0!");
}
return num / denom;
}
这将产生错误消息:
java.lang.IllegalArgumentException: Tried to divide by 0!
您还会注意到方法签名发生了变化。在参数列表之后但在左花括号之前有额外的文本。如果存在将故意抛出异常的路径,我们需要将其添加到方法签名中。如果不将这些行添加到方法签名中,您将无法编译代码,这些行用于向调用者指示可能会抛出异常。
16.5. Try/Catch 块¶
到目前为止,我们已经了解了如何故意抛出错误。然而,如果希望在抛出错误时让代码回退到其他情况该怎么办呢?正如我们可以抛出异常一样,我们也可以 捕获 异常。让我们重新审视我们的示例:
public double div(double num, double denom)
throws IllegalArgumentException
{
if (denom == 0.0)
{
throw new IllegalArgumentException("Tried to divide by 0!");
}
return num / denom;
}
/**
* divides two randomly generated numbers
*/
public double divideRandom()
{
Random rand = Random.generator();
double numerator = 5.5 * rand.nextInt(5);
double denominator = 5.5 * rand.nextInt(5);
double quotient = div(numerator, denominator);
return quotient;
}
在这里,我们生成两个随机双精度浮点数,然后运行 div 方法。然而,回想一下我们对代码的了解: rand.nextInt(5) 。此代码将生成一个介于 0 和 4 之间的随机整数。这意味着任何一个变量都有可能被设置为 5.5 * 0 的结果或 0。将 numerator 设置为 0 没什么大不了的,但如果 denominator 为 0,我们将会看到该异常被抛出。
如果我们想优雅地摆脱这个潜在问题而不抛出错误,该怎么办?我们可以使用"try/catch"块。
/**
* divides two randomly generated numbers
*/
public double divideRandom()
{
Random r = Random.generator();
double numerator = 5.5 * r.nextInt(5);
double denominator = 5.5 * r.nextInt(5);
double quotient = 0.0;
try
{
quotient = div(numerator, denominator);
}
catch (IllegalArgumentException err)
{
quotient = div(numerator, 2.0);
}
return quotient;
}
Java 中的异常处理分为两个部分。首先,我们 try (尝试)执行一些可能会也可能不会引发异常的语句。这些语句被包含在 try 子句中。
try
{
quotient = div(numerator, denominator);
}
其次,我们提供一个或多个 catch 子句来处理我们预期 可能 发生的特定类型的异常。在这种情况下,我们只处理一种 IllegalArgumentException ,因此只需提供一个 catch (尽管如果需要,你可以提供多个,每个针对一种不同类型的异常)。
catch (IllegalArgumentException err)
{
quotient = div(numerator, 2.0);
}
这段代码的工作方式是:先运行 try 块中的代码。如果运行顺利,那就很好——无需额外操作。然而,如果在 try 部分内部抛出了 IllegalArgumentException ,catch 块会 捕获 该异常,而不会导致整个程序停滞。相反,若抛出该异常,则变量 quotient 将被赋值为运行 div(numerator, 2.0) 的结果。
正如我们之前所说,抛出异常就像拉响火警警报。 throw 发生在 try 块的某个作用域内。此时的“消防部门”是紧跟在 try 之后的 catch 子句中包含的代码。
16.5.1. Try/Catch/Finally 语法¶
try 块 以关键字 try 开头,后跟一个用花括号括起来的代码块。 catch 子句 或 catch 块 由关键字 catch 组成,后跟一个参数声明,用于标识被捕获的 Exception 类型,再后跟一组用花括号括起来的语句。这些语句通过采取适当的措施来处理异常。因此, catch 子句也常被称为 异常处理器 。
一旦抛出异常,控制权将从 try 块转移到相应的 catch 块。控制权不会返回到 try 块。
try
{
// block of statements at least one of which may throw an exception
}
catch (ExceptionClassName parameterName)
{
// block of statements to be executed if ExceptionClassName is thrown
}
catch (DifferentExceptionClassName otherParameterName)
{
// block of statements to be executed if DifferentExceptionClassName is thrown
}
我们还可以添加另一个可选子句,无论是否抛出异常都会执行,使用关键字 finally 。
try
{
// block of statements at least one of which may throw an exception
}
catch (ExceptionClassName parameterName)
{
// block of statements to be executed if ExceptionClassName is thrown
}
catch (DifferentExceptionClassName otherParameterName)
{
// block of statements to be executed if DifferentExceptionClassName is thrown
}
finally
{
// optional block of statements that is executed
// whether or not an exception is thrown
// this code is always run
}
在 try 中的代码执行后,以及在任何匹配的 catch 块执行后, finally 块内的代码将 始终 在最后运行。即使在 try 内部抛出了不匹配任何 catch 子句的异常, finally 块仍会被执行。
注意,一个给定的 try 块可以关联多个 catch 子句,且它们的排列顺序很重要。抛出的异常将由第一个匹配该异常类型的 catch 捕获。因此,最好将 catch 子句按从最具体到最一般的顺序排列。
try
{
// block of statements at least one of which may throw an exception
}
catch (Exception exc)
{
// This will catch any possible exception thrown as Exception
// is the general parent class.
}
catch (IllegalArgumentException illegal)
{
// This code is not reachable in this format.
}
同样重要的是要记住, try 块之后必须立即跟随一个或多个 catch 子句,而 catch 子句只能跟在 try 块之后。
