工程项目中,定义 API 总是一个慎之又慎的操作。不能少,不满足调用方的需求就惨了;也不能多,不然就乱套了,自己维护困难,调用方也开始了自我发挥。虽然足够慎重,但绝大多数都逃不过最终不得不“改” API 的情况。今天要讨论的是在同一个类内同方法名不同参数(入参/出参)的情况。

想要做到同方法名不同入参,很简单,就是“重载(Overload)”,日常都在使用。不再赘述!

想要做到同方法名不同出参,答案就不再那么肯定了。当然,如果问把void add(int) API 改写成 int add(int),可能得到的大多数回答都是可以。

看山是山

首先举一个具体点的例子来描述(为了方便,就不定义CountService的接口类了)

package com.ffutop.signature;

/**
 * 主类
 * @author fangfeng
 * @since 2019-02-26
 */
public class Main {

    public static void main(String[] args) {
        CountService countService = new CountService();
        countService.add(1);
        System.out.println(String.format("currentValue = %d", countService.getCurrentValue()));
    }
}
package com.ffutop.signature;

/**
 * @author fangfeng
 * @since 2019-02-26
 */
public class CountService {

    private int currentValue = 0;

    /**
     * 请把 add(int) 理解成 API
     * 虽然已经做了实现
     */
    public void add(int addend) {
        currentValue += addend;
    }

    public int getCurrentValue() {
        return currentValue;
    }
}

现在已经有 void add(int) 方法,完成的工作是累加。现在要把 API 改成 int add(int),是否可以呢?看似本来没有返回值,现在只是加上一个返回值,对于原来的代码没有任何问题,而新代码又能拿到int类型的返回值,皆大欢喜啊。

看山不是山

先给大家执行下代码吧!(请严格按照顺序,且避免使用IDE,否则将得不到预想的结果)

$ # 准备好两个类的代码(CountService的API是 `void add(int)`)
$ # 编译Main类
$ javac com/ffutop/signature/Main.java
$  
$ # 修改CountService的API为`int add(int)`
$ # 编译 CountService 类
$ javac com/ffutop/signature/CountService.java
$ 
$ # 执行主程序
$ java com.ffutop.signature.Main
Exception in thread "main" java.lang.NoSuchMethodError: com.ffutop.signature.CountService.add(I)V
	at com.ffutop.signature.Main.main(Main.java:11)

很遗憾,执行失败了,报了个Error(没有这样的方法)。方法写的是 com.ffutop.signature.CountService.add(I)V 。简单的翻译一下就是需要类名+方法名=x.y.CountService.add,且入参为int,出参为void的方法(想了解更多请优先学习Java ClassFile Format)。

那么,现在得到的结论是不行。

看山还是山

那么,就一定不行吗?都是同名方法,不同参数。凭什么改个入参是可以的,改个出参就不行了。

从 Java 的角度来说,一定不行。“同一类中不能存在两个名字及描述符完全相同的方法”

但是,如果从 JVM 的角度来说,完全是可行的。“在class文件中,两个方法可以拥有完全相同的特征签名,前提是返回值不能相同”

什么意思呢?我们都知道建立在 JVM 之上的语言不只有 Java。像 Groovy、Kotlin 等语言都实现了各自的区别于 Java 独有的特征。这些特征的实现都是依赖于 JVM,但为什么 Java 没有呢?只能说 Java 语言的规范是 JVM 的子集(好像这话有点不严谨啊)

通过直接操作字节码,就能够达到在同一个类中建立两个同名、相同入参,但返回值不同的方法。 先通过javap命令看看最终提供的CountService.class

$ javap com.ffutop.signature.CountService
public class com.ffutop.signature.CountService {
  public int add(int);
  public com.ffutop.signature.CountService();
  public void add(int);
  public int getCurrentValue();
}

有两个同名的方法add(int),至于执行,也会相当顺利。

还是写个程序来说明,在原有 Main.java 的基础上,再创建一个全限定名为 com.ffutop.signature.other.Main2 的类

package com.ffutop.signature.other;
import com.ffutop.signature.CountService;

/**
 * @author fangfeng
 * @since 2019-02-27
 */
public class Main2 {

    public static void main(String[] args) {
        CountService countService = new CountService();
        System.out.println(String.format("currentValue = %d", countService.add(1)));
    }
}

与 Main.java 比较,很明显的就是一个调用了 CountServiceint add(int) 方法,而另一个调用 void add(int) 方法。

那么是否有效呢?先来验证一下(这里要模拟的一个场景是,CountService 类(只有 void add(int) 方法)作为二方库C.jar version 1.0 的主要服务类。CountService 类(同时有 void add(int)int add(int) 方法)作为二方库C.jar version 2.0 的主要服务类。Main 类根据 C.jar version 1.0 做的编译,而 Main2 类根据 C.jar version 2.0 做的编译。在 Main 和 Main2 类运行时只提供 C.jar version 2.0)

$ # 编译 Main 类和 CountService 类
$ javac com/ffutop/signature/Main.java
$
$ # 操作 CountService.class 字节码,增加方法 `int add(int)` 
$ # 通过 ASM 实现,详见附录源码 com/ffutop/signature/support/Generator.java
$ 
$ # 编译 Main2 类 (提供 classpath,即根据 CountService.class 编译)
$ javac com/ffutop/signature/other/Main2.java -classpath .
$
$ # 验证
$ java com.ffutop.signature.Main
currentValue = 1
$ java com.ffutop.signature.other.Main2
currentValue = 1
$ # OK,验证通过

总结

一旦 API 定义错误,并已经作为二方库提供给调用方。后果必然是灾难性的。至少,只能重新定义 API 。如果对于入参定义错误,Java 语言级别也能够支持。但如果是返回值,如果非要保持方法名一致,那就不得不下沉到 JVM 级别来进行处理了。当然,在工程项目中是否应该直接操作字节码?至少个人还没直接接触过。

做个记录,未来可以翻一翻,至少是一种可行的解决方案。

Update: 如果直接操作字节码为添加了不同返回值的同名同参数方法,可能引起调用方静态编译失败。此操作需慎之又慎。也许提供类加载器加载时的字节码操作能更加完美地解决这个问题。

参考

源码.zip

  __                    __                  
 / _| __ _ _ __   __ _ / _| ___ _ __   __ _ 
| |_ / _` | '_ \ / _` | |_ / _ \ '_ \ / _` |
|  _| (_| | | | | (_| |  _|  __/ | | | (_| |
|_|  \__,_|_| |_|\__, |_|  \___|_| |_|\__, |
                 |___/                |___/