Effective Java 学习笔记--36-38条 枚举类型

目录

枚举之前--int常量

枚举类型

枚举类型的特性

自定义方法和域的设置

实例的统一行为

实例的不同行为

switch语句

抽象方法

公共接口

策略枚举

用实例域代替默认序数

枚举集合

EnumSet

EnumMap


枚举类型用于定义一组固定的命名常量,在枚举类型中定义的每一个枚举常量都是枚举类型的一个实例,而且这个实例是不可变的。

枚举之前--int常量

枚举类型是Java 5版本引入的,在引入之前通常使用int常量来表示枚举类型,每一个int常量代表一个类型实例:

public class FruitMarket {public static final int APPLE = 0;public static final int ORANGE = 1;public static final int BANANA = 2;public static final int PINEAPPLE = 3;}

int常量的确能够轻松的列举出有限的枚举,但是有几个问题:

一是int常量没有安全性可言,只要是int值相同,不同类型的枚举都视为一样:

class FruitMarket {public static final int APPLE = 0;public static final int ORANGE = 1;public static final int BANANA = 2;public static final int PINEAPPLE = 3;}class VegetableMarket{public static final int CARROT = 0;public static final int CABBAGE = 1;public static final int TOMATO = 2;public static final int CUCUMBER = 3;}public class Application{public static void main(String[] args){VegetableMarket.CARROT == FruitMarket.APPLE;}}

二是Int常量作为静态常量在编译时就会被赋值,一旦常量做了改变(或者有了新增),就要重新编译文件,而且要重新检视应用了这些常量的Java文件。

三是Int常量没有丰富的专用方法,比如遍历所有的枚举、将具体的常量值映射到对应的String标识等等,都要通过循环遍历和switch语句来实现。

四是Int常量受限于Integer类型,无法顺畅的加入一些枚举特有的方法。

但是Int常量的优势在于其存储的轻量化,所以如果是需要轻量化的枚举(比如只需要作为简单的标识作用),Int常量还是可以考虑的。 

枚举类型

枚举类型的特性

Java的枚举类型对于枚举的使用做了针对性的设计,同时配置了齐全的功能。

一是通过公有的静态final域为每一个枚举常量导出一个枚举实例。

二是没有可以外部访问的构造器,进一步保证了外部无法通过构造器来新增枚举实例,但是可以允许有私有构造器,将枚举实例与相关的数据对应起来:

public enum Planet{MERCURY(3.302e+23, 2.439e6),VENUS(4.869e+24, 6.052e6),EARTH(5.972e+24, 6.378e6),MARS(6.419e+23, 3.396e6),JUPITER(1.899e+27, 7.149e7),SATURN(5.685e+26, 6.052e7),URANUS(8.681e+25, 2.556e7),NEPTUNE(1.002e+25, 1.188e6);private double mass;private double radius;private double surfaceGravity;private final static double G = 6.67300E-11;Planet(double mass, double radius){this.mass = mass;this.radius = radius;surfaceGravity = G*mass/(radius*radius);}}//这里将Planet实例与其质量(mass)、半径(radius)和重力常量对应起来。

三是枚举类提供了丰富的内置方法,其中绝大部分都是继承自java.lang.Enum基类(除了values()方法)

  • name()

    • 返回枚举常量的名称,即定义枚举时使用的标识符。
    • 例如,对于枚举 Day.MONDAYname() 方法将返回 "MONDAY"
  • ordinal()

    • 返回枚举常量在其枚举类型中的位置索引(从0开始)。
    • 例如,对于枚举 Day.MONDAYordinal() 方法将返回 0,因为它是第一个枚举常量。
  • toString()

    • 返回枚举常量的名称,与 name() 方法相同。
    • 默认情况下,toString() 方法返回枚举常量的名称。
  • compareTo(E other)

    • 比较两个枚举常量的顺序。
    • 参数 other 是同一枚举类型的另一个枚举常量。
    • 返回值表示当前枚举常量与 other 枚举常量的相对顺序。
  • valueOf(Class<E> enumType, String name)

    • 静态方法,用于根据枚举类型的名称获取枚举常量。
    • 如果名称匹配枚举类型中的一个常量,则返回该枚举常量;否则,抛出 IllegalArgumentException
  • values()

    • 静态方法,返回枚举类型的数组,包含该枚举类型的所有枚举常量。
    • 这个方法可以用来遍历枚举类型的全部常量。

