类与接口(四)方法重载解析

一、方法重载简介

方法重载: 当两个(或多个)方法的名称相同,而参数的对应类型或个数不同时,我们就说方法重载了。当然,编译器也能识别出来。

编译器是如何识别调用了哪个方法?

  在往下讲前,我们先来了解一下:编译器是怎么才能识别出程序调用了那个方法。其实,这个问题就是在问:在调用方法处,编译器能得到调用方法的什么信息,从而能找到对应的方法?我们一般的方法调用是这样的:

method( vars );

也就是说,方法调用处,一共为编译器提供两个信息:方法名、参数列表。
所以,编译器只能通过 方法名 和 参数列表 来识别调用方法。

有一道面试题问:为什么不能通过返回类型来重载方法?
就是上面所说的,方法调用处并没有提供返回类型的信息,所以当多个方法只有返回类型不一样时,编译器就不知道调用了那个方法了。

我们已经知道了编译器是怎么识别方法的了,而对于方法重载,其要求方法名是一样的,那么我们只需要关注 参数列表 便可以了。参数列表区分,或者说重载方法的区分:

  • 参数的个数
  • 参数的类型
  • 参数的顺序

二、方法重载的匹配选择

  方法重载后,方法调用处可能会遇到应该选择哪个重载方法的问题,如果只有唯一个重载方法可以匹配,那么就没问题。然而,大部分情况却是有多个重载方法是可以匹配的,那么这时候就应该选择最合适的重载方法.

匹配最合适、最明确的重载方法,其实就是实参列表去匹配当前重载方法中形参列表,寻找与实参列表最相近的形参列表

1、基本类型之间的重载

对于基本类型来说,从“短类型”扩展成“长类型”是默认允许、自动进行的,这就可能造成了实参可能匹配到多个“长类型”的形参,看个简单例子:

 public static void main(String[] args) {
	short s = 4;
	m(s);
}
	
 public static void m(int x){//方法一
	System.out.println("重载方法一");
}
	
 public static void m(float x){//方法二
	System.out.println("重载方法二");
}

运行结果:

重载方法一

short类型 可以默认自动转换成int、'float'类型。但m(s)真正匹配选择的是m(int x)方法,而不是形参长度更长的m(float x)。所以可以看出,基本类型的形参匹配规则是: 如果没有匹配到精确类型的形参,则优先匹配 存储长度(范围)大于且是最接近实参的存储长度的形参,从而确定调用哪个重载方法

2、引用类型间的重载

  对于引用类型来说,可以匹配到多个重载方法的原因是:引用类型的对象进行类型上转也是JVM默认自动进行的,那么就可能匹配多个祖先类型的形参看下面的例子:

public class Test_3 {
   
public static void main(String[] args)  {
    
   Children children = new Children();
   someMethod(children);
}	

public static void someMethod(Ancestor an) {//重载方法1
	System.out.println("this is Ancestor Method!");
}


public static void someMethod(Parent an) {//重载方法2
	
	System.out.println("this is Parent Method!");
}
}

//3个具有继承关系的类
class Ancestor{//祖先类	
}

class Parent extends Ancestor{//父类,继承于Ancestor	
}

class Children extends Parent{//子类,继承于Parent	
}

运行结果:

this is Parent Method!

  可以看出,引用类型与基本类型一样,都是选择”最明确的方法“, 引用类型间选择最明确的重载方法的规则是: 如果找不到重载方法的形参的引用类型与实参一致,则实参优先匹配 在继承树结构上,离实参类型最近的形参,则此形参所在的重载方法便是最明确的重载方法。

3、自动装箱拆箱、可变参数类型

  装箱拆箱、以及可变参数列表的处理都是由编译器自动处理,也就是说是默认自动进行的,这同样会让实参列表可以匹配多个形参列表 ,可以匹配多个重载方法。

  此小节将会涉及到基本类型、引用类型、自动装箱拆箱可变参数的重载方法匹配的优先级。

看下面的例子,这个例子包括很多情况:

public class Test_3 {
   
public static void main(String[] args)  {
    
   short s = 5;	
   overloadMethod(s);// test1
   
   Integer i = 10;
   overloadMethod(i);//test2
   
   overloadMethod(s,s);//test3
}	

public static void overloadMethod(int a) { //m1
	 
	System.out.println("调用  overloadMethod(int)");
}

public static void overloadMethod(Short in) {//m2

	System.out.println("调用  overloadMethod(short)");
}

public static void overloadMethod(int a,int b) {//m3
	
	System.out.println("调用  overloadMethod(int,int)");
}

public static void overloadMethod(short... s) { //m4
	
	System.out.println("调用  overloadMethod(short...)");
}

public static void overloadMethod(Integer... i) {//m5
	
	System.out.println("调用  overloadMethod(Integer...)");
}
}

运行结果

