OOP题目集4-6总结

前言:

又过去一个月了,本次博客主要对题目集4-6做出总结。

这三次题目集做下来的感觉明显没有1-3题目集挑战性满满,具体就体现在,做题目集1-3的时候,我几乎每天都会打开eclipse来调试、改进程序,或者针对某一测试点、知识点想破脑袋或去查找很多资料来填充自己的空白,而这个月的三次题目集就并没有做出什么高的要求,每次题目集中,老师都增加了几道比较简单的题目,所以比起上个月的题目集,题量明显增加,然而,虽然也有2-3道较难题,但我还是更喜欢上个月的题目集——每道题都需要花点心思。

另外,这次题目集需要用到

  1. 封装、继承、多态,让我深入理解了继承与多态的原理和用法,
  2. 还有类间关系(主要是用到了聚合、组合),让我也更加体会到了聚合、组合的差别,
  3. 其次就是接口的运用,List、Map、ArrayList、Set等
    • ArrayList的常用方法与数组之间的关系
    • 泛型的应用
    • Arrays与Collection的简单应用
  4. 当然,也少不了正则表达式的运用
  5. 最后,还有抽象类的应用
    • 这里又涉及到了方法的重写

设计与分析:

题目集4(7-2)、题目集5(7-4)两种日期类聚合设计的优劣比较

首先来看SourceMonitor的分析结果:

下图是题目集4(7-2)的sourceMonitor分析图:

DateUtil类、Day类、Month类及Year类的圈复杂度还算是可观,Main类复杂度超过了10,主要是因为对用户输入的判断都在Main类中,然后在switch语句中又嵌套了对输入日期合法性的判断,于是复杂度就高起来了。

DateUtil类一共写了有12个方法,最大圈复杂度为7,控制在10以内,我觉得还算可以了。这里又提醒了我们,在实现一个功能的时候,要尽量做到一个方法实现一个功能,这样圈复杂度能得到明显降低,代码的可读性也有提高。这次题目是先给出了类图,类图上也提供了各种需要我们去完成的方法,所以说,我们只是根据已有的方法框架来写代码,而没有自己去设计思考应该要写哪些方法,以后的题目大概都是要自己去设计该有哪些方法,因此,我觉得自己还是应该加强这方面的能力。

以下是本题给出的类图:

类图.jpg

从图中可以看出,DateUtil类与Day类、Day类与Month类及Month类与Year类间的类间关系都应设置为聚合(空心菱形代表着聚合关系),然而按照类图的要求写下来,我感觉这其实有点像是组合?总之是比聚合更强的关联关系。因为在DateUtil类中,DateUtil的构造方法中其实会调用day的构造方法,说明了day的生存周期和DateUtil一致。

public DateUtil(int d,int m,int y) {
		super();
		this.day = new Day(d,m,y);
}

另外,在Day类Day的构造方法又会调用Month的构造方法,Month类中Month的构造方法又会调用Year的构造方法:

public Day(int dayValue,int monthValue,int yearValue) {
		super();
		this.month = new Month(yearValue,monthValue);
		this.value = dayValue;
}
public Month(int yearValue,int monthValue) {
		super();
		this.year = new Year(yearValue);
		this.value = monthValue;
}

这也体现了month与day的生存期一致,year与month的生存期一致。

总的来说,虽然DateUtil类与Day类、Day类与Month类及Month类与Year类间关系都设置为聚合,但此题应该设置为DateUtil类与Day类、Month类、Year类都保持为聚合关系比较好。

再来看题目集5(7-4)

也是因为把输入选项判断、进而调用输入数据合法性判断的函数来判断布尔值等都写在了主类里,所以主类的圈复杂度最大。其他类的圈复杂度还算控制得不错。

下面主要是来看DateUtil类得分析:

DateUtil类中一共写了15个方法,最大圈复杂度为6。其实这个类中的主要方法基本和题目集4中日期类聚合题是一样的,然而我按照上次题目集的思路来写时,发现了存在了很多问题,虽然上一题的测试点都通过了,但可能真就是刚好踩着测试点通过的吧。题目中遇到的一些问题我将在下面的踩坑心得中描述。

