posts - 75,comments - 83,trackbacks - 0
第五章“可变参数”

方法重载是Java和其他面向对象语言最具特色的特性之一。当许多人可能认为Java的优势是它的类型,或者是它所带的API库,其实让相同的方法名与各种各样可接受的参数搭配也是一件很好的事。


Guitar guitar = new Guitar("Bourgeois", "Country Boy Deluxe",
GuitarWood.MAHOGANY, GuitarWood.ADIRONDACK,1.718);
Guitar guitar = new Guitar("Martin", "HD-28");
Guitar guitar = new Guitar("Collings", "CW-28"
GuitarWood.BRAZILIAN_ROSEWOOD, GuitarWood.ADIRONDACK,1.718,
GuitarInlay.NO_INLAY, GuitarInlay.NO_INLAY);

This code calls three versions of the constructor of a (fictional) Guitar class, meaning that information can be supplied when it’s available,rather than forcing a user to know everything about their guitar at one time (many professionals couldn’t tell you their guitar’s width at the nut).
Here are the constructors used:
public Guitar(String builder, String model) {
}
public Guitar(String builder, String model,
GuitarWood backSidesWood, GuitarWood topWood,
float nutWidth) {
}
public Guitar(String builder, String model,
GuitarWood backSidesWood, GuitarWood topWood,
float nutWidth,
GuitarInlay fretboardInlay, GuitarInlay topInlay) {
}


这段代码调用了Guitar类中三个版本的构造器,意味着当信息可见时,这些信息会被支持,而不是迫使每一个使用者每一次都要去了解关于Guitar类的所有知识。许多专家不会在关键时候告诉你他们的Guitar的内容。下面是用到的构造器:

public Guitar(String builder, String model) {
}
public Guitar(String builder, String model,GuitarWood backSidesWood, GuitarWood topWood,float nutWidth) {
}
public Guitar(String builder, String model,GuitarWood backSidesWood, GuitarWood topWood,float nutWidth,
GuitarInlay fretboardInlay, GuitarInlay topInlay) {
}



然而,当你想要去增加无限的信息时,事情开始变得有一点不是那么有用了。例如:假设你想允许在这个构造器中增加额外的未指明的特性。下面就是一些可能的调用的例子:

Guitar guitar = new Guitar("Collings", "CW-28"
GuitarWood.BRAZILIAN_ROSEWOOD, GuitarWood.ADIRONDACK,1.718,
GuitarInlay.NO_INLAY, GuitarInlay.NO_INLAY,"Enlarged Soundhole", "No Popsicle Brace");
Guitar guitar = new Guitar("Martin", "HD-28V","Hot-rodded by Dan Lashbrook", "Fossil Ivory Nut","Fossil Ivory Saddle", "Low-profile bridge pins");


对于这两个单独的情况,你不得不去增加一个构造器来接受两个额外的字符串,另外一个构造器来接受四个额外的字符串。试图将这些相似的版本应用于早已重载的构造器。根据这样的话,你最终会得到20或30个那样愚蠢的构造器的版本!

原因在于我们常称做的可变参数。可变参数是Tiger的增加的另一个特性,它用一种相当巧妙的方法彻底地解决了这儿提出的问题。这一章讲述了这种相对简单的特性的各个方面。这将会使你迅速写出更好、更整洁、更灵活的代码。

创建一个可变长度的参数列表

可变参数使得你可以指定某方法来接受多个同一类型的参数,而且并不要求事先确定参数的数量(在编译或运行时)。
这就是Tiger的一个集成部分。事实上,正是因为Java语言的一些新特性组合在一起才表现出了可变参数的特性。

我如何去实现呢?
首先,你要习惯的书写省略号(。。。)。这三个小点是可变参数的关键,你将会经常键入它们。下面是Guitar类的构造器使用可变参数来接受不确定数量字符串的一个例子:

public Guitar(String builder, String model, String...features);


参数String... features 表明任何数量的字符串都可能被接受。 所以,下面所有的调用都合法的。

Guitar guitar = new Guitar("Martin", "HD-28V","Hot-rodded by Dan Lashbrook", "Fossil Ivory Nut","Fossil Ivory Saddle", "Low-profile bridge pins");
Guitar guitar = new Guitar("Bourgeois", "OMC","Incredible flamed maple bindings on this one.");
Guitar guitar = new Guitar("Collings", "OM-42","Once owned by Steve Kaufman--one of a kind");
You could add the same variable-length argument to the other constructors:
public Guitar(String builder, String model,
GuitarWood backSidesWood, GuitarWood topWood,float nutWidth, String... features)
public Guitar(String builder, String model,
GuitarWood backSidesWood, GuitarWood topWood,float nutWidth,
GuitarInlay fretboardInlay,GuitarInlay topInlay,String... features)