可以看到绝大部分方法都是继承了Object类和实现Comparable与Serializable接口所实现的方法。

自定义方法和域的设置

实例的统一行为

枚举类型由于不是基本数据类型,所以在构建的时候可以添加自定义方法和域,这些方法和域可以实现将各种支持性数据和它对应的常量对应起来(比如将Apple实例与其图片、介绍等内容关联起来),自定义域的定义的在前面说私有构造器的时候已经举过相关例子了,这里把自定义的方法扩展一下:

public enum Planet{MERCURY(3.302e+23, 2.439e6),VENUS(4.869e+24, 6.052e6),EARTH(5.972e+24, 6.378e6),MARS(6.419e+23, 3.396e6),JUPITER(1.899e+27, 7.149e7),SATURN(5.685e+26, 6.052e7),URANUS(8.681e+25, 2.556e7),NEPTUNE(1.002e+25, 1.188e6);private final double mass;private final double radius;private final double surfaceGravity;private final static double G = 6.67300E-11;Planet(double mass, double radius){this.mass = mass;this.radius = radius;surfaceGravity = G*mass/(radius*radius);}public double getMass(){return mass;}public double getRadius(){return radius;}public double surfaceWeight(double mass){return mass*surfaceGravity;}
}

这里作者强调由于枚举类本身是不可变的,因此所有的域都要改成final,而且要做好封装,能私有化的尽量都私有化,通过公有接口来调用。

实例的不同行为

比如四则运算,每一个枚举实例的运算规则都不相同,这种实例的不同行为可以有多种实现方式:

switch语句
public enum Operation{PLUS, MINUS, TIMES, DIVIDE;public double apply(double x, double y){switch(this){case PLUS: return x+y;case MINUS: return x-y;case TIMES: return x*y;case DIVIDE: return x/y;}throw new AssertionError("Unknow op:"+this);}}//由switch语句来实现apply方法

这种方法比较简洁,但是有几个问题:

一是必须声明throw方法,不然无法通过编译,但是枚举类保证了实例的范围,所以这段代码是永远不会被执行的。

二是一旦新增了枚举常量,就需要记得给switch方法中再增加一个case。

抽象方法
package Operation;public enum BasicOperation{PLUS("+"){public double apply(double x, double y){return x + y;}},MINUS("-"){public double apply(double x, double y){return x-y;}},MULTIPLY("*"){public double apply(double x, double y){return x * y;}},DIVIDE("/"){public double apply(double x, double y){return x / y;}};public abstract double apply(double x, double y);//在枚举类中设置抽象方法,然后让各个实例来自行实现。 private final String symbol;BasicOperation(String symbol){this.symbol = symbol;}@Overridepublic String toString(){return symbol;}
}

这个方法将公共方法抽象到了类中,实现了高内聚,但是还可以进一步的抽象为公共接口。

公共接口
package Operation;//将apply方法抽象到接口当中
public interface Operation {public double apply(double x, double y);
}//具体的枚举类通过实现接口来调用接口方法apply
public enum BasicOperation implements Operation{PLUS("+"){public double apply(double x, double y){return x + y;}},MINUS("-"){public double apply(double x, double y){return x-y;}},MULTIPLY("*"){public double apply(double x, double y){return x * y;}},DIVIDE("/"){public double apply(double x, double y){return x / y;}};private final String symbol;BasicOperation(String symbol){this.symbol = symbol;}@Overridepublic String toString(){return symbol;}
}