类图如下:

从图中也可以看出,DateUtil类与Day类、Month类及Year类间的类间关系应设置为聚合。并且,这种聚合明显会比题目集4(7-2)要好,按照类图,在DateUtil类的实例产生前就会有Day类、Month类与Year类的实例产生,且它们可以单独存在,不会相互影响,接着将Day类、Month类与Year类的实例作为参数传入DateUtil的构造方法中。

public DateUtil(Year year, Month month, Day day) {
		super();
		this.year = year;
		this.month = month;
		this.day = day;
}

题目集4(7-3)、题目集6(7-5、7-6)三种渐进式图形继承设计的思路与技术运用(封装、继承、多态、接口等)

题目集4(7-3)代码分析:

主类最大圈复杂度为13,也还是因为把输入选项判断、数据合法性判断都写在主类。但是也只能这么写了吧,我也考虑过该如何能降低点圈复杂度,于是我把校验数据合法性单独写成了一个方法:

/*
 *以下贴出部分代码。
 */
case 2:
			//输入长和宽
			double width = input.nextDouble();
			double length = input.nextDouble();
			if(checkVality(length) && checkVality(width)) {//判断输入数据合法性
				Rectangle rectangle = new Rectangle(width,length);
				//输出矩形面积
				System.out.printf("Rectangle's area:%.2f",rectangle.getArea());
				break;
			}
			break;
	……
public static boolean checkVality(double a) {
		boolean flag = false;
		if(a>0) {
			flag = true;
		}
		else {
			System.out.println("Wrong Format");
		}
		return flag;
	}

题目集4(7-3)类图如下:

分析:Shape为父类,Circle类与Rectangle类继承自Shape类,Ball继承自Circle类,Box继承自Rectangle类。Shape类中有一个getArea()方法,这个方法将在它的子类中被覆写。Circle类中定义了一个私有属性radius(封装性的体现),并且覆写了Shape中的getArea()方法,这体现出了多态性,Ball类作为子类另外还增加了一个getVolume()方法。Rectangle类、Box类同上。

所以说,关于继承,子类可以继承父类的所有属性和方法,并且子类可以给自身增加父类没有的方法

  • 特别的,父类中声明为private的属性或方法,子类继承父类以后,仍然认为获取了父类中私有的结构。

  • 只有因为封装性的影响,使得子类不能直接调用父类的结构而已。

    此时可以使用get(),set()方法来获取父类的私有属性

  • 2.2 子类继承父类以后,还可以声明自己特有的属性或方法:实现功能的拓展。

题目集6(7-5)代码分析:

主类圈复杂度15,原因是主类分支语句过多,分支语句(%Branches)占比为27.1%。

import java.util.ArrayList;
import java.util.Collections;
import java.util.Scanner;

