网站描述,wordpress 封包 app,wordpress商城主题模板,上饶网站优化作者简介#xff1a;大家好#xff0c;我是smart哥#xff0c;前中兴通讯、美团架构师#xff0c;现某互联网公司CTO 联系qq#xff1a;184480602#xff0c;加我进群#xff0c;大家一起学习#xff0c;一起进步#xff0c;一起对抗互联网寒冬 枚举简介 枚举是一种特…作者简介大家好我是smart哥前中兴通讯、美团架构师现某互联网公司CTO 联系qq184480602加我进群大家一起学习一起进步一起对抗互联网寒冬 枚举简介 枚举是一种特殊的类和class、interface等是一个级别的其实就是一个类一般用于表示多种固定的状态。 《阿里巴巴开发手册》对枚举的介绍 我们在设计山寨枚举中用WeekDay表示周一到周日但正版枚举的格式要精简很多 注意WeekDayEnum命名规则并不是固定的但推荐以Enum结尾方便见名知意。当一个类声明为enum时表示这是一个枚举类比如enum WeekDayEnum。 对于枚举为何出现你可以理解为JDK1.5以前很多人和我们之前一样都采用自定义山寨枚举的办法制作枚举类所以JDK干脆提供了官方版而且JDK枚举的底层原理正是“抄袭”了我们的山寨枚举。 枚举类底层原理
我们对上面的WeekDayEnum反编译 是不是和我们山寨枚举很相似 区别在于
enum关键字修饰的WeekDayEnum在编译后会自动继承Enum类且声明为final比山寨版枚举多了valueOf()方法传入名称返回对应的枚举实例values()并不是直接返回WeekDay[]而是$VALUES.clone() 特别是最后一点我们来对比下。山寨版枚举 正版枚举反编译 为什么正版的枚举要$VALUES.clone()呢
因为如果像山寨版枚举这样返回整个实际数组外部调用者就可以随意增删枚举对象 另外我们可以尝试分别打印正版枚举和我们的山寨枚举 说明正版枚举重写了toString()方法。
光看WeekDayEnum的代码好像看不出来哪里重写了toString()于是推测肯定是它的父类Enum重写了 在Enum这里有且仅有一个构造器 而我们都知道子类在new的时候会调用父类的构造器。 所以子类WeekDayEnum在哪里调了这个构造器、又往它里面传入了什么呢 我们发现虽然当初编写WeekDayEnum时构造器只传入了两个参数但编译器却给我们额外加了两个见图一用来为父类Enum中的name和ordinal赋值见图二。name来自MONDAY、TUESDAY等枚举名称而ordinal则是编写序号。 特别注意name并不是MONDAY(1, 星期一)里的desc而是MONDAY字符串。这很好理解因为不是所有枚举都有desc字段它是我们自己定义的字段。如果你愿意完全可以这样定义枚举 枚举中的每个对象都有默认的name就是你看到的通常意义上的枚举名称而默认toString()会打印name。 name和ordinal
勇敢面对内心的疑问吧为什么编译器要给构造器额外添加参数name、ordinal 这个问题其实我们在设计山寨版枚举时讨论过了 总的来说就是为了区分枚举对象。因为在编写枚举时极有可能会这样写 这个是无参的反编译后如果不额外添加name、ordinal就会变成这样 此时如果你把整个枚举存入数据库就会都变成{}那么下次取出来就没什么特征性了。 另一个原因是新增的参数是为了满足valueOf()方法调用者可以传入字符串匹配对应的枚举类型。比如
FruitEnum BANANA FruitEnum.valueOf(BANANA) 这样就可以根据name得到BANANA枚举对象。 枚举 VS 常量类
枚举和常量类有什么区别呢 我想起了另一个经常会被问到的问题
Dubbo和SpringCloud什么区别 Dubbo只是一个RPC框架而SpringCloud几乎代表了一整个微服务解决方案从网关、注册中心到配置中心一应俱全完全不是一个概念。 同样的枚举本质是一个对象可以设置对象该有的一切而常量类往往只有字段public static final枚举对象和常量类的字段是没有可比性的。比如枚举可以自定义方法常量可以吗 总之应该把枚举看成特殊的对象而不是仅仅将其等同于常量。 作为常量使用时区别不大
如果实际编码时传递的是枚举的字段而不是枚举本身比如WeekDayEnum.TUESDAY.getCode()此时枚举和常量类的用法差别不大。 甚至单纯作为常量用的话常量类似乎更容易理解且方便。 枚举优点一限制重复定义常量
枚举作为常量使用时编译器会自动限制取值不能相同而常量类做不到有可能会重复 其实这个优点没什么卵用凑数。 枚举优点二包含更多维度的信息
枚举毕竟是对象可以包含更多的信息 枚举优点三枚举Switch比常量if/else简洁
枚举可以直接用于Switch判断代码通常会比常量if/else简洁一些 至此终于把上面三个“优点”讲完这也是网上抄来抄去的一些内容。实话实说我不觉得if/else就怎么了。我的想法是上面的优点好像都是强行凑数用来捧枚举然后踩一脚常量类个人认为没什么卵用。 枚举真要谈优点一定要从它作为对象的角度来说而不是强行把它限定为“特殊的常量类”。 利用枚举完善状态码
枚举做统一返回结果可以有效规范错误码及错误信息。我们在山寨版枚举中演示过了。 如果不用枚举code和desc做不到绑定用户传入1的同时可以传入参数错误这样状态码和信息就错乱了。 所以最好不要这样设计 状态码和状态描述之间没有必然联系是散乱的。 枚举策略 再讲一个利用枚举消除if/else的案例具体这是什么设计模式其实不重要不管黑猫白猫能抓老鼠就是好猫。 比如一个网上商城有会员制度
黄金会员6折白银会员7折青铜会员8折 无论在商城里买什么都会按照会员的折扣优惠。 用常量类实现
public class MemberDemo {public static void main(String[] args) {User user new User(1L, bravo, Constants.GOLD_MEMBER);BigDecimal productPrice new BigDecimal(1000);BigDecimal discountedPrice calculateFinalPrice(productPrice, user.getMemberType());System.out.println(discountedPrice);}/*** 根据会员身份返回折扣后的商品价格** param originPrice* param user* return*/public static BigDecimal calculateFinalPrice(BigDecimal originPrice, Integer type) {if (Constants.GOLD_MEMBER.equals(type)) {return originPrice.multiply(new BigDecimal(0.6));} else if (Constants.SILVER_MEMBER.equals(type)) {return originPrice.multiply(new BigDecimal(0.7));} else if (Constants.BRONZE_MEMBER.equals(type)) {return originPrice.multiply(new BigDecimal(0.8));} else {return originPrice;}}
}Data
AllArgsConstructor
class User {private Long id;private String name;/*** 会员身份* 1黄金会员6折优惠* 2白银会员7折优惠* 3青铜会员8折优惠*/private Integer memberType;
}class Constants {/*** 黄金会员*/public static final Integer GOLD_MEMBER 1;/*** 白银会员*/public static final Integer SILVER_MEMBER 2;/*** 青铜会员*/public static final Integer BRONZE_MEMBER 3;
} 上面的代码有两个缺点
if/else过多如果会员制度再复杂些语句会更长阅读性不佳不利于扩展。如果后期和百度一样不要脸地在vip基础上新增svip需要修改逻辑判断的代码这很容易出错 用枚举实现在枚举类中定义抽象方法强制子类实现
/*** author mx* date 2023-11-25 14:57*/
public class MemberDemo {public static void main(String[] args) {User user new User(1L, bravo, MemberEnum.GOLD_MEMBER.getType());BigDecimal productPrice new BigDecimal(1000);BigDecimal discountedPrice calculateFinalPrice(productPrice, user.getMemberType());System.out.println(discountedPrice);}/*** 根据会员身份返回折扣后的商品价格** param originPrice* param user* return*/public static BigDecimal calculateFinalPrice(BigDecimal originPrice, Integer type) {return MemberEnum.getEnumByType(type).calculateFinalPrice(originPrice);}
}Data
AllArgsConstructor
class User {private Long id;private String name;/*** 会员身份* 1黄金会员6折优惠* 2白银会员7折优惠* 3青铜会员8折优惠*/private Integer memberType;
}enum MemberEnum {// ---------- 把这几个枚举当做本应该在外面实现的MemberEnum子类不要看成MemberEnum内部的 ----------GOLD_MEMBER(1, 黄金会员) {Overridepublic BigDecimal calculateFinalPrice(BigDecimal originPrice) {return originPrice.multiply(new BigDecimal((0.6)));}},SILVER_MEMBER(2, 白银会员) {Overridepublic BigDecimal calculateFinalPrice(BigDecimal originPrice) {return originPrice.multiply(new BigDecimal((0.7)));}},BRONZE_MEMBER(3, 青铜会员) {Overridepublic BigDecimal calculateFinalPrice(BigDecimal originPrice) {return originPrice.multiply(new BigDecimal((0.8)));}},;// ---------- 下面才是MemberEnum类的定义 ---------private final Integer type;private final String desc;MemberEnum(Integer type, String desc) {this.type type;this.desc desc;}/*** 定义抽象方法留个子类实现** param originPrice* return*/protected abstract BigDecimal calculateFinalPrice(BigDecimal originPrice);public Integer getType() {return type;}public String getDesc() {return desc;}public static MemberEnum getEnumByType(Integer type) {MemberEnum[] values MemberEnum.values();for (MemberEnum memberEnum : values) {if (memberEnum.getType().equals(type)) {return memberEnum;}}throw new IllegalArgumentException(Invalid Enum type: type);}
}
枚举类本身比较抽象上面的代码可能一部分人会一时转不过弯。那你就想象着把三个枚举抽出来 当然你也可以新增一个接口在接口内部定义calculateFinalPrice()让MemberEnum实现该接口 我敢肯定必然有部分同学觉得我在故弄玄虚搞了一大堆但看起来反而更麻烦。 大家不要光看当前的代码量而要考虑扩展。如果此时需要新增会员类型那么使用常量的版本需要改动两处
常量类if/else所在的类 而枚举版本只要在枚举类中新增一个类型 而修改if/else的犯错概率一般远大于新增枚举类。这其实也体现了设计模式的思想用增量代替修改。 还是觉得枚举麻烦 此时应该给代码加上一根时间轴动态地看待问题。比如假设随着时间推移不可避免地需要在很多地方对用户的身份进行判断于是你的同事把你写的if/else拷贝到项目的各个角落。 而产品的需求改到第二版时才发现漏算了星耀会员于是你需要找到所有相关的if/else进行修改...你必须细致只要有一处忘了改就会出错增加了测试和维护的成本。 但如果采用策略枚举的方式把易变动的逻辑抽取出来共同维护就会方便很多。 设计模式中有个说法越抽象越稳定越具体越不稳定所以提倡面向抽象编程。设计模式的目的不是消除变化而是隔离变化。在软件工程中变化的代码就像房间里一只活蹦乱跳的兔子你并不能让它绝对安静不要奢望需求永不变更但可以准备一个笼子把它隔离起来从而达到整体的稳定。 上面的代码中if/else是兔子而MemberEnum则扮演着笼子的角色。如果不用MemberEnum把兔子关在一起那么兔子就会在整个项目蹦跶很难管理。 上面案例充分说明作为对象的枚举比作为字段的常量可玩性高很多。 枚举代码优化
优化1尽量使用foreach
举个例子对于普通的for循环 每次循环其实都会判断ilist.size()每次都会调用size()获取大小而foreach则会进行优化 其他写法 如果你学过Java8的Stream还可以这样写 抛不抛异常看业务个人觉得返回null意义也不大反正要么空指针要么插入不可预计的值。 优化2用Map替代For缓存枚举值利用key提高检索速度实用小算法 或 这种写法是我在研究MyBatis枚举转换器时看源码发现的 优化3静态导入简化代码看个人喜好不是很好用
一般都是这样写 但有时代码会看起来很长此时可以使用静态导入 优化4结合Lombok简化代码
Getter
AllArgsConstructor
public enum PlatformEnum {TAOBAO(1, 淘宝, taobao, http://xxx),PDD(2, 拼多多, pdd, http://xxx);private final Integer code;private final String sourceKey;private final String iconUrl;public static PlatformEnum getByCode(Integer code) {for (PlatformEnum platformEnum : PlatformEnum.values()) {if (platformEnum.getCode().equals(code)) {return platformEnum;}}return null;}
} 枚举与常量类的取舍
最后说一下个人平时对枚举和常量类的取舍
错误码的定义用枚举字段状态用枚举或常量类都可以个人更倾向于使用枚举可玩性会比常量类高一些全局变量的定义可以使用常量类比如USER_INFO、SESSION_KEY 一点小建议
大部分人设计枚举时都是使用以下格式
MONDAY(1, 星期一)
这种格式在当前情境下是合理的但如果是其他场景
WAIT_FOR_DEAL(1, 等待处理)
SUITABLE(2, 合适)
UNSUITABLE(3, 不合适)
INVITE(4, 邀请面试)
...
随着状态越来越多前端同学会变得很痛苦。所以如果你的项目允许可以使用MONDAY(MONDAY, 星期一)这种形式也就是存字符串。 这个时候其实没必要考虑索引键长问题你就当它是一个普通字段。退一步想如果这是name字段难道你会因为索引的原因而使用int类型吗...考虑前后端对接及维护代码的可读性有时比一丁点所谓的性能还重要。 扩展内容
在网上看到的写法但个人觉得不是很好。他的出发点是每个枚举类都要写getEnumByType()方法太麻烦了希望把各个类中的getEnumByXxx()方法抽取到工具类中
public class EnumUtil {private static final MapObject, Object key2EnumMap new ConcurrentHashMap();private static final SetClass? enumSet ConcurrentHashMap.newKeySet();/*** 获取枚举带缓存** param enumType 枚举Class类型* param enumToValueMapper 抽取字段按哪个字段搜索* param key 待匹配的字段值* param T* return*/public static T extends EnumT, R OptionalT getEnumWithCache(ClassT enumType, FunctionT, R enumToValueMapper, Object key) {if (!enumSet.contains(enumType)) {// 不同的枚举类型互相不影响synchronized (enumType) {if (!enumSet.contains(enumType)) {// 添加枚举enumSet.add(enumType);// 缓存枚举键值对for (T enumConstant : enumType.getEnumConstants()) {// enumToValueMapper.apply()表示从枚举中获得value。但不同枚举可能value相同因此getKe()进一步加工为value拼接路径做前缀String mapKey getKey(enumType, enumToValueMapper.apply(enumConstant));key2EnumMap.put(mapKey, enumConstant);}}}}return Optional.ofNullable((T) key2EnumMap.get(getKey(enumType, key)));}/*** 获取key* 注带上枚举类名作为前缀避免不同枚举的Key重复** param enumType* param key* param T* return*/public static T extends EnumT, R String getKey(ClassT enumType, R key) {return enumType.getName().concat(key.toString());}/*** 获取枚举不缓存** param enumType* param enumToValueMapper* param key* param T* param R* return*/public static T extends EnumT, R OptionalT getEnum(ClassT enumType, FunctionT, R enumToValueMapper, Object key) {for (T enumThis : enumType.getEnumConstants()) {if (enumToValueMapper.apply(enumThis).equals(key)) {return Optional.of(enumThis);}}return Optional.empty();}// ----------- 测试工具类 ------------public static void main(String[] args) {// 按code搜索寻找code2的FruitEnum对象OptionalFruitEnum fruitEnum EnumUtil.getEnumWithCache(FruitEnum.class, FruitEnum::getCode, 2);fruitEnum.ifPresent(fruit - System.out.println(fruit.getName()));}Getterenum FruitEnum {APPLE(1, 苹果),BANANA(2, 香蕉),ORANGE(3, 橘子),;FruitEnum(Integer code, String name) {this.code code;this.name name;}private final Integer code;private final String name;}
} 为什么我认为上面的Util工具不好呢
首先getEnumWithCache()最后返回值需要强转IDEA提示看着很难受。但似乎无法解决因为key2EnumMap只能用Object所以注定了需要强转。其次也是最重要的没有对Function enumToValueMapper做限制。也就是说用户可以随意传入映射参数有些人把枚举映射为name有些人把枚举映射为oridnal还有其他属性。也就说很乱。到时取出时你都不知道自己当初存的key是什么。 当然如果你实在需要一个EnumUtil简化操作那么下面这个简化版的可能更适合你其实hutool这类第三方工具都有提供类似的功能
Slf4j
public class EnumUtil {/*** 获取枚举类型的枚举值通过属性值** param clazz 枚举类* param fieldFunction 属性值获取函数* param fieldValue 属性值* param E 枚举类* param V 属性值* return 匹配的枚举值可能为null*/public static E extends EnumE, V E getEnum(ClassE clazz, FunctionE, V fieldFunction, V fieldValue) {Assert.notNull(clazz);E[] es clazz.getEnumConstants();for (E e : es) {if (Objects.equals(fieldFunction.apply(e), fieldValue)) {return e;}}return null;}/*** 获取枚举类型的枚举值通过属性值** param clazz 枚举类* param fieldFunction 属性值获取函数* param fieldValue 属性值* param E 枚举类* param V 属性值* return 匹配的枚举值Optional类型*/public static E extends EnumE, V OptionalE getEnumOptional(ClassE clazz, FunctionE, V fieldFunction, V fieldValue) {Assert.notNull(clazz);E[] es clazz.getEnumConstants();for (E e : es) {if (Objects.equals(fieldFunction.apply(e), fieldValue)) {return Optional.of(e);}}return Optional.empty();}
} 补充解答读者疑惑
在本文评论区看到一个提问觉得也是很多人的疑问。这里特别补充一下。 为什么枚举类内部可以定义抽象方法 关于这个问题可以先去看看JDK的Enum是什么样的 Enum本身就是一个抽象类却没有定义抽象方法。按之前小册讲过的一个类没有抽象方法却被定义为抽象类主要是为了不被new。同理 UserStatusEnum extends Enum后自己也是一个抽象类也没有任何抽象方法。 OK回到问题本身。 我们通常说的 枚举一般是指枚举对象而不是枚举类。
Getter
AllArgsConstructor
public enum UserStatusEnum { // 这个才是枚举类// 这两个看起来是“字段”的玩意儿其实是对象枚举对象DELETE(-1, 删除),DEFAULT(0, 默认);private final Integer status;private final String desc;}所以我在一个抽象类UserStatusEnum中定义一个抽象方法有什么问题呢再者既然我在UserStatusEnum中既然定义了抽象方法那么DELETE、DEFAULT两个枚举对象就必须实现这个方法。
Getter
AllArgsConstructor
public enum UserStatusEnum {DELETE(-1 删除){// 实现UserStatusEnum定义的抽象方法Overridepublic void test() {}},DEFAULT(0, 默认) {Overridepublic void test() {}};private final Integer status;private final String desc;// 定义一个抽象方法public abstract void test();}
枚举最令人困惑的地方在于枚举类的定义和枚举对象是混在一个文件中的再加上一些后期的自动编译处理让初学者看起来觉得很抽象。但你如果把DELETE、DEFAULT单独拎出去想象成一个独立的对象或实现类会更容易接受。
作者简介大家好我是smart哥前中兴通讯、美团架构师现某互联网公司CTO 进群大家一起学习一起进步一起对抗互联网寒冬