例5-1描写了一个把所有的这些特性放在一起的简单类,甚至使用XX来一起传递一些可变参数。
Example 5-1. Using varargs in constructors

package com.oreilly.tiger.ch05;
public class Guitar {
private String builder;
private String model;
private float nutWidth;
private GuitarWood backSidesWood;
private GuitarWood topWood;
private GuitarInlay fretboardInlay;
private GuitarInlay topInlay;
private static final float DEFAULT_NUT_WIDTH = 1.6875f;
public Guitar(String builder, String model, String... features) {
this(builder, model, null, null, DEFAULT_NUT_WIDTH, null, null, features);
}
public Guitar(String builder, String model,
GuitarWood backSidesWood, GuitarWood topWood,
float nutWidth, String... features) {
this(builder, model, backSidesWood, topWood, nutWidth, null, null, features);
}
public Guitar(String builder, String model,
GuitarWood backSidesWood, GuitarWood topWood,float nutWidth,
GuitarInlay fretboardInlay, GuitarInlay topInlay,String... features) {
this.builder = builder;
this.model = model;
this.backSidesWood = backSidesWood;
this.topWood = topWood;
this.nutWidth = nutWidth;
this.fretboardInlay = fretboardInlay;
this.topInlay = topInlay;
}
}


刚才发生了什么?
当你指定了一个可变长度参数列表,Java编译器实际上读入 “create an array of type <参数类型>”。你键入:

public Guitar(String builder, String model, String... features)


然而:编译器解释这些为:
public Guitar(String builder, String model, String[] features)


这意味着重复参数列表变得简单(这将在“重复可变长度参数列表”里讲述),这与你需要完成的其他程序设计目标是一样。
你可以像使用数组一样来使用可变参数。
然而,这同样存在一些限制。第一,在每个方法中,你只可以使用一次省略号。所以,下面的书写是不合法的:
public Guitar(String builder, String model,
String... features, float... stringHeights)


另外,省略号必须作为方法的最后一个参数。


如果你不需要传递任何可变参数呢?
那没关系,你只需要以旧的方式调用构造器:
Guitar guitar = new Guitar("Martin", "D-18");


我们再仔细看看,虽然程序中没有与下面代码相匹配的构造器:
public Guitar(String builder, String model)


那么,代码到底传递了什么呢?作为可变参数的特例,在参数中不传递东西是一个合法的选项。所以,当你看到 String... features,你应该把它认为是零个或者更多个String参数。这省却你再去创建另一个不带可变参数构造器的麻烦。

重复可变长度参数类表

所有这些可变参数是很好的。但是实际上,如果你不在你的方法中使用它们的话,他们显然仅仅是吸引眼球的东西或是窗户的装饰品而已。
然而,你可以像你使用数组一样来使用可变参数,你会觉得这种用法很简单。

那我怎么来使用可变参数呢?
首先你要确保阅读了“创建一个可变长度的参数列表”,你会从中了解到可变参数方法最重要的东西,那就是我们把可变参数当作数组来看待。
所以,继续前面的例子,你可以写出下面的代码:
public Guitar(String builder, String model,
GuitarWood backSidesWood, GuitarWood topWood,float nutWidth,
GuitarInlay fretboardInlay, GuitarInlay topInlay,String... features) {
this.builder = builder;
this.model = model;
this.backSidesWood = backSidesWood;
this.topWood = topWood;
this.nutWidth = nutWidth;
this.fretboardInlay = fretboardInlay;
this.topInlay = topInlay;
for (String feature : features) {
System.out.println(feature);
}
}


上面的这段代码看上是不是不是那么的有吸引力?但这确实体现了可变参数的精髓。作为另一个例子,下面这个简单的方法从一组数字中计算出最大值:
public static int max(int first, int... rest) {
int max = first;
for (int i : rest) {
if (i > max)
max = i;
}
return max;
}


是不是,够简单吧?


