前言: 何为重载,类中两个方法同名,但是参数列表不同,称为重载。注意,是参数列表,不是返回值,实际上同名方法同参数列表,但不同返回值是不允许的。
// 重载示例,重载方法允许有不同的返回值类型
public void dosh(){
//
}
public Long dosh(String s){
//
}
// 重载方法允许重载的参数列表是父子类。
//如以下的重载。在实际执行的时候,会优先选择更精确的匹配,
//也就是如果参数是 Integer,就走第一个,如果参数不是Integer,才走第二个方法
public void dosh(Integer i){
//
}
public void dosh(Object o){
//
}
// 重载也包括参数个数不同
public void dosh(Integer i){}
public void dosh(Integer i,String s){}
// 但是,重载不能是方法参数列表相同而返回值不同,如下重载是错误的,会编译报错
public String dosh(){}
public void dosh(){}
public Integer dosh(){}
注意: 重载方法的选择是在编译时就确认而不是运行时确认。
如下实例,参数o 实际就是 String , 但在编译时,它用了 Object 来修饰,那么 jvm 就选择它执行的方法参数为 Object,而不是根据它实际运行时的 String 类型
public class Period {
public static void main(String[] args) {
String s = "str";
Object o = s;
Period period=new Period();
period.dosh(o);
}
public void dosh(String s){
System.out.println("参数为 String");
}
public void dosh(Object o){
System.out.println("参数为 Object");
}
}
// 方法重载的选择是会选择最精确的匹配,如上面的实例,我们的参数为 Integer,当方法重载的时候,java 会优先使用参数为 integer 的方法,也就是 dosh(Integer i). 当此方法不存在(被删除)时,也会自动选择 dosh(Object o ) ,因此,重载方法可能造成的问题就是在重构的时候,可能导致程序编译不报错,但实际执行的方法已经变了,可能造成程序在运行时才会出错,更大的麻烦可能是程序没有报错,但执行一段时间后,我们发现了数据错误,又很难找到原因
比如,程序最开始设计如下
// class A
@Autowired
private B b;
public void test(){
// 获取参数 m
Integer m = xxx;// 根据业务,算出一个值
b.dosh(m);
}
-------------------------------------------------------
// class B
//情况1
public void dosh(Integer i){
}
//情况2
public void dosh(Object o){
}
上面的程序中,情况1 和2 走向了不同的处理。 为了处理这些情况,我们提供了 B 类来辅助完成。
一段时间后,B 的维护者因为某些原因,移除了 dosh(Integer i)
这时,class A 依然可以正常编译,不会报错,但其执行路径却已经悄悄换成了情况2
// class A
@Autowired
private B b;
public void test(){
// 获取参数 m
Integer m = xxx;// 根据业务,算出一个值
b.dosh(m);
}
-------------------------------------------------------
// class B
public void dosh(Object o){
}
对于 A 来说,没有发现B 相关的编译错误,也不可能天天关心上层有没有发生变化,或者说从使用者角度来看待,B 既然提供了这个方法,并且我没有编译错误,那B 就应该能正确执行
但对于 B 来说,他可能是无心的,即便他随着升级也可能提供相应的 change log, 或者其他方法的文档,但这种远远不如 直接程序报错来的实在
如果 ide 编译错误,直接提示我们这个方法不存在,那我们就可以针对性地找相应文档,检查应该如何更新。而如果没有报错,很多时候我们也就默认程序没有发生变化。
这就是重载容易造成的问题。
上面的例子还算容易处理,稍微注意观察一下就能看出来。
我们现在用的 jdk 多数是8,即便不是8,也基本上都是 jdk5及之后的了。
jdk5 有个重要的新增特性,自动装箱拆箱,也就是 包装类和基本类的转换 jdk会自动处理
比如 有个方法需要的参数是 Integer ,但我们传参时候传一个 int 是可以的。同样的,一个需要 int 参数的方法,我们传 Integer 也可以
并且,泛型也是5版本引入的,也就是,我们现在所开发的程序绝大多数都有这两个特性。
泛型 + 自动装箱拆箱 + 方法重载,这三个本来是方便开发的功能,但他们混合在一起时,却很容易造成方法执行路径不是我们预期的。很多程序员不注意 Integer 和int 的选择,而这就是重载方法选择错误的原因
典型的例子
// 这个是正常的
public void removeInvalid(List<String> list,String s){
if(list.contains(s)){
list.remove(s);
}
}
// 这个方法执行的就不是预期的结果,甚至可能报错
public void removeInvalid(List<Integer> list,int x){
if(list.contains(x)){
list.remove(x);
}
}
上面例子中,两个方法逻辑是类似的,如果 list 中有指定的元素,就执行移除。
但我们需要注意 , List 中的 api 方法
remove(E e) ,移除指定的元素,其逻辑是使用 e 与 List 中的元素进行对比,如果 == 或 equals(当然,==是 equals 的特殊情况)
remove(int index) , 移除指定下标位置的元素
在第一个例子中, E 泛型实际类型是 String, list.remove 实际执行的是 remove(E e)
而第二个例子中,java 帮我们处理了装箱拆箱的问题,但却恰恰因为这个过程是自动的,我们看不见,就出现了错误
contains方法是 contains(E e),这里用了 Integer, 所以 x 被自动装箱,也就是这一行代码的实际执行是 if(list.contains(Integer.valueOf(x))){
而到了第二行,根据精确匹配,java 选择了 List 接口的 remove(int index),所以,其实际移除的并不是想要的元素 x,而是把x当成下标,去移除指定位置的元素
如何避免因为重载导致的这种问题?
可以使用更精确的命名来处理
比如,之前的两个方法可以根据参数改为
doshString(String str)
doshObject(Object o)
doshInteger(Integer i)
实际上,java.util.List 因为历史兼容问题,只能沿用旧的名称,导致 remove 方法出现了这种歧义,如果有更精确的名称,这样的问题也就不存在了
remove(E e) -> removeElement( E e)
remove(int index) -> removeByIndex(int index)
在本条目中,我们应该得到的是,区别“可以使用重载”与“应该使用重载”
什么时候合适用重载?什么时候不应该用重载
当重载方法背后的逻辑一致时才应该使用重载,否则建议使用更全的方法名来区分这些方法。
应该重载
// 如下,此重载仅仅是照顾入参的格式,实际的行为是一致的
// 这种重载设计在我的程序中比较常见,也就是考虑到不同平台交互的时候传输的数据格式问题
// 实际需要的是 String,但涉及到数据交互,多给个重载方法,也方便用户使用方便
public boolean dosh(String num){
return dosh(Integer.valueOf(num));
}
public boolean dosh(Integer num){
}
不应该重载
// 下面两个方法就不应该做成重载
// 虽然他们都是新增操作,但新增教师和学生,所做的后续处理可能完全不同,这种就更应该做成 addStudent(Student student) addTeacher(Teacher teacher)
public boolean add(Student student){
// 实际业务逻辑
}
public boolean add(Teacher teacher){
// 实际业务逻辑
}