项目中枚举的进阶用法(携带Java原理分析:泛型擦除、反射)

遇到项目中这样一种写法,在枚举类的参数是一个.class对象,遂研究一二,发现别有洞天,其背后涉及到了泛型擦除、反射原理、运行时多态等,可以说这次研究,涉及到Java的方方面面,对Java的理解更上一层楼,希望读者可以耐心品味本文作者传达思想
在这里插入图片描述

1 枚举的普通用法

因为涉及到枚举,所以第一章节便给个示例,告诉读者为何要使用枚举?使用枚举的好处?使用枚举的细节?
希望通过此章节,以后遇到类似需求,能立马判断出能否使用枚举处理以及实现思路。

1.1 无参

需求:现在有两个规则(KEYWORDS_RULE,REGULAR_EXPRESSION_RULE)需要在一个枚举类中集中管理,方便管理以后拓展的规则,并且需要将规则信息发送给张三。

// 定义无参枚举
public enum ReferenceRelationSyncEnum {
    KEYWORDS_RULE,
    REGULAR_EXPRESSION_RULE;
}
// 单元测试
public static void main(String[] args) {
        // 第一种方式输出
        ReferenceRelationSyncEnum keywordsRule = ReferenceRelationSyncEnum.KEYWORDS_RULE;
        System.out.println(keywordsRule);

        // 第二种方式输出
        String s = ReferenceRelationSyncEnum.REGULAR_EXPRESSION_RULE.toString();
        System.out.println(s);

    }

输出效果
在这里插入图片描述

即使日后有100个规则,我们也仅需调用枚举类实现调用,获得这些规则的名称,并组装为一个List集合发送给张三,张三可能需要名称是REGULAR_EXPRESSION_RULE的规则,那他就会解析这个list

1.2 单个参数

需求:虽然我们可以发送这些规则的名称给张三,张三会解析这个List中的规则名称。一旦某一天我需要修改某个规则的名称,那张三为了适配我,也需要修改他的代码。这是我不希望看到的。
为了避免这个情况,我将每个规则指定一个type,例如KEYWORDS_RULE就是1,REGULAR_EXPRESSION_RULE就是2。如此一来,我传给张三的就是纯数字,即使我修改了规则名称,他也不需要变动他的代码。

public enum ReferenceRelationSyncEnum {
    KEYWORDS_RULE(1),

    REGULAR_EXPRESSION_RULE(2);

    private int type;

    public int getType() {
        return type;
    }

    ReferenceRelationSyncEnum(int type) {
        this.type = type;
    }
}
    public static void main(String[] args) {
        int type = ReferenceRelationSyncEnum.KEYWORDS_RULE.getType();
        System.out.println(type);

        int type2 = ReferenceRelationSyncEnum.REGULAR_EXPRESSION_RULE.getType();
        System.out.println(type2);
    }

在这里插入图片描述

优化前:张三按照规则名解析
优化后:信息更简洁了,张三按照type解析

1.3 两个参数

需求:正常的逻辑是:用户在界面勾选中文规则(关键字规则,正则规则),我们后端为了管理这种规则,创建枚举类,并给它们对应的英文名称(为了规范)。此外为了保障和张三之间消息的传递,我们又引入了type。

  public static void main(String[] args) {
        int type = ReferenceRelationSyncEnum.KEYWORDS_RULE.getType();
        System.out.println(type);

        int type2 = ReferenceRelationSyncEnum.REGULAR_EXPRESSION_RULE.getType();
        System.out.println(type2);

        System.out.println("------------------------------------");
        // 获取规则名称:通过枚举的实例方法调用
        String ruleName = ReferenceRelationSyncEnum.KEYWORDS_RULE.getRuleName();
        System.out.println(ruleName);

        // 获取规则名称:通过枚举类直接调用方法
        String ruleName2 = ReferenceRelationSyncEnum.getRuleName(2);
        System.out.println(ruleName2);
    }
public enum ReferenceRelationSyncEnum {
    KEYWORDS_RULE(1,"关键字规则"),

    REGULAR_EXPRESSION_RULE(2,"正则规则");

    private int type;

    private String ruleName;

    public int getType() {
        return type;
    }

    public String getRuleName() {
        return ruleName;
    }
    
	
	// java基本功:这里使用static有何益处-------可以被枚举类直接调用,不需要创建枚举实例
    public static String getRuleName(int type) {
        for (ReferenceRelationSyncEnum c : ReferenceRelationSyncEnum.values()) {
            if(c.getType() == type){
                return c.ruleName;
            }
        }
        return null;
    }

    ReferenceRelationSyncEnum(int type,String ruleName) {
        this.ruleName = ruleName;
        this.type = type;
    }
}
}

