2.6 方法

方法是有名称的 Java 语句序列,可被其他 Java 代码调用。调用方法时,可以传入零个或多个值,这些值叫参数。方法执行一些计算,还可以返回一个值。2.4 节介绍过,方法调用是 Java 解释器计算的表达式。不过,因为方法调用可以有副作用,因此,也能作为表达式语句使用。本节不讨论方法调用,只说明如何定义方法。

2.6.1 定义方法

你已经知道如何定义方法的主体了,方法主体就是放在花括号里的任意语句序列。更有趣的是方法的签名(signature)。3 签名指定下述内容:

3在 Java 语言规范中,术语“signature”有技术层面的意义,和这里使用的稍有不同。本书使用方法签名较不正式的定义。

  • 方法的名称;

  • 方法所用参数的数量、顺序、类型和名称;

  • 方法的返回值类型;

  • 方法能抛出的已检异常(签名还能列出未检异常,不过不是必需的);

  • 提供方法额外信息的多个方法修饰符。

方法签名定义了调用方法之前需要知道的一切信息,是方法的规范,而且定义了方法的 API。若想使用 Java 平台的在线 API 参考指南,需要知道如何阅读方法签名。若想编写 Java 程序,需要知道如何定义自己的方法。方法都以方法签名开头。

方法签名的格式如下:

  1. modifiers type name ( paramlist ) [ throws exceptions ]

签名(方法规范)后面是方法主体(方法的实现),即放在花括号里的 Java 语句序列。抽象方法(参见第 3 章)没有实现部分,方法主体使用一个分号表示。

方法签名中可能包含类型变量声明,这种方法叫泛型方法(generic method)。泛型方法和类型变量在第 4 章介绍。

下面是一些方法定义示例,都以签名开头,后面跟着方法主体:

  1. // 这个方法传入的是字符串数组,没有返回值
  2. // 所有Java程序的入口都是这个名称和签名
  3. public static void main(String[] args) {
  4. if (args.length > 0) System.out.println("Hello " + args[0]);
  5. else System.out.println("Hello world");
  6. }
  7. // 这个方法传入两个double类型的参数,返回一个double类型的数字
  8. static double distanceFromOrigin(double x, double y) {
  9. return Math.sqrt(x*x + y*y);
  10. }
  11. // 这是抽象方法,没有主体
  12. // 注意,调用这个方法时可能会抛出异常
  13. protected abstract String readText(File f, String encoding)
  14. throws FileNotFoundException, UnsupportedEncodingException;

modifiers 是零个或多个特殊的修饰符关键字,之间使用空格分开。例如,声明方法时可以使用 publicstatic 修饰符。允许使用的修饰符及其意义在下一节介绍。

方法签名中的 type 指明方法返回值的类型。如果方法没有返回值,type 必须是 void。如果声明方法时指定了返回类型,就必须包含一个 return 语句,返回一个符合(或能转换为)所声明类型的值。

构造方法是一段类似方法的代码,用于初始化新建的对象。第 3 章会介绍,构造方法的定义方式和方法类似,不过签名中没有 type 部分。

方法的修饰符和返回值类型后面是 name,即方法名。方法名和变量名一样,也是 Java 标识符。和所有 Java 标识符一样,方法名可以包含 Unicode 字符集能表示的任何语言的字母。定义多个同名方法是合法的,往往也很有用,只要各方法的参数列表不同就行。定义多个同名方法叫方法重载(method overloading)。

2.6 方法 - 图1 和某些其他语言不同,Java 没有匿名方法。不过,Java 8 引入了 lambda 表达式,作用类似于匿名方法,但是 Java 运行时会自动把 lambda 表达式转换成适当的具名方法,详情参见 2.7.5 节。

例如,我们见过的 System.out.println() 方法就是重载方法。具有这个名字的某个方法打印字符串,而具有这个名字的其他方法打印各种基本类型的值。Java 编译器根据传入这个方法的参数类型决定调用哪个方法。

定义方法时,方法名后一定是方法的形参列表(parameters list),而且必须放在括号里。形参列表定义零个或多个传入方法的实参(argument)。4 如果有形参的话,每个形参都包含类型和名称,(如果有多个形参)形参之间使用逗号分开。调用方法时,传入的实参值必须和该方法签名中定义的形参数量、类型和顺序匹配。传入的值不一定要和签名中指定的类型一样,但是必须能不经校正转换为对应的类型。

