Java对方法重载(Overloading)的定义:如果有两个方法的方法名相同,但参数不一致,那么可以说一个方法是另一个方法的重载。具体说明如下:

  • 方法名相同
  • 方法的参数类型,参数个不一样
  • 方法的返回类型可以不相同
  • 方法的修饰符可以不相同
  • main 方法也可以被重载

 

 

一文读懂《Effective Java》第41条:慎用重载_泛型集合方法的重载

 

我们看个例子,根据一个集合的实现类型对其进行分类:

 

  •  

/** * <p> * overloaded,需要调用哪个重载方法是在编译时做出的觉得。对于三个重载方法,他们的参数编译时类型(类型擦除)都是一样的:Collection<?> s * 重载(overloaded method)区别于覆盖方法(overridden method): * 重载方法选择是静态的,覆盖方法选择是动态的。(被覆盖的方法是在运行时决定,依据是被调用方法所在对象的运行时类型) * </p> */
public class CollectionClassifier { //重载1 public static String classify(Set<?> s) { return "set"; } //重载2 public static String classify(List<?> s) { return "list"; } //重载3 public static String classify(Collection<?> s) { return "Unknown Collection"; }
public static void main(String[] args){ Collection<?>[] collections = { new HashSet<String>(), new ArrayList<String>(), new HashMap<String, String>().values(), }; for (Collection<?> c : collections) { System.out.println(classify(c)); } }}

 

输出结果:

  •  
Unknown CollectionUnknown CollectionUnknown Collection

 

一文读懂《Effective Java》第41条:慎用重载_spark_02

 

一文读懂《Effective Java》第41条:慎用重载_子类_03

你期望打印结果是:“Set”,然后是“List”,以及“Unknown Collection”,实际上不是。原因是:classify 方法被重载了,具体需要调用哪个重载方法是在编译时做出决定的(因为Java在编译期间,所有的泛型信息都会被擦掉),因此,唯一合适的重载方法是第三个 classify(Collection<?> s)。

 

我们反编译下.java文件,可以看到编译后的.class文件内容

一文读懂《Effective Java》第41条:慎用重载_子类_04

 

JD-GUI打开CollectionClassifier.class:

一文读懂《Effective Java》第41条:慎用重载_子类_05

 

 

一文读懂《Effective Java》第41条:慎用重载_泛型方法重载与方法覆盖的区别

 

 

通过例子,可以比较出方法覆盖与方法重载的区别:

  •  
public class Overriding {  public static void main(String[] args) {    Wine[] wines = {new Wine(), new SparklingWine(), new Champagne()};    for (Wine wine : wines) {      System.out.println(wine.name());    }  }}
class Wine{ String name(){return "wine";}}
class SparklingWine extends Wine{ @Override String name() { return "SparklingWine"; }}class Champagne extends SparklingWine{ @Override String name() { return "Champagne"; }}

 

输出结果:

  •  
wineSparklingWineChampagne

 

 

重载方法选择是静态的,覆盖方法选择是动态的(被覆盖的方法是在运行时决定,依据是被调用方法所在对象的运行时类型)

这里说明下,当一个子类包含的方法声明与祖先类中的方法声明具有相同的签名时,方法就被覆盖。如果实例方法在子类中被覆盖了,而且这个方法是在子类的实例被调用的,那么子类的覆盖方法将被执行,不管该子类实例的编译类型是什么。

得出结论:与方法重载相比,对象运行时类型并不影响“哪个重载方法版本将被执行”;选择方法是在编译时进行,完全基于参数的编译时类型。

06

 

一文读懂《Effective Java》第41条:慎用重载_泛型覆盖机制与重载机制

 

覆盖进制是规范,而重载机制是例外。所以,覆盖机制满足人们对于方法调用方法行为的期望。如果编写出来的代码行为可能使得程序员困惑,那就是糟糕的实现。

  • 应该避免胡乱使用重载机制,安全而保守的策略是,永远不导出两个具有相同参数数目的重载方法。如果方法是“可变参数(varargs)”,保守策略是你根本不要重载它。
  • 对于构造器,我们没有选择使用不同名称的机会,一个类的多个构造器总是重载的。
一文读懂《Effective Java》第41条:慎用重载_spark_02

 

一文读懂《Effective Java》第41条:慎用重载_泛型自动装箱和泛型对重载机制的影响

 

所有的基本类型都根本不同于所有的引用类型,但是当自动装箱出现之后,就不再如此了,它会导致真正的麻烦。见下面的例子:

 

  •  
public class SetList {  public static void main(String[] args) {    TreeSet<Integer> set = new TreeSet<>();    ArrayList<Integer> list = new ArrayList<>();
for (int i = -3; i < 3; i++){ set.add(i); list.add(i); } System.out.println(set); System.out.println(list);
for (int i = 0; i<3; i++){ set.remove(i); //i:object to be removed from this set, if present list.remove(i); // i:the index of the element to be removed,每次操作,底层数组的元素下标都会变化 } System.out.println(set + " " + list); }}

 

输出结果:

 

  •  
[-3, -2, -1, 0, 1, 2][-3, -2, -1, 0, 1, 2][-3, -2, -1] [-2, 0, 2]

 

 

      实际情况是:set.remove(i) 选择重载方法remove(E),这里E是集合的元素类型(Integer),将 i 自动装箱到Integer中;list.remove(i)调用选择重载方法remove(i),从指定位置去除元素。

    因为List<E> 接口有两个重载的remove方法:remove(E)和remove(int)。

  • 在Java 1.5 发行版中被泛型化前,List 接口有一个 remove(Object) 而不是 remove(E),参数类型是:Object 和 int

  • 有了自动装箱和泛型之后,这两种参数类型就不再根部不同了。即是说,Java语言添加了泛型和自动装箱破坏了List 接口。

  • 幸运的是,Java 类库几乎没有API 受到同样的破坏。但也说明了,我们需要谨慎重载显得更加重要了。

06

 

 

一文读懂《Effective Java》第41条:慎用重载_泛型总结

 

“能够重载方法”并不意味着就“应该重载方法”。

一般情况下,多个具有相同参数数目的方法来说,应该尽量避免重载方法。

至少避免这种情形:同一组参数只需经过类型转换就可以被传递给不同的重载方法。

如果不能避免这种情形,例如为正在改造的一个现有的类以实现新的接口,就应该保证:当传递同样的参数时,所有重载方法的行为必须一致。如果不能做到这一点,程序员就难有效使用被重载的方法或者构造器,因为不能理解它为什么不能正常的工作。

一文读懂《Effective Java》第41条:慎用重载_spark_02

 

 

一文读懂《Effective Java》第41条:慎用重载_spark_12END

 

 

扫描二维码

获取知识干货

后台技术汇

一文读懂《Effective Java》第41条:慎用重载_自动装箱_13