当前位置: 首页 > 后端技术 > Java

模仿C#的Java扩展方法

时间:2023-04-01 18:40:54 Java

我主要使用C#、JavaScript和TypeScript。但是最近因为一些原因,需要用到Java,只好重新捡起。回想起来,上一次用Java写出完整的应用,还是1.4版本。这么多年过去了,Java确实进步了很多,比如Stream,var等等,我还是知道一些的。但是用的时候还是有点束缚感,有种用不上的感觉。这肯定和语法习惯有关,但也有Java本身的原因。例如,我经常在C#中使用的“扩展方法”在Java中并不存在。C#的“扩展方法”语法可以在不修改类定义或继承类的情况下,为某些类及其子类添加公共方法。当这些类的对象调用扩展方法时,与调用类本身声明的方法是一样的,没有任何违和之处。为了理解这个语法,这里给出一个例子(不管你会不会C#,只要你有OOP基础,你应该能看懂这个例子)usingSystem;//定义一个Person类而不定义方法publicclassPerson{publicstringName{get;放;}}//下面的类定义了扩展方法PrintName()publicstaticclassPersonExtensions{publicstaticvoidPrintName(thisPersonperson){Console.WriteLine($"Personname:{person.Name}");}}//主程序,提供Main入口,这里使用扩展方法publicclassProgram{publicstaticvoidMain(string[]args){Personperson=newPerson{Name="John"};person.PrintName();}}有OOP基础的开发者常识可以判断:Person类没有定义方法,不应该调用person.PrintName()。但由于PrintName()是静态方法,因此应该可以使用PersonExtensions.PrintName(person)。事实上,如果你尝试像PersonExtensions.PrintName(person)这样的语句,你会发现这句话也能正常工作。但请注意,PrintName()声明的第一个参数已用此修改。这是特定于C#的扩展方法语法。编译器会识别扩展方法,然后将person.PrintName()翻译成PersonExtensions.PrintName(person)来调用——这是一个语法糖。C#在2007年发布的3.0版本中加入了“扩展方法”语法,那是10多年前的事了。不知道Java什么时候支持。但是说Java不支持扩展方法也不完全正确。毕竟有个东西叫Manifold,它以Java编译插件的形式提供扩展方法特性。IDEA中需要插件支持,使用起来感觉和C#差不多。可惜每月$19.9的租金直接把我劝退了。但是程序员往往有一种不撞南墙不回头的执念。没有一个大概的方法来处理这个问题吗?要分析疼痛的来源,就需要使用扩展方法。其实主要原因有一个:想扩展SDK中的类,但是又不想使用静态调用。尤其是需要链式调用的时候,静态方法确实不太好用。还是以Person为例(这次是Java代码):classPerson{privateStringname;publicPerson(Stringname){this.name=name;}publicStringgetName(){returnname;}}classPersonExtension{publicstaticPersontalk(Personperson){...}publicstaticPersonwalk(Personperson){...}publicstaticPersoneat(Personperson){......}publicstaticPersonsleep(Personperson){...}}业务流程是:谈判完出去吃饭,然后回来睡觉。带链接的调用应该是:person.talk().walk().eat().walk().sleep()注:不说换Person,我们假设是第三方SDK封装的,而PersonExtension是我们写的,但是显然不能这样调用。根据PersonExtension中的方法,应该这样调用:sleep(walk(eat(walk(talk(person)))));疼痛?!下面我们来分析一下我们目前除了痛点之外的需求:链式调用没有别的了……链式调用的典型应用场景既然需要链式调用,那么我们来思考一下链式调用的典型应用场景:构建模式。如果我们用构造方式写Extension类,使用的时候把原来的对象封装起来,是不是可以实现链式调用呢?类PersonExtension{privatefinalPersonperson;PersonExtension(Personperson){this.person=person;}publicPersonExtensionwalk(){out.println(person.getName()+":walk");归还这个;(){out.println(person.getName()+":talk");归还这个;}publicPersonExtensioneat(){out.println(person.getName()+":eat");归还这个;PersonExtensionsleep(){out.println(person.getName()+":sleep");归还这个;}}使用起来非常方便:newPersonExtension(person).talk().walk().eat().walk().sleep();引申到一般情况如果就此结束,这篇博文就太水了。解决连锁调用的问题我们走了弯路,但??是人心总是不容易得到满足的。一个新的需求出现了:扩展方法可以编写无数个扩展类。有没有办法让定义在这无数个类中的方法连接调用呢?你看,在当前的包装类中,我们不能调用第二个包装类的方法。但是如果我们可以从当前的包装类切换到第二个包装类不是很好吗?这个转换过程,大致就是获取当前封装的对象(比如person),作为参数传递给下一个封装类的构造函数,构造一个这个类的对象,作为调用体继续写。.这样,我们就需要有个约定:扩展类必须提供一个可以传入封装对象类型参数的构造函数;扩展类必须实现转换为另一个扩展类的方法在程序中,契约通常由接口来描述,所以这里定义一个接口:publicinterfaceExtension{>Eto(Classtype);}这个接口的意思很明确:封装的对象类型是Tto提供从当前Extension对象切换到另一个实现了Extension接口的对象可想而知。这个to需要做的就是找到E的构造函数,用它来构造一个E对象。该构造函数需要定义一个唯一的参数,参数类型为T或其父类型(可传入)。这样就可以在构造E对象时将当前扩展对象中封装的T对象传递给E对象。如果找不到合适的构造函数,或者构造过程中出现错误,则应该抛出异常来描述类型E不正确。由于E是类型参数,我们不妨改用IllegalArgumentException。此外,大多数扩展类的to行为应该是相同的,可以使用默认方法来提供支持。另外,你也可以在Extension中添加一个静态的create()方法,而不是使用new来创建一个扩展类对象——让一切从Extension开始。完整的扩展在这里:publicinterfaceExtension{/***给定一个封装的对象值,构造一个类E的对象来封装它。*/@SuppressWarnings("unchecked")static>Ecreate(Tvalue,ClassextensionType)throwsIllegalArgumentException{Constructorcstr=(Constructor)数组.stream(extensionType.getConstructors())//在constructor.filter(c->c.getParameterCount()==1&&c.getParameterTypes()[0].isAssignableFrom(value.getClass())中找到符合要求的)).findFirst().orElse(null);try{//如果没有找到合适的构造函数(cstr==null),或者在其他情况下发生错误//throwIllegalArgumentExceptionreturn(E)Objects.requireNonNull(cstr).newInstance(value);}catch(InstantiationException|IllegalAccessException|InvocationTargetExceptione){thrownewIllegalArgumentException("扩展的无效实现",e);}}//要想得到wrapTo当前封装的对象,必须getValue()接口TgetValue();//wrapTo接口及其默认实现default>Eto(Classtype)throwsIllegalArgumentException{returncreate(getValue(),type);}}现在把上面的PersonExtension拆成两个扩展类来作表演:classPersonExt1implementsExtension{privatefinalPersonperson;PersonExt1(Personperson){this.person=person;}@OverridepublicPersongetValue(){返回人;}publicPersonExt1walk(){out.println(person.getName()+":walk");归还这个;}publicPersonExt1talk(){out.println(person.getName()+":talk");归还这个;}}classPersonExt2implementsExtension{privatefinalPersonperson;publicPersonExt2(Personperson){this.person=person;}@OverridepublicPersongetValue(){返回人;}publicPersonExt2eat(){out.println(person.getName()+":eat");归还这个;}公共PersonExt2sleep(){out.println(person.getName()+":sleep");归还这个;}}调用示例:publicclassApp{publicstaticvoidmain(String[]args)throwsException{Personperson=newPerson("James");Extension.create(person,PersonExt1.class).talk().walk().to(PersonExt2.class).eat().to(PersonExt1.class).walk().to(PersonExt2.class).sleep();}}结语一般来说,要实现没有语法支持的扩展方法,其基本思路是实现在目标对象上调用的所谓扩展方法,实际上是通过静态方法调用的语法糖这个静态方法的第一个参数就是target目的。把静态方法的第一个参数拿出来,封装到扩展类中,同时把静态方法改成实例方法。这样就避免了调用时传入目标对象。如果需要进行链式调用,则需要通过接口约定,提供一些实用函数来辅助目标对象在各个扩展类之间穿梭。本文主要是尝试在没有语法/编译器支持的情况下,在Java中模块化C#的扩展方法。虽然有结果,但在实际使用中未必好用。请读者注意分析,实际开发时酌情考虑。