4parameter 是定义方法时声明的参数,argument 是调用方法时传入的参数。如果二者同时出现,parameter 译为“形参”,argument 译为“实参”。在不引起歧义的情况下,则都译为“参数”。——译者注

2.6 方法 - 图2 如果 Java 方法没有实参,其形参列表是 (),而不是 (void)。C 和 C++ 程序员要特别注意,Java 不把 void 当作一种类型。

Java 允许程序员定义和调用参数数量不定的方法,使用的句法叫变长参数(varargs),本章后面会详细介绍。

方法签名的最后一部分是 throws 子句,列出方法能抛出的已检异常(checked exception)。已检异常是一系列异常类,必须在能抛出它们的方法中使用 throws 子句列出。如果方法使用 throw 语句抛出一个已检异常,或者调用的其他方法抛出一个没有捕获或处理的已检异常,声明这个方法时就必须指明能抛出这个异常。如果方法能抛出一个或多个已检异常,要在参数列表后面使用 throws 关键字指明能抛出的异常类。如果方法不会抛出异常,无需使用 throws 关键字。如果方法抛出的异常类型不止一个,要使用逗号分隔异常类的名称。稍后还会再说明。

2.6.2 方法修饰符

方法的修饰符包含零个或多个修饰符关键字,例如 publicstaticabstract。下面列出允许使用的修饰符及其意义。

  • abstract

使用 abstract 修饰的方法没有实现主体。组成普通方法主体的花括号和 Java 语句使用一个分号代替。如果类中有使用 abstract 修饰的方法,类本身也必须使用 abstract 声明。这种类不完整,不能实例化(参见第 3 章)。

  • final

使用 final 修饰的方法不能被子类覆盖或隐藏,能获得普通方法无法得到的编译器优化。所有使用 private 修饰的方法都隐式添加了 final 修饰符;使用 final 声明的任何类,其中的所有方法也都隐式添加 final 修饰符。

  • native

native 修饰符表明方法的实现使用某种“本地”语言编写,例如 C 语言,并且开放给 Java 程序使用。native 修饰的方法和 abstract 修饰的方法一样,没有主体:花括号使用一个分号代替。

实现 native 修饰的方法

Java 刚出现时,使用 native 修饰方法有时是为了提高效率。现在几乎不需要这么做了。现在,使用 native 修饰方法的目的是,把 Java 代码集成到现有的 C 或 C++ 库中。native 修饰的方法和所在平台无关,如何把实现和方法声明所在的 Java 类链接起来,取决于 Java 虚拟机的实现方式。本书没有涵盖使用 native 修饰的方法。

  • publicprotectedprivate

这些访问修饰符指定方法是否能在定义它的类之外使用,或者能在何处使用。这些非常重要的修饰符在第 3 章说明。

  • static

使用 static 声明的方法是类方法,关联在类自己身上,而不是类的实例身上(第 3 章会详细说明)。

  • strictfp

在这个很少使用的奇怪修饰符中,fp 的意思是“浮点”(floating point)。一般情况下,Java 会利用运行时所在平台的浮点硬件提供的可用扩展精度。添加这个关键字后,运行 strictfp 修饰的方法时,Java 会严格遵守标准,而且就算结果不精确,也只使用 32 位或 64 位浮点数格式进行浮点运算。

  • synchronized

synchronized 修饰符的作用是实现线程安全的方法。线程调用 synchronized 修饰的方法之前,必须先为方法所在的类(针对 static 修饰的方法)或对应的类实例(针对没使用 static 修饰的方法)获取一个锁,避免两个线程同时执行该方法。

synchronized 修饰符是实现的细节(因为方法可以通过其他方式实现线程安全),不是方法规范或 API 的正式组成部分。好的文档应该明确说明方法是否线程安全;当编写多线程程序时,不应通过方法中是否有 synchronized 关键词来判断方法是否线程安全。

2.6 方法 - 图3 注解是特例(注解的详细介绍参见第 4 章)——注解可以看作方法修饰符和额外补充信息的折中方案。

2.6.3 已检异常和未检异常

Java 的异常处理机制会区分两种不同的异常类型:已检异常未检异常

已检异常和未检异常之间的区别在于异常在什么情况下抛出。已检异常在明确的特定情况下抛出,经常是应用能部分或完全恢复的情况。

例如,某段代码要在多个可能的目录中寻找配置文件。如果试图打开的文件不在某个目录中,就会抛出 FileNotFoundException 异常。在这个例子中,我们想捕获这个异常,然后在文件可能出现的下一个位置继续尝试。也就是说,虽然文件不存在是异常状况,但可以从中恢复,这是意料之中的失败。

