枚举与应用

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

枚举简介

枚举是一种特殊的类,和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循环:

每次循环其实都会判断i<list.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 Map<Object, Object> key2EnumMap = new ConcurrentHashMap<>();private static final Set<Class<?>> enumSet = ConcurrentHashMap.newKeySet();/*** 获取枚举(带缓存)** @param enumType          枚举Class类型* @param enumToValueMapper 抽取字段(按哪个字段搜索)* @param key               待匹配的字段值* @param <T>* @return*/public static <T extends Enum<T>, R> Optional<T> getEnumWithCache(Class<T> enumType, Function<T, 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 Enum<T>, R> String getKey(Class<T> enumType, R key) {return enumType.getName().concat(key.toString());}/*** 获取枚举(不缓存)** @param enumType* @param enumToValueMapper* @param key* @param <T>* @param <R>* @return*/public static <T extends Enum<T>, R> Optional<T> getEnum(Class<T> enumType, Function<T, 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搜索,寻找code=2的FruitEnum对象Optional<FruitEnum> 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 Enum<E>, V> E getEnum(Class<E> clazz, Function<E, 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 Enum<E>, V> Optional<E> getEnumOptional(Class<E> clazz, Function<E, 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

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/169252.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CentOS 7 使用异步网络框架Libevent

CentOS 7 安装Libevent库 libevent github地址&#xff1a;https://github.com/libevent/libevent 步骤1&#xff1a;首先&#xff0c;你需要下载libevent的源代码。你可以从github或者源代码官方网站下载。并上传至/usr/local/source_code/ 步骤2&#xff1a;下载完成后&…

C语言 - 基础

C 语言 1. Hello World #include <stdio.h>int main(int argc, const char *argv[]) {printf("hello world\n");return 0; }注意: 所有的标点符号必须在英文状态下输入单词不要写错注意空格 创建 C语言 程序步骤&#xff1a; 1、创建一个文档&#xff0c;以…

Springboot 南阳旅游平台-计算机毕设 附源码 31829

Springboot 南阳旅游平台 目 录 摘要 1 绪论 1.1 研究背景 1.2 研究意义 1.3 论文结构与章节安排 2 南阳旅游平台系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 法律可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能性分析…

芙蓉花 欣赏

芙蓉&#xff08;Hibiscus mutabilis Linn&#xff09;是一种锦葵科、木槿属植物原名&#xff1a;木芙蓉&#xff0c;别名&#xff1a;芙蓉花、拒霜花、木莲、地芙蓉、华木、酒醉芙蓉。其花或白或粉或赤&#xff0c;皎若芙蓉出水&#xff0c;艳似菡萏展瓣&#xff0c;故有“芙蓉…

Ubuntu服务器/工作站常见故障修复记录

日常写代码写方案文档&#xff0c;偶尔遇上服务器出现问题的时候&#xff0c;也需要充当一把运维工程师&#xff0c;此帖用来记录服务器报错的一些解决方案&#xff0c;仅供参考&#xff01; 文章目录 一、服务器简介二、机箱拆解三、基本操作3.1 F2进入BIOS3.2 F12进入Boot Me…

教你IDEA解决GIT冲突

前言 GIT基本上贯穿我们的开发生涯&#xff0c;之所以要使用git也是有很多优点的 &#x1f339;&#x1f339;&#x1f339;&#x1f339;&#x1f339;&#x1f339;&#x1f339;&#x1f339; 1.通俗易懂点&#xff0c;保存代码不丢失&#xff1a;防止因内存&#xff0c;操…

字符串函数

读取字符串的函数 1.gets()函数 说明&#xff1a;gets函数简单易用&#xff0c;它读取整行输入&#xff0c;直至遇到换行符&#xff0c;然后丢掉换行符&#xff0c;储存其余字符&#xff0c;并在末尾添加一个空字符使其成为一个C字符串。它经常和puts&#xff08;&#xff09;…

从零开始的RISC-V模拟器开发(一)环境搭建

前言 博主这系列文章是跟随中科院吴伟老师的b站公开课&#xff1a;[完结]从零开始的RISC-V模拟器开发第一季2021春季_哔哩哔哩_bilibili 记录的笔记。仅供学习使用&#xff0c;侵删&#xff01; 苦逼的博主现在自己毕设也是要设计类似的东西。哎。我需要做的是给一个现成的 R…

STM32 MAP文件

文章目录 1 生成Map2 map中概念3 文件分析流程3.1 Section Cross References3.2 Removing Unused input sections from the image&#xff08;移除未使用的段&#xff09;3.3 Memory Map of the image&#xff08;映像的内存分布&#xff09;3.3.1 加载域3.3.2 运行域 4 代码运…

机器学习探索计划——KNN算法流程的简易了解

文章目录 数据准备阶段KNN预测的过程1.计算新样本与已知样本点的距离2.按照举例排序3.确定k值4.距离最近的k个点投票 scikit-learn中的KNN算法 数据准备阶段 import matplotlib.pyplot as plt import numpy as np# 样本特征 data_X [[0.5, 2],[1.8, 3],[3.9, 1],[4.7, 4],[6.…

软件测试:超详细的Jmeter基础教程

JMeter 介绍&#xff1a; 一个非常优秀的开源的性能测试工具。 优点&#xff1a;你用着用着就会发现它的重多优点&#xff0c;当然不足点也会呈现出来。 从性能工具的原理划分 Jmeter工具和其他性能工具在原理上完全一致&#xff0c;工具包含4个部分&#xff1a; &#xff…

基于springboot实现高校食堂移动预约点餐系统【项目源码】

基于springboot实现高校食堂移动预约点餐系统演示 Java语言简介 Java是由SUN公司推出&#xff0c;该公司于2010年被oracle公司收购。Java本是印度尼西亚的一个叫做爪洼岛的英文名称&#xff0c;也因此得来java是一杯正冒着热气咖啡的标识。Java语言在移动互联网的大背景下具备…

rancher2.6 docker版本部署

1. 拉取镜像 docker pull rancher/rancher:v2.6.5 注&#xff1a; 上面命令中rancher的版本v2.6.5&#xff0c;仅仅是我因为我们环境中使用的k8s都是 1.20.1 到1.23.6 之间的版本。rancher支持的k8s版本&#xff0c;在github上查看&#xff1a;Release Release v2.6.5 ranche…

2023 年 认证杯 小美赛 国际大学生数学建模挑战赛 |数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 cs数模团队在认证杯 小美赛前为大家提供了许多资料的内容呀&am…

基于springboot实现乒乓球预约管理系统项目【项目源码】

基于springboot实现乒乓球预约管理系统演示 系统的开发环境 浏览器&#xff1a;IE 8.1&#xff08;推荐6.0以上&#xff09; 开发使用语言&#xff1a;JAVA JDK版本&#xff1a;JDK_8 数据库管理系统软件&#xff1a;Mysql 运行平台&#xff1a;Windows 7 运行环境&#…

破案现场:Docker容器资源限制导致的oom问题

破案现场&#xff1a;Docker容器资源限制导致的oom问题 01 事故现场02 问题定位03 对症下药04 后记 原文来自于微信公众号“运维之美” https://mp.weixin.qq.com/s?__bizMzA5NDY1MTM3MA&mid2247484902&idx1&sn8394aefd884ee09ea546fcd400dd233c&chksm904a136…

是否有无限提取的代理IP?作为技术你需要知道这些

最近有互联网行业的技术小伙伴问到&#xff0c;有没有可以无限提取的代理IP&#xff1f;就是比如我一秒钟提取几万、几十万次&#xff0c;或者很多台机器同时调用API提取链接&#xff0c;这样可以吗&#xff1f;看到这个问题&#xff0c;不禁沉思起来&#xff0c;其实理论上是存…

python基础教程:动态参数

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 如果有什么疑惑/资料需要的可以点击文章末尾名片领取源码 Python的动态参数有两种&#xff0c;分别是*args和**kwargs&#xff0c; 这里面的关键是一个和两个星号的区别&#xff0c;而不是args和kwargs在名字上的区别&#…

js粒子效果(二)

效果: 代码: <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Particle Animation</title><…

一篇文章让你入门python集合和字典

嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 一、集合: 增加 add 删除 del 删除集合 discard(常用)删除集合中的元素 &#xff0c;删除一个不存在的元素不会报错 remove 删除一个不存在的元素会报错 pop随…