调用 overloadMethod(int)
调用 overloadMethod(int)
调用 overloadMethod(int,int)

我们来分析一下上面的例子中,方法调用处可以匹配到的方法:

  • test1 处的方法调用可以匹配的重载方法有:m1(基本类型的短类型自动转为长类型)、m2(自动装箱)、m4(可变参数列表)
  • test2 处的方法调用可以匹配的重载方法有:m1(自动拆箱)、m5(可变参数列表);
  • test3 处的方法调用可以匹配的重载方法有:m3(基本类型的短类型自动转换成长类型)、m4(可变参数列表)

查看输出结果,发现:test1处选择了m1、test2选择了m1,test3选择了m3。
根据这样的结果,也就是这几种形参匹配规则还是有个匹配的顺序的。对重载方法的选择作以下总结:

  1. 先按照实参的类型(基本类型或引用类型)对应匹配规则,进行查找最相近的形参列表,从而找到最明确的重载方法;找不到,则执行第二步;
  2. 对实参进行装箱或拆箱转换(前提是实参是基本类型或者是包装类),再安按照转换得到的类型进行匹配形参的类型(形参类型与转换类型要一致,特别注意基本类型);找不到,则执行第三步;
  3. 匹配形参是可变参数的重载方法,此时,形参的类型可以是 实参的类型以及通过 基本类型的短转长、自动装箱拆箱、祖先类型 得到的转换类型。

将上面的总结再简化一下,可以简化成 重载方法的形参匹配规则的优先级:

当前类型(基本类型或引用类型)的匹配规则 > 自动装箱拆箱 > 可变参数列表

再看一个例子:

public class MyTest {
	public static void main(String[] args) {
      int a = 5;
      short s = 8;
      m(a,s);
	}
	
public static void m(int a,Short b) {//m1
	System.out.println("调用了m(int,Short)");
}

public static void m(float f,short s) {//m2
	System.out.println("调用了m(float,short)");
}	
}

运行结果:

调用了m(float,short)

分析: 实参都是基本类型,优先考虑形参列表都是基本类型的重载方法,找不到才考虑自动装箱拆箱

4、泛型方法的重载

泛型方法的重载规则: 将泛型方法的类型变量擦除,然后与非泛型方法一样,按照上面所说的三种规则一一匹配

public static void main(String[] args)  {
//创建Runnable对象
Runnable r = new Runnable() { public void run(){} };
//调用泛型方法
  m(r); 
}

public static <T> void m(T t) {//m1
	System.out.println("调用了<T> void m(T)");
}

public static <T extends Runnable> void m(T t) {//m2
	System.out.println("调用了<T extends Runnable> void m(T t)");
}

运行结果:

调用了 <T extends Runnable> void m(T t)

上面的两个泛型方法m(T t)进行类型擦除后是:

public static void m(Object t);

public static void m(Runnable t);

显然,调用方法应该是m2,与运行结果相符;

5. 没法确定的重载方法调用

  尽管编译器会按照上面所说的三种优先级别去让实参匹配形参,然而匹配的结果却不一定是唯一的,也就是说会匹配到多个方法,从而无法确定调用那个方法,编译失败

情况一: 实参列表的所有最佳匹配的形参不在同一个方法中

public class MyTest {
	public static void main(String[] args) {
      int aa = 5;
      short ss = 8;
      m(aa,ss);//编译不通过,无法确定调用了那个重载方法
	}
	
public static void m(int a,double b) {//m1
	System.out.println("调用了m(int,Short)");
}

public static void m(float f,int c) {//m2
	System.out.println("调用了m(float,short)");
}
}

分析:

m(aa,ss)的调用编译失败,因为实参aa的最佳匹配m(int,double)的第一个形参,而实参ss的最佳匹配则是m(float,short)的第二个形参。
因此,实参列表的(aa,ss)的最佳形参类型匹配分开在了两个重载方法中。
注意一下,即使某个重载方法的形参列表包含最多的最相近的形参类型,只要不是全部,那么依旧无法确定调用了哪个重载方法。

情况二: 可变参数列表的特殊性 -- 无法根据可变参数的类型来重载方法

public static void m(short... s) {}

public static void m(Short... s) {}

public static void m(int... s) {}

调用测试例子:

short s = 8;
Short sl = 10;
m(s,s);//编译不通过
m(s,sl);//编译不通过
m(sl,sl);//编译不通过

重写 与 重载的区别

  • 重写是针对父类与子类间的方法,即必须先得继承父类的方法。而重载则没有这种限制。
  • 重写要求方法的 而方法重载则只需要 方法名相同,参数列表不同就行了。
  • 方法重载时,方法的调用是在编译时期就已经确定了调用那个方法;方法重写,则要在运行时,才能确定调用的是子类还是父类的方法。
posted @ 2018-03-03 21:46  jinggod  阅读(2529)  评论(0编辑  收藏  举报