Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >谁说 Java 不能多继承

谁说 Java 不能多继承

作者头像
科技新语
发布于 2023-02-22 09:42:43
发布于 2023-02-22 09:42:43
61000
代码可运行
举报
运行总次数:0
代码可运行

我正在参加「掘金·启航计划」

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

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

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

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

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

此继承非彼继承

那你这又是唱哪出?

直接上图!

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

就好像B同时继承了A1A2

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

来人,把.class文件带上来

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

这玩意儿有啥用

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class A {

    private String a;
    
    public A(String a) {
        this.a = a;
    }
    
    @BuilderWith(A.class)
    public static class Builder {
    
        //注解自动生成 a 属性和 a(String a) 方法
    
        public A build() {
            if (a == null) {
                throw new IllegalArgumentException("a is null");
            }
            return new A(a);
        }
    }
}
复制代码

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

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

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

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

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

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

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

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

Builder

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class A {

    private String a;
    
    public A(String a) {
        this.a = a;
    }
    
    @InheritField(sources = A.class, flags = InheritFlag.BUILDER)
    public static class Builder {
    
        //注解自动生成 a 属性和 a(String a) 方法
    
        public A build() {
            if (a == null) {
                throw new IllegalArgumentException("a is null");
            }
            return new A(a);
        }
    }
}
复制代码

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

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

参数组合

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

比如我们现在有两个实体AB

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Data
public class A {

    private String a1;
    
    private String a2;
    
    ...
    
    private String a20;
}

@Data
public class B {

    private String b1;
    
    private String b2;
    
    ...
    
    private String b30;
}
复制代码

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Data
public class Params extends B {

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@InheritField(sources = {A.class, B.class}, flags = {InheritFlag.GETTER, InheritFlag.SETTER})
public class Params {

}
复制代码

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

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

手把手教你实现

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

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

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

Annotation Processor

我们先来实现注解处理器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class InheritProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //自定义的处理流程
    }
    
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //需要扫描的注解的全限定名
        return new HashSet<>();
    }
}
复制代码

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class InheritProcessor extends AbstractProcessor {

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
JavacElements elementUtils = (JavacElements) processingEnv.getElementUtils();

JCTree.JCClassDecl targetClassDef = (JCTree.JCClassDecl) elementUtils.getTree(targetClassElement);
复制代码

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

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//获得类上所有的注解
List<? extends AnnotationMirror> annotations = targetClassElement.getAnnotationMirrors();

//解析@Repeatable获得实际的注解
List<AnnotationMirror> children = (List<AnnotationMirror>)annotation.getElementValues().values();
复制代码

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private Map<String, Object> getAttributes(AnnotationMirror annotation) {
    Map<String, Object> attributes = new LinkedHashMap<>();
    for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : annotation.getElementValues().entrySet()) {
        Symbol.MethodSymbol key = (Symbol.MethodSymbol) entry.getKey();
        attributes.put(key.getQualifiedName().toString(), entry.getValue().getValue());
    }
    return attributes;
}
复制代码

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

而对应的值是特定了类型

值的类型

值的类

Attribute.Class

字符串

Attribute.Constant

枚举