那么如何存储可变长度参数呢?
正因为Java编译器把这些看作数组,所以数组显然是一个存储的好选择,这将在下面的例5-2中体现。
Example 5-2. 存储作为成员变量的可变参数
package com.oreilly.tiger.ch05;
public class Guitar {
private String builder;
private String model;
private float nutWidth;
private GuitarWood backSidesWood;
private GuitarWood topWood;
private GuitarInlay fretboardInlay;
private GuitarInlay topInlay;
private String[] features;
private static final float DEFAULT_NUT_WIDTH = 1.6875f;
public Guitar(String builder, String model, String... features) {
this(builder, model, null, null, DEFAULT_NUT_WIDTH, null, null, features);
}
public Guitar(String builder, String model,
GuitarWood backSidesWood, GuitarWood topWood,
float nutWidth, String... features) {
this(builder, model, backSidesWood, topWood, nutWidth, null, null, features);
}
public Guitar(String builder, String model,
GuitarWood backSidesWood, GuitarWood topWood,
float nutWidth,
GuitarInlay fretboardInlay, GuitarInlay topInlay,
String... features) {
this.builder = builder;
this.model = model;
this.backSidesWood = backSidesWood;
this.topWood = topWood;
this.nutWidth = nutWidth;
this.fretboardInlay = fretboardInlay;
this.topInlay = topInlay;
this.features = features;
}
}


你可以简单地在Java的Collection类中存储这些可变参数。
//变量声明
private List features;
//在方法中或是构造器中的书写
this.features = java.util.Arrays.asList(features);


允许零长度的参数列表
可变参数的一个显著的特性是可变长度参数可以接受零到N个参数。这就意味着你可以调用这些方法中的一个方法而不传递任何参数,程序同样可以运行。从另一方面来说,这又意味着,作为一个程序员,你最好意识到你必须防范这种情况的发生。

如何实现它呢?
记得在“重复可变长度参数类表”中,你读到过下面这个简单的方法:
public static int max(int first, int... rest) {
int max = first;
for (int i : rest) {
if (i > max)
max = i;
}
return max;
}