公共接口的方式进一步提升了抽象的层次,使得任何枚举类都可以通过实现接口来调用相关方法,这种方式适合新建另一个枚举类的时候使用(比如新建一个AdvancedOperation类实现了幂运算和开方运算)。

策略枚举

策略枚举是一种较为特殊的方式,它适用的场景是对于枚举实例设置一个新的分类方式,并基于这个分类方式实行差异化的方法,比如把周一到周日分为工作日和休息日,其中工作日超过八小时的工作时间会产生加班工资,在休息日所有工作都产生加班工资:

package PayrollDay;public class PayrollDay {MONDAY(PayType.WEEKDAY),TUESDAY(PayType.WEEKDAY),WEDNESDAY(PayType.WEEKDAY),THURSDAY(PayType.WEEKDAY),FRIDAY(PayType.WEEKDAY),SATURDAY(PayType.WEEKEND),SUNDAY(PayType.WEEKEND);private final PayType, payType;GoodPayrollDay(PayType payType){this.payType = payType;}int pay(int minutesWorked, int payRate){return payType.pay(minutesWorked, payRate);}}
//这里将PayTYpe设置为新的分类方式,并且作为基本枚举类PayrollDay的一个域映射到每一个实例上,而pay的实现方法都定义在PayTYpe中enum PayType{WEEKDAY{int overtimePay(int minutes, int PayRate){return minutes <= MINS_PER_SHIFT ? 0 : (minutes - MINS_PER_SHIFT) * PayRate/2;};},WEEKEND{int overtimePay(int minutes, int PayRate){return minutes * PayRate/2;}};abstract int overtimePay(int minutes, int PayRate);private static final int MINS_PER_SHIFT = 8 * 60; int pay(int minutes, int PayRate){int basePay = minutes*PayRate;return basePay + overtimePay(minutes, PayRate);}
}

用实例域代替默认序数

枚举类有默认实现的ordinal()方法来返回每个枚举常量在类型中对应的数字位置,但是这个数字并不是程序员可以直接控制的,而且没有实际的意义,因此作者建议自行定义关联值并将它保存在一个实例域中:

package Ensemble;public enum ensemble {SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8), NONET(9), DECTET(10);private int numberOfMusicians;ensemble(int size){this.numberOfMusicians = size;}public int numberOfMusicians(){return numberOfMusicians;}
}

枚举集合

EnumSet

EnumSet是同一个枚举类常量的集合,在枚举类出现之前的int常量时期,往往使用位域操作来实现int常量的集合运算:

public class text {public static final int STYLE_BOLD = 1<<0;public static final int STYLE_ITALIC = 1<<1;public static final int STYLE_UNDERLINE = 1<<2;public static final int STYLE_STRIKETHROUGH = 1<<3;public static int applyStyle(int style){return style;};
}

这里由int常量来表示枚举,由于int有32位,这意味着可以使用int类型来表示最多32中不同的状态,并且通过or运算来做并集(比如将0001和1000合并成为1001)。

但是这种方式有很大的问题,一是它继承了int常量所有的缺陷,二是难以遍历位域运算形成的集合,三是在编写API的时候,必须先预测最多需要多少位,同时要选择合适的类型(如果特性较多可能要使用long类型),不然会导致溢出。

所以EnumSet可以完美解决上述的缺陷,它实现了Set接口的集合处理方法,使得对于集合的各种处理功能更加丰富。

public class Text{public enum Style{BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}publi void applyStyles(Set<Style> styles){};
}Text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

EnumMap

EnumMap即以枚举常量为key的Map,在实现EnumMap之前一般通过序数索引来构建枚举实例为key对应的Map类型,而序数索引一般由ordinal方法实现。

