工程项目中,定义 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 比较,很明显的就是一个调用了 CountService
的 int 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: 如果直接操作字节码为添加了不同返回值的同名同参数方法,可能引起调用方静态编译失败。此操作需慎之又慎。也许提供类加载器加载时的字节码操作能更加完美地解决这个问题。
参考
__ __
/ _| __ _ _ __ __ _ / _| ___ _ __ __ _
| |_ / _` | '_ \ / _` | |_ / _ \ '_ \ / _` |
| _| (_| | | | | (_| | _| __/ | | | (_| |
|_| \__,_|_| |_|\__, |_| \___|_| |_|\__, |
|___/ |___/