java总结性Blog-3

最后一次博客作业,打卡⛳


一、前言

关于PTA大作业六、七、八的分析】

知识点:主要涉及到了类的设计;面向对象程序设计的三大特性:封装、继承、多态;正则表达式;泛型;接口;内部类;方法重写等等。

题量:这三次的题目总体上看仍是一种递进关系,所以在写第六次题目时代码量较大外,其余两次相当于完善代码,题目量不是很大。

难度:这三次题目相较于之前的几何计算,难度大大减小,基本没有涉及到算法,主要是类中方法与变量的定义,需要较好的逻辑思维能力。


 

二、设计与分析

【PTA大作业六】

第一题(上来就是一道硬菜😆):电信计费-座机计费

PTA平台有详细的题目内容,这里我就不再赘述,主要和大家分享我的代码实现思路。

总览全题,我们看到了很多类图,这个时候不要害怕,也先别急着去写,仔细阅读题目很重要。其实类的设计方面题目已经给出,我们只需按照类图实现一些方法即可。最关键的是要将这些类串起来从而构成我们最终的代码


 

代码整体框架大致如下:

一个User用户有手机号,余额,收费方式以及用户记录。收费方式是一个抽象类,由具体的用户信息而定(在本题中,收费方式是座机收费,所以定义了收费方式的实现类LandLinePhoneCharging),收费方式包含了收费规则(ChargeRule)。用户记录类包含电话记录类和短信记录类。其中,电话记录和短信记录均为通讯记录的实现类。此外,收费规则类的子类是电话收费规则,而电话收费又细分为座机省内收费,座机市内收费以及座机省外收费。程序运行时,我们通过输入用户信息即可得知该用户的收费情况。


 

上述类中大多含有calCost()方法,这是我们要实现的核心方法。

有了整体的思路之后,我们就可以动手写代码啦。

-------------------------------------------------------------------------------------------------------------------------------------------

首先实现User类中的calCost():user的收费情况由收费方式而定,所以我们直接调收费方式类的calCost()。正所谓物尽其用,不用白不用。

public double calCost(){
        return chargeMode.calCost(userRecords);
    }

 

由于收费方式类中的calCost()是抽象方法,所以我们不用具体定义(又少写一点代码,哈哈😋),但是我们需要定义收费方式类的子类(LandLinePhoneCharging)中的calCost()方法,即方法重写。具体方法如下:

public double calCost(UserRecords userRecords) {
        double sum = 0;
        for(int i = 0; i < this.getChargeRules().size(); i++){

            sum +=  this.getChargeRules().get(i).calCost(userRecords.getCallingInCityRecords())
                    + this.getChargeRules().get(i).calCost(userRecords.getCallingInProvinceRecords())
                    + this.getChargeRules().get(i).calCost(userRecords.getCallingInLandRecords());
        }
        return sum;
    }

这个方法也比较简单。根据该类中的收费规则中的calCost()方法,我们可以将用户的每一条通话记录对应于一个收费规则,并返回该条记录花了多少钱,将这些记录返回的金额相加,即可知道该用户一共花了多了钱。

 


但是,在该方法中我们看到一条规则进行了三次calCost()计算,且参数不同,即通话记录不同。这是因为我们不知道具体得到的规则是什么,索性都加一遍。宁可错杀一千,也不放过一个,哈哈。当然了,我们这里错杀是不存在的,因为具体规则类的calCost()方法会进行判定你这条规则是不是对应我这个方法。

 


那么重点来了,根据题目所给信息我们可以定义各个规则的calCost()。当然别忘了先给规则类的父类ChargeRule类声明抽象方法calCost()。

也许有同学会好奇为什么父类的calCost()要声明成抽象的,别急,听听看我的解释:

我们已经知道ChargeRule类会有很多子类,即很多的收费规则,并且每个规则都不一样,所以calCost()也都不一样。最最特别的是,我们压根不知道ChargeRule类中的calCost()咋写啊,对吧。所以干脆别写,直接声明成abstract类型。我们对外说是高度抽象,实际上是这个方法不好定义,写不出来,要根据具体的子类实现。


 