package Plant;import java.util.HashSet;
import java.util.Set;public class BadApplication {public static void main(String[] args) {Set<Plant> garden = new HashSet<>();garden.add(new Plant("Violets", Plant.LifeCycle.ANNUAL));garden.add(new Plant("Radishes", Plant.LifeCycle.PERENNIAL));garden.add(new Plant("Apple", Plant.LifeCycle.BIENNIAL));garden.add(new Plant("Pears", Plant.LifeCycle.BIENNIAL));garden.add(new Plant("Grapes",Plant.LifeCycle.ANNUAL));Set<Plant>[] plantByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];for(int i = 0; i < Plant.LifeCycle.values().length; i++){plantByLifeCycle[i] = new HashSet<>();}for(Plant p : garden){plantByLifeCycle[p.getLifeCycle().ordinal()].add(p);}for(int i=0; i<plantByLifeCycle.length; i++){System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantByLifeCycle[i]);}}}

这方法首先数组与泛型合用时程序需要进行未受检的转换,不然会在编译是出告警;二是ordinal()方法返回的序数和plantByLifeCycle序数的对应需要人工进行核对,程序是不保证的。

而EnumMap可以很好的解决这个问题:

public class Application {public static void main(String[] args) {Set<Plant> garden = new HashSet<>();garden.add(new Plant("Violets", Plant.LifeCycle.ANNUAL));garden.add(new Plant("Radishes", Plant.LifeCycle.PERENNIAL));garden.add(new Plant("Apple", Plant.LifeCycle.BIENNIAL));garden.add(new Plant("Pears", Plant.LifeCycle.BIENNIAL));garden.add(new Plant("Grapes",Plant.LifeCycle.ANNUAL));Map<Plant.LifeCycle, List<Plant>> plantByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);for(Plant.LifeCycle lc: Plant.LifeCycle.values()){plantByLifeCycle.put(lc, new ArrayList<Plant>());}for(Plant p: garden){plantByLifeCycle.get(p.getLifeCycle()).add(p);}for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {System.out.println(lc+": ");for (Plant plant : plantByLifeCycle.get(lc)) {System.out.println(plant);}System.out.println("\n");}}

这里有的读者会质疑EnumMap和普通Map相比在起初构建时并无显著不同,那它的优势在哪里:

  1. 类型安全EnumMap 的键必须是某个枚举类型的枚举值。这意味着在编译时就会检查键的有效性,从而避免了运行时可能出现的 ClassCastException
  2. 高效EnumMap 内部使用数组实现,因此在查找枚举键时非常高效。这是因为枚举类型的枚举值数量是固定的,而且是已知的,因此 EnumMap 可以预先计算出每个枚举值的位置。
  3. 有序EnumMap 会按照枚举值的声明顺序来维护键的顺序。这意味着当你遍历 EnumMap 时,键的顺序是确定的,这有助于简化代码逻辑。

除了上述方法之外,作者还展示了如何使用Stream的形式来实现EnumMap:

System.out.println(Arrays.stream(garden).collect(groupingBy(p->p.lifeCycle, ()->new EnumMap<>(LifeCycle.class), Collector.toSet())));

区别在于后一种方式可以仅包含garden涉及的枚举类型,而第一种的key中会列示所有的枚举常量。 

这里补充一下Stream的用法解析:

  1. 使用 Stream API:使用 Arrays.stream(garden) 将列表转换为 Stream。
  2. 分组:使用 Collectors.groupingBy 方法按 Plant 的 lifeCycle 属性进行分组。
    1. Plant::getLifeCycle:指定分组键的提取器。
    2. () -> new EnumMap<>(LifeCycle.class):指定创建 EnumMap 的工厂方法。
    3. Collectors.toSet():指定收集器,将每个分组的结果收集到 Set 中。

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

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

相关文章

秋冬春夏,纪念在CSDN的第365天

目录 时光 收获 工作 生活 憧憬 时光 再次收到创作纪念日的消息时&#xff0c;已在CSDN创作和度过了一年的时光。创作&#xff0c;成了自己的第二工作空间&#xff0c;成为了日常的一种习惯。 每当看到第1篇文章的提醒消息&#xff0c;都会想起当时创作的初衷和情景。是一…

在手机在线预览3D模型,是如何实现的?

在手机在线预览3D模型&#xff0c;主要依赖于几个关键技术和步骤来实现。以下是一个概括性的流程&#xff1a; 一、模型上传 选择平台&#xff1a;首先&#xff0c;用户需要选择一个支持3D模型在线预览的平台&#xff0c;如51建模网、Sketchfab等。这些平台通常提供用户友好的…

五分钟本地部署Uptime Kuma运维监控结合内网穿透实现远程访问

文章目录 前言**主要功能**一、前期准备本教程环境为&#xff1a;Centos7&#xff0c;可以跑Docker的系统都可以使用本教程安装。本教程使用Docker部署服务&#xff0c;如何安装Docker详见&#xff1a; 二、Docker部署Uptime Kuma三、实现公网查看网站监控四、使用固定公网地址…

Kafka的生产者和消费者机制

目录 1.基础的客户端 1.1消息发送者的主流程 1.2消息消费者主流程 2.客户端工作机制 2.1消费者分组消费机制 2.2生产者拦截器机制 2.3消息序列化机制 2.4消息分区路由机制 2.5生产者消息缓存机制 2.6发送应答机制 2.7生产者消息幂等性 (1)生产者消息幂等性介绍 (2…

XDMA原理

目录 1. BAR Space Map2. PCIe to AXI Lite Master2.1. BAR Address to AXI Address 3. PCIe to DMA Bypass4. AXI4 Memory Mapped4.1. Register Space 5. AXI Lite Slave Configuration Interface6. MSI/MSI-X Interrupt6.1. Interrupts Configuration6.2. IRQ Module 7. IP E…

CentOS 7 更换为国内YUM源详细教程

CentOS 7 将 yum 源更换为国内源的保姆级教程如下&#xff0c;这个过程包括备份原有源、下载国内源、清理缓存、生成新缓存以及验证更换效果等步骤。 一、备份原有源 在更换 yum 源之前&#xff0c;建议先备份原有的 yum 源配置文件&#xff0c;以防万一更换后出现问题需要恢…

Gitlab迁移到新的服务器后点击Integrations报500错误的解决方法

目录 一、问题描述二、解决方法1.方法12.方法2(1)备份 /etc/gitlab/gitlab-secrets.json(2)进入数据库(3)查看 Project ID(4)查找 hook ID 并删除(5)申请访问令牌并执行 api 删除操作一、问题描述 Gitlab迁移到新的服务器后点击Integrations报500错误: 查后台日志…

Linux - 深入探讨 Linux `ls` 命令:一个全面的技术指南

作者&#xff1a;逍遥Sean 简介&#xff1a;一个主修Java的Web网站\游戏服务器后端开发者 主页&#xff1a;https://blog.csdn.net/Ureliable 觉得博主文章不错的话&#xff0c;可以三连支持一下~ 如有疑问和建议&#xff0c;请私信或评论留言&#xff01; 前言 在 Linux 系统中…

打卡57天------图论(两种算法)

最近的算法题都太高深莫测了&#xff0c;对于一个前端工程师来说&#xff0c;要求没有那么严吧。 今天在学习prim 和 kruskal的同时&#xff0c;也要清楚这两个算法的区别所在。 一、prim算法精讲 代码随想录 无JS官方题解代码。 二、kruskal算法精讲 代码随想录 无JS官方题解…

前后端开发学习路线 囊括Dubbo、Elasticsearch等

以下都是博主本人看过后给出的推荐。 文章目录 前端入门Web开发基础&#xff08;HTML、CSS、JS&#xff09;写项目前置&#xff08;AJAX、Vue等&#xff09;开始写项目&#xff08;Vue、Uniapp&#xff09;重点Future 入门Java后端基础部分&#xff08;Java、MySQL&#xff09;…

kubectl陈述式资源管理方式、声明式资源管理

一、命令行: kubectl命令行工具 优点: 90%以上的场景都可以满足 对资源的增&#xff0c;删&#xff0c;查比较方便&#xff0c;对改不是很友好 缺点:命令比较冗长&#xff0c;复杂难记 声明方式&#xff1a;k8s当中的yaml文件实现资源管理----声明式 GUI:图形化工具的管理…

【终端IDPS】开源安全平台Wazuh之Wazuh Server

引言 Wazuh是一个开源的、免费的企业级安全监控解决方案&#xff0c;专注于威胁检测、完整性监控、事件响应和合规性。它由部署在受监控系统的端点安全代理和管理服务器组成&#xff0c;服务器收集并分析代理收集的数据。Wazuh支持多平台&#xff0c;包括Windows、Linux、macOS…

VLM 系列——Mini-Monkey——论文解读

一、概述 1、是什么 Mini-Monkey 论文全称《Mini-Monkey: Multi-Scale Adaptive Cropping for Multimodal Large Language Models》&#xff0c;是一个轻量级MLLM&#xff08;多模态的视觉-文本模型&#xff09;&#xff0c;基于InternViT、MLP和InternLLM&#xff0c;其实就是…

基于元神操作系统编程写硬盘扇区

1. 背景 本文介绍了“调用元神操作系统API向硬盘扇区写数据”的程序实现及测试结果。 2. 方法 &#xff08;1&#xff09;调用元神操作系统API读硬盘扇区 本部分内容已在前面的文章中进行介绍&#xff0c;详细内容请参考“编写程序调用元神操作系统的API”。 &#xff08;…

STM32F103与ESP8266 WIFI模块的USART中断调试

本文概述了利用STM32F103的HAL库&#xff0c;通过USART中断方式调试ESP8266&#xff08;ESP-01&#xff09;WIFI模块的方法。首先介绍了开发环境和ESP-01模块的基本特点与功能&#xff0c;随后展示了如何通过电脑PC的串口助手进行初步调试与验证。最后&#xff0c;重点阐述了如…

SpringBoot-读取配置文件方式

前言 Spring Boot提供了多种灵活的方式来读取配置文件&#xff0c;以适应不同的开发和部署需求&#xff0c;SpringBoot启动的时候&#xff0c;读取配置文件的时候&#xff0c;首先获取的是file:/config/文件下的配置文件&#xff0c;也就是项目下config文件里面的配置文件&…

jmeter 响应乱码

Jmeter在做接口测试的时候的&#xff0c;如果接口响应的内容中有中文&#xff0c;jmeter的响应内容很可能显示乱码&#xff0c;为了规避这种出现乱码的问题&#xff0c;就要对jmeter的响应结果进行编码处理。 打开jmeter进行接口、压力、性能等测试&#xff0c;出现以下乱码问…

等保需要的设备部署vs网络安全域划分原则

等保需要的安全设备和部署 技术要点–等保2级 技术要点–等保3级 安全产品/服务全景图 选型产品说明 安全区域边界配置 资产探测及漏洞检测配置 网络安全域划分原则 网络安全域 定义 网络安全域是一种网络划分方式&#xff0c;将具有相同安全需求、访问控制、相同信任级别、…

【MySQL 14】用户管理

文章目录 &#x1f308; 一、用户管理⭐ 1. 用户信息⭐ 2. 创建用户⭐ 3. 修改密码⭐ 4. 删除用户 &#x1f308;二、数据库的权限⭐ 1. 权限列表⭐ 2. 查看权限⭐ 3. 授予权限⭐ 4. 回收权限 &#x1f308; 一、用户管理 MySQL 也会区分普通用户和超级用户。如果只是访问某一…

【云原生系列之SkyWalking的部署】

1、分布式链路追踪 1.1概念 在较大的web集群和微服务环境中&#xff0c;客户端的一次请求需要经过不同的模块&#xff0c;多个不同中间件&#xff0c;多个不同机器一起相互协作才能处理完成客户端的请求&#xff0c;而在这一系列的请求过程之中,处理流程可能是串行执行,也可能…