public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner input = new Scanner(System.in);
		//输入要创建的Circle Rectangle Triangle的数量
		int numOfCircle = input.nextInt();
		int numOfRect = input.nextInt();
		int numOfTri = input.nextInt();
		ArrayList<Shape> list = new ArrayList<Shape>();
		if(checkNumOfShape(numOfCircle,numOfRect,numOfTri)) {
            /*
             *以下三行的本意是想调用三个方法来把图形对象添加到list中
             */
//			addCircle(numOfCircle,list);
//			addRectangle(numOfRect,list);
//			addTriangle(numOfTri,list);
			for(int i = 0;i<numOfCircle;i++) {
				double radius = input.nextDouble();
				Circle circle = new Circle(radius);
				if(circle.validate()){
					list.add(circle);
				}
				else {
					System.out.println("Wrong Format");
					System.exit(0);
				}
			}
			for(int i = 0;i<numOfRect;i++) {
				double width = input.nextDouble();
				double length = input.nextDouble();
				Rectangle rectangle = new Rectangle(width,length);
				if(rectangle.validate()) {
					list.add(rectangle);
				}
				else {
					System.out.println("Wrong Format");
					System.exit(0);
				}
			}
			for(int i = 0;i<numOfTri;i++) {
				double side1 = input.nextDouble();
				double side2 = input.nextDouble();
				double side3 = input.nextDouble();
				Triangle tri  = new Triangle(side1,side2,side3);
				if(tri.validate()) {
					list.add(tri);
				}
				else {
					System.out.println("Wrong Format");
					System.exit(0);
				}
			}
			
		}
		else {
			System.out.println("Wrong Format");
			System.exit(0);
		}
		double sumArea = 0;
		System.out.println("Original area:");
		for(int i = 0;i<list.size();i++) {
			list.get(i).setArea(list.get(i).getArea());
			System.out.print(list.get(i).toString()+" ");
			sumArea = list.get(i).getSumArea();
		}
		System.out.printf("\nSum of area:%.2f\n",sumArea);
		
		Collections.sort(list);
		System.out.println("Sorted area:");
		for(int i = 0 ;i<list.size();i++) {
			System.out.print(list.get(i).toString()+" ");
		}
		double sum = 0;
		
		for(int i = 0;i<list.size();i++) {//第二次求sum的时候,会在第一次求的基础上累加
			sum = list.get(i).getSumArea();
		}
		System.out.printf("\nSum of area:%.2f\n",sum-sumArea);
	}
	
	public static boolean checkNumOfShape(int circle,int rect,int tri) {
		if(circle < 0 || rect < 0 || tri  < 0) {
			return false;
		}
		else {
			return true;
		}
	}
}

为了将圈复杂度降低点,我有考虑过输入图形各属性后,将图形对象添加到list中这一过程写成一个方法(即上面代码注释的一个地方),如下:

public static void addCircle(int n,ArrayList<Shape> list) {//添加Circle对象到list中
		Scanner input = new Scanner(System.in);
		for(int i = 0;i<n;i++) {
			double radius = input.nextDouble();
			Circle circle = new Circle(radius);
			list.add(circle);
		}
	}
	
	public static void addRectangle(int n,ArrayList<Shape> list) {//添加Rectangle对象到list中
		Scanner input = new Scanner(System.in);
		for(int i = 0;i<n;i++) {
			double width = input.nextDouble();
			double length = input.nextDouble();
			Rectangle rectangle = new Rectangle(width,length);
			list.add(rectangle);
		}
	}
	
	public static void addTriangle(int n,ArrayList<Shape> list) {//添加Triangle对象到list中
		Scanner input = new Scanner(System.in);
		for(int i = 0;i<n;i++) {
			double side1 = input.nextDouble();
			double side2 = input.nextDouble();
			double side3 = input.nextDouble();
			Triangle tri  = new Triangle(side1,side2,side3);
			list.add(tri);
		}
	}

但是吧,这样写完之后运行,直接连续输入输入样例1的测试数据(1 1 1 2.3 3.2 3.2 6.5 3.2 4.2),出不了运行结果,因为其实程序会停在输入三个整型值(例如a b c),分别代表想要创建的Circle、Rectangle及Triangle对象的数量这一步,也就是说程序只会读取这个测试数据中1 1 1这三个数字,因为后面进入addCircle、addRectangle、addTriangle方法中需要再次输入数据,所以数据必须分开输入才能运行出结果,如下:

题目集6(7-5)类图如下:

本题将Shape类设计为抽象类,并作为Circle类、Rectangle类、Triangle类的父类,此外,由于题目要求根据图形的面积大小进行升序排序,要求必须对 list 中的图形对象在 list 中进行排序,而不 是对求得的面积进行排序,排序后再次求出各图形的面积并输出,所以我设置Shape类实现Comparable接口,实现接口后就能调用Collections.sort(list)方法来对图形对象进行排序。

public abstract class Shape implements Comparable<Shape> {
	public static double sum = 0;
	public abstract double getArea();
	public abstract boolean validate();
	public abstract String toString();
	private double area;
	

	public void setArea(double area) {
		this.area = area;
	}
	
	public double getSumArea() {
		sum += getArea();
		return sum;
	}
	
	@Override
	public int compareTo(Shape o) {
		// TODO Auto-generated method stub
		//升序
		return (int) (area - o.getArea());
	}
}

题目集6(7-6)代码分析:

这题比较简单,直接按照给出的类图写代码,写出代码的圈复杂度也根本高不起来吧。真的过于简单了,代码质量我也不好分析了……

题目集6(7-6)类图如下:

  • GetArea为一个接口,无属性,只有一个GetArea(求面积)的抽象方法;
  • Circle及Rectangle分别为圆类及矩形类,分别实现GetArea接口
  • 在Main类的主方法中分别定义一个圆类对象及矩形类对象(其属性值由键盘输入),使用接口的引用分别调用圆类对象及矩形类对象的求面积的方法,直接输出两个图形的面积值。(要求只保留两位小数)

其实就是写一个GetArea接口,这个接口里写一个抽象方法getArea(),然后Circle类和Rectangle类要实现GetArea接口,覆写getArea()方法,真的是比较简单了……

对三次题目集中用到的正则表达式技术的分析总结

先记录一下这几个比较常用的正则表达式:

1.正则表达式训练-QQ号校验

校验键盘输入的 QQ 号是否合格,判定合格的条件如下:

  • 要求必须是 5-15 位;
  • 0 不能开头;
  • 必须都是数字;
String regex = "^[1-9]\\d{4,14}$";

2.正则表达式训练-验证码校验

接受给定的字符串,判断该字符串是否属于验证码。验证码是由四位数字或者字母(包含大小写)组成的字符串。

String regex = "^(\\d|[a-zA-z]){4}$";

3.偶数校验,只要末尾数字是02468就可以判断为偶数

正则式为: ^\d*[02468]$

4.用正则表达式表示一定范围内的数字,如取值范围为[1,1000)的数字

第一个数字1-9,第二个数字0-9,第三个数字0-9

([1-9][0-9][0-9]|[1-9][0-9]|[1-9])

5.用正则表达式表示注释

\/\/[\s\S]*?\n|\/\*[\s\S]*?\*\/

另外,关于正则表达式的一些笔记我另外写了一篇博客,内容摘自《正则表达式必知必会》

这里附上博客链接:https://www.cnblogs.com/3D2Y/articles/14698226.html

题目集5(7-4)中Java集合框架应用的分析总结

编写程序统计一个输入的Java源码中关键字(区分大小写)出现的次数。说明如下:

  • Java中共有53个关键字(自行百度)
  • 从键盘输入一段源码,统计这段源码中出现的关键字的数量
  • 注释中出现的关键字不用统计
  • 字符串中出现的关键字不用统计
  • 统计出的关键字及数量按照关键字升序进行排序输出
  • 未输入源码则认为输入非法

要求:题目必须使用List、Set或Map中一种或多种

首先说一下我的思路:我打算写三个类,一个Main类(主类),一个DealData类,一个CheckData类。

主类中,输入字符串,这里我受之前处理水文数据题的启发,在处理输入的代码字符串时,采取了将字符串类型声明为StringBuffer,

首先创建一个String 类型的string变量,当输入回车时结束输入(nextLine()),然后创建一个StringBuffer类型的str变量,进入循环,

这里为什么还需要声明一个String类型de变量呢?而不直接str = input.nextLine();

因为nextLine()方法返回的是一个String类型值,所以编译器会提醒我们要将str改成为String类型

将此前输入的String类型字符串转换为StringBuffer类型,运用StringBuffer类中的append()方法,将该字符串扩展到str中,

为了我后面希望一行一行处理代码字符串,紧接着我又append了一个回车到str中,以便后面根据换行来分割到字符串数组中。