言归正传,首先看ChargeRule类的子类----LandPhoneInCity,市内收费规则的calCost()。具体方法如下:

public double calCost(ArrayList<CallRecord> callRecords) {

        double cost = 0;
        for(int i = 0; i < callRecords.size(); i++) {
            if (callRecords.get(i).getAnswerAddressAreaCode().equals("0791")) {
                long start = callRecords.get(i).getStartTime().getTime();
                long end = callRecords.get(i).getEndTime().getTime();
                long min = ((end - start) / 1000) / 60;
                if (!(((end - start) / 1000) % 60 == 0)) {
                    min += 1;
                }
                cost += min * 0.1;
            }
        }
        return cost;
    }

 实现思路不难,即如果接听地点是市内,我们就咔咔计算,否则返回0。

那么问题来了,我们实际上输入的是年月日时分秒,这么多数字算起来是真的要秃头的。

别急,我们另辟蹊径,保护头发。对于拨打电话的时长,我们根据通话记录的起始时间和终止时间来getTime(),这个方法返回1970年1月1日以来的毫秒数二者相减即可获得这通电话一共打了多少毫秒。接着我们将其转化为以秒为单位,并通过除以60模60得到总的分钟数(不满一分钟按一分钟计算)。

依次类推,其余的两个方法也是如此,即“先判断,在计算”。


 

至此,我们的calCost()均定义完成,这个题就快写完啦,坚持就是胜利

那么我们又迎来另一个重头戏----main()的定义

主方法中我们主要应用的还是正则表达式校验用户的输入是否合法:

if (data.matches("u-" + regexL + " 0")) {}//判断开户信息是否合法

其中regexL是"\\d{11, 12}",表示的是11或12位的数字。 

 


对于通话记录的合法检验,我们首先根据空格将它分为多个子串,再对子串进行检验。

datas2 = data.split(" ");

 

其中,对于日期的检验,我们采用SimpleDateFormat的方式:如果给start和end赋值成功,则输入日期合法,否则我们就忽略它,继续等待控制台输入

SimpleDateFormat timeFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
timeFormat.setLenient(false);//严格解析:必须匹配一致
Date start , end ;
try {
    start = timeFormat.parse(time1);
    end = timeFormat.parse(time2);

} catch (ParseException e) {
    data = input.nextLine();
    continue;
}

 


 

接下来的事就好办了,在输入合法的情况的下,我们根据String类中的subString()方法可以得到接电话和打电话的区号,在通过if elseif等语句判断区域即可


但是,还有一些隐患我们是没有考虑到的,例如一个用户重复开户(具体说明在手机收费的踩坑心得中),一种收费规则的重复创建(同一种收费规则若创建两遍相当于重复收费一次),都会使得我们的结果出错,因此我们还要写一写语句使我们的代码更加健壮。

 

在本题中,令我十分苦恼的是怎样才能不重复创建收费规则。在经过各种无用的尝试之后,我想到这么一种解决办法:若是一开始就造好三种收费规则的对象,给用户添加收费规则前先判断一下有没有这条规则,如果没有,则给他加上。因为自始至终都是用这三个规则对象做判断,所以不会出问题。具体代码如下:

LandPhoneInCityRule landPhoneInCityRule = new LandPhoneInCityRule();
LandPhoneInProvinceRule landPhoneInProvinceRule = new LandPhoneInProvinceRule();
LandPhoneInLandRule landPhoneInLandRule = new LandPhoneInLandRule();//在开头就造好
---------------------------------------------------------------------------------------
if(!users.get(i).getChargeMode().getChargeRules().contains(landPhoneInProvinceRule)) {
    users.get(i).getChargeMode().getChargeRules().add(landPhoneInProvinceRule);
}

 


最后还有一个小点,即输出时要给用户排序

 