在这里插入图片描述

这样一来,不仅能保障之前的功能(和张三传递type),还能输出对应的中文规则
我们用这个中文规则能够干什么呢?
例前端传递给我们规则的中文名称 “正则规则” ,但我希望前端传递给我 “关键字规则” ,那么就需要加以判断
if( 前端的规则 == ReferenceRelationSyncEnum.getRuleName(1) )
是不是非常方便

2 枚举的进阶用法(核心)

需求:因为业务要求,我们会提供一个包含简单信息的Map,然后需要每个规则需要按照自己的特点重新组装得到新的NewMap。
例如我们为所有规则提供了Map,若是调用需要关键字规则,就包装得到NewMap1,发送给张三。若是需要正则规则,就包装为NewMap2,发送给张三。
在这里插入图片描述

先不考虑枚举,上述场景的最常见的解决思路就是定义一个抽象类,然后每个规则继承这个抽象类并重写这个抽象方法,如下图所示

在这里插入图片描述

// 抽象类
public abstract class AbstractRuleMsg {

    public abstract void buildRuleMag(Map<String, Object> map);
}
// 关键字规则继承
public class KeyWords extends AbstractRuleMsg {
    @Override
    public void buildRuleMag(Map<String, Object> map) {
    	System.out.println("这是NewMap1");
		// 重新包装得到NewMap1
    }
}

// 正则规则继承
public class RegularExperssion extends AbstractRuleMsg {
    @Override
    public void buildRuleMag(Map<String, Object> map) {
    	System.out.println("这是NewMap2");
		// 重新包装得到NewMap2
    }
}

重新梳理需求:
基于1.3章节实现的效果,指定一个type,我们就可以得到这个规则的中文名称,下一步我希望的效果是,指定一个type,我们就可以得到这个规则的类(KeyWords 或者RegularExperssion ),得到了这个类,我就可以调用这个类的方法:buildRuleMag(map )

那我们尝试按照之前的逻辑改造枚举类试试

public enum ReferenceRelationSyncEnum {
	
    KEYWORDS_RULE(1,"关键字规则",KeyWords),        // 改动点:第三个参数我们指定为对应的 类

    REGULAR_EXPRESSION_RULE(2,"正则规则",RegularExperssion);  // 改动点:第三个参数我们指定为对应的 类


    private int type;

    private String ruleName;
    
	
    private AbstractRuleMsg targetClass;              // 改动点:增加参数需要定义,既然都是继承的抽象方法,那就定义为抽象方法吧

    public int getType() {
        return type;
    }

    public String getRuleName() {
        return ruleName;
    }
											//  改动点:根据type获取对应的类
    public static AbstractRuleMsg getClass(int type) {
        for (ReferenceRelationSyncEnum c : ReferenceRelationSyncEnum.values()) {
            if(c.type == type){
                return c.targetClass;
            }
        }
        return null;
    }
											// 改动点:构造参数也需要适配
    ReferenceRelationSyncEnum(int type,String ruleName,AbstractRuleMsg targetClass) {
        this.ruleName = ruleName;
        this.type = type;
        this.targetClass= targetClass;
    }
    
    public static String getRuleName(int type) {
        for (ReferenceRelationSyncEnum c : ReferenceRelationSyncEnum.values()) {
            if(c.type == type){
                return c.ruleName;
            }
        }
        return null;
    }

   
}

看起来很简单的样子,我们单元测试一下

public static void main(String[] args) {
        
        KeyWords targetClass = (KeyWords) ReferenceRelationSyncEnum.getClass(1);
        msg.buildRuleMag(new HashMap<>());

        RegularExperssion targetClass2 = (RegularExperssion) ReferenceRelationSyncEnum.getClass(2);
        msg2.buildRuleMag(new HashMap<>());

    }

一切看起来都很顺利的样子,是的没错,大致逻辑是没有问题的,只不过这里存在一处语法错误。
在枚举类的参数中,我们不应该直接传一个类本身,而是应该传一个类对象,这是枚举类的语法要求

KEYWORDS_RULE(1,"关键字规则",new KeyWords()),        // 改动点:第三个参数我们指定为对应的**类对象**

REGULAR_EXPRESSION_RULE(2,,"正则规则",new RegularExperssion());  // 改动点:第三个参数我们指定为对应的**类对象**

看看效果
在这里插入图片描述

按照这样的方式,我们基本上就实现了,指定一个type,通过枚举类获得对应的类,调用该类的方法。如此一来,这个枚举类不仅仅管理了所有的规则名称,也管理了每个规则对应的类。简直是太方便维护了。(日后想知道该系统支撑哪些规则,或者改造某个规则类,通过枚举即可)

