从Arrays里面的toString方法是不是…

来源:互联网 发布:linux开机自动挂载 编辑:程序博客网 时间:2024/05/21 09:08

java.util.Arrays 提供了一系列常用的静态方法,如 sort,asList等等,toString

是其中之一。我们经常使用却没有好好认识它,有一天看着这个名字可能会觉得很眼熟,一个疑问在我们的脑海里浮现——它是不是重写的

Object 类的 toString 方法?

要解答这个问题,要回到“重写(override)是什么”这个本源问题上去,而要想解答“重写(override)是什么”,最好先搞清楚更加原始的问题——“怎么区分一个方法和另外一个方法”。
Java

用类作为代码组织的基本结构,里面包裹着属性和方法。为了满足多样化的需求,或者为了实现多样化的功能,一个类往往需要定义很多方法。方法一多,起名字就成了一个大问题。这就像玩网络游戏起个昵称一样,由于中国人多,随便想一个名字往往都已经被占用了,于是最后你会看到很多类似的名称,都在自嘲起名字这件事。比如妈妈说系列:“妈妈说名字长藏在树后不容易被发现”,“妈妈说名字长藏在树后容易被发现”,“妈妈说名字长容易被集火”,“妈妈说名字起的长队友才帮忙”……

在游戏世界,ID

是区别一个玩家和另一个玩家的关键。在编程语言里面,区别一个方法和另一个方法显得稍微复杂了一点(但这是为了满足多样化的需求,或者为了实现多样化的功能)。

在 Java 中,区分一个方法和另一个方法的关键是“方法签名”。
方法签名不是一个单一元素,而是一个组合。方法签名由方法名+参数列表构成。而参数列表又因参数个数,参数类型,参数顺序的不同而不同。在

Java

中,方法签名不同才代表两个不同的方法。而且,同一个类里面,只能有一个某种方法签名类型的方法存在(即使除了方法签名以外的其它内容如访问控制符不同)。就像是一个人穿着一件黑色长裙坐在屋子里,不可能与此同时,她穿着一件白色长裙也在这个屋子里,想想都可怕。