谈到排序,我们就不得不说起Comparable接口实现该接口并重写compareTo(Object o)方法,将当前对象与o作比较,我们即可实现排序功能。

在compareTo()方法中,返回负数表示当前对象小,返回0表示一样大,返回正数表示当前对象大

具体代码如下:

public int compareTo(User o) {
    return this.getNumber().compareTo(o.getNumber());
}

 

由于本题中我们只需对user中String型的number做比较,而String类已经重写过了compareTo()方法,所以我们可以直接调。

最后在主类中,我们只需要小小的调用下面的语句即可完成排序:

Collections.sort(users);

 

第一道题地分析至此结束。🧐


 

踩坑心得:

1.避免空指针异常的错误。 

在检验输入的通话记录是否合法时易出现空指针异常,因为代码能够正常运行的前提是我们可以以空格为分隔符对字符串进行切割,即输入的内容中要有空格。但是如果输入的是“ss”等等,就会报出NullPointerException。

对于这个问题,我采用的方法是先判断这个字符串有没有空格,可以使用int indexOf(" ")的方法,若返回-1,则表示无空格。

 

2.避免数组角标越界错误。

假设我们已经得到了切割之后的子串数组,此时需要判断数组长度再进行下一步的操作

why?

因为若是合法输入,则子串数组的长应为6,如果是非法输入就不一定了。特别地,如果子串数组的长度小于6,则我们在使用数组元素时很可能就会出现ArrayIndexOutOfBoundsException错误。

 


 

【PTA大作业七】

第一题:电信计费系列2-手机📱+座机☎️计费

ps: 为了思路的连贯性,我将连续分析电信计费系列问题


 

这一题主要是增加了开户对象,即多了手机用户(之前只有座机用户),实现思路与第一题大致相同。并且有了第一题的基础,我们这一题会比第一题写的更顺手。

仔细阅读完题目之后,我们发现手机功能与座机大致相同,但是收费方式多了接听收费这一项。而拨号收费时也要注意区号的变化


 

具体步骤:

①向收费方法增加新的方法,即手机收费方式:

MobilePhoneCharge extends ChargeMode

 

②仿照座机收费,向收费规则中添加手机的各种收费规则,并实现其中的calCost()方法:

class SmartPhoneInCityRule extends CallChargeRule 
class SmartPhoneInLandRule extends CallChargeRule
class SmartPhoneInProvinceRule extends CallChargeRule
class SmartPhonesInProvince extends CallChargeRule
class SmartPhonesOutProvince extends CallChargeRule

说明:从上至下依次为:

  1.市内打市内

  2.市内打省内

  3.市内打省外

  4.省内互打(不包括市内)

  5.省外互打

类虽然多,但基本套路还是不变,即“先判断,再计算”,这里我以"市内打省内"向大家详细说明:

class SmartPhoneInLandRule extends CallChargeRule{
    @Override
    public double calCost(ArrayList<CallRecord> callRecords) {
        double call = 0;//首先定义总的花费金额
        String answerAddressAreaCode, callingAddressAreaCode;//声明接听区号和拨打区号
        for(int i = 0; i < callRecords.size(); i++) {//遍历通话记录
            answerAddressAreaCode = callRecords.get(i).getAnswerAddressAreaCode();//获取通话记录中的接听区号
            callingAddressAreaCode = callRecords.get(i).getCallingAddressAreaCode();//获取通话记录中的拨打号
            if ((callingAddressAreaCode.equals("0791")) && !answerAddressAreaCode.equals("0701") &&
                    !answerAddressAreaCode.matches("079[0-9]")){//判断拨打区号是否为市内,接听区号是否为省内
                long start = callRecords.get(i).getStartTime().getTime();
                long end = callRecords.get(i).getEndTime().getTime();
                long min = ((end - start) / 1000) / 60;//计算分钟数
                if (!(((end - start) / 1000) % 60 == 0)) {//若有不满足一分钟的,则分钟数加1
                    min += 1;
                }
                call += min * 0.3;
            }
        }
        return call;
    }
}

 

 