然而,在 Java 环境中有些失败是无法预料的,这些失败可能是由运行时条件或滥用库代码导致的。例如,无法正确预知 OutOfMemoryError 异常;又如,把无效的 null 传给使用对象或数组的方法,会抛出 NullPointerException 异常。

这些是未检异常。基本上任何方法在任何时候都可能抛出未检异常。这是 Java 环境中的墨菲定律:“会出错的事总会出错。”从未检异常中恢复,虽说不是不可能,但往往很难,因为完全不可预知。

若想区分已检异常和未检异常,记住两点:异常是 Throwable 对象,而且异常主要分为两类,通过 ErrorException 子类标识。只要异常对象是 Error 类,就是未检异常。Exception 类还有一个子类 RuntimeExceptionRuntimeException 类的所有子类都属于未检异常。除此之外,都是已检异常。

处理已检异常

Java 为已检异常和未检异常制定了不同的规则。如果定义的方法会抛出已检异常,就必须在方法签名的 throws 子句中声明这个异常。Java 编译器会检查方法签名,确保的确声明了;如果没声明,会导致编译出错(所以才叫“已检异常”)。

就算自己从不抛出已检异常,有时也必须使用 throws 子句声明已检异常。如果方法中调用了会抛出已检异常的方法,要么加入异常处理代码处理这个异常,要么使用 throws 子句声明这个方法也能抛出这个异常。

例如,下述方法使用标准库中的 java.netURL 类(第 10 章会介绍)访问网页,尝试估算网页的大小。所用的方法和构造方法会抛出各种 java.io.IOException 异常对象,所以在 throws 子句中声明了:

  1. public static int estimateHomepageSize(String host) throws IOException {
  2. URL url = new URL("htp://"+ host +"/");
  3. try (InputStream in = url.openStream()) {
  4. return in.available();
  5. }
  6. }

其实,上述代码有个问题:协议名拼写错了——没有名为 htp:// 的协议。所以,estimateHomepageSize() 方法会一直失败,抛出 MalformedURLException 异常。

你怎么知道要调用的方法会抛出已检异常呢?可以查看这个方法的签名。如果签名中没有,但又必须处理或声明调用的方法抛出的异常时,Java 编译器会(通过编译错误消息)告诉你。

2.6.4 变长参数列表

方法可以声明为接受数量不定的参数,调用时也可以传入数量不定的参数。这种方法一般叫作变长参数方法。格式化打印方法 System.out.printf()String 类相关的 format() 方法,以及 java.lang.reflect 中反射 API 的一些重要方法,都使用变长参数。

变长参数列表的声明方式为,在方法最后一个参数的类型后面加上省略号(),指明最后一个参数可以重复零次或多次。例如:

  1. public static int max(int first, int... rest) {
  2. /* 暂时省略主体 */
  3. }

变长参数方法纯粹由编译器处理,把数量不定的参数转换为一个数组。对 Java 运行时来说,上面的 max() 方法和下面这个没有区别:

  1. public static int max(int first, int[] rest) {
  2. /* 暂时省略主体 */
  3. }

把变长参数方法的签名转换为真正的签名,只需把 换成 []。记住,参数列表中只能有一个省略号,而且只能出现在最后一个参数中。

下面填充 max() 方法的主体:

  1. public static int max(int first, int... rest) {
  2. int max = first;
  3. for(int i : rest) { // 合法,因为rest其实就是数组
  4. if (i > max) max = i;
  5. }
  6. return max;
  7. }

声明这个 max() 方法时指定了两个参数,第一个是普通的 int 类型值,但是第二个可以重复零次或多次。下面对 max() 方法的调用都是合法的:

  1. max(0)
  2. max(1, 2)
  3. max(16, 8, 4, 2, 1)

因为变长参数方法被编译成接受数组参数的方法,所以在编译对这类方法的调用得到的代码中,包含创建和初始化这个数组的代码。因此,调用 max(1,2,3) 被编译成:

  1. max(1, new int[] { 2, 3 })

其实,如果参数的方法已经存储在数组中,完全可以直接把数组传给变长参数方法,而不用把数组中的元素取出来一个一个传入。 参数可以看成一个数组。不过,反过来就不行了:只有使用省略号声明为变长参数方法,才能使用变长参数方法调用的句法。