看一个简单的代码示例:
public class TestMethodName {
public void speak(Japanese japanese) {}
public void speak(Chinese chinese) {} //和①相比,参数类型不同
public void speak(Japanese japanese1, Japanesejapanese2){}//和①相比,参数个数不同
public void speak(Chinese chinise, Japanese japanese) {}
public void speak(Japanese japanese, Chinesechinese){}//和④相比,参数顺序不同
//public void speak(Japanese kansai, ChinesePutonghua){}//和⑤相比,仅仅是参数名称不同,是不行的
//public String speak(Japanese japanese, Chinesechinese){return "what?";}//和⑤冲突
//private void speak(Japanese japanese, Chinesechinese){}//和⑤冲突
//public static void speak(Japanese japanese, Chinesechinese){}//和⑤冲突
public final void speak(Japanese japanese, Chinesechinese){}//和⑤冲突

这里,speak 是方法名称,圆括号里面的是参数列表。
②和①相比,参数类型不同,所以它们是不同的方法。就像上面的这些方法例子,仅仅方法名称部分相同而已,这种情况就是重载,是方法命名的特例。假如方法名称都不同,他们就理所当然地是不同的方法了,不会产生任何混淆。
③和①先比,参数个数不同。
⑤和④相比,参数顺序不同。
⑥和⑤相比,参数个数,顺序,类型都相同,只是参数名称不同而已。这种情况并不构成重载,而且在运行时没法区分它和⑤哪个才是应该被调用的方法,因为这里的参数只是形参而已,名称可以随便起,没法标识一个方法。

⑦和⑤相比,参数个数,顺序,类型都相同,只是返回值类型不同而已。这种情况也不构成重载,而且在在运行时没法区分它和⑤哪个才是应该被调用的方法。在我们仅凭个人感觉的情况下,好像返回值类型不同足以区分一个方法和另一个方法了。但实际的情况不是这样的。关于以返回值类型区分重载方法,《Thinking

in Java》里面有下面这样一小段论述:


------------------------------------------------------------------------------------
读者可能会想:“在区分重载方法的时候,为什么只能以类名和方法的形参列表作为标准呢?能否考虑用方法的返回值来区分呢?”比如下面两个方法,虽然它们有同样的名字和形参,但却很容易区分它们:
void f() {};
int f(){return1;}
只要编译器可以根据语境明确判断出语义,比如在 int x= f()中,那么的确可以据此区分重载方法。不过,有时你并不关心方法的返回值,你想要的是方法调用的其它效果(这常被称为“为了副作用而调用”),这时你可能会调用方法而忽略其返回值。所以,如果像下面这样调用方法:
f();
此时别人该如何理解这种代码呢?因此,根据方法的返回值来区分重载方法是行不通的。
------------------------------------------------------------------------------------
写得简洁明了。其实从另外一个角度来理解这个事也是很好地。我们之所以感觉可以通过返回值类型来区分不同的方法,是因为我们只看到了源代码。在实际运行的时候,方法被加载到方法区,当我们需要调用某个方法,虚拟机就要去寻找这个方法(实际是去方法表查实际存储方法代码的地址,类似于老电影里通过公共电话亭的电话簿查某个人的家庭住址)。寻找的时候先通过方法名检索出所有以该名称作为方法名的方法,然后通过参数列表进行匹配,确定最终应该调用哪一个方法。确定以后,通过方法表的这个地址找到方法区对应的代码,把它加载到栈里运行。方法表里面只有方法名和参数列表的信息,是不管你拥有什么返回值类型的,你拥有什么返回值类型要等到方法执行以后才知道。所以从这个角度来说,也是可以得出不能以返回值类型作为判断是否重载或者区分不同方法的依据的。
⑧⑨⑩和⑤相比,参数个数,顺序,类型都相同,分别只是前面的访问控制符,static修饰符,final修饰符不同而已。这样也是无法区分两个方法的。参数个数,顺序,类型都相同,这已经唯一确定了一个方法。而这样的方法在当前类内部只能存在一个。
知道了怎么区分一个方法和另一个方法以后,我们来看重写(override)。
重写的前提是继承。通过继承,子类把父类的非 privae

属性和方法都拿来。子类如果对从父类继承过来的某个方法的方法体(一般来说,只改方法体)进行修改,使得子类方法和父类方法中有一对儿方法签名相同但是方法体(方法实现)不同的方法的话,这就构成了重写。
如下面这个例子,Man 类就成功重写了 Person 类的 speak 方法:
package mon20;
public class Person {
protected String speak(String content) {
return "person is speaking " + content;
}
}
package mon20;
public class Man extends Person{
protected String speak(String content) {
return "man is speaking " + content;
}
}
需要注意的是,虽然准确意义上来说,只要子类的方法和父类的方法的方法签名相同,方法体变一变就可以说明是重写了,但是事实上,方法的返回值类型一般也必须保持一致。原因在于,重写是为多态做准备的,这是它存在的唯一意义。在多态中,父类引用指向子类对象,通过父类引用调用的方法,既有可能是父类自己的方法,也有可能是子类重写后的方法,视具体情况而定。子类重写父类的方法,往往是由于父类中的方法没有方法的实现(有意设计成抽象类和抽象方法),子类继承过来以后进行具体代码实现。这样的话,它们其实是为了实现相同的功能,只不过父类中声明了方法,指明了方向,而子类实现了方法,完成了大业。就好比,父类定义的方法是反清复明,返回值类型是明朝,子类却推翻了满清,返回了共和国。这已经违背了最初的想法的目的。(从这个比喻来看,其实把

override 翻译成重写还是有一定的歧义的)

如下面代码会报错的:
package mon20;
public class Person {
protected String speak(String content) {
return "person is speaking " + content;
}
}
package mon20;
public class Man extends Person{
protected int speak(String content) {
return 1;
}
}
这还不算完,只是说一般情况下,子类重写的方法的返回值类型必须和父类保持一致,还有特殊的情况。
比如,父类的返回值类型是 Object 而子类的返回值类型是 String

等任意一个,这种情况是没问题的,而且好像有一个很高大上的术语叫“协变返回”。也就是说,如果子类的返回值类型是父类的返回值类型的之类,比如例子中的

Object 和 String 或者其它自定义类但是具有继承关系的情况。(但是需要注意,假如父类中是 Object而子类中是
int

是有问题的,如果子类中是 Integer 就没问题)

package mon20;
public class Person {
protected Object speak(String content) {
return "person is speaking " + content;
}
}
package mon20;
public class Man extends Person{
protected String speak(String content) {
return "man is speaking " + content;
}
}
尽管如此,子类在重写父类的方法时,还是最好保持和父类方法的返回值一样。真正需要用到协变返回的情况并不多见。
当然在重写中,还有下面这些需要注意的点。
①子类重写的方法的访问控制符不能比父类的可见性小(Cannot reduce the visibility of the
inherited

method from base class)
,可以一样,也可以比父类的大,但是一般都保持一样。
如父类用的是 protected,子类改成了默认,这样就不行。原因出在多态上。
package mon20;
public class Person {
protected Object speak(String content) {//protected,可以被同一个package内的类和位于其它package但是属于该Person类的子类的类访问(引用)
return "person is speaking " + content;
}
}
package mon20;
public class Man extends Person{
String speak(String content) {

//默认修饰符,只能被同一个package内的类访问(引用)
return "man is speaking " + content;
}
}
②在抛出异常方面,子类重写的方法抛出的异常的检查范围不能比父类的大。
比如父类抛出的是 IOException 而子类的重写方法抛出的是 Exception,这样是有问题的。
原因还是出在多态。比如在多态的代码中,父类引用指向子类对象,当调用该引用的方法,比如重写方法时,默认是捕获父类中的声明的那个方法可能会抛出的异常(因为引用的类型是父类),但是子类重写的方法抛出的Exception异常包含了更多的信息,就捕捉不到了。

了解了以上种种,再回头看 Arrays 这个类的 toString 方法。

很明显,Object 类中声明的 toString 方法是下面这样的:
public String toString()
而 Arrays 中声明的一些类重载方法则是这样的:
public static String toString(int[] a)
public static String toString(char[] a)
public static int hashCode(byte a[])
方法名称相同,但是参数列表不同,所以方法签名不一致。据此可以判断,这些重载方法是 Arrays这个子类特有的方法,而不是从Object 继承过来以后重写了。而实际上 Arrays 仍然从 Object 类中继承了原汁原味的toString方法,并没有重写它,一般我们也用不到它。这个其实不重要,重要的是 Arrays 提供了相当多的static修饰的方法供我们调用,我们只需利用它提供的toString 方法输出我们的数组就可以了。它甚至都把自己的构造器设计成了private 类型的,只是为了让我们可以直接使用它提供的各种静态方法而不必创建 Arrays 对象。相当于说,Arrays把自己的一生都献给了解放程序员的伟大事业中。
所以你看 Arrays 就是一个彻彻底底的工具类,而这也正是它位于 java.util 包里的原因吧。
原创粉丝点击