③收费规则新加一个接电话收费规则AnswerChargeRule,并为其加上calCost()方法,具体代码如下

abstract class AnswerChargeRule extends ChargeRule{
    public abstract double calCost(ArrayList<CallRecord> callRecords);
}

 

④定义AnswerChargeRule的具体实现类SmartPhoneAnswerRule

根据题意,只有手机接省外电话时才收费,所以在重写calCost()方法时,我们只需判断接电话的区号是否为省外即可。


 

⑤修改main方法
 
仿照座机收费,我们主要是多加几个if else语句判断手机收费的情况。
 
到这里,手机计费题目就全部完成啦~🧐
 
 

 
【踩坑心得】
 
问题:用户的重复开户
 
解决方法:在添加用户之前判断用户列表中有没有这个用户,即要添加用户的号码是否已经存在
 
1.首先定义用户人数userNum
2.若人数为0,则直接添加,否则for循环遍历用户列表,判断要开户的号码是否已经存在
3.若存在,boolean isExist赋值为true
4.若isExist为false,则添加该用户
 
注意:不要把添加操作放到for循环里面,必须遍历完再进行判断
【ps:我之前试过直接contains()判断有没有这个用户,但是运行结果有问题。现在想想可能是没有重写user的equals()方法。】
 
具体代码如下:
if (userNum != 0) {
    for (int i = 0; i < userNum; i++) {
        if (datas1[0].substring(2).equals(users.get(i).getNumber())) {
            isExist = true;
            break;
        }
    }
}
if (!isExist) {
    if (datas1[0].charAt(2) == '0') {
        users.add(new User(datas1[0].substring(2), new LandlinePhoneCharge()));
    }else{
        users.add(new User(datas1[0].substring(2), new MobilePhoneCharge()));
    }
    userNum++;
}

 
 
【PTA大作业八】
 
第一题:电信计费系列3-短信计费(一道饭后甜点🍨)
 
这一题其实要实现的功能很少,难度不大,所以分值也比较低。
 
我们第一步依旧是仔细读题,很神奇的发现,这一题实际上和前两题没什么关系,没有涉及到打电话,测试点全是发短信的操作。所以我们可以只截取前两题中相关的类到第三题中:
 
 
hhh,这里我就只用了八个类。
 

 
 
具体实现步骤:
 
①.向短信收费添加“发短信收费”方式,即MessageCharge类(之前已经定义过)添加实现类“SendMessageRule”。
②.重写SendMessageRule类中的calCost()方法:
 
 
根据题意,我们知道,短信实际上以分段函数的形式收费,因此,我们可以先计算出用户一共发了多少短信,再计算收费金额。
核心代码如下:
for(int i = 0; i < messageRecords.size(); i++){//遍历短信记录,计算短信条数
    data = messageRecords.get(i).getMessage();
    count += data.length() / 10;
    if(data.length() % 10 != 0){
        count++;
    }
}
if(count <= 3){//分段计算收费金额
    sum += 0.1 * count;
}else if(count <= 5){
    sum += 0.2 * (count - 3) + 0.3;
}else{
    sum += 0.7 + (count - 5) * 0.3;

 


③.修改main()方法:

根据题目,短信内容只能由数字、字母、空格、英文逗号、英文句号组成,这里我们可以使用正则表达式(正则表达式,你是我的神!)
 
检验短信内容的正则表达式:
[a-zA-Z\\d ,.]+

说明:a-zA-Z判断字母,\\d判断数字,[ ]+的含义短信的内容可以一个或多个字符,并且必须在“a-zA-Z\\d ,.”范围内。

 

检验整个输入的正则表达式:

"m-1\\d{10} 1\\d{10} [a-zA-Z\\d ,.]+"

 

好的,到这里我们的电信系列就全部写完啦!!!👏👏👏

 


硬菜吃完了给大家整点餐后水果🍎🍉🍈

 

【PTA大作业八 7-2】

这一题主要涉及内部类的使用,我们先简单回顾一下什么是内部类(内部类分为四种,我们这里讲的是成员内部类):

内部类,顾名思义,就是定义类内部的类,比如在A类中定义B类,,此时A称为外部类。

内部类可以直接调用外部类的一切变量和方法。

外部类调用内部类的方法:内部类 对象名 = 外部类对象.new 内部类( );

 

eg: A a = new A();

      B b = a.new B();

    b.fn();//此时可以通过b 调用内部类的方法

 

注意:这里不可以直接new 内部类,必须通过外部类的对象 new 内部类(先有外再有内)

回顾完毕,关于内部类的具体知识点大家可以自行查找资料。

 


 

根据题目要求,Shop类的成员变量有

private int milkCount;
InnerCoupons coupons50;
InnerCoupons coupons100;

其中,coupons50和coupons100均为内部类对象,这就说明它们可以直接调用内部类的方法和变量。

定义内部类和外部类都比较简单,代码就不贴了。

 

我们主要看main方法的定义:

public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int count = input.nextInt();
        if(count >= 3){
            Shop myshop = new Shop(count);
            myshop.coupons50.buy();
            myshop.coupons100.buy();
        }
    }

