前言:

本学期也是开始学习Java这门面向对象的编程语言了,经过这几周的学习,也是发现Java的学习方法和上学期的C语言有较大区别,我自己也是感受到Java开始上强度了,不同于C语言,Java有迭代大作业和迭代实验,尤其是迭代大作业最让我难受。说实话,本人心里对大作业是十分抗拒的,因为上学期基本上从来没有敲过几百行的代码,稍微长一点的代码也才七八十行,但这学期的大作业开始就是200行起步,而且题目还长,让人看一眼就想逃避的那种,一点写下去的欲望都没有。但是一看到周围的人都在做大作业,自己也是开始写大作业了哇。

第一次大作业:

题目:

设计一个电梯类,具体包含电梯的最大楼层数、最小楼层数(默认为1层)当前楼层、运行方向、运行状态,以及电梯内部乘客的请求队列和电梯外部楼层乘客的请求队列,其中,电梯外部请求队列需要区分上行和下行。
电梯运行规则如下:电梯默认停留在1层,状态为静止,当有乘客对电梯发起请求时(各楼层电梯外部乘客按下上行或者下行按钮或者电梯内部乘客按下想要到达的楼层数字按钮),电梯开始移动,当电梯向某个方向移动时,优先处理同方向的请求,当同方向的请求均被处理完毕然后再处理相反方向的请求。电梯运行过程中的状态包括停止、移动中、开门、关门等状态。当电梯停止时,如果有新的请求,就根据请求的方向或位置决定移动方向。电梯在运行到某一楼层时,检查当前是否有请求(访问电梯内请求队列和电梯外请求队列),然后据此决定移动方向。每次移动一个楼层,检查是否有需要停靠的请求,如果有,则开门,处理该楼层的请求,然后关门继续移动。
使用键盘模拟输入乘客的请求,此时要注意处理无效请求情况,例如无效楼层请求,比如超过大楼的最高或最低楼层。还需要考虑电梯的空闲状态,当没有请求时,电梯停留在当前楼层。
请编写一个Java程序,设计一个电梯类,包含状态管理、请求队列管理以及调度算法,并使用一些测试用例,模拟不同的请求顺序,观察电梯的行为是否符合预期,比如是否优先处理同方向的请求,是否在移动过程中处理顺路的请求等。为了降低编程难度,不考虑同时有多个乘客请求同时发生的情况,即采用串行处理乘客的请求方式(电梯只按照规则响应请求队列中当前的乘客请求,响应结束后再响应下一个请求),具体运行规则详见输入输出样例。

输入格式:
第一行输入最小电梯楼层数。
第二行输入最大电梯楼层数。
从第三行开始每行输入代表一个乘客请求。

电梯内乘客请求格式:<楼层数>
电梯外乘客请求格式:<乘客所在楼层数,乘梯方向>,其中,乘梯方向用UP代表上行,用DOWN代表下行(UP、DOWN必须大写)。
当输入“end”时代表输入结束(end不区分大小写)。
输出格式:
模拟电梯的运行过程,输出方式如下:

运行到某一楼层(不需要停留开门),输出一行文本:
Current Floor: 楼层数 Direction: 方向
运行到某一楼层(需要停留开门)输出两行文本:
Open Door # Floor 楼层数
Close Door
输入样例:
在这里给出一组输入。例如:

1
20
❤️,UP>
<5>
<6,DOWN>
<7>
<3>
end
输出样例:
在这里给出相应的输出。例如:

Current Floor: 1 Direction: UP
Current Floor: 2 Direction: UP
Current Floor: 3 Direction: UP
Open Door # Floor 3
Close Door
Current Floor: 4 Direction: UP
Current Floor: 5 Direction: UP
Open Door # Floor 5
Close Door
Current Floor: 6 Direction: UP
Current Floor: 7 Direction: UP
Open Door # Floor 7
Close Door
Current Floor: 6 Direction: DOWN
Open Door # Floor 6
Close Door
Current Floor: 5 Direction: DOWN
Current Floor: 4 Direction: DOWN
Current Floor: 3 Direction: DOWN
Open Door # Floor 3
Close Door

设计与分析:

对于第一次大作业,“初级版”的单部电梯调度程序是最让我心累的,改了特别久,因为就一个测试点,过了就是过了,没过就是没过,万一运气好自己思路和出题老师的思路想到一块去了就过,但是我起初愣是没有和出题老师擦出一点思想火花,一点碰撞都没,我开始的代码是可以将测试样例和老师发的一些测试样例对得上的,但结果却十分令人失望,如图:
后来好像都快到题目截止了都没人能够和出题老师的想法擦出一点火花,出题老师也是迫不得已延长了题目的截止日期,貌似也是把测试点换了一个。虽然仍然是一个测试点,但随着时间推移,越来越多的同学把这道题写出来,此时我仍然没有过,一直想不通自己错在哪里,自己也是临近崩溃的边缘,后来老师也是不断地提示我们,核心算法为简化版的LOOK算法,将内外分为两个队列,主要是要搞清楚什么是队列,什么是出队入队,理解好方向优先和串行处理就差不多了。仔细想了之后,我将这个单部电梯调度程序题目转换成人话就是:将内外两个不同的请求分为两个大组,内部请求为一个大组,外部请求为一个大组,然后就是类似于双指针互相比较两个队列的头队列哪个在电梯运行的方向上或者哪个离电梯现在的位置更近,再决定去处理内部头队列还是去处理外部头队列的问题,关键在于头队列不处理,就不会去处理排在头队列后面的请求,当处理完头队列之后,该队列指针向后移动一位,产生新的头队列,再循环之前说的比较两个队列的头队列,再判断哪个头队列符合要求,再决定去处理哪一个头队列这样的问题。
有了这样的想法改变后,自己也是将核心算法和代码大纲做了一些调整,核心算法调整为:
`private boolean hasPendingRequests() { //判断两个队列中是否还存在未处理的请求
return innerHead < innerTail || outerHead < outerTail;
}

private void processDualQueues() {           //核心逻辑
    if (direction == Direction.IDLE) {        //如果还有未处理的情况
        determineInitialDirection();
    }

    // 检查内部队列头部请求(无方向要求)
    if (innerHead < innerTail && innerQueue[innerHead].floor == currentFloor) {
        handleStop(innerQueue[innerHead], false);
        innerHead++; // 仅移动内部队列指针
        return;
    }

    // 检查外部队列头部请求(需方向匹配)
    if (outerHead < outerTail && 
        outerQueue[outerHead].floor == currentFloor &&
        (outerQueue[outerHead].direction == direction || direction == Direction.IDLE)) {
        handleStop(outerQueue[outerHead], true);
        outerHead++; // 仅移动外部队列指针
        return;
    }

    // 移动电梯
    moveToNextTarget();
}

private void determineInitialDirection() {
// 情况1:存在内部请求且是第一个外部DOWN请求
if (innerHead < innerTail && outerHead == 0 && 
    outerHead < outerTail && outerQueue[outerHead].direction == Direction.DOWN) {
    direction = innerQueue[innerHead].floor > currentFloor ? Direction.UP : Direction.DOWN;
    return;
}

// 情况2:没有内部请求时,直接处理外部请求
if (innerHead >= innerTail && outerHead < outerTail) {
    direction = outerQueue[outerHead].direction;
    return;
}

// 情况3:非首个外部请求时
if (outerHead != 0 && outerHead < outerTail && innerHead < innerTail) {
    int innerFloor = innerQueue[innerHead].floor;
    int outerFloor = outerQueue[outerHead].floor;
    Direction outerDirection = outerQueue[outerHead].direction;

    boolean innerSameDirection = (direction == Direction.UP && innerFloor > currentFloor) ||
                               (direction == Direction.DOWN && innerFloor < currentFloor);
    boolean outerSameDirection = outerDirection == direction;
    boolean outerSamePath = (direction == Direction.UP && outerFloor > currentFloor) ||
                           (direction == Direction.DOWN && outerFloor < currentFloor);

    if (innerSameDirection) {
        if (!outerSameDirection) {
            // 内部同向,外部反向 -> 优先内部
            direction = innerFloor > currentFloor ? Direction.UP : Direction.DOWN;
        } else {
            // 都同向 -> 选择更近的
            if (Math.abs(innerFloor - currentFloor) <= Math.abs(outerFloor - currentFloor)) {
                direction = innerFloor > currentFloor ? Direction.UP : Direction.DOWN;
            } else {
                direction = outerDirection;
            }
        }
    } else {
        if (outerSamePath) {
            // 内部反向,外部同路径 -> 优先外部
            direction = outerDirection;
        } else {
            // 都反向 -> 选择更近的
            if (Math.abs(innerFloor - currentFloor) <= Math.abs(outerFloor - currentFloor)) {
                direction = innerFloor > currentFloor ? Direction.UP : Direction.DOWN;
            } else {
                direction = outerDirection;
            }
        }
    }
    return;
}

// 默认情况
if (innerHead < innerTail) {
    direction = innerQueue[innerHead].floor > currentFloor ? Direction.UP : Direction.DOWN;
} else if (outerHead < outerTail) {
    direction = outerQueue[outerHead].direction;
}

}

private void moveToNextTarget() {
// 1. 获取当前有效目标(已考虑方向约束)
Integer innerTarget = getCurrentInnerTarget();
Integer outerTarget = getCurrentOuterTarget();

// 2. 两个队列都有有效请求时的决策
if (innerTarget != null && outerTarget != null) {
    boolean innerSameDirection = isSameDirection(innerTarget, direction);
    boolean outerOnPath = isOnPath(outerTarget, direction);
    Direction outerDirection = outerQueue[outerHead].direction;

    if (innerSameDirection) {
        if (!outerSameDirection(outerTarget)) {
            // 内部同向,外部反向 -> 优先内部
            moveToFloor(innerTarget);
        } else {
            // 都同向 -> 选择更近的
            moveToFloor(
                Math.abs(innerTarget - currentFloor) <= Math.abs(outerTarget - currentFloor) ? 
                innerTarget : outerTarget
            );
        }
    } else {
        if (outerOnPath) {
            // 内部反向,外部同路径 -> 优先外部
            moveToFloor(outerTarget);
        } else {
            // 都反向 -> 选择更近的
            moveToFloor(
                Math.abs(innerTarget - currentFloor) <= Math.abs(outerTarget - currentFloor) ? 
                innerTarget : outerTarget
            );
        }
    }
} 
// 3. 只有内部有有效请求
else if (innerTarget != null) {
    moveToFloor(innerTarget);
} 
// 4. 只有外部有有效请求
else if (outerTarget != null) {
    moveToFloor(outerTarget);
} 
// 5. 无有效请求时准备转向或停止
else {
    direction = Direction.IDLE;
}

}`
当我经过这样的调整后,在题目截止的前两三个小时,也是终于通过了该测试点。

设计类图:

image

正确后代码分析:

image

分析结果

基本代码指标
Lines (275):代码总行数为275行,属于小型项目。

Statements (127):实际执行语句127条。

Percent Lines with Comments (13.1%):注释行占比13.1%,略低于推荐的15-20%标准,可考虑增加注释。

Classes and Interfaces (3):有3个类/接口。

Methods per Class (4.67):每个类平均4.67个方法,分布合理。

复杂度相关指标
Average Statements per Method (5.43):每个方法平均5.43条语句,长度适中。

Maximum Complexity (6):最复杂方法的圈复杂度为6(DualQueueElevator.determineInitialDirecti方法),这个值在可接受范围内(一般建议不超过10)。

Percent Branch Statements (18.1%):分支语句占比18.1%,控制流复杂度适中。

块深度指标
Maximum Block Depth (8):最深嵌套块深度为8(位于第246行),这个值偏高,建议重构以减少嵌套层次。

Block Histogram:大部分语句在较浅的块深度(0-3),但有部分语句达到了较深的嵌套(7-8)。

潜在问题

嵌套过深:最大块深度8表明存在过度嵌套的代码块,这会影响可读性和可维护性。

注释不足:13.1%的注释率略低,特别是对于复杂逻辑部分。

最复杂方法:DualQueueElevator.determineInitialDirecti方法需要特别关注,虽然复杂度6尚可接受,但建议检查是否可以简化。

改进建议

重构第246行附近的代码,减少嵌套层次(可以使用卫语句、提取方法等技术)。

为复杂方法(特别是determineInitialDirecti)增加注释说明其逻辑。

检查方法调用语句(52条)是否过于密集,考虑是否需要进行封装。

第二次大作业

题目变化:

1、乘客请求楼层数有误,具体为高于最高楼层数或低于最低楼层数,处理方法:程序自动忽略此类输入,继续执行。
2、乘客请求不合理,具体为输入时出现连续的相同请求,例如<3><3><3>或者<5,DOWN><5,DOWN>,处理方法:程序自动忽略相同的多余输入,继续执行,例如<3><3><3>过滤为<3>。
3、要求多类设计,类设计要求遵循单一职责原则

设计与分析

此时我稍微觉得有点棘手的就是改成多类设计,因为之前我的代码设计只有两个类,一个电梯类,一个Main类。改成多类设计的话要求有乘客请求类、电梯类、请求队列类及控制类。这样虽然意味着需要创建更多的对象来调动方法关系来完成整个电梯程序,但是职责更加单一,以后万一出现BUG也更容易找出错误,出现错误也只需要在出现错误的类中的某个方法中进行修改,而不需要在整个程序中寻找和修改BUG。其他要求我的想法为:对于超过楼层的请求忽略我们可以在添加请求的方法中添加要求,对于超过楼层的请求我们可以选择不加进去。对于连续的相同请求,我们也可以选择在添加请求的方法中添加要求:添加这个请求后添加一个判断,判断该队列的下一个请求是否和这个相同,如果相同,则检索该队列下一个请求,直到不相同为止。

设计类图:

image

正确后代码分析:

image
image
image

分析结果

极低注释率:仅2.1%的代码行有注释(远低于推荐的15-20%)
最大复杂度仅为2(Direction.fromString()方法)
平均复杂度2.00
平均块深度1.11
最大块深度3
→ 控制流非常简单,几乎没有复杂逻辑
代码结构问题
方法调用语句105条(占全部语句225条的46.7%)
平均每个方法4.45条语句(长度合理)
块深度分布:
深度0:63语句
深度1:87语句
深度2:63语句
深度3:12语句

潜在问题:

2.1%的注释率几乎等同于无文档

不利于团队协作和后期维护

虽然复杂度低但结构不合理:

表面看起来"简单"是因为所有代码都平铺在一个类中

实际是缺乏良好架构的表现

改进建议

至少达到15%的注释率

重点注释公共接口和复杂业务逻辑

虽然当前复杂度低,但要注意:

随着功能增加,当前结构会迅速恶化

建议在拆分后引入单元测试

架构建议:

考虑使用MVC或其他分层架构

识别领域对象,建立明确的对象关系

第三次大作业

题目变化:

乘客请求输入变动情况:外部请求由之前的<请求楼层数,请求方向>修改为<请求源楼层,请求目的楼层>
对于外部请求,当电梯处理该请求之后(该请求出队),要将<请求源楼层,请求目的楼层>中的请求目的楼层加入到请求内部队列(加到队尾)

设计与分析

对于第一个要求,我的想法是将外部队列的组成换一下就行,将方向DIrection换成楼层Floor即可
对于第二个要求,我的想法是电梯在处理外部队列的请求楼层时,将请求楼层的目标楼层记录下来,再在内部队列尾部加上即可,剩下的就是之前电梯一样的运行模式

设计类图:

image

正确后代码分析

image
image
image

分析结果

极低注释率:

仅1.6%的代码行有注释(严重不足)

远低于行业推荐的15-20%标准

"Methods per Class"显示39.00(但后面又显示没有方法)
→ 这个矛盾数据表明分析工具可能出现了识别错误

块深度分布:

最大块深度:4(位于第271行)

深度分布:

0:54语句

1:69语句

2:50语句

3:11语句

4:2语句→ 代码主要为线性执行,嵌套层次很浅

潜在问题

分析工具可能失效:

显示有39个方法但又说没有方法

复杂度全为零不符合常规情况→ 可能是分析工具失效

质量风险:

几乎无注释(1.6%)

如果确实是代码,维护性极差

缺乏模块化结构(显示有类但可能实际没有)

改进建议

紧急增加代码注释(至少达到15%注释率)

重构为模块化结构:
提取函数/方法
合理使用类组织代码

为深度4的代码块(第271行附近)降低嵌套层次

踩坑心得

对于第一次大作业,开始时是排斥的,不知道从哪里开始下手,内心是有恐惧感的,读题也是不想读,不想去分析题目,第一次大作业测试样例能过完全是因为运气好碰上的,内部逻辑并没有符合LOOK算法,只有我们敢写,不去恐惧大作业,静下心来分析是能够写出来的。

题目改进建议

希望老师测试点能稍微多一些,尤其是第一次大作业,只有一个测试点,不是0就是100,我感觉很难分析老师单独一个测试点是什么。

总结:

通过这三次大作业,也是对自己从C语言到Java的转变起到了极大的帮助作用,同时也使自己学会了SourceMonitor代码分析工具和PowerDesigner画类图工具的使用。
最重要的是使自己加深了对面向对象的思维模式,多类设计,职责单一。