从今以后,只要谁说Java不能多继承

我都会说,是的没错(秒怂)

要不你再看看标题写了啥?

没毛病啊,你说Java不能多继承,我也说Java不能多继承

这不是巧了么,没想到我们对一件事物的看法竟如此一致,看来这就是猿粪啊

此继承非彼继承

那你这又是唱哪出?

直接上图!

可以看到当我们在B类上添加注解@InheritClass并指定A1.class和A2.class之后,我们的B实例就有了A1和A2的属性和方法

就好像B同时继承了A1和A2

这。。。难道是黑魔法?(为什么脑子里会突然冒出来巴啦啦能量?)

来人,把.class文件带上来

其实就是把A1和A2的属性和方法都复制到了B上,和继承没有半毛钱关系!

这玩意儿有啥用

说起来现在实现的功能和当初的目的还是有点出入的

众所周知,Lombok中提供了@Builder的注解来生成一个类对应的Builder

但是我想在build之前校验某些字段就不太好实现

于是我就考虑,能不能实现一个注解,只是生成对应的字段和方法(毕竟最麻烦的就是要复制一堆的属性),而build方法由我们自己来实现,类似下面的代码

publicclassA {private String a;publicA(String a) {this.a = a;}@BuilderWith(A.class)publicstaticclassBuilder {//注解自动生成 a 属性和 a(String a) 方法public A build() {if (a == null) {thrownewIllegalArgumentException("a is null");}returnnewA(a);}}
}
复制代码

这样的话,我们不仅不用手动处理大量的属性,还可以在build之前加入额外的逻辑,不至于像Lombok的@Builder那么不灵活

然后在后面实现的过程中就发现:

可以把一个类的属性复制过来,那也可以把一个类的方法复制过来!

可以复制一个类,那也可以复制多个类!

于是就发展成了现在这样,给人一种多继承的错觉

所以说这种方式也会存在很多限制和冲突,比如相同名称但不同类型的字段,相同名称相同入参但不同返回值的方法,或是调用了super的方法等等,毕竟只是一个缝合怪

这也许就是Java不支持多继承的主要原因,不然要校验要注意的地方就太多了,一不小心就会有歧义,出问题

目前我主要能想到两种使用场景

Builder

Builder本来就是我最初的目的,所以肯定要想着法儿的实现

publicclassA {private String a;publicA(String a) {this.a = a;}@InheritField(sources = A.class, flags = InheritFlag.BUILDER)publicstaticclassBuilder {//注解自动生成 a 属性和 a(String a) 方法public A build() {if (a == null) {thrownewIllegalArgumentException("a is null");}returnnewA(a);}}
}
复制代码

这个用法和之前设想的没有太大区别,就是对应的注解有点不太一样

@InheritField可以用来复制属性,然后flags = InheritFlag.BUILDER表示同时生成属性对应的方法

参数组合

另一种场景就是用来组合参数

比如我们现在有两个实体A和B

@DatapublicclassA {private String a1;private String a2;...private String a20;
}@DatapublicclassB {private String b1;private String b2;...private String b30;
}
复制代码

之前遇到过一些类似的场景,有一些比较老的项目,要加参数但是不能改参数的结构

一般情况下,如果要一个入参接收所有的参数我们会这样写

@DatapublicclassParamsextendsB {private String a1;private String a2;...private String a20;
}
复制代码

新写一个类继承属性多的B,然后把A的属性复制过去

但是如果修改了A就要同时修改这个新的类

如果用我们的这个就是这样的

@InheritField(sources = {A.class, B.class}, flags = {InheritFlag.GETTER, InheritFlag.SETTER})publicclassParams {}
复制代码

不需要手动复制了,A和B如果有修改也会自动同步

当然这个功能也是很只因肋了,因为我想不出还有其他的用法了,哭

手把手教你实现

要实现这个功能需要分别实现对应的注解处理器和IDEA插件

注解处理器用于在编译的时候根据注解生成对应的代码

IDEA插件用于在标记注解后能够有对应的提示

Annotation Processor

我们先来实现注解处理器

publicclassInheritProcessorextendsAbstractProcessor {@Overridepublicbooleanprocess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {//自定义的处理流程}@Overridepublic Set<String> getSupportedAnnotationTypes() {//需要扫描的注解的全限定名returnnewHashSet<>();}
}
复制代码

首先我们要继承javax.annotation.processing.AbstractProcessor这个类

其中getSupportedAnnotationTypes方法中返回需要扫描的注解的全限定名

然后就可以在process方法中添加自己的逻辑了,第一个参数Set<? extends TypeElement> annotations就是扫描到的注解

我们先拿到这些注解标注的类

publicclassInheritProcessorextendsAbstractProcessor {@Overridepublicbooleanprocess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {for (TypeElement annotation : annotations) {//获得标注了注解的类Set<? extendsElement> targetClassElements = roundEnv.getElementsAnnotatedWith(annotation);}}
}
复制代码

通过第二个参数RoundEnvironment的方法getElementsAnnotatedWith就能获得标注了注解的类

接着我们来获得这些类的语法树,获得这些类的语法树之后,我们就可以通过语法树来修改这个类了

JavacElementselementUtils= (JavacElements) processingEnv.getElementUtils();JCTree.JCClassDecltargetClassDef= (JCTree.JCClassDecl) elementUtils.getTree(targetClassElement);
复制代码

processingEnv是AbstractProcessor中自带的,直接用就行了,通过processingEnv可以获得JavacElements对象

再通过JavacElements就可以获得类的语法树JCTree.JCClassDecl

为了后面更好区分,我们把这些标注了注解的类叫做【目标类】,把注解上标记的类叫做【来源类】,我们要将【来源类】中的字段和方法复制到【目标类】中

我们只要拿到【来源类】的语法树,就可以获得对应的字段和方法然后添加到【目标类】的语法树中

先通过【目标类】获得类上的注解然后筛选出我们需要的注解,这里我的注解因为支持了@Repeatable,所以还要多解析一步

//获得类上所有的注解
List<? extendsAnnotationMirror> annotations = targetClassElement.getAnnotationMirrors();//解析@Repeatable获得实际的注解
List<AnnotationMirror> children = (List<AnnotationMirror>)annotation.getElementValues().values();
复制代码

拿到注解之后,就可以获得注解上的属性

private Map<String, Object> getAttributes(AnnotationMirror annotation) {Map<String, Object> attributes = newLinkedHashMap<>();for (Map.Entry<? extendsExecutableElement, ? extendsAnnotationValue> entry : annotation.getElementValues().entrySet()) {Symbol.MethodSymbolkey= (Symbol.MethodSymbol) entry.getKey();attributes.put(key.getQualifiedName().toString(), entry.getValue().getValue());}return attributes;
}
复制代码

通过方法getElementValues就可以获得注解方法和返回值的键值对,其中键为方法,所以直接强转Symbol.MethodSymbol就行了

而对应的值是特定了类型

值的类型

值的类

Attribute.Class

字符串

Attribute.Constant

枚举

Attribute.Enum

还有一些我没有用到所以这里就没有列出来了

所以我们拿到的值有的时候不能直接用,比如我们现在要获得【来源类】的语法树

Attribute.ClassattributeClass= ...
Type.ClassTypesourceClass= (Type.ClassType)attribute.getValue();
JCTree.JCClassDeclsourceClassDef= (JCTree.JCClassDecl) elementUtils.getTree(sourceClass.asElement());
复制代码

通过上述的方式我们就可以拿到注解上的【来源类】的语法树

接着我们就可以把【来源类】上的字段和方法复制到【目标类】了

for (JCTree sourceDef : sourceClassDef.defs) {//如果是非静态的字段if (InheritUtils.isNonStaticVariable(sourceDef)) {JCTree.JCVariableDeclsourceVarDef= (JCTree.JCVariableDecl) sourceDef;//Class 中未定义if (!InheritUtils.isFieldDefined(targetClassDef, sourceVarDef)) {//添加字段targetClassDef.defs = targetClassDef.defs.append(sourceVarDef);           }}//方法类似,这里不具体展示了
}
复制代码

通过【来源类】语法树的defs属性就能获得所有的字段和方法,筛选出我们需要的字段和方法之后再通过【目标类】语法树的defs属性的append方法添加就行了

这样我们就把一个类的字段和方法复制到了另一个类上

最后一步,我们需要在resources/META-INF/services下添加一个javax.annotation.processing.Processor的文件,并在文件中添加我们实现类的全限定类名

这一步也可以使用下面的方式自动生成

compileOnly 'com.google.auto.service:auto-service:1.0.1'
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'复制代码
@AutoService(Processor.class)publicclassInheritProcessorextendsAbstractProcessor {}
复制代码

引入auto-service包后,在我们实现的InheritProcessor上标注@AutoService(Processor.class)注解就会在编译的时候自动生成对应的文件

到此我们的注解处理器就开发完成了

我们只需要用compileOnly和annotationProcessor引入我们的包就可以啦

Intellij Plugin

虽然我们实现了注解处理器,但是IDEA上是不会有提示的,这就需要另外开发IDEA的插件来实现对应的功能了

推荐一下大佬写的小册《IntelliJ IDE 插件开发指南》,能够比较系统的了解IDEA的插件开发

这是我的 推广链接,如果大家真的要买的,那就顺手点我的 推广链接 买吧,嘿嘿

所以项目搭建之类的我就不啰嗦了

IDEA提供了很多接口用于扩展,这里我们要用到的就是PsiAugmentProvider这个接口

publicclassInheritPsiAugmentProviderextendsPsiAugmentProvider {@Overrideprotected@NotNull <Psi extendsPsiElement> List<Psi> getAugments(@NotNull PsiElement element, @NotNull Class<Psi> type) {returnnewArrayList<>();}
}
复制代码

getAugments方法就是用于获得额外的元素

其中第一个参数PsiElement element就是扩展的主体,以我们当前需要实现的功能来说,如果这个参数是类并且类上标注了我们指定的注解,那么我们就需要进行处理

第二个参数是需要的类型,以我们当前需要实现的功能来说,如果这个类型是字段或方法,那么我们就需要进行处理

直接看代码会清晰一点

publicclassInheritPsiAugmentProviderextendsPsiAugmentProvider {@Overrideprotected@NotNull <Psi extendsPsiElement> List<Psi> getAugments(@NotNull PsiElement element, @NotNull Class<Psi> type) {//只处理类if (element instanceof PsiClass) {if (type.isAssignableFrom(PsiField.class)) {//如果标记了注解,则返回额外的字段}if (type.isAssignableFrom(PsiMethod.class)) {//如果标记了注解,则返回额外的方法}}returnnewArrayList<>();}
}
复制代码

也就是说扩展的字段和方法是分开来获取的,另外需要注意是额外的字段和方法,不是全部的字段和方法

接下来我们需要先获得类上的注解

private Collection<PsiAnnotation> findAnnotations(PsiClass targetClass) {Collection<PsiAnnotation> annotations = newArrayList<>();for (PsiAnnotation annotation : targetClass.getAnnotations()) {if ("注解的全限定名".contains(annotation.getQualifiedName())) {annotations.add(annotation);}if ("@Repeatable注解的全限定名".contains(annotation.getQualifiedName())) {handleRepeatableAnnotation(annotation, annotations);}}return annotations;
}/*** 获得 Repeatable 中的实际注解*/privatevoidhandleRepeatableAnnotation(PsiAnnotation annotation, Collection<PsiAnnotation> annotations) {PsiAnnotationMemberValuevalue= annotation.findAttributeValue("value");if (value != null) {PsiElement[] children = value.getChildren();for (PsiElement child : children) {if (child instanceof PsiAnnotation) {annotations.add((PsiAnnotation) child);}}}
}
复制代码

获得注解之后,我们就可以通过注解获得注解的属性了

Collection<PsiType> sources = findTypes(annotation.findAttributeValue("sources"));private Collection<PsiType> findTypes(PsiElement element) {Collection<PsiType> types = newHashSet<>();findTypes0(element, types);return types;}privatevoidfindTypes0(PsiElement element, Collection<PsiType> types) {if (element == null) {return;}if (element instanceof PsiTypeElement) {PsiTypetype= ((PsiTypeElement) element).getType();types.add(type);}for (PsiElement child : element.getChildren()) {findTypes0(child, types);}
}
复制代码

这里需要注意,Psi是文件树而不是语法树

比如这样的写法@InheritClass(sources = {A.class, B.class})

我们通过findAttributeValue("sources")得到的是{A.class, B.class},最上层是{},{}的子节点才是A.class, B.class,所以Psi体现的是文件的结构

接着我们就可以获得对应的字段和方法了

PsiClasssourceClass= PsiUtil.resolveClassInType(PsiType);/*** 获得所有字段*/publicstatic Collection<PsiField> collectClassFieldsIntern(@NotNull PsiClass psiClass) {if (psiClass instanceof PsiExtensibleClass) {returnnewArrayList<>(((PsiExtensibleClass) psiClass).getOwnFields());} else {return filterPsiElements(psiClass, PsiField.class);}
}/*** 获得所有方法*/publicstatic Collection<PsiMethod> collectClassMethodsIntern(@NotNull PsiClass psiClass) {if (psiClass instanceof PsiExtensibleClass) {returnnewArrayList<>(((PsiExtensibleClass) psiClass).getOwnMethods());} else {return filterPsiElements(psiClass, PsiMethod.class);}
}privatestatic <T extendsPsiElement> Collection<T> filterPsiElements(@NotNull PsiClass psiClass, @NotNull Class<T> desiredClass) {return Arrays.stream(psiClass.getChildren()).filter(desiredClass::isInstance).map(desiredClass::cast).collect(Collectors.toList());
}
复制代码

上面这几个方法我都是从Lombok里面复制过来的,至于else分支我也看不懂,可能会有多种情况,我也没遇到过,hhh

然后我们就可以对字段和方法进行复制啦

StringfieldName= field.getName();
LightFieldBuilderfieldBuilder=newLightFieldBuilder(manager, fieldName, field.getType());
//访问限定
fieldBuilder.setModifierList(newLightModifierList(field));
//初始化
fieldBuilder.setInitializer(field.getInitializer());
//所属的Class
fieldBuilder.setContainingClass(targetClass);
//是否 Deprecated
fieldBuilder.setIsDeprecated(field.isDeprecated());
//注释
fieldBuilder.setDocComment(field.getDocComment());
//导航
fieldBuilder.setNavigationElement(field);
复制代码
LightMethodBuildermethodBuilder=newLightMethodBuilder(manager, JavaLanguage.INSTANCE, method.getName(), method.getParameterList(), method.getModifierList(), method.getThrowsList(), method.getTypeParameterList());
//返回值
methodBuilder.setMethodReturnType(method.getReturnType());
//所属的 Class
methodBuilder.setContainingClass(targetClass);
//导航
methodBuilder.setNavigationElement(method);
复制代码

这里大家一定要新实例化所有的字段和方法,不要直接返回【来源类】的字段和方法,因为【来源类】的字段和方法是和【来源类】关联的,而我们返回的是【目标类】的字段和方法,两者不匹配会导致IDEA直接报错

最后我们只需要在plugin.xml中添加这个扩展就行了

<extensionsdefaultExtensionNs="com.intellij"><lang.psiAugmentProviderimplementation="xxx.xxx.xxx.InheritPsiAugmentProvider"/></extensions>复制代码

最后的最后,需要发布一下插件或是本地集成

结束

附一张插件图

作者:不够优雅

链接:https://juejin.cn/post/7202272345834094652

谁说 Java 不能多继承相关推荐

  1. java 的继承_关于java中的继承

    我们都知道Java中的继承是复用代码.扩展子类的一种方式,继承使得Java中重复的代码能够被提取出来供子类共用,对于Java程序的性能以及修改和扩展有很大的意义,所以这是一个非常重要的知识点. 那么对 ...

  2. 零基础Java学习之继承

    继承 继承的概述 继承的理解 继承的好处 继承的格式 继承的特点一:成员变量 私有化(private) 成员变量不重名 成员变量重名 继承的特点二:成员方法 成员方法不重名 成员方法重名--重写(Ov ...

  3. Java异常以及继承的一些问题

    Java异常以及继承的一些问题 参考文章: (1)Java异常以及继承的一些问题 (2)https://www.cnblogs.com/rookieJW/p/8059864.html 备忘一下.

  4. java容器类的继承结构

    摘要: java容器类的继承结构 Java容器类库定义了两个不同概念的容器,Collection和Map Collection 一个独立元素的序列,这些元素都服从一条或多条规则.List必须按照插入的 ...

  5. java自学手记——继承

    java面向对象三大特点封装.继承和多态.继承作为三大特点之一,主要是为了实现多态的,即多态的前提条件是继承.代码示例: 1 class Person{ 2 String name; 3 String ...

  6. Java基础:继承、多态、抽象、接口

    第一讲    继承 一.继承概述 1.多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可. 2.通过extends关键字可以实现类与类的 ...

  7. java面向对象——包+继承+多态(一)

    文章目录 包(package) 概念: 创建包 注意事项: 导入包中的类: 直接导入 import语句导入 注意事项: 静态导入(了解即可) 包的访问权限 常见的系统包 继承 继承的语法规则 注意要点 ...

  8. Java类的继承总结

                       本文主要是讲述Java类的继承,更多Java技术知识,请登陆疯狂软件教育官网.加疯狂软件官方微信号:fkitorg,免费赢大奖,有机会赢得iOS培训课程一套. 在 ...

  9. 腾讯架构师讲解Java接口的继承与抽象类

    在实施接口中,我们利用interface语法,将interface从类定义中独立出来,构成一个主体.interface为类提供了接口规范. 在继承中,我们为了提高程序的可复用性,引入的继承机制.当时的 ...

  10. java中抽象类继承抽象类_用Java中的抽象类扩展抽象类

    java中抽象类继承抽象类 示例问题 当我创建Java :: Geci抽象类AbstractFieldsGenerator和AbstractFilteredFieldsGenerator我遇到了一个不 ...

最新文章

  1. Java Native Interface 六JNI中的异常
  2. centos下排查vsftpd出现put零字节问题的记录
  3. Oracle 10.2.0.4 高负载 触发 ORA-00494 错误
  4. linux精华文章汇总
  5. HDU - 3126 Nova(最大流+二分+简单几何)
  6. 关于MyEclipse项目的名字的修改对项目导入导出的影响
  7. Spring Boot 1.5.x新特性:动态修改日志级别
  8. 【软件工程】计算资源
  9. grub4dos linux live,grub4dos硬盘引导fedora12 livecd失败
  10. 美国DHS向国会提交政府《移动设备安全研究》报告
  11. 【SQL】数值型函数
  12. 【Verilog】移位寄存器总结:移位寄存器、算数移位寄存器、线性反馈移位寄存器(LFSR)
  13. Matlab Coder将m文件转换成C/C++
  14. mysql mpm_Zabbix和MPM监控MySQL
  15. 如何用计算机克数和斤换算,克数换算斤计算器(克千克斤公斤计算器)
  16. python怎么读?如何正确的发音?
  17. error: %preun(mysql-community-server-5.7.36-1.el6.x86_64) scriptlet failed
  18. js实现购物车结算界面
  19. 【C语言】冷知识——前置++和后置++
  20. 从零开始学USB(二十一、USB接口HID类设备(三)_报表描述符Global类)

热门文章

  1. 职中选什么专业好_读职中选什么专业比较好就业
  2. 如何利用空闲玩转咸鱼
  3. OpenAI热钱投向造芯!押注一老一少半导体传奇组合,乔布斯和马斯克都曾赞不绝口...
  4. WebView加载附件,和pnd格式显示
  5. 斐讯K2(PSG1218)打开telnet及刷机
  6. 数据挖掘实验之Apriori算法
  7. struts2 mysql_Struts2连接Mysql的Crud使用
  8. 即时通讯(IM)开源项目OpenIM本周版本发布- v1.0.7-web端一键部署
  9. Gitlab/GitHub:迁移代码,并保留历史记录
  10. JRTPLIB 文档