2.1 优化

上述代码功能上是没有任何问题了,但是在代码规范上不够优雅
需优化点A:在单元测试时,虽然我们指定了type,但是只能获得一个抽象类(注释的代码部分),为了得到该type对应的类,我们不得不进行一次类型转化,这就要求我们知道这个type对应的类是什么,这简直太麻烦了,接下里我们考虑如何避免这种情况
在这里插入图片描述
实际期待的效果:根据type自动的返回对应的类(大白话:我若指定type=1,返回类型自动是KeyWords;我若指定type=2,返回类型自动是RegularExperssion)
在这里插入图片描述

如何才能实现这个效果呢?什么效果----避免类型转化带来的麻烦
答案就是:使用泛型

为什么使用泛型就能避免类型转化代码的麻烦呢,如果你对泛型的知识还不够了解, 建议先阅读此文,点击查看

现在让我们开始尝试改造吧

2.1.1 需要改造的代码

调用getClass返回值类型是AbstractRuleMsg,需要将这一部分修改为泛型
在这里插入图片描述

2.1.2 直接使用泛型

tips:这一部分为小科普,正文从下面思路一开始
区分T和?的区别
观察如下代码: 在mian函数中,指定为?表示此时不确定类型,若确定类型就直接指定为String了。

public class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<?> box = new Box<>();
        box.setItem("Hello");
        String item = box.getItem(); // 获取物品时只能使用Object类型接收
        System.out.println(item);
		
		System.out.println("-----------------------------");
		
		Box<String> box = new Box<>();
        box.setItem("Hello");
        String item = box.getItem(); // 获取物品时只能使用Object类型接收
        System.out.println(item);
    }
}

思路一 ↓


直接使用<T> 可以吗?

先回顾一下,使用<T> 会改变什么?

使用<T> ,可以直接指定类型

之前的代码:List list =Stringnew ArrayList();
使用<T>之后的代码: List<String> list = new ArrayList<>();

可以发现,使用<T>虽然解决了强制类型转化,但是仍需要指定类型,这并未满足我们的需求。因此使用<T>是不可以的


思路二 ↓


直接使用<> 可以吗?

tips中介绍了T和?区别,答案是:不可以

看来直接使用泛型是无法实现的

2.1.3 使用反射—Class

除了泛型,还有什么办法可以动态得到返回类型呢,答案就是反射!
如果你对反射还不了解, 可以阅读此文--------------------链接

接下来,就让我们尝试改造吧

类.class -------> 表示该类的类对象的引用,有了引用就有了一切
再啰嗦解释一次为何枚举的第三个参数必须为 KeyWords.class类型。注意看有参构造函数的参数targetClass类型是不是被定义为了Class,所以枚举的第三个参数必须与之对应,也是Class类型 Class a = KeyWords.class
在这里插入图片描述

public static void main(String[] args) throws Exception {
		// 获取类的.class,这个.class文件存这个类的元数据。就可以获取该类名称/该类的方法/创建该类实例....
        Class aClass = ReferenceRelationSyncEnum.getClass(1);
        Object o = aClass.newInstance();
         // 获取方法对象 buildRuleMag()
        Method method = aClass.getMethod("buildRuleMag",Map.class);
        Map <String,Object> map = new HashMap<>();
        method.invoke(o, map);
        }

		// 简化代码写法
		Object o = ReferenceRelationSyncEnum.getClass(2).newInstance();  // 为何这里返回的是Object,因为枚举中我们仅仅指定返回值是Class,若是Class<AbstractRuleMsg> ,这里返回值类型就是AbstractRuleMsg abstractRuleMsg = .....
		Method method = aClass.getMethod("buildRuleMag",Map.class);
		method.invoke(o, new HashMap<>());
		

在这里插入图片描述
确实,这样操作就可以在不知道type==1对应的返回类型KeyWords的情况下,仍调用该类的方法

Ps:这是之前的效果
在这里插入图片描述

看来使用反射确实能解决动态返回值类型的问题

2.1.4 反射+泛型

这是上述单元测试的代码

		// 简化代码写法
		Object o = ReferenceRelationSyncEnum.getClass(2).newInstance();  // 为何这里返回的是Object,因为枚举中我们仅仅指定返回值是Class
		Method method = aClass.getMethod("buildRuleMag",Map.class);
		method.invoke(o, new HashMap<>());

每次指定一个type,都需要使用反射,这样还是比较麻烦,要是能再简化一下就好了。
这是目前的单元测试代码
在这里插入图片描述