说明:

  • count是牛奶的箱数,根据题目要求,若count>3,我们就执行“买牛奶”的操作
  • 在shop的构造方法中,我们已经为coupons50和coupons100 new 好了对象,他们的value值分别为50和100
  • 根据coupons50和coupons100中的buy方法,即可完成牛奶的购买

 

注意:这里是用coupons50买完牛奶后继续用coupons100买剩下的。


 

 

【PTA大作业七 7-2】

 这题总体来说就两个要求,一是删除重复元素,二是元素排序

①.删除元素

这里有两种方法:

  • 创建Arraylist 列表,然后遍历元素删除重复元素
  • 创建hashSet集合,这个集合的特点是“无序,无重复”。所以即使我们输入两个相同的元素,它也只会存储一个,免去了删除元素的步骤

注意:使用hashSet时我们要重写hashCode()和equals()方法。如果不重写,比较的是地址值而不是具体的对象内容。

 

②.元素排序

  1. 使需要排序的元素所属的类实现 Comparable  接口
  2. 重写该接口的compareTo()方法

由于本题中是按照学生的学号排序,而学号是String类型,所以可以按如下方式写:

public int compareTo(Student o) {
        return this.number.compareTo(o.number);
    }

原因是String类已经重写了compareTo()方法。

注意:String类中的排序默认是按从小到大,如果想要从大到小排序,在返回值前加上负号即可。


 

三、改进意见

不要频繁使用遍历操作,可以思考一些效率更高的算法。

例如在本次的电信系列计费中,我就用了很多次的循环遍历。这无疑影响了代码的运行速度。


 

四、总结

过去几周我学到了:

  1. 日期格式的校验
  2. 内部类的使用
  3. Comparable接口的使用即compareTo()方法的重写
  4. Collection接口的实现类ArrayList,HashSet等的使用
  5. hashCode()的重写

感谢各位的阅读,若有不对之处敬请各位读者指正~~~❤️🧡💛💚💙💜

 

 


 最后的最后,我想说的是,在写代码的过程中,我们或多或少都会为写不出题而焦虑,或是在和他人对比之后妄自菲薄......其实这些感受都是正常的,再优秀的程序员也写不出完美的代码,正所谓“金无足赤,码无完码”。但是我们要明白,每一次的PTA作业,实验题,不仅仅是老师布置的任务,更是对我们编程能力的淬炼。我们在写题的过程中不断学习,积累经验,突破自我。此外,编程题总是免不了改错和调试,但请坚持下去,不论运行结果正确与否,我们的努力都不会白费。

少年不惧岁月长,彼方尚有荣光在。我们大二见🥳

 

 

posted @ 2022-06-10 19:59  今天写代码了么  阅读(54)  评论(1)    收藏  举报