Arrays里的各个重载的toString方法是否继承自Object,arraystostring
Arrays里的各个重载的toString方法是否继承自Object,arraystostring
java.util.Arrays 提供了一系列常用的静态方法,如 sort,asList等等,toString是其中之一。我们经常用但很少去思考这样一个问题——它是否是重写的 Object 类的 toString 方法?
这个问题的答案很简单。但是本着借着问题发散思维的原则,咱们好好地探讨一下这个事儿。
要解答这个问题,我们有必要回到“重写(override)是什么”这个本源,而要想回答“重写(override)是什么”,最好先搞清楚一个更加原始的问题——“怎么区分一个方法和另外一个方法”。
Java用类作为代码组织的基本结构,里面包裹着属性和方法。为了满足多样化的需求,一个类往往需要定义很多方法。方法一多,方法命名就成了大问题。这就像玩网游的时候起昵称一样,由于中国人多,随便想一个昵称往往都已经被占用了,让人很头疼。在游戏世界,昵称是区别一个玩家和另一个玩家的关键。在编程语言里面,区别一个方法和另一个方法显得稍微复杂了一点(但这是为了满足多样化的需求,或者为了实现多样化的功能)。
在 Java 中,区别一个方法和另一个方法的关键是“方法签名”。
方法签名不是单一元素,而是一个组合。方法签名由方法名+参数列表构成,而参数列表又因参数个数,参数类型,参数顺序的不同而不同。(方法签名的这种组合形式类似于数据库中数据表的复合主键,多个列的组合才能唯一确定一行记录)在 Java 中,方法签名不同才代表两个不同的方法。
代码示例:
public class TestMethodName {
public void speak(Japanese japanese) {}
public void speak(Chinese chinese) {} //和①相比,参数类型不同
public void speak(Japanese japanese1, Japanese japanese2){}//和①相比,参数个数不同
public void speak(Chinese chinise, Japanese japanese) {}
public void speak(Japanese japanese, Chinese chinese){}//和④相比,参数顺序不同
public void speak(Japanese kansai, Chinese Putonghua){}//和⑤相比,仅仅是参数名称不同,是不行的
public String speak(Japanese japanese, Chinese chinese){return "what?";}//和⑤冲突
private void speak(Japanese japanese, Chinese chinese){}//和⑤冲突
public static void speak(Japanese japanese, Chinese chinese){}//和⑤冲突
public final void speak(Japanese japanese, Chinese chinese){}//和⑤冲突
}
代码分析:
这里,speak 是方法名称,圆括号里面的是参数列表。(以上示例代码中的各方法的方法名相同,只是参数列表不同,这种情况即我们平常所说的重载)
②和①相比,参数类型不同,所以它们是不同的方法。
③和①先比,参数个数不同。
⑤和④相比,参数类型的顺序不同。
⑥和⑤相比,参数个数,顺序,类型都相同,只是参数名称不同而已。这种情况并不构成重载,而且在运行时没法区分它和⑤哪个才是应该被调用的方法,因为这里的参数只是形参而已,名称可以随便起,没法标识一个方法,编译通不过。
⑦和⑤相比,参数个数,顺序,类型都相同,只是返回值类型不同而已。凭我们的直觉,好像返回值类型不同足以区分一个方法和另一个方法了,但实际不行。关于这一点,《Thinking in Java》里有下面一段论述:
读者可能会想:“在区分重载方法的时候,为什么只能以类名和方法的形参列表作为标准呢?能否考虑用方法的返回值来区分呢?”比如下面两个方法,虽然它们有同样的名字和形参,但却很容易区分它们:
void f() {};
int f() {return1;}
只要编译器可以根据语境明确判断出语义,比如在 int x = f() 中,那么的确可以据此区分重载方法。不过,有时你并不关心方法的返回值,你想要的是方法调用的其它效果(这常被称为“为了副作用而调用”),这时你可能会调用方法而忽略其返回值。所以,如果像下面这样调用方法:
f();
此时别人该如何理解这种代码呢?因此,根据方法的返回值来区分重载方法是行不通的。
写得简洁明了。我们也可以从另外一个角度来理解这个事。我们之所以感觉可以通过返回值类型来区分不同的方法,是因为我们只看到了源码。在实际运行的时候,方法被加载到方法区,调用方法的时候,JVM需要先寻找该方法(实际是去方法表查存储该方法代码的地址,有点类似于老电影里通过公共电话亭的电话簿查某个人的住址)。具体是先通过方法名检索出所有匹配的代码,然后通过参数列表进行匹配,确定最终应该调用哪一个方法。方法表里面只有方法名和参数列表的信息,是不管返回值类型的。返回值类型要等到方法执行以后才知道。所以从这个角度来说,也是可以得出不能以返回值类型作为判断是否重载或者区分不同方法的依据的。
⑧⑨⑩和⑤相比,参数个数,顺序,类型都相同,分别只是前面的访问控制符,static修饰符,final修饰符不同而已。这样也是无法区分两个方法的。参数个数,顺序,类型都相同,这已经唯一确定了一个方法。而这样的方法在当前类里只能存在在一个。
知道了怎么区别一个方法和另一个方法以后,我们来看重写(override)。
重写的前提是继承。通过继承,子类把父类的非 private 属性和方法都拿来。子类如果对从父类继承过来的某个方法的方法体(一般来说,只改方法体)进行修改,使得子类方法和父类方法方法签名相同但是方法体(方法实现)不同的话,这就构成了重写。但是需要注意的是,虽然说只要子类方法和父类方法的方法签名相同,方法体不同就可以说是重写了,但是一般来说,方法的返回值类型也必须保持一致。原因在于,重写是为多态做准备的,这是它存在的唯一意义。在多态中,父类引用指向子类对象,通过父类引用调用的方法是子类重写后的方法。子类重写父类的方法,往往是由于父类中的方法没有方法的实现(有意设计成抽象类和抽象方法),子类继承过来以后进行具体代码实现。这样的话,它们其实是为了实现相同的功能,只不过父类中声明了方法,指明了方向,而子类实现了方法,完成了大业。就好比,父类定义的方法是反清复明,返回值类型是明朝,子类成功推翻了满清,却返回了共和国。这显然已经违背了最初的想法。(从这个比喻来看,其实 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 不构成协变返回,int 是基本数据类型,和 Object 不构成继承关系。如果是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)。可以一样,也可以比父类的大,一般都保持一致。
比如父类用的是 public,子类改成了默认的包访问权限,这样就会报错。原因在于,在多态中,父类引用指向子类对象,将父类引用作为参数传递给另外一个 package 中的方法,该方法采用通用的写法,调用父类引用的方法。而在实际运行的时候传递过去子类的对象引用,从而调用子类重写后的方法,以达到多态的目的。但是,由于子类的访问控制符的权限是同一个包内可访问,这样就会由于无法调用(访问)子类的方法而报错。
示例代码(编译时报错 attempting to assign weaker access privileges):
package util;
import entity.Person;
import entity.Japanese;
public class Controller {
public void speak(Person p) {
p.speak();
}
public static void main(String[] args) {
Controller c = new Controller();
Japanese japanese = new Japanese();
c.speak(japanese);
}
}
package entity;
public abstract class Person {
//public访问权限
public abstract void speak();
}
package entity;
public class Japanese extends Person{
//默认的包访问权限
void speak() {
System.out.println("おはよう");
};
}
②在抛出异常方面,子类重写的方法抛出的异常的检查范围不能比父类的大。
比如父类抛出 IOException 而子类的重写方法抛出的是 Exception,这样是有问题的。原因还是出在多态。在多态中,父类引用指向子类对象,将父类引用作为参数传递给另外一个类的方法,该方法采用通用的写法,调用父类引用的方法。由于父类只抛出IOException,所以在此处也会声明抛出IOException。而在实际运行的时候传递过去子类的对象引用,从而调用子类重写后的方法,以达到多态的目的。但是,由于子类定义为可能会抛出更多的异常(检查范围更大如本例子中Exception大于IOException,或者子类多抛出一个ClassNotFoundException),所以在实际运行的时候,多出的异常部分无法被合适地处理(无法按照自己预想的那样被处理)。
再回头看 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 方法,但是没有重写它,因为没有任何意义。(重写 toString 方法一般是借助对象内部的属性,表述当前对象的相关信息。比如在 Student 类里重写 toString,从而在创造对象时可以生成一个姓名为某某某,年龄为多少的实体对象。而 Arrays 作为一个工具类,完全没有这方面的必要。)使用 Arrays 特有的 toString 方法,可以很方便地把各种类型的数组按照一定的格式输出。而且,这些方法都被 static 修饰,所以我们不必创建 Arrays 对象就可以使用它们。实际上,Arrays 把自己的构造器设计成了 private 类型的,toString 以外的方法也都加了 static, 这样以来,它就变成了一个彻底的工具类了。
相关文章
- 暂无相关文章
用户点评