下图是我们期待的最终效果,解释一下这段代码:无论我们创建的是keyWords类的实例还是RegularExperssion类的实例,都用父类AbstractRuleMsg接收,调用子类重写父类的方法。(这是java多态的特点)

更详细的解释:


ReferenceRelationSyncEnum.getClass(1)时通过枚举类中的定义会得到KeyWords的类对象引用关系,newInstance()指使用KeyWords的类对象引用关系创建KeyWords类的实例。用父类AbstractRuleMsg接收,调用buildRuleMag()实际上调用的是KeyWords的重写方法,而不是AbstractRuleMsg的原始方法。


这就是java的运行时多态的特点,不知你可曾记得一个知识点:父类 A = new 子类,A只能调用父子共有的方法,不能调用子类特有的方法。jvm会在运行时动态决定调用谁的方法

在这里插入图片描述


如何才能实现这种操作,接下来请学习一种全新的思路

这是目前的代码
在这里插入图片描述

// 简化代码写法
		Object o = ReferenceRelationSyncEnum.getClass(2).newInstance();  // 为何这里返回的是Object,因为枚举中我们仅仅指定返回值是Class,若是Class<AbstractRuleMsg> ,这里返回值类型就是AbstractRuleMsg abstractRuleMsg = .....
		Method method = aClass.getMethod("buildRuleMag",Map.class);
		method.invoke(o, new HashMap<>());

因此我们需要定义返回值类型,改为什么合适呢?

若你想知道为什么仅定义为Class编译器就报错,可以看这里:
当编译器执行ReferenceRelationSyncEnum.getClass(2)时,发现枚举类定义的是Class,意思就是没有返回值类型限制(就是没有指定返回值类型),如下图。然后继续执行AbstractRuleMsg abstractRuleMsg2 = ReferenceRelationSyncEnum.getClass(2).newInstance();赋值时发现,将一个没有指定返回值类型 转化为 AbstractRuleMsg 类,会存在类型转化异常。

改为Class<AbstractRuleMsg>吧,
在这里插入图片描述
发现有参构造改动后,顶部代码提示类型不匹配。构造参数定义第三个参数为AbstractRuleMsg类型,顶部参数我们给的是KeyWords和RegularExperssion,自然不一致。那么该怎么办呢? 如下图

Class<? extends AbstractRuleMsg> (更规范,推荐)
这个通配符表示可以是 AbstractRuleMsg 或其任何子类,这种方式被称为通配符的上界(upper-bounded wildcard)。

Class<?> 也可以实现效果

具体区别阅读此文
在这里插入图片描述

2.2 最终效果

public enum ReferenceRelationSyncEnum {
    KEYWORDS_RULE(1,"关键字规则",KeyWords.class),

    REGULAR_EXPRESSION_RULE(2,"正则规则",RegularExperssion.class);


    private int type;

    private String ruleName;

    private Class<? extends AbstractRuleMsg> targetClass;

    public int getType() {
        return type;
    }

    public String getRuleName() {
        return ruleName;
    }

    public static Class<? extends AbstractRuleMsg> getClass(int type) {
        for (ReferenceRelationSyncEnum c : ReferenceRelationSyncEnum.values()) {
            if(c.getType() == type){
                return c.targetClass;
            }
        }
        return null;
    }

    ReferenceRelationSyncEnum(int type,String ruleName,Class<? extends AbstractRuleMsg> targetClass) {
        this.ruleName = ruleName;
        this.type = type;
        this.targetClass = targetClass;
    }

    public static String getRuleName(int type) {
        for (ReferenceRelationSyncEnum c : ReferenceRelationSyncEnum.values()) {
            if(c.getType() == type){
                return c.ruleName;
            }
        }
        return null;
    }


}

    public static void main(String[] args) throws Exception {
        // 实际期待的结果
        AbstractRuleMsg abstractRuleMsg = ReferenceRelationSyncEnum.getClass(1).newInstance();
        abstractRuleMsg.buildRuleMag(new HashMap<>());

        AbstractRuleMsg abstractRuleMsg2 = ReferenceRelationSyncEnum.getClass(2).newInstance();
        abstractRuleMsg.buildRuleMag(new HashMap<>());
       }

在这里插入图片描述

2.3 思考:类型擦除

这种写法没问题
在这里插入图片描述
这种写法就会有问题
在这里插入图片描述

是什么原因导致的?为什么用AbstractRuleMsg类型接收就有问题,这是因为Class aClass = ReferenceRelationSyncEnum.getClass(1);这样写,原本保存KeyWords类的全部元数据(继承关系,类名等等),现在jvm类型擦除,丢失了继承关系,所以就是Object,而不能是AbstractRuleMsg了。

希望阅读完有所收获!

  • 18
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值