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

本文探讨了Java中方法重载(Overloading)与方法覆盖(Overriding)的区别,通过实例详细解释了如何确定调用哪个重载方法,并讨论了在编译期与运行期的选择差异。此外,还分析了自动装箱和泛型对重载机制的影响。

点击上方「蓝字」关注我们

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

  • 方法名相同

  • 方法的参数类型,参数个不一样

  • 方法的返回类型可以不相同

  • 方法的修饰符可以不相同

  • main 方法也可以被重载

集合方法的重载

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



/**
 * <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 Collection
Unknown Collection
Unknown Collection

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

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

JD-GUI打开CollectionClassifier.class:

方法重载与方法覆盖的区别

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

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";
  }
}


输出结果:

wine
SparklingWine
Champagne

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

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

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

06

覆盖机制与重载机制

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

  • 应该避免胡乱使用重载机制,安全而保守的策略是,永远不导出两个具有相同参数数目的重载方法。如果方法是“可变参数(varargs)”,保守策略是你根本不要重载它。

  • 对于构造器,我们没有选择使用不同名称的机会,一个类的多个构造器总是重载的。

自动装箱和泛型对重载机制的影响

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

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

总结

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

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

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

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

—END—

扫描二维码

获取知识干货

后台技术汇

点个“在看”表示朕

已阅

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后台技术汇

对你的帮助,是对我的最好鼓励。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值