目录
- 1 枚举的普通用法
- 1.1 无参
- 1.2 单个参数
- 1.3 两个参数
- 2 枚举的进阶用法(核心)
- 2.1 优化
- 2.1.1 需要改造的代码
- 2.1.2 直接使用泛型
- 2.1.3 使用反射---Class
- 2.1.4 反射+泛型
- 2.2 最终效果
- 2.3 思考:类型擦除
遇到项目中这样一种写法,在枚举类的参数是一个.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 = (String)new 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了。
希望阅读完有所收获!