当输入exit来结束输入后,我需要对输入的所有代码字符串进行检查,因为题目要求:

  • 当未输入源码时,程序输出`Wrong Format

所以说,我需要将所有输入数据与空格或多个空格(用正则表达式\s*就能做到)做匹配来检验是否为合法输入,如果输入合法,则进入DealData类中的dealResult()方法。

在dealResult()中,首先,我需要将注释、类似于”……“的内容、一些标点符号全部替换,替换成空格就好,把这些会影响判断有效关键字的内容都排除在外。处理完这些无效内容后,我把输入的所有数据以\n分割存进字符串数组,即这个数组里的每个元素将会是输入的每一行代码,之后创建ArrayList list = new ArrayList();,意图是把每行中的每个单词添加到list中,最后我只需要遍历list时,如果该单词匹配上某个关键字,则将之放进Map中,并且出现次数加一。

Map<String,Integer> map = new HashMap<>();
		 for(int i = 0 ;i<list.size();i++) {
			 Matcher matcher3 = pattern3.matcher(list.get(i));
			 if(matcher3.find()) {
				 if(!map.containsKey(list.get(i))) {
					 map.put(list.get(i), 1);
				 }
				 else {
					 int n = map.get(list.get(i));
					 map.put(list.get(i), 1+n);
				 }
			 }
		 }

踩坑心得:

题目集四7-3:

我犯了一个非常低级的错误:

求对象测试点未通过

我一开始写的:return 4/3*Math.PI*Math.pow(getRadius(), 3.0);
后面改正的:return (4*Math.PI*Math.pow(getRadius(), 3.0))/3;

正确结果:

Ball's surface area:12.57
Ball's volume:4.19

错误结果:

Ball's surface area:12.57
Ball's volume:3.14

分析原因:没有注意到运算符的结合性。(非常低级的错误)

题目集四7-2:

思路:

  • 相距一年:为第一年的剩下日子+第二年开始的日子
    相距一年以上:为初始年的剩下日子+末尾年开始的日子+中间年份日子
    相距几个月:计算几个月的日期差

  • 已经过去的天数

  • 如果是闰年,则366-已经过去的天数为剩下的日子

  • 所以还是重点在求过去的天数

    每个月份的天数

    月份天数数组,for循环

    if(fabs(year-anotherYear) == 1){
    
    }
    for(i =  2019;i<2020;i++){
    	//当两个年份相邻,即相邻跨年时,不能通过这个for循环,所以在for之前要有一个判断
    }
    
    private int[] mon_maxnum = {0,31,28,31,30,31,30,31,31,30,31,30,31};//平年
    private int[] mon_maxnum2 = {0,31,29,31,30,31,30,31,31,30,31,30,31};//闰年
    int sum = 0;
    if(isLeapYear()){
    	for(int i = 0;i<this.month;i++){
    		sum += mon_maxnum2[i];
    }
    sum = sum+this.day.value;//已经过去的天数
    }
    else{
    for(int i = 0;i<this.month;i++){
    		sum += mon_maxnum[i];
    }
    sum = sum+this.day.value;
    }
    //366-已经过去的天数=这一年剩下的天数
    
    

第二题测试数据:

3 2020 1 1 2019 12 1

3 2020 6 14 2014 2 14
2020过去的天数:166
2014剩下的天数:320
中间年份 2015 16 17 18 19
166+320
maxDate.getDay().getMonth().getYear().isLeapYear()

错误代码:

public boolean validate() {
		boolean flag = false;
		if(this.value>0 && this.value<32) {
			if(this.getMonth().getYear().isLeapYear() && this.getMonth().getValue() == 2) {
				if(this.value<30) {
					flag = true;
				}
			}
			else if(!this.getMonth().getYear().isLeapYear() && this.getMonth().getValue() == 2){
				if(this.value<29) {
					flag = true;
				}
			}
			else {
				flag = true;
			}
		}
		return flag;
	}

修改之后:

public boolean validate() {
		boolean flag = false;
		int day = this.value;
		resetMax();//获取该月份的最大日期
		if(day < this.value) {
			flag = true;
		}
		else {
			flag = false;
		}
		this.value = day;//这句不能少,要不然后面调用this.value时,this.value是所在月份的日期最大值

		return flag;
	}

题目集四7-1:

这个测试点有点迷惑,因为好像没看到题目有要求当输入连续多个空时应该输出Wrong Format还是应该

输出:

System.out.println("Max Actual Water Level:0.00");		//判断连续输入为空
System.out.println("Total Water Flow:0.00");

然后就是我一开始是以为测试当前一条水文信息是否是连续输入为空,然而其实是应该判断所有的输入信息为连续输入为空,

接着我写了一个用来匹配连续输入为空的正则表达式,所以说就不能将输入为连续多个空的判断写在CheckData类的validate方法中。

String inputnull = "\\s*";
Pattern pattern1 = Pattern.compile(inputnull);
Matcher matcher1 = pattern1.matcher(str);

但是当我试图用

matcher1.find()来检验是否匹配时,发现无论输入是否说连续多个空它都匹配,不过很快就找到原因了,因为,正确输入中本来就应该包含空格,而find()方法只要是有一个匹配就会返回true。因此我后来又改成用str.matches(),但由于str是StringBuffer类型,因此str没有matches()方法,所以首先得把str转换为String类型,这一步可通过toString()方法来完成。

改进建议:

  1. 关于在减小圈复杂度方面,要尽量减少分支语句,并且尽量做到一个方法实现一个功能。
  2. 类与类直接的关系尽量设计为高聚合低耦合,每个模块尽可能独立完成自己的功能,不依赖于模块外部的代码。

总结:

类之间四种关系

  • 关联
public static void fun2(){//在方法里创建对象来调用方法
	Circle circle = new Circle();
	return circle.getArea();
}
public static void fun1(Circle circle){//把对象作为参数
	circle.getArea();
}
public class Cylinder{
    private height;
	private Circle circle;//把类的对象作为另一个类得的属性,通过属性来调用它的方法
}
public double getVolumn(){
	return this.height * circle.getArea();
}
  • 聚集: 聚集的耦合性较强(把一个类的对象作为另一个类的属性,聚集就是关联,只不过它增加了一个语义上的整体与部分的关系)

    • 聚合:类A是整体,类B是部分,类A和类B不是同时生成

    • 组合:类A和类B同时出现,生存期一致。

      public class A{
      	public A(){
      		objB = new B();
      	}
      }
      main(){
      	A objA = new A();/*创建objA的时候,objB同时生成,因为objB在A的构造方法里*/
      }
      
  • 依赖:依赖是最松散的

  • 泛化(继承):耦合性最强

面向对象的三大基础特性:

封装性:
  • 其实就是类class:private、public、protected
  • 把属性和方法封装到一个盒子
  • 封装的好处:模块化
继承性:

复用:(可重复利用)

继承主要的好处就是实现可复用。

多态性:**
  1. 理解多态性:可以理解为一个事物的多种形态。
  2. 何为多态性:对象的多态性:父类的引用指向子类的对象(或子类的对象赋给父类的引用)
  3. 多态的使用:虚拟方法调用,有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,我们实际执行的是子类重写父类的方法。
  4. 总结:编译,看左边;运行,看右边。
  5. 多态性的使用前提: ① 类的继承关系 ② 方法的重写
  6. 对象的多态性,只适用于方法,不适用于属性(编译和运行都看左边)

Java中关于继承性的规定:

1.一个类可以被多个子类继承。

2.Java中类的单继承性:一个类只能有一个父类

3.子父类是相对的概念。

4.子类直接继承的父类,称为:直接父类。间接继承的父类称为:间接父类

5.子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法

如果我们没有显式的声明一个类的父类的话,则此类继承于java.lang.Object类

所有的java类(除java.lang.Object类之外)都直接或间接的继承于java.lang.Object类

意味着,所有的java类具有java.lang.Object类声明的功能。

super关键字的使用

  1. super.operation();调用父类方法
  2. super();

super理解为:父类的

super可以用来调用:属性、方法、构造器

3.super的使用:调用属性和方法

  • 我们可以在子类的方法或构造器中。通过使用"super.属性"或"super.方法"的方式,显式的调用
  • 父类中声明的属性或方法。但是,通常情况下,我们习惯省略"super."
  • 特殊情况:当子类和父类中定义了同名的属性时,我们要想在子类中调用父类中声明的属性,则必须显式的
  • 使用"super.属性"的方式,表明调用的是父类中声明的属性。
  • 特殊情况:当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的
  • 使用"super.方法"的方式,表明调用的是父类中被重写的方法。
posted @ 2021-05-02 00:51  N3D2Y  阅读(187)  评论(0)    收藏  举报