从foo到foo()再到Python的metaprogramming,这回又折腾回Java了。
最初失败的实现在这里: https://0xffff.one/d/447/10
最初我猜测这个问题是句法细节上出了问题,于是问了一个有Java经验的人,他把问题定位成metaprogramming并检查使用break point的逐行纠错功能debug,然而由于我使用的版本跟他的有所不同,Intellj IDEA的逐行纠错,暂时搁置。
第二天重新看源码时候发现public Method getMethod(String name, Class<?>... parameterTypes) 签名里的Class<?>... parameterTypes 之前被错误解读了。这个通配符可以暂时去掉,这里要求的是输入被搜索方法的参数的类型,比如输 myAccount 的类型是BankAccount , "hello"的类型是String。
于是我抛开最开始的类,新整了一个main方法,采用命令式编程中间增加输入的方法探索,发现果然问题出在这里。
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
BankAccount myAccount= new BankAccount("Shawn");
Class cls= myAccount.getClass();
// System.out.println(cls.toString());
Method m1= cls.getMethod("deposit", double.class);
// System.out.println(m.toString());//public void BankAccount_Proxy.BankAccount.deposit(double)
m1.invoke(myAccount,30);
System.out.println(myAccount.getBalance());
Method m2= cls.getMethod("getBalance");
System.out.println(m2.invoke(myAccount));
}
这样顺腾摸瓜修改原来的class,一开始方法有这么些内容:
public Object callMethod(String methodName, Class classType, Object ...params){
Class cls = realAccount.getClass();
try{
if (classType!=null){
Method method=cls.getMethod(methodName,classType);
Object returned= method.invoke(realAccount,params);
return returned;
}
else {
Method method=cls.getMethod(methodName);
Object returned= method.invoke(realAccount);
return returned;
}
}
//省略很多
这个尝试是可以运行的。
然而一方面被调用方法参数的类型要自己声明,另一方面参数(Object ...params)是一个vararg。看起来很周到齐全,但运行时首先面临getBalance里面输入null之后的vararg警告,另一方面声明方法参数的类型显得多余且滑稽。
进一步探索之后发现这跟Java的primitive type问题有关,王垠指出Java的原始类型(“值类型”)其实是引用类型「https://www.yinwang.org/blog-cn/2016/06/08/java-value-type」,Sebesta的COPLs通过C类语言和Ruby的对比也探讨过这个问题,结论一样。
Java,Scheme 等语言的原始类型,比如 char,int,boolean,double 等,在“实现”上确实是通过值(而不是引用,或者叫指针)直接传递的,然而这完全是一种为了效率的优化(叫做 inlining)。这种优化对于程序员应该是不可见的。Java 继承了 Scheme/Lisp 的衣钵,它们在“语义”上其实是没有值类型的。
语义上的类型同一(int本质和BankAccount一类)与class(类)的分类方面乍一看严格二分:原始类型的.class()
和derived type(派生类型;继承object的具体类)的o.getClass()```。然而Object类和Class类本身是跟原始类型一样
.class()````的,这说明Java设计过程中考虑到了这个问题,避免了将原始类型从对象中剔除。
而null没有class(类),感觉这可能涉及罗素悖论了,以后有机会再比较Python、Ruby、R甚至Scheme对这个问题的处理吧。
由于上面的原因,接下来我出于简便考量把vararg的params改成一个参数的param、删去Class classType并直接对param进行.getClass()
的操作失败了,导致deposit
无法调用。
经过一番尝试,我决定来个权宜之计,删去Class classType的同时直接把param分成两种情况:要么是没有参数(void方法)要么就是double,因为这个案例中的方法非常简单,根据“不过多提前考虑没有遇到的情况”的原则,代码修改成如下:
public class AccountReflectionProxy {
public BankAccount realAccount;
public AccountReflectionProxy(BankAccount realAccount) {
this.realAccount = realAccount;
}
public Object callMethod(String methodName, Object param){
Class cls = realAccount.getClass();
try{
if (param!=null){
//here I made a workaround to avoid evaluating
//the class type of any primitive types
//as here it is known in advance that
//the method parameter will be only double unless null
Method method=cls.getMethod(methodName,double.class);
Object returned= method.invoke(realAccount,param);
return returned;
}
else {
Method method=cls.getMethod(methodName);
Object returned= method.invoke(realAccount);
return returned;
}
}
catch (NoSuchMethodException e){
throw new IllegalArgumentException(cls.getName() + " does not support "+methodName);
}
catch (IllegalAccessException e){
throw new IllegalArgumentException("Insufficient access permission to call" +methodName + " in class "+ cls.getName());
}
catch (InvocationTargetException e){
throw new RuntimeException(e);
}
}
}
继续无参数和有参数方法的二分,但对有参数的直接指明double类型了。
然后主程序也就愉快的修改好了:
public class AccountReflectionProxyRunner {
public static void main(String[] args) {
BankAccount myAccount = new BankAccount("Shawn");
AccountReflectionProxy firstAccount = new AccountReflectionProxy(myAccount);
firstAccount.callMethod("deposit", 30);
firstAccount.callMethod("withdraw", 5.6);
Object currentBalance = firstAccount.callMethod("getBalance", null);
System.out.println("The account balance is now: " + currentBalance);
}
}
一句正确输出 The account balance is now: 24.4
折腾了这么多,是不是也萌萌哒?
讨论:
1 Java的类型系统还是周全的。听说Spring里面有一个ClassUtils.getClass()可以直接放任何类型的东西,避免了原始类型和派生类型的对立,我会在未来关注。
2 Java的reflection不是metaprogramming,后者的定义是把程序本身作为数据。虽然我没用很多reflection,但核心的原理还是用到了,完全没有元编程的内容。反而这是我姑且称之为“静态中体现动态”/“静中取动”的Java特点,是运行过程中确定和返回一些大框架之下的内容(比如int也好,String也好都逃不出Object)。
3 结合以上两点,我能部分理解王垠对Ruby甚至Javascript、Python的批评原因了,虽然他没具体指出他们问题我也没怎么用过Javascript,说白了就是片面从“面向对象”出发搞一些小技巧,但这些小技巧/优化从深层不利于整个语法结构和程序的稳定。
这样说来,Python从严谨度方面高于Ruby,毕竟Python是高度基于C「有人是它就是C的shell」。 subject.send(name, *args)
这种东西在Ruby中很自然,而Python的实现比较复杂,Java的实现更复杂了。Python和Java同样要求必须确定method的存在,Java则要提前知道method的参数类型。
4 异常机制在这个例子中显得非常重要,Java的异常机制用王垠的话说是“union type”,这个概念在有些书中有误解,留带后续了解。
其他语言的异常机制和设计也值得研究,不了解异常就是不真正了解一门语言。
5 在解决问题过程中一度参考汇编语言层面的Java类型系统,这是一个错误的角度。结合https://0xffff.one/d/447/9 提到的“低级到高级”范式误区,任何语言的设计都自有一套,编译器(虽然还不懂,但提上日程了)是有自己独立原理的设计,比如tail recursion(尾归)问题,有的Scheme编译器能自行优化到非尾归的情况,这个据我所知Java没有这种原理可以实现。
6 工具是给人用的。1.5/5是Java的里程碑,之后新出现的很多工具被人诟病或者很少用,某种意义反映了Java的成熟。
Pysonar里面没有用到任何Reflection , Hashmap和Map用了不到15次,而且多是一个类中用一次。 虽然我的水平距离看懂Pysonar还很远,但光看里面代码的数量和Java工具的使用频率就能感觉到一种简约的力量。
7 我不是任何人粉丝。