Attribute.Enum

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Attribute.Class attributeClass = ...
Type.ClassType sourceClass = (Type.ClassType)attribute.getValue();
JCTree.JCClassDecl sourceClassDef = (JCTree.JCClassDecl) elementUtils.getTree(sourceClass.asElement());
复制代码

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (JCTree sourceDef : sourceClassDef.defs) {
    //如果是非静态的字段
    if (InheritUtils.isNonStaticVariable(sourceDef)) {
        JCTree.JCVariableDecl sourceVarDef = (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的文件,并在文件中添加我们实现类的全限定类名

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
compileOnly 'com.google.auto.service:auto-service:1.0.1'
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'
复制代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@AutoService(Processor.class)
public class InheritProcessor extends AbstractProcessor {

}
复制代码

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

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

我们只需要用compileOnlyannotationProcessor引入我们的包就可以啦

Intellij Plugin

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class InheritPsiAugmentProvider extends PsiAugmentProvider {

    @Override
    protected @NotNull <Psi extends PsiElement> List<Psi> getAugments(@NotNull PsiElement element, @NotNull Class<Psi> type) {
        return new ArrayList<>();
    }
}
复制代码

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

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

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

直接看代码会清晰一点

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class InheritPsiAugmentProvider extends PsiAugmentProvider {

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private Collection<PsiAnnotation> findAnnotations(PsiClass targetClass) {
    Collection<PsiAnnotation> annotations = new ArrayList<>();
    for (PsiAnnotation annotation : targetClass.getAnnotations()) {
        if ("注解的全限定名".contains(annotation.getQualifiedName())) {
            annotations.add(annotation);
        }
        if ("@Repeatable注解的全限定名".contains(annotation.getQualifiedName())) {
            handleRepeatableAnnotation(annotation, annotations);
        }
    }
    return annotations;
}

/**
 * 获得 Repeatable 中的实际注解
 */
private void handleRepeatableAnnotation(PsiAnnotation annotation, Collection<PsiAnnotation> annotations) {
    PsiAnnotationMemberValue value = annotation.findAttributeValue("value");
    if (value != null) {
        PsiElement[] children = value.getChildren();
        for (PsiElement child : children) {
            if (child instanceof PsiAnnotation) {
                annotations.add((PsiAnnotation) child);
            }
        }
    }
}
复制代码

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Collection<PsiType> sources = findTypes(annotation.findAttributeValue("sources"));

private Collection<PsiType> findTypes(PsiElement element) {
        Collection<PsiType> types = new HashSet<>();
        findTypes0(element, types);
        return types;
    }

private void findTypes0(PsiElement element, Collection<PsiType> types) {
    if (element == null) {
        return;
    }
    if (element instanceof PsiTypeElement) {
        PsiType type = ((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体现的是文件的结构

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PsiClass sourceClass = PsiUtil.resolveClassInType(PsiType);

/**
 * 获得所有字段
 */
public static Collection<PsiField> collectClassFieldsIntern(@NotNull PsiClass psiClass) {
    if (psiClass instanceof PsiExtensibleClass) {
        return new ArrayList<>(((PsiExtensibleClass) psiClass).getOwnFields());
    } else {
        return filterPsiElements(psiClass, PsiField.class);
    }
}

/**
 * 获得所有方法
 */
public static Collection<PsiMethod> collectClassMethodsIntern(@NotNull PsiClass psiClass) {
    if (psiClass instanceof PsiExtensibleClass) {
        return new ArrayList<>(((PsiExtensibleClass) psiClass).getOwnMethods());
    } else {
        return filterPsiElements(psiClass, PsiMethod.class);
    }
}

private static <T extends PsiElement> 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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
String fieldName = field.getName();
LightFieldBuilder fieldBuilder = new LightFieldBuilder(
    manager, 
    fieldName, 
    field.getType());
//访问限定
fieldBuilder.setModifierList(new LightModifierList(field));
//初始化
fieldBuilder.setInitializer(field.getInitializer());
//所属的Class
fieldBuilder.setContainingClass(targetClass);
//是否 Deprecated
fieldBuilder.setIsDeprecated(field.isDeprecated());
//注释
fieldBuilder.setDocComment(field.getDocComment());
//导航
fieldBuilder.setNavigationElement(field);
复制代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
LightMethodBuilder methodBuilder = new LightMethodBuilder(
    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中添加这个扩展就行了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<extensions defaultExtensionNs="com.intellij">

    <lang.psiAugmentProvider implementation="xxx.xxx.xxx.InheritPsiAugmentProvider"/>

</extensions>
复制代码

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Lombok技术揭秘 _ 自动生成带代码的幕后机制
首先,我们知道 Lombok 功能是作用在类编译时期,那我们来看下一个类编译的过程。
政采云前端团队
2023/09/26
1.1K0
Lombok技术揭秘 _ 自动生成带代码的幕后机制
Spring Boot: Lombok 注解原理分析及实践
对于 Lombok 我相信大部分人都不陌生,但对于它的实现原理以及缺点却鲜为人知,而本文将会从 Lombok 的原理出发,手撸一个简易版的 Lombok,让你理解这个热门技术背后的执行原理,以及它的优缺点分析。
Freedom123
2024/03/29
3480
Spring Boot: Lombok 注解原理分析及实践
99%的程序员都在用Lombok,原理竟然这么简单?我也手撸了一个!|建议收藏!!!
  99%的程序员都在用Lombok,原理竟然这么简单?我也手撸了一个!|建议收藏!!!
不会飞的小鸟
2020/03/30
6370
Lombok 原理分析与功能实现
这两天没什么重要的事情做,但是想着还要春招总觉得得学点什么才行,正巧想起来前几次面试的时候面试官总喜欢问一些框架的底层实现,但是我学东西比较倾向于用到啥学啥,因此在这些方面吃了很大的亏。而且其实很多框架也多而杂,代码起来费劲,无非就是几套设计模式套一套,用到的东西其实也就那么些,感觉没啥新意。刚这两天读”深入理解 JVM ”的时候突然想起来有个叫 Lombok 的东西以前一直不能理解他的实现原理,现在正好趁着闲暇的时间研究研究。
周三不加班
2019/06/04
1K0
使用Google开源库AutoService进行组件化开发
在JDK 1.5之后,java提供了对注解的支持,这些注解与普通代码一样,在运行期间发挥作用。在JDK 1.6中实现了JSR-269规范,提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,可以看作是一组编译器的插件,可以读取/修改/添加抽象语法树中的任意元素。
Android技术干货分享
2019/04/18
6.6K0
使用Google开源库AutoService进行组件化开发
annotationProcessor 自动生成代码(下)
在annotationProcessor 自动生成代码(上)中,我们介绍了如何通过注解和javapoet生成一个简单的HelloWorld.java。AbstractProcessor可以做的事还有很多,我们不可能一一列举。我们最重要的是明白,AbstractProcessor的运行时机、可操作范围和能提供给我们的东西。
Oceanlong
2018/12/19
1.5K0
什么是插入式注解,一文读懂!
插入式注解处理器在《深入理解Java虚拟机》一书中有一些介绍(前端编译篇有提到),但一直没有机会使用,直到碰到这个需求,觉得再合适不过了,就简单用了一下,这里做个记录。
终码一生
2023/08/22
4350
什么是插入式注解,一文读懂!
Java字节码深挖 第四站:JSR-269
本文参考了 Java-JSR-269-插入式注解处理器 ,笔者也是根据该文入门,感谢大佬。
相思不扫积久弥厚
2023/10/26
7670
Java注解与原理分析
注解即标注与解析,在Java的代码工程中,注解的使用几乎是无处不在,甚至多到被忽视;
知了一笑
2022/11/30
4290
Java注解与原理分析
Java 中的屠龙之术:如何修改语法树?
来源:https://my.oschina.net/u/4030990/blog/3211858
程序员黄小斜
2021/11/24
1.2K0
聊聊AbstractProcessor和Java编译流程
我:我写过一个路由跳转库,我通过了AbstractProcessor生成了路由表的注册类。
逮虾户
2020/10/15
4.8K0
聊聊AbstractProcessor和Java编译流程
“终于懂了” 系列:组件化框架 ARouter 完全解析(二)APT技术
在上一篇《“终于懂了” 系列:组件化框架 ARouter 完全解析(一) 原理详解》中,详细介绍了ARouter的核心原理。其中提到了“帮助类”的概念,也就是在运行时生成 用于帮助填充WareHouse路由元信息的类,这里就涉及到了APT技术。那么本篇就对这一技术点进行介绍,并详细分析ARouter中是如何使用APT来生成帮助类的。
胡飞洋
2022/11/08
1.9K0
“终于懂了” 系列:组件化框架 ARouter 完全解析(二)APT技术
Lombok,简化代码的神器,你值得拥有
本文给大家介绍一个Java代码简化的神器 -- Lombok。主要从如下几个方面进行展开:
孟君
2019/08/26
1.6K0
Lombok,简化代码的神器,你值得拥有
Lombok天天用,却不知道它的原理是什么?
来源丨掘金 链接:https://juejin.im/post/5e54d38a6fb9a07cbf46b3ca
趣学程序-shaofeer
2020/06/29
3790
Lombok天天用,却不知道它的原理是什么?
如何在 Android 中完成一个 APT 项目的开发?
APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具。
Android技术干货分享
2019/07/15
2K1
如何在 Android 中完成一个 APT 项目的开发?
apt 与 JavaPoet 自动生成代码
吴涛
2017/10/26
4.6K0
apt 与 JavaPoet 自动生成代码
JVM系列六(自定义插入式注解器).
从前面 文章 中我们可以了解到,javac 的三个步骤中,程序员唯一能干预的就是注解处理器部分,注解处理器类似于编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。因此,只要有足够的创意,程序员可以通过自定义插入式注解处理器来实现许多原本只能在编码中完成的事情。我们常见的 Lombok、Hibernate Validator 等都是基于自定义插入式注解器来实现的。
JMCui
2020/01/13
1.1K0
JVM系列六(自定义插入式注解器).
Android APT(编译时代码生成)最佳实践
越来越多第三方库使用apt技术,如DBflow、Dagger2、ButterKnife、ActivityRouter、AptPreferences。在编译时根据Annotation生成了相关的代码,非常高大上但是也非常简单的技术,可以给开发带来了很大的便利。
用户1269200
2018/07/30
1.4K0
聊聊如何通过APT+AST来实现AOP功能
如果有使用过spring aop功能的小伙伴,应该都会知道spring aop主要是通过动态代理在运行时,对业务进行切面拦截操作。今天我们就来实现一下如何通过APT+AST在编译期时实现AOP功能。不过在此之前先科普一下APT和AST相关内容
lyb-geek
2023/04/25
5120
聊聊如何通过APT+AST来实现AOP功能
《Kotlin 程序设计》第七章 Kotlin 编译过程分析第七章 Kotlin 编译过程分析
http://mp.weixin.qq.com/s/lEFRH523W7aNWUO1QE6ULQ
一个会写诗的程序员
2018/08/17
1.8K0
推荐阅读
相关推荐
Lombok技术揭秘 _ 自动生成带代码的幕后机制
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验