你可以以多种形式来调用这个方法:
int max = MathUtils.max(1, 4);
int max = MathUtils.max(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int max = MathUtils.max(18, 8, 4, 2, 1, 0);



有一点不是那么令人满意的地方是,在很多情况下,你要传递的数字已经存储在数组里,或是至少是在某些集成的形式中:
//用这种方法来取得数字
int[] numbers = getListOfNumbers( );


要把这些数字传递给max()方法是不可能的。你需要检查list的长度,从中截取掉第一个对象(如果存在第一个对象的话),然后检查类型来确保是int型。完成了这些,你才可以带着数组中剩余的部分一起传递进入方法。而这数组中剩余的部分还要重复,或者要人工地转化为适合的格式。总之,这个过程会很辛苦,你需要做许多琐碎的事情。仔细想想,你要记得编译器是将这个方法解释为下面的语句:
public static int max(int first, int[] rest)
所以,你可以做些调整,把max()方法改写成下面这个样子:
public static int max(int... values) {
int max = Integer.MIN_VALUE;
for (int i : values) {
if (i > max)
max = i;
}
return


你现在已经定义了一个可以很容易接受数组的方法。
//用这种方法来取得数字
int[] numbers = getListOfNumbers( );
int max = MathUtils.max(numbers);


当接受单一的可变长度参数时,你使用这种方法会很简单。但是,如果在最好的情况下,你传递了一个零长度的数组进去,这就会带来问题,你会得到难以预料的结果。为了解决这个问题,你需要一个小的错误检查。例5-3是MathUtils类的完整代码列表,在这里是一个功能更强的MathUtil类。


例5-3 处理零参数的方法
package com.oreilly.tiger.ch05;
public class MathUtils {
public static int max(int... values) {
if (values.length == 0) {
throw new IllegalArgumentException("No values supplied.");
}


任何时候,你都可能会要处理零长度的参数列表,这时你就需要执行这类的错误检查。通常,一个功能强大的IllegalArgumentException类是一个好的选择。
int max = Integer.MIN_VALUE;
for (int i : values) {
if (i > max)
max = i;
}
return max;
}
}



那么关于调用同样的方法来处理通常参数不是数组的方法,又会如何呢?这当然是完全合法的。下面的代码都是合法调用max()方法的手段:
int max = MathUtils.max(myArray);
int max = MathUtils.max(new int[] { 2, 4, 6, 8 });
int max = MathUtils.max(2, 4, 6, 8);
int max = MathUtils.max(0);
int max = MathUtils.max( );


指定对象参数,而非基本类型

在第四章中我们谈到,Tiger通过拆箱增加了一系列的新特征。你可以在处理可变参数时,在你的方法接受的参数中使用对象包装类。

如何实现?
你一定记得在Java中所有的类最终都是java.lang.Object的子类。这就意味着任何对象可以被转化成一个Object对象。更进一步说,因为像int和short这样的基本类型会自动转化成他们对应的对象包装类(就像Integer和Short),任何Java类型可以被转化成一个Object对象。
所以,如果你需要你的可变参数方法可以接受最多种参数的类型,那么你可以将Object类型作为参数的类型。更好的是,为了达到多重功能,绝大多数情况下都会使用Object对象。例如,写个用来打印方法。

private String print(Object... values) {
StringBuilder sb = new StringBuilder( );
for (Object o : values) {
sb.append(o)
.append(" ");
}
return sb.toString( );
}


这儿最简单的意思是打印出所有的东西。然而,这个方法更通用的定义是下面的样子:
private String print(String... values) {
StringBuilder sb = new StringBuilder( );
for (Object o : values) {
sb.append(o)
.append(" ");
}
return sb.toString( );
}


这个方法的问题是方法自身不能接受字符串,整数,浮点数,数组和其他的类型数据,而这些数据你都想要正常的打印出来。通过使用Object这个更为通用的类型,你可以来打印所有的一切。
private String print(Object... values) {
StringBuilder sb = new StringBuilder( );
for (Object o : values) {
sb.append(o)
.append(" ");
}
return sb.toString( );
}


避免数组自动转化

Tiger增加了各种类型的自动转化和便利,这些东西在绝大多数的情况下是很好用的。不幸的是,有些时候所有的这些东西会变成你的障碍。其中一种情况是,在可变参数方法中将多个Object对象转化为Object[]数组对象,你会发现在个别的情况下,你需要用Java来书写。


如何实现?
在将要仔细讨论这件事情前,你要确信自己理解这个问题。Java新的printf()方法是一个很好的便利,举这个方法作个例子:
System.out.printf("The balance of %s's account is $%(,6.2f\n",account.getOwner().getFullName( ),account.getBalance( ));




如果你看一下Java文档中关于printf()方法的说明,你就会看到它是一个可变参数的方法。它有两个参数:一个是用于设置字符串格式的String类型变量,另一个是所有要传递进字符串的Object对象:
PrintStream printf(String format, Object... args)


现在,你可以把上面的代码默认为下面的形式:
PrintStream printf(String format, Object[] args)


两种书写是不是完全相同呢?大多数情况下是相同的。考虑一下下面的代码:
Object[] objectArray = getObjectArrayFromSomewhereElse( );
out.printf("Description of object array: %s\n", obj);


这是乎有点牵强,然而要把它看作是为了自省的代码而付出的正常开销。比起其它代码,这样写要简洁的多。如果你正在编写一个代码分析工具,或者一个集成开发环境,或者其他可能使用reflection或简单API来判断出应用程序会需要何种对象的东西,这些马上会成为一个通用的案例。这儿,你不是真正关心对象数组的内容,就像你同样不会去关心数组自身一样。它是什么类型?它的内存地址是多少?它的字符串代表什么意思?请紧记所有这些问题都是和数组本身有关的,和数组的内容无关。例如:我们来看看下面的数组代码:
public Object[] getObjectArrayFromSomewhereElse( ) {
return new String[] {"Hello", "to", "all", "of", "you"};
}



在这种情况下,你肯能会写一些像下面一样的代码来回答某些关于数组的问题:
out.printf("Description of object array: %s\n", obj);


然而,输出结果并不是你所期望的那样:
run-ch05:
[echo] Running Chapter 5 examples from Java Tiger: A Developer's Notebook
[echo] Running VarargsTester...
[java] Hello


这倒是怎么回事?这就不是你想看到的结果。然而,编译器做了它应该做的,它把在printf()方法里的Object...转换为Object[]。实际上,当编译器得到你方法的调用时,它看到的参数是Object[]。所以编译器不是把这个数组看作一个Object对象本身,而是把它分成不同的部分。这样被传递给字符串格式 (%s)的就是第一个参数部分“Hello”字符串,所以结果“Hello”就显示出来了。

仔细看看这件事,你需要去告诉编译器你要把整个对象数组obj看作是一个简单的对象,而不是一组参数。请看下面奇特的代码:
out.printf("Description of object array: %s\n", new Object[] { obj });


作为选择,还有一种更为简单的方法:
out.printf("Description of object array: %s\n", (Object)obj);



在上面两种书写情况下,编译器不再认为是对象的数组,而是直接认为是一个简单的Object对象,而这个Object对象又恰好是一个对象数组。那么结果就如你所愿(至少在这种简单的应用下):
run-ch05:
[echo] Running Chapter 5 examples from Java Tiger: A Developer's Notebook
[echo] Running VarargsTester...
[java] [Ljava.lang.String;@c44b88


看到结果,你肯能会感到有点错乱。这大概是基于reflection或者其他自省代码需要的结果。

全章完.
posted on 2008-07-25 16:57 梓枫 阅读(196) 评论(0)  编辑  收藏 所属分类: java

只有注册用户登录后才能发表评论。


网站导航: