2023 BUAA 面向对象程序先导(OOPre)课程总结

一、最终作业的架构设计

二、历次作业分析总结

历时九周的OOPre课程暂时告一段落了,下个学期我们将迎来OO正课。我想正好趁着第八次作业的机会来总结一下历次作业中踩过的坑点以及一路走来的心路历程。

第一次作业:

由于是第一次接触java语言以及面向对象的编程理念,第一次课的作业是给定一份关于小孩去水果店买水果的代码,让我们来找出其中语法上的错误以及bug,大概分为以下几类:

(1) 在类的构造中不需要加任何类型修饰,直接public Child(){}即可;

(2) 在一个类中不能调用其他类里private的属性,只可以调用其public的方法,如在Seller类中:

child.money -= APPLE_PRICE;

 应该修改为:

child.subMoney(APPLE_PRICE);

(3) 在定义函数的时候要注意是否有static修饰,如果有static则函数调用的结果不会访问或者修改任何对象(非static)数据成员,例如:

private int appleCount;
private int bananaCount;
public static void addOneFruit(String goal) {
        if (goal.equals("apple")) {
            appleCount++;
        } else if (goal.equals("banana")) {
            bananaCount++;
        }
    }

此时IDEA会有Warning提示无法引用非static字段appleCount和bananaCount。

这里想顺带说一说我对于面向对象的理解:和C语言这种面向过程的编程语言不同,以java和C++为代表的面向对象编程语言,顾名思义就是以对象为一个编程单位来进行操作,在一个对象中包含了其所拥有的各种属性以及操作方法。二者之间一种很直观的区别体现在:我们在程序设计和数据结构课程中的C语言代码往往只需要一个.c文件就可以解决一整道问题,但是到了面向对象我们需要很多.java文件才可以实现题目要求的种种功能。这些.java文件就是一个个类(class),我们通过类来进行对这些属性和方法的封装以及对象的声明。初次接触时感觉和C语言中的结构体(struct)很相似,区别主要在于类里可以定义方法,也就是我们经常说的函数,而结构体里只能定义成员变量,没有与其相关的方法。

第二次作业:

第二次作业正式开始了本学期的迭代开发之旅,后续的每一次作业基本都是在第二次作业的基础上进行功能的增加和修改。在接下来的几次作业中,请想象你是一个穿越到魔法大陆上的冒险者,在旅途中,你需要收集各种道具,使用各种装备,招募其他冒险者加入队伍,提升自己的等级并体验各种战斗。

第二次作业主要练习了类的创建和封装以及像ArrayList以及HashMap等容器的使用。我创建了Adventurer,Bottle,Equipment三个类来分别表示冒险者,药水瓶以及装备。其中在定义类的构造器的时候可以用关键词this来表示当前自身的对象:

public Adventurer(int id, String name) {
        this.id = id;
        this.name = name;
    }

在创建容器时首先要用new 关键词来在内存中开辟一块空间来存储元素,这里需要注意在容器中声明元素的类型不能用int, double, long这类的基本类型,而是要用它们相对应的类,比如Integer, Double, Long等等。

 private ArrayList<Integer> numbers = new ArrayList<>();
 private HashMap<Integer,String> students = new HashMap<>();

在进行容器遍历的时候可以使用java自带的for-each循环:

for (int num: numbers) {
            System.out.println(num);
        }
for (String stu : students.values()) {
            System.out.println(stu);
        }

第三次作业:

第三次作业增加了一个Food类,同时引入了“背包”这一概念,将冒险者的物品进行了拥有和携带的区别。这次作业卡了我很长时间,难点是在于利用什么样的容器来实现关于背包功能的种种限制。后来我意识到可以用容器的嵌套来实现name和id这两种属性对于物品的指向,因此在Adventurer类中新添加了以下属性: 

    private HashMap<String, Equipment> equipmentBag = new HashMap<>();
    private HashMap<String, TreeMap<Integer, Bottle>> bottleBag = new HashMap<>();
    private HashMap<String, TreeMap<Integer, Food>> foodBag = new HashMap<>();

由于装备不存在会出现有相同名字的两件装备的情况(后来的装备会顶替掉前面的装备),所以我直接用(name->object)这样的一个映射来进行装备的查找。在携带装备的时候先判断是否有同名装备,如果有的话就把这件装备remove掉,然后再把新装备put进去。

但是对于bottle和food会出现同名装备的问题,并且在使用的时候要首先使用id最小的物品,所以我采取了在HashMap中嵌套了一个TreeMap(TreeMap可以将键值对按照key值从小到大的顺序进行排列),也就是(name->(id->object))。通过查找name可以获得一系列同名的物品,再调用TreeMap中的firstEntry方法来找到id最小的物品。

 Adventurer thisAdv = adventures.get(id);
 Food thisFood = thisAdv.getFoodBag().get(name).firstEntry().getValue();
 thisAdv.eat(thisFood);

第四次作业:

第四次作业加入了战斗模式以及战斗日志的查询功能,这也是我第一次接触到正则表达式。基本思路就是根据解析输入进来的战斗日志与正则表达式进行匹配来确定该条战斗日志属于饮用药水、单体攻击还是AOE攻击。在最开始出现的很多错误都是由于正则表达式的书写错误导致相关战斗日志没能匹配上。

由于代码风格对于方法最多有60行代码的限制,在本次作业中我采用ArrayList<ArrayList<String>> inputInfo这种类似于二维字符串数组的形式对输入的内容进行解析。与课程组在前几次作业中提供的输入解析不同,由于输入中增加了战斗日志的输入,且战斗日志不算入操作指令个数n的范围中,所以要进行一下修改。我把之前的for循环改成了while循环,利用一个计数器cnt来记录输入指令的条数,一旦cnt==n就跳出循环。由于战斗日志的输入是不加空格的,所以我用contains(" ")来区分这一行的输入是指令还是战斗日志。

但是最后强测的时候有一个点出bug了,后来经过排查我发现是那个测试点的最后一条数据是14操作,而我的输入解析检测到是最后一条指令所以直接就跳出循环了,导致后面的战斗日志没有保存到容器中。最后我加入了单独对于这种情况的判断。

public static void MyScan(Scanner in, ArrayList<ArrayList<String>> inputInfo, int n) {
        int cnt = 0;
        while (in.hasNextLine()) {
            String nextLine = in.nextLine(); // 读取本行指令
            String[] strings = nextLine.trim().split(" +"); // 按空格对行进行分割
            inputInfo.add(new ArrayList<>(Arrays.asList(strings))); // 将指令分割后的各个部分存进容器中
            if (nextLine.contains(" ")) {
                cnt++;
            }
            if (cnt == n) {
                int size = inputInfo.size();
                if (Integer.parseInt(String.valueOf(inputInfo.get(size - 1).get(0))) == 14) {//如果是第14条操作
                    int temp = Integer.parseInt(String.valueOf(inputInfo.get(size - 1).get(2)));
                    for (int i = 0; i < temp; i++) {
                        String nextLine1 = in.nextLine(); // 读取本行指令
                        String[] strings1 = nextLine1.trim().split(" +"); // 按空格对行进行分割
                        inputInfo.add(new ArrayList<>(Arrays.asList(strings1)));
                    }
                }
                break;
            }
        }
    }

第五次作业:

本次作业又回归到了第一次作业的给一份给定的代码debug模式,本次作业的bug主要以下这几类:

(1) 当判断两个对象的属性是否相等时,不能用==来判断,主要是因为当我们声明两个变量soldier1和soldier2时,由于new关健词在内存中分别开辟了一块区域用来存放该对象,soldier1和soldier2相当于C语言中的指针变量(java中没有指针),都指向的是内存中一块单独崭新的对象,虽然这两个对象的一些属性是一样的,但是这两个对象是存放在两块区域内毫不相干的两个对象。在debug的时候可以发现在两个对象后面有两个不同的数字@1057和@1058,就可以很直观地看出来两个对象的差异。

(2) 当魔王将未被编队的士兵编队后,要记得将unformedSoldier中的该士兵及时删除,这类bug在我们迭代的作业中也很常见,并且很致命。

(3) 进行咒语的检测时,原代码是从头向后开始遍历,然而根据题意应该是在第一个‘@’字符出现的位置开始遍历,这里我们可以使用String自带的indexOf()方法,来返回到第一个‘@’字符的索引位置。

(4) 我们在HashMap这类容器中遍历对象一般都用到for-each循环,但是用这个循环边遍历边删除对象会发生报错提示:

Exception in thread "main" java.util.ConcurrentModificationException

指导书中建议的写法是用到迭代器(Iterator)本身提供的删除方法,但是IDEA提示了一个相对来说更加便捷的删除操作removeIf:

    public void screen(int standard) {
        soldiers.removeIf(soldier -> soldier.notQualifiedByStandard(standard));
    }

很好理解,就是当容器中的对象满足->后面的条件时就将其删除。

第六次作业:

第六次作业引入了子类的继承和接口的实现,两种药水瓶和两种装备分别继承自基本类型Bottle和Equipment,子类的构造以及引用父类的属性的时候可以直接用super来调用父类的属性,同时原来的bottles和bottleBag等基本类型的容器也不用进行更改,当判断冒险者持有的某一物品的类型的时候可以用instanceof关键词来判断该对象是否属于某一子类,以便安全地进行向下转型。

有一个坑点在于,在计算不同类型的装备造成的伤害时,要看好是与攻击者的某个属性有关还是被攻击者的属性有关,最开始我没有很好地区分导致最后计算出的伤害一团乱。

同时受到第五次作业中给定作业的启发,我单独创建了一个Manager类来管理所有的操作指令,之前的作业把指令上的操作都堆积在了Main类里,这样一是会导致main函数不是很清晰,二是在编写Junit单元测试的时候对Main类不好测试。在调整后直接在main方法中调用Manager里的solve()一个方法就可以进行指令的操作,同时也使得main方法中不会发生代码行数过多的报错。

与此同时我也将第四次作业中跟战斗日志有关的属性和方法都提取成一个FightLog类,前几次作业中的战斗日志放在Adventurer类中,就导致Adventurer类的方法太多,行数太多,很有可能出现每一次迭代开发都要推翻之前的重来,所以让不同的类来各司其职,各自封装,可以方便日后的迭代和维护过程,减小工作量。

第七次作业:

最后一次作业可以说是最让我崩溃的一次作业了,当时debug将近de了三天,一度有过放弃的念头,最后还是咬咬牙挺了过来,好在在临ddl前找到了bug。这次的作业是采用一些设计模式来进行商店买卖东西的相关操作,买卖东西的逻辑相对简单,需要注意的是在22号命令卖出冒险者携带的所有物品的时候,我们在遍历背包内的物品时,边遍历边删除会导致运行错误,正确做法是采用迭代器Iterator自带的remove方法来进行删除(这个在第五次作业指导书中有提到过)。但是我对迭代器的用法不是很熟,所以最后采取的操作是先遍历将所有物品的钱加在一起,然后用HashMap的clear方法直接一键清空背包,但是在遍历的时候一定要记得把冒险者拥有的这件物品也一并删除掉。

debug到最后发现bug几乎不是这次作业的错误,都是之前的作业的历史遗留问题(奇怪的是之前作业的强测居然没有测出来)。首先是在使用瓶子的时候,我之前采取的是直接将capacity清零,但是到了这次作业由于卖空瓶到商店的时候商店要记录的是空瓶原本的容量,这就会导致卖空瓶的时候容量的记录会出现错误,所以我给Bottle又加了一个boolean类型的empty属性和isEmpty方法来判断空瓶。然后是在eat方法中我忘记把吃过的食物从背包中删除了,就很奇怪之前强侧近万条数据都没有测出来,反倒被这次的中测给测出来了,属于是之前的作业埋下的雷了。最后本次作业的强测不出意外地寄了,跟室友的代码对拍了一晚上,找到出现问题的地方,然后跟踪出问题的这一个冒险者,在两千多行的输入指令中一点点缩小范围看是哪一条指令的问题,最后发现是在被雇佣者给雇主钱的时候,被雇佣者的钱不够,结果我把被雇佣者的钱先给清零了再加到雇主身上,加了个寂寞,调换一下清零和给钱顺序就AC了。

在写最后一次作业的时候和室友交流发现其实背包最好也单独提取出一个类来单独进行管理,这样可以使Adventurer类里的方法更加简洁一些。但是考虑到如果按照这种方法就要把之前的部分大改,再加上这次已经是最后一次迭代了,所以想想还是将错就错了(doge)。其实到了最后发现自己的架构设计的还是不够完善,类与类之间的业务逻辑还是过多地纠缠在了一起,代码的耦合程度还是太高,没有实现相对独立地执行各自功能。但是这也算是提前为OO正课进行试错,也许这才是OOPre的意义所在。

三、使用JUnit心得

在最开始的几次作业中我感觉JUnit的作用比较有限,因为类和方法的数量都很有限,但是到了后面的作业随着功能的增加,将各个类中的各个方法独立测试就可以很方便的找到一些bug,尤其是在创造样例的时候要多考虑一些边界条件,这样能找到一些中测的时候可能忽略掉的bug,以便顺利地通过强测。总的来说JUnit在我们进行作业迭代的过程中是一个好帮手。

四、对OOPre课程的简单建议

1、感觉可以稍微增强一下中测的强度,数据点增加一些极端的情况,不然强测扣分真的很难受。

2、在指导书里丰富一些设计模式的使用方法以及应用实例。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值