BlogWork(1)-Elevator-电梯类设计作业
前言
前三次作业点题目除了电梯类设计,难度都算简单。电梯类设计所涉及到的知识点大体并不太多,难度由于题干信息的缺乏,算比较难,重要的点在于理解题意,所以可以说涉及到的算法是。
- 题意所述的类
SCAN/LOOK电梯寻路算法
不能按常规SCAN/LOOK算法来实现的原因还算比较好理解,因为题目没有时间维度,而正常电梯运作是有时间维度的,电梯所在的楼层,请求等,都是动态变化的。
其他的题目并没有涉及什么很和自己相关的,很额外的知识点,但是如果算上实现中附带的一些Java的基础特性,那整体涉及的知识点就比较多了。
- 类的编写与使用
- Java视角的函数
- Java的标准输入输出
- Java的集合框架
- 正则表达式处理
- MVC设计模式
作业设计路径
这次作业设计经过了三次迭代,其中最后一次由于第二次代码的易懂性和易维护性比较高,使用很快的写完了(不到10分钟),前两次都化费了一段时间,且化费的时间在不同地方。
第一次作业-复杂度爆炸的起始
第一次作业由于建立在信息极少,以及题目不太常规的视角下,在程序实现的过程中存在普遍的迷茫,在程序设计的过程中,很多个地方充满了多种解法,而不知道答案是什么解法。这种不确定性导致了代码的复杂度急剧增加,使得维护和理解变得异常困难。例如:
- 优先处理同方向的请求:这里指的是优先处理在当前层次以上的内部请求以及在当前层次上的任意方向的外部请求,还是优先处理在当前层次以上的内部请求以及对应方向的外部请求。
- 当同方向的请求均被处理完毕然后再处理相反方向的请求:这里的实现指的是相反方向的请求根本不出队列,优先处理方向相同的队列的请求,直到两个队列均反向。还是指的将相反请求出队列暂存,两个队列同时继续上述步骤,直到反向后开始处理暂存的请求。这两种解释导致了完全不同的代码结构和复杂性。
- 电梯内部乘客的请求队列和电梯外部楼层乘客的请求队列:电梯外部楼层乘客的请求队列是否分为两个方向,每个方向都是一个队列,还是混在一起。队列的设计直接影响了整个系统的运行逻辑。
这里,我们知道正确的解法是,第一个选择是均考虑,第二个选择是前者,相反方向的请求根本不出队列,优先处理方向相同的队列的请求,第三个选择是混在一起。
但是我在写这次作业的时候,尝试了很多次才试出来,几乎遍历了它们的组合数。
但是我仍然没有尝试出完全和标准答案一样的解法,不过它确实通过了这次题目集,由于这次题目集测试点少,第二次就无法通过了,对于第二点,我采取的是请求暂存,虽然在大部分场合都和标准答案一致,甚至更智能一点,但是带来了过度的复杂度。
复杂度
对于第一次作业,由于上面提到的,错误考虑了请求暂存,而这个实现较为复杂,可以说这个程序复杂度爆炸了,我自己都看不太懂。代码中的嵌套条件语句、复杂的逻辑分支和大量的边界情况处理使得整个系统变得难以理解和维护。

这里各项指标都到了严重失衡的地步,特别是对于圈复杂度(CogC)。高圈复杂度通常意味着函数中有过多的条件分支,这不仅增加了理解的难度,也增加了测试的复杂性和错误的可能性。
平均复杂度高和我的函数少有些许关系,但是无法否认的是,
Total总复杂度非常高。这反映了整体代码质量的问题,而不仅仅是个别函数的问题。
如果好奇它有多复杂,参考圈复杂度最高的函数electNextTargetBasedOnHeads()
private void selectNextTargetBasedOnHeads()
{
enum QueueType
{
INTERNAL,
UP,
DOWN
}
ElevatorRequest potential_internal = internal_queue_.peek();
ElevatorRequest potential_up = external_up_queue_.peek();
ElevatorRequest potential_down = external_down_queue_.peek();
ElevatorRequest chosen_request = null;
QueueType chosen_queue = null;
if (status_ == Direction.UP)
{
if (potential_internal != null && potential_internal.floor() >= current_floor_
&& potential_up != null && potential_up.floor() >= current_floor_)
{
int dist_up = Math.abs(potential_up.floor() - current_floor_);
int dist_internal = Math.abs(potential_internal.floor() - current_floor_);
if (dist_internal < dist_up)
{
chosen_request = potential_internal;
chosen_queue = QueueType.INTERNAL;
} else
{
chosen_request = potential_up;
chosen_queue = QueueType.UP;
}
}
else if (potential_internal != null && potential_internal.floor() >= current_floor_)
{
chosen_request = potential_internal;
chosen_queue = QueueType.INTERNAL;
}
else if (potential_up != null && potential_up.floor() >= current_floor_)
{
chosen_request = potential_up;
chosen_queue = QueueType.UP;
}
}
else if (status_ == Direction.DOWN)
{
if (potential_internal != null && potential_internal.floor() <= current_floor_
&& potential_down != null && potential_down.floor() <= current_floor_)
{
int dist_down = Math.abs(potential_down.floor() - current_floor_);
int dist_internal = Math.abs(potential_internal.floor() - current_floor_);
if (dist_internal < dist_down)
{
chosen_request = potential_internal;
chosen_queue = QueueType.INTERNAL;
}
}
else if (potential_internal != null && potential_internal.floor() <= current_floor_)
{
chosen_request = potential_internal;
chosen_queue = QueueType.INTERNAL;
}
else if (potential_down != null && potential_down.floor() <= current_floor_)
{
chosen_request = potential_down;
chosen_queue = QueueType.DOWN;
}
}
if (chosen_request == null && hasAnyRequest())
{
int dist_internal_ = (potential_internal == null) ? Integer.MAX_VALUE : Math.abs(potential_internal.floor() - current_floor_);
int dist_up_ = (potential_up == null) ? Integer.MAX_VALUE : Math.abs(potential_up.floor() - current_floor_);
int dist_down_ = (potential_down == null) ? Integer.MAX_VALUE : Math.abs(potential_down.floor() - current_floor_);
if (dist_internal_ < dist_up_ && dist_internal_ < dist_down_)
{
chosen_request = potential_internal;
chosen_queue = QueueType.INTERNAL;
}
else if (dist_up_ < dist_down_)
{
chosen_request = potential_up;
chosen_queue = QueueType.UP;
}
else
{
chosen_request = potential_down;
chosen_queue = QueueType.DOWN;
}
}
if (chosen_request != null)
{
if (chosen_queue == QueueType.INTERNAL)
{
current_request_ = internal_queue_.poll();
if (status_ == Direction.UP)
{
if (external_up_queue_.peek() != null)
{
if (external_up_queue_.peek().floor() == current_floor_)
{
current_request_ = external_up_queue_.poll();
}
}
}
}
else if (chosen_queue == QueueType.DOWN)
{
current_request_ = external_down_queue_.poll();
if (internal_queue_.peek() != null)
{
if (internal_queue_.peek().floor() == current_floor_)
{
current_request_ = internal_queue_.poll();
}
}
}
else if (chosen_queue == QueueType.UP)
{
current_request_ = external_up_queue_.poll();
if (internal_queue_.peek() != null)
{
if (internal_queue_.peek().floor() == current_floor_)
{
current_request_ = internal_queue_.poll();
}
}
}
if (current_request_.floor() > current_floor_)
{
status_ = Direction.UP;
}
else if (current_request_.floor() < current_floor_)
{
status_ = Direction.DOWN;
}
else
{
status_ = Direction.NONE;
}
}
else
{
current_request_ = null;
status_ = Direction.NONE;
}
}
这段代码确实展示了令人有点恐怖的复杂度。嵌套的条件语句、多层次的逻辑分支和缺乏清晰的结构使得理解和维护变得异常困难。这种代码不仅容易引入bug,也使得未来的修改变得非常棘手。
思考和反思
在第一次作业内,由于之前的不太正确的编程理念,认为OOP理念强耦合于世界的抽象,即可以被描述为什么可以干什么的都可以构成一个类,冰箱,现实世界的电梯显然可以接受请求,发出请求,移动等,所以我只是单纯把它们作为一个类。这种过度的现实映射导致了类的职责不明确,功能混杂,违反了单一职责原则。
但是实际上,为了保证程序的可维护性,可读性,可能会需要不按现实世界的抽象,而按程序的可维护性和可读性的人出于代码质量都角度进行分类。软件设计更多地应该关注系统的结构和组织,而不是简单地模仿现实世界的实体关系。
这让我重新思考作为软件基础学科的,设计模式的重要性,在当今,所有人都极其在意数据结构算法,甚至过度在意,虽然它确实很重要,但是设计模式理论上,工程上应当得到和数据结构与算法的同等待遇。设计模式提供了解决特定问题的经验总结,能够显著提高代码的可维护性和可扩展性,这在实际工程中可能比纯粹的算法优化更为重要。
外部请求记录与状态枚举
在我的第一次作业内,状态的定义如下。
class Elevator
{
public enum Direction {UP, DOWN, NONE}
private Direction status_ = Direction.UP;
}
而外部请求的定义如下
class Elevator
{
public record ElevatorRequest(boolean isInner, Direction way, int floor)
{ }
}
它们都定义于Elevator类的内部,是一种内部类。同时,由于它们的定义是记录record与枚举enum,它们都自动成为静态内部类。
使用枚举与记录的原因
对于一个状态,显然可以通过一系列互相独立常量(在Java枚举)来表示,或是一种数字来表示,但是为了区分数字之间的意义,我们可以使用符号常量(C/C++枚举)来表示。
显然,这种的表示方法比单纯开一堆Bool变量更好,同时比位表示BitMask更加易用。使用多个布尔变量容易导致无效的状态组合,而位掩码虽然高效但可读性较差,难以调试。
很显然,这里Direction,即方向是由多个可能的值构成的变量,且这种值是有限的,用枚举是十分符合枚举的定义和适用场合的。
使用枚举除了可以优美实现有限值约束和表示的场合,还支持某些额外的高级操作,比如对于由枚举变量作为键的
Map,Java内内置了一个EnumMap,它很高效,由于枚举的有限性,可以使用数组映射表示键,而数组访问是高效($O(1)$)的。
这里有意思的事情是,如果枚举的元素很多的话,比如一个枚举类型有几万个枚举量,这个时候需要开相当大的数组,可能会想Java在这种情况下有没有退化策略。
实际上,如你所想,Java就是那么神奇,并没有。
不过也这种情况不太可能,你愿意开几万的枚举尝试一下吗:)
而对于记录,前面提到,记录描述了一种值的概念,就和自然数一样。记录类型(Java 16引入)提供了一种简洁的方式来定义不可变的数据持有类,自动生成了很多通常需要手动编写的方法。
这里ElevatorRequest描述了一种请求的概念,它由是否为内部请求,请求方向和请求楼层组成,我们并不需要对它进行任何操作,我们只需要它被存储,同时我们不希望它被修改,因为一个请求就只是请求,它自己不能变换自己,就像1就是1一样,只需要存储和访问。
在这种场合下,使用记录去描述这种概念是十分合适的,因为记录类型为值的概念的表示提供了自动生成的构造函数,访问器方法、equals()、hashCode()和toString()方法,同时将所有字段置为private final,将自己置为final,这样省去了手动书写的复杂度,同时也作为一种约束去约束值的概念。
如果自己实现值的概念的话,很容易漏掉一些方法的覆写,比如
equals()与hashCode(),它们的默认行为基于地址(没错,就是C/C++里面听到的内存地址,不过有些许区别,不过概念一致),这样就会导致不同对象但是有相同字段的类的取等判断是否的,而作为值,我们显然不希望这样子,毕竟你希望在这张草稿纸的1和在另外一张草稿纸的1不相等吗?
请求作为一个不可变类,不止实现了值的概念,其还有一个很好的性质,由于它本身是不可变类,如果它的成员字段也是不可变类或者没有对象字段,那它是绝对线程安全的。这种线程安全性在并发环境中非常宝贵,可以减少同步需求,提高性能并降低复杂度。
第二次作业-重构一切,包括算法
第二次作业的条件是拆分一个类到多个类,但是更改了一些测试条件,我人之常情尝试了能不能改完第一次类然后直接通过,实际上并不行。
我写第二次作业的时候,对于第一次作业的那些疑问和可能的实现都有说了解了,因此这次作业比较具有方向性。有了明确的理解和目标后,代码的结构和质量都得到了显著改善。
由于第一次代码就算我拆分完类,算法部分仍然集中在Controller类内,所以还是很复杂,很难懂,其实没什么区别。仅仅将代码分散到不同的类中,而不改变其基本算法和结构,并不能真正解决复杂度问题。
考虑到我的第三次作业,我觉得是时候再次重写一次,优美得重写。
重写主要基于算法部分,结构沿用了基于第一次作业的类职责拆分,重构的过程并不困难,实际上还算比较简单。
复杂度
第二次的作业的复杂度扫尾小于第一次作业,同理,由于过于简单的函数较少,所以Average复杂度不算低,但是Total已经显著低于以前的作业了。
当然不能满意于这一点,它还有很多提升空间,有一些提升空间非常明显,我都能感觉出来。

如果你好奇之前那坨超级复杂的函数的变化,我只能说,它职责几乎没变,但是简单优美了不止一点,同时它的命名相比之前更加优美,虽然有点长。
private void selectNextTargetBasedOnHeads() {
Integer internal_buffer = queue_.getInternalQueue().peek();
ExternalRequest external_buffer = queue_.getExternalQueue().peek();
ElevatorRequest internal_target = internal_buffer != null ? new ElevatorRequest(true, Direction.IDLE, internal_buffer) : null;
ElevatorRequest external_target = external_buffer != null ? new ElevatorRequest(false, external_buffer.way(), external_buffer.floor()) : null;
if (internal_target == null && external_target == null)
return;
else if (internal_target == null) {
current_request_ = external_target;
return;
} else if (external_target == null) {
current_request_ = internal_target;
return;
}
boolean is_internal_higher = internal_target.floor() >= elevator_.getCurrentFloor();
boolean is_external_higher = external_target.floor() >= elevator_.getCurrentFloor();
boolean internal_higher_then_external = internal_target.floor() >= external_target.floor();
switch (elevator_.getStatus()) {
case UP -> {
if (is_internal_higher && is_external_higher) {
if (internal_higher_then_external && external_target.way() == Direction.UP) {
current_request_ = external_target;
} else {
current_request_ = internal_target;
}
} else if (is_internal_higher) {
current_request_ = internal_target;
} else if (is_external_higher) {
current_request_ = external_target;
}
}
case DOWN -> {
if (!is_internal_higher && !is_external_higher) {
if (!internal_higher_then_external && external_target.way() == Direction.DOWN) {
current_request_ = external_target;
} else {
current_request_ = internal_target;
}
} else if (!is_internal_higher) {
current_request_ = internal_target;
} else if (!is_external_higher) {
current_request_ = external_target;
}
}
}
if (current_request_ == null) {
int dist_internal = internal_target != null ? Math.abs(internal_target.floor() - elevator_.getCurrentFloor()) : Integer.MAX_VALUE;
int dist_external = external_target != null ? Math.abs(external_target.floor() - elevator_.getCurrentFloor()) : Integer.MAX_VALUE;
if (dist_internal <= dist_external) {
elevator_.setStatus(internal_target.floor() >= elevator_.getCurrentFloor() ? Direction.UP : Direction.DOWN);
}
else{
elevator_.setStatus(external_target.floor() >= elevator_.getCurrentFloor() ? Direction.UP : Direction.DOWN);
}
}
}
光看行数就少了非常多了。更重要的是,代码结构更加清晰,逻辑分支更加合理,使用了更具描述性的变量名和更现代的Java语法。
思考与反思
这一次的作业在一个地方卡了一段时间,究其原因为我误以为题目的所谓的忽略重复请求是位于同一行的重复请求,比如<2><2><2>,事实上题干也是这么写的,但是测试样例给了潜在的提示。
这种理解上的差异体现了需求理解的重要性,以及测试样例在澄清需求方面的价值。
因此,我一开始一直不忽略这种情况。
<3>
<3>
<3>
这造成了我有一个测试点根本过不去,直到我去询问别人关于重复的处理。需求理解的偏差导致了实现上的问题,这是软件开发中常见的挑战之一。
明明在第一次作业中,自己是什么都情况考虑,什么情况都做规划的,但是在第二次作业,由于知道大概的实现思路,就摒弃了这个特性,从而在一个点上卡了很久。
因此,尽管你知道一个东西大概是怎么样子的,仍然应当去做出规划和最坏情况考虑。这是软件工程中的一条重要原则:无论问题看起来多么简单或熟悉,都应该仔细分析需求,考虑边界情况,并进行充分的测试。
第三次作业-快速扩展
第三次作业的修改相比第一次作业到第二次作业的修改更多,要求如下:
- 乘客请求输入变动情况:外部请求由之前的
<请求楼层数,请求方向>修改为<请求源楼层,请求目的楼层> - 对于外部请求,当电梯处理该请求之后(该请求出队),要将
<请求源楼层,请求目的楼层>中的请求目的楼层加入到请求内部队列(加到队尾)
但是对于这些修改,容易看出,这些修改大抵在我原来的算法和框架都并不困难,得益于我第二次作业对第一次作业的完全重构,第三次的作业的实现得以非常简单。
事实上,我只修改了不到10行,并且全局替换了ExternalRequest名称到Passenger,所用时间不到10分钟,这让我非常意外。这种高效率的适应能力证明了前期重构的价值,也体现了良好设计的回报。
这都得益于第二次作业的努力,和程序的可扩展性和可维护性的重要性。投入时间在设计和结构上的努力最终会在未来的变更和扩展中得到回报。
复杂度
由于没做什么复杂的更改,只对第二次作业做了少数的修改,所以复杂度相比第二次作业变化不大。

思考与反思
这次作业写的非常块,这得益于我第二次代码的可维护性与可扩展性相对比第一次好很多。说实话,很难想象使用第一次代码为基础进行修改会花10分钟的多少次次方。第一次代码的复杂结构和混乱逻辑会使得任何变更都变得异常困难,可能需要大量时间来理解代码,并确保修改不会引入新的bug。
这次作业让我感受到了,尽管不保证代码的优良性可能可以省去第一次编写代码的时间,但是在之后的维护和扩展中,可能会花费相当多,甚至比之前更多的时间,而我的代码仍然具有很多改进空间。这是软件工程的一个重要教训:前期投入在代码质量上的时间可能会在长期维护中节省更多时间。
在之后的编写中,应当注意一下代码的初次编写,确保初次编写就有比较好的代码质量,才能在之后省去很多时间,带来长期的收益。
一点小小的建议
如果下面的话略失妥当的话,请相信我是带着尊重的心态书写这个可能比较冒犯的部分的。
在之前也有提到过,在第一次作业的时候,信息是相当少的,这就导致了在书写算法的时候,在代码的某个阶段的时候,比如电梯转向的时候,可能有许多种选择,这会带来许多试错成本,而且这些试错彼此独立。
试错虽然比较独立,但是之间关系仍然比较多,也就是说,虽然我们改错的成本很低,但是仍然相当机械,而且学不到什么东西。
而且有点吃运气……
在现实的世界中,少的可怕的信息其实似乎并不太常见,在第二次的作业时候的一些补充可以认为是比较少信息的现实的开发场合,但是第一次实在是有点过于少了。
在这次作业的代码迭代的过程中,实际上并不太能突出MVC设计模式的好处,周围的很多人只是机械的按类图设计,而且在这三次作业也没从MVC模式中得到什么。
我觉得可以在更加具体的场合实践对应的模式,个人曾经在MVVM模式适合的场合(Qt编程)中使用MVVM模式得到过明显的受益,但是对于这次的MVC模式,我的感受实在是有点少。
在这次的作业编写中,我体验到了Java中两个特殊类型,枚举和记录的用处和易用性,尽管它们的功能都可以通过类来实现,但是适当运用它们,可以简化开发,精简代码,并且提供更好的可读性和标准性。
下面的内容和上文作业分析无关,如果你感兴趣的话,也欢迎看下去诺。
记录(Record)和枚举(Enum)
由于我在这几次作业中大量使用了记录(Record)和枚举(Enum)类型,所以这里做一些解释和介绍。
Enum是我的比较早期的文章,风格可能没有Record成熟。
不可变类与不可变承诺
不可变类描述了一种类的概念,即类对应对象的状态都是不可变的,在Java内体现为类的状态字段声明都是final的。
String类就是一个很经典的不可变类,JVM对String的共享特性的实现依赖于String的不可变承诺。
不可变类只可以在构造的时候规定它的状态 之后都不能动态改变。
由于Java没有像C++一样区分类的更改器与访问器方法,区分常量对象与变量对象,所以无法在类内包含某一个对象的时候实现彻底的不可变类。
当类包含某个对象的时候,即包含某个对象的引用的时候,此时状态指的应该是引用本身,final引用无法更改其指向,但可以更改其引用的对象的值。
如果想让不可变类的不可变承诺适用于多线程的共享场合,要注意Java
final引用的对象的内部的值也是可能可以被更改的。
记录(Record)类型
如果我们不关注不可变承诺带来的性质,而去思考其概念的话,可以发现,对于一个场合 需要一个载体来传递一系列信息。一个透明的不可变类是非常合适的,因为它能确保它里面的数据能被访问,传递,但是也能确保它不被复用。因为它单纯只是信息,不能更改。
换一句话 一个不可变类实现了一个值的概念,即值是唯一确认的,是纯粹的信息,不可以更改一个值本身。
但是我们要通过class去书写这种具有值特性的不可变类其实不太方便 而且不方便和普通类辨认。
Java提供了一种特殊类型,记录Record类型,来比较方便的,易于区分得去定义这种不可变类。
定义:记录(Record)是不可变类型的透明载体
其实我们可以简单地认为
Java的记录对应C#的元组,不过它是不匿名的。
对于记录 其内的所有字段(成员变量)必须也默认是final和private的,其透明化不是由直接暴露public的成员变量提供,而是会自动且默认提供对应成员的访问方法提供,Java为这些成员变量提供了一种名称,叫做记录的组件。
Record类型本质上也是一种类Class,它在定义的时候便继承自java.lang.Record基类,该基类同理继承自java.lang.Object基类,但是覆写了一些方法,也默认提供了一些方法。
Record覆写了这些Objects方法:
boolean equals(Object obj)比较记录的每一个组件是否相等,对于引用类型 会使用静态方法Objects.equals(Object, Object)比较,对于基础类型,装箱后正常比较。int hashCode()通过特殊的方式(取决于实现) 组合每一个组件对应的hashCode()结果的值。String toString()给出简洁完整的 包含组件以及组件对应的字符串(toString()结果)的组合结果
同时 Record提供了每一个组件对应的访问 因为组件都是private的,它们对应的访问方法的名称为组件名称本身,如对于成员变量(组件)x 对应的访问方法就是x()
Java在这种默认名称上并不遵循获取值的
get与is命名原则,有利有弊。
枚举(Enum)
还记得C/C++怎么描述枚举类型的吗?
- 一种快速定义整型符号常量的方式。
- 提供了一种类型只具有少数值的解决方案(值约束)。
Java的枚举类型与C++11的作用域内枚举非常相似。Java枚举的设计目标与C/C++的第一种用法(单纯作为整型常量别名)有所不同,它更侧重于提供类型安全的对象集合。
枚举本质上是一种不可变类,其枚举常量(实例)是public static final的,枚举类型本身也是final的(不可被继承)。
C/C++传统枚举
在某些应用场合,变量取值被限定在特定范围内,C语言为此创造了枚举类型,C++自然也继承了这一特性。
C++的enum关键字提供了批量创建符号常量的方式,这些常量值为整型,但类型为枚举类型,这与const创建的常量不同。const创建的常量类型为const类型,而枚举创建的常量类型为声明的枚举类型。
枚举定义了一种特殊数据类型和变量,该变量只接受对应枚举类型的常量,且不发生自动类型转换。因此,只能通过对应的枚举类型值(不进行强制类型转换的情况下)与它交互。
枚举值表定义了一系列符号常量,每个符号常量对应一个整型值,这些符号常量被称为枚举量,对应的整型值称为枚举值。
虽然枚举变量是整型的一种,但无法通过普通整型与枚举变量交互,必须使用对应的枚举量,因为枚举变量只接受对应的枚举型常量。
需要注意的是,枚举类型本身是常量,定义的枚举量是枚举类整型常量,而枚举变量是变量,可以赋予不同的枚举值。
枚举声明
用enum关键字声明枚举数据类型的格式如下:
enum enumName {Em1=iNum1, Em2=iNum2, Em3=iNum3};
大括号内的部分称为枚举值表,定义了枚举的所有枚举值及对应的枚举量。
可以省略对枚举量的显式赋值,此时枚举值会按顺序从0开始分配:
enum enumName {Em1, Em2, Em3};
这里Em1的值为0,Em2为1,Em3为2。
在声明枚举时,程序完成两件事:
- 创建新的数据类型enumName
- 以对应整型值初始化一系列类型为enumName的符号常量
当省略枚举名时,可以只创建一系列符号常量而不创建新数据类型。
enum {H, Li, Na, K, Rb, Cs};
这样创建的符号常量类型为未命名枚举,实际上,系统的名称命名应该为unname-enum-枚举值名,且无法通过C/C++语法系统访问。
枚举量本质是符号常量,可在其作用域内作为整型常量使用,且可在运算时进行整型提升。但需注意,枚举常量与const常量及预处理器常量有本质区别。
枚举变量
声明枚举类型后,可如下创建枚举变量:
enumName aEnum;
枚举变量本质是整型,但只接受枚举值表中定义的符号常量,不接受直接的整型赋值:
enum eleEnum {F, Cl, Br, I, At};
eleEnum haloEnum;
int a;
haloEnum = 0; //不合法,其他整型无法转化为枚举型
haloEnum = F; //合法,F与haloEnum同属eleEnum类型
a = haloEnum + 1; //合法,枚举可在运算中提升为int
a = F + 1; //合法,枚举常量也可被提升
枚举类型变量和常量都可被整型提升,但其他整型不能转为枚举型,可理解为。
- 枚举型是低级整型,其他整型无法降级。
- C/C++未定义从整型到枚举型的转换。
由于枚举变量使用规则严格,枚举常被用于其主要功能:批量创建整型符号常量。
枚举与强制类型转换
枚举的限制主要源于枚举变量的特殊性,可通过强制类型转换克服这些限制,但可能导致未定义行为:
eleEnum haloEnum2;
haloEnum2 = eleEnum(5); //5不在枚举值表中
haloEnum2 = eleEnum(9999); //可能导致整型溢出
枚举变量的大小
枚举变量有最大长度,通常较短(char级别),其大小取决于:
- 上限:枚举值表中最大值对应的2的整数幂。
- 下限:若枚举值都大于0,下限为0;若有负值,则为对应负的2的整数幂。
枚举变量大小依赖于枚举值范围,若范围较大,变量可能较大,但通常不超过long long。由于枚举变量宽度不统一,溢出行为未定义。
C++11 作用域内枚举
传统枚举的问题是可能在同一作用域引起名称冲突:
enum {x = 1};
enum enumTest1 {x = 2, y = 1}; //编译错误
C++11引入了作用域内枚举,将枚举限定在特定作用域内:
enum class enumTest1 {x = 2, y = 1};
enum struct enumTest2 {x = 2, y = 1}; //两者等效
访问这些枚举需使用作用域解析运算符:
enumTest1::x;
enumTest2::y;
作用域内枚举比传统枚举更严格,不能自动转换为原始整型:
enum class enumTest1 {x = 2, y = 1};
enum {x = 2, y = 1}; //常规匿名枚举
int z = x; //允许自动类型转换
z = enumTest1::x; //不允许自动转换
可手动指定作用域内枚举的底层类型:
enum class:int enumTest1 {x = 2, y = 1};
enum struct:short enumTest2 {x = 2, y = 1};
Java枚举
Java枚举与C++作用域内枚举非常相似,将枚举量存储于类内。由于Java是纯OOP语言,它使用.访问枚举量,而非C++的::。
C++使用
::访问名称空间内的字段 使用.访问值的成员 这样可以区分名称空间内的普通(顶级)变量与函数(静态字段也可以理解为有访问限制的在类名称空间内的顶级变量/函数)类内定义的成员变量与函数。
Java需要这种区分吗,对于Java,任何名称都位于类内,类又位于包内,这种区分是不必要的,因为不存在顶格的,单纯定义于名称空间的,不属于任何类的顶级变量/函数,所以不需要区分。
Java枚举的基本语法如下:
enum Size { SMALL, MEDIUM, LARGE };
Java枚举会记录声明顺序,但与C/C++不同,Java允许枚举包含任意类型的内部值,这是一种对枚举的绑定,可以通过枚举访问对应的值。
enum Size { SMALL("S"), MEDIUM("M"), LARGE("L") };
Java使用泛型化赋予枚举存储任意类型(但显然不能是基础类型)的能力,这里枚举内的变量字段显然是Enum<String>类型,且为静态的。
虽然枚举通常用于静态字段,但也可以添加字段、构造器和方法,不过构造器必须是私有或包访问级别:
enum Size
{
SMALL("S"), MEDIUM("M"), LARGE("L");
int UUID_;
private Size() {}
};
内部枚举变量(Enum<>)
Java枚举内部存储模板类Enum<>而非直接数值,提供了丰富功能:
- 静态方法
valueOf(Class enumClass, String name)返回指定名称的枚举变量。 - 方法
toString()返回枚举变量名称。 - 方法
ordinal()返回枚举变量的声明位置(从0开始)。 - 方法
compareTo()比较枚举变量位置,等同于比较ordinal()。
由于枚举变量是final的,可直接用==比较引用判断相等性,这比使用ordinal()更类型安全。
两种方法在性能上没有显著差异,因为其底层表示都是类整型,且都有潜在的高效的可原子性,不过对于比较这个场合,建议选择类型安全的前者。
ordinal()可以和整型互相比较,带来了潜在的类型不安全性。

浙公网安备 33010602011771号