20175318 2018-2019-2 实验二《Java面向对象程序设计》实验报告
一、实验报告封面
课程:Java程序设计 班级:1753班 姓名:李浩然 学号:20175318
指导教师:娄嘉鹏 实验日期:2018年4月15日
实验时间:13:45 - 3:25 实验序号:实验二
实验名称:Java面向对象程序设计
实验内容:
- 1.初步掌握单元测试和TDD
- 2.理解并掌握面向对象三要素:封装、继承、多态
- 3.初步掌握UML建模
- 4.熟悉S.O.L.I.D原则
- 5.了解设计模式
实验要求:
- 1.没有Linux基础的同学建议先学习《Linux基础入门(新版)》《Vim编辑器》 课程;
- 2.完成实验、撰写实验报告,注意实验报告重点是运行结果,遇到的问题(工具查找,安装,使用,程序的编辑,调试,运行等)、解决办法(空洞的方法如“查网络”、“问同学”、“看书”等一律得0分)以及分析(从中可以得到什么启示,有什么收获,教训等);
- 3.实验报告中统计自己的PSP(Personal Software Process)时间;
- 4.严禁抄袭。
二、实验内容及步骤
(一)单元测试与TDD使用
(1)三种代码
用程序解决问题时,要学会写以下三种代码:
- 伪代码
- 产品代码
- 测试代码
(2)“测试驱动开发”(TDD) - 开发顺序:
伪代码
→测试代码
→产品代码
。 - TDD的一般步骤如下:
明确当前要完成的功能,记录成一个测试列表
快速完成编写针对此功能的测试用例
测试代码编译不通过(没产品代码呢)
编写产品代码
测试通过
对代码进行重构,并保证测试通过(重构下次实验练习)
循环完成所有功能的开发
- 使用IDEA中Junit进行TDD,参考Intellj IDEA 简易教程
- TDD的编码节奏是:
- 增加测试代码,JUnit出现红条
- 修改产品代码
- JUnit出现绿条,任务完成
任务一:在一个MyUtil类中解决一个百分制成绩转成“优、良、中、及格、不及格”五级制成绩的功能。
1.首先要明确程序要实现什么功能?要实现这些功能需要哪些操作?
`伪代码`从意图层面来解决问题。最终,伪代码是产品代码最自然的、最好的注释。因此,可以利用`伪代码`来明确以上这些要求。
伪代码
百分制转五分制:
如果成绩小于60,转成“不及格”
如果成绩在60与70之间,转成“及格”
如果成绩在70与80之间,转成“中等”
如果成绩在80与90之间,转成“良好”
如果成绩在90与100之间,转成“优秀”
其他,转成“错误”
1.用Java语言翻译伪代码,生成产品代码。
产品代码
public class MyUtil{
public static String percentage2fivegrade(int grade){
//如果成绩小于0,转成“错误”
if ((grade < 0))
return "错误";
//如果成绩小于60,转成“不及格”
else if (grade < 60)
return "不及格";
//如果成绩在60与70之间,转成“及格”
else if (grade < 70)
return "及格";
//如果成绩在70与80之间,转成“中等”
else if (grade < 80)
return "中等";
//如果成绩在80与90之间,转成“良好”
else if (grade < 90)
return "良好";
//如果成绩在90与100之间,转成“优秀”
else if (grade <= 100)
return "优秀";
//如果成绩大于100,转成“错误”
else
return "错误";
}
}
1.给新建好的test设置环境变量,也就是让IDEA知道这里存的是测试代码。在test
上右击然后MakeDirectoryas
之后选择testSourceRoot
,这样就把这个文件夹设置成了存放测试代码的源文件的文件夹:
2.如下图:创建测试类
,在test目录中编写测试代码MyUtilTest,其中的测试用例分为测试正常testNormal测试边界testBoundary测试异常testException三部分:
可利用以下测试代码对写成的产品代码进行测试,检查是否有不完善的地方
测试代码
import junit.framework.TestCase;
import org.junit.Test;
import static org.junit.Assert.*;
public class MyUtilTest extends TestCase {
@Test
public void testNormal() {
assertEquals("不及格", MyUtil.percentage2fivegrade(55));
assertEquals("及格", MyUtil.percentage2fivegrade(65));
assertEquals("中等", MyUtil.percentage2fivegrade(75));
assertEquals("良好", MyUtil.percentage2fivegrade(85));
assertEquals("优秀", MyUtil.percentage2fivegrade(95));
}
@Test
public void testException(){
assertEquals("错误",MyUtil.percentage2fivegrade(-58));
assertEquals("错误",MyUtil.percentage2fivegrade(118));
}
@Test
public void testBoundary(){
assertEquals("不及格",MyUtil.percentage2fivegrade(0));
assertEquals("及格",MyUtil.percentage2fivegrade(60));
assertEquals("中等",MyUtil.percentage2fivegrade(70));
assertEquals("良好",MyUtil.percentage2fivegrade(80));
assertEquals("优秀",MyUtil.percentage2fivegrade(90));
assertEquals("优秀",MyUtil.percentage2fivegrade(100));
}
}
如果出现问题,JUnit会出现红条,IDEA会提示哪一个测试用例出现问题,由此可以对应改正产品代码中的问题,直到JUnit出现绿条,任务完成。
测试成功截图
任务二:以TDD的方式研究学习StringBuffer
对老师给的StringBufferDemo
产品代码进行改写,并写出StringBuffer
中的charAt
、length
、capcity
这几个方法对应的测试代码进行测试。
产品代码public class StringBufferDemo{ StringBuffer buffer = new StringBuffer(); public StringBufferDemo(StringBuffer buffer){ this.buffer = buffer; } public Character charAt(int i){ return buffer.charAt(i); } public int capacity(){ return buffer.capacity(); } public int length(){ return buffer.length(); } public int indexOf(String buf) { return buffer.indexOf(buf); } }
首先要理解以上代码中的方法。通过查询API文档,可知:
- charAt(int i)
:返回此序列中指定索引处的 char 值。第一个 char 值在索引 0 处,第二个在索引 1 处,依此类推,这类似于数组索引。
- `indexOf(String s)`:返回输入的子字符串的第一个字母在母字符串的位置。
- `capacity()`:返回当前容量。容量指可用于最新插入的字符的存储量,超过这一容量就需要再次进行分配。
- `length()`:返回子浮窗的长度。
编写测试代码对其进行测试:
测试代码:
import junit.framework.TestCase;
import org.junit.Test;
import static org.junit.Assert.*;
public class StringBufferDemoTest extends TestCase {
StringBuffer string1 = new StringBuffer("Students");
StringBuffer string2 = new StringBuffer("Students of class 1753");
StringBuffer string3 = new StringBuffer("Students of class 1753 and class 1752");
@Test
public void testCharAt() throws Exception{
assertEquals('t',string1.charAt(1));
assertEquals(' ',string2.charAt(8));
assertEquals('1',string3.charAt(18));
}
@Test
public void testCapacity() throws Exception{
assertEquals(24,string1.capacity());
assertEquals(38,string2.capacity());
assertEquals(53,string3.capacity());
}
@Test
public void testindexOf() throws Exception{
assertEquals(1, string1.indexOf("tud"));
assertEquals(8, string2.indexOf(" of"));
assertEquals(18, string3.indexOf("1753"));
}
@Test
public void testlength() throws Exception{
assertEquals(8, string1.length());
assertEquals(22, string2.length());
assertEquals(37, string3.length());
}
}
测试成功截图:
(二)面向对象三要素
面向对象(Object-Oriented)的三要素包括:封装、继承、多态。面向对象的思想涉及到软件开发的各个方面,如面向对象分析(OOA)、面向对象设计(OOD)、面向对象编程实现(OOP)。OOA根据抽象关键的问题域来分解系统,关注是什么(what)。OOD是一种提供符号设计系统的面向对象的实现过程,用非常接近问题域术语的方法把系统构造成“现实世界”的对象,关注怎么做(how),通过模型来实现功能规范。OOP则在设计的基础上用编程语言(如Java)编码。贯穿OOA、OOD和OOP的主线正是抽象。
- 过程抽象的结果是函数,数据抽象的结果是抽象数据类型(Abstract Data Type,ADT),类可以作具有继承和多态机制的ADT。数据抽象才是OOP的核心和起源。
- 封装:面向对象三要素的第一个要素是封装,封装就 是将数据与相关行为包装在一起以实现信息就隐藏。封装实际上使用方法(method)将类的数据隐藏起来,控制用户对类的修改和访问数据的程度,从而带来模块化(Modularity)和信息隐藏(Information hiding)的好处;接口(interface)是封装的准确描述手段。
- 继承:继承指一个类的定义可以基于另外一个已经存在的类,即子类基于父类,从而实现父类代码的重用。既存类称作基类、超类、父类(base class、super class、parent class),新类称作派生类、继承类、子类(derived class、inherited class、child class)。继承关系表达了”Is a kind of“的关系,称为“ISA”关系。继承的关键在于确认子类为父类的一个特殊类型。
继承是实现软件可重用的根基,是提高软件系统的可扩展性与可维护性的主要途径。
以封装为基础,继承可以实现代码复用,需要注意的是,继承更重要的作用是实现多态。
- 多态:面向对象中允许不同类的对象对同一消息做出响应,即同一消息可以根据发送对象的不同而采用多种不同的行为方式,我们称此现象为多态性。Java中,多态是指不同的类对象调用同一个签名的成员方法时将执行不同代码的现象。多态是面向对象程序设计的灵活性和可扩展性的基础。
在Java中,当我们用父类声明引用,用子类生成对象时,多态就出现了。
(三)设计模式
面向对象三要素是“封装、继承、多态”,任何面向对象编程语言都会在语法上支持这三要素。如何借助抽象思维用好三要素特别是多态还是非常困难的,S.O.L.I.D类设计原则是一个很好的指导:
SRP(Single Responsibility Principle,单一职责原则)
OCP(Open-Closed Principle,开放-封闭原则)
LSP(Liskov Substitusion Principle,Liskov替换原则)
ISP(Interface Segregation Principle,接口分离原则)
DIP(Dependency Inversion Principle,依赖倒置原则)
任务三:对MyDoc类进行扩充,让其支持Byte类,初步理解设计模式
OCP是OOD中最重要的一个原则,OCP的内容是:
软件实体(类,模块,函数等)应该对扩充开放,对修改封闭。
对扩充开放(Open For Extension )要求软件模块的行为必须是可以扩充的,在应用需求改变或需要满足新的应用需求时,我们要让模块以不同的方式工作; 对修改封闭(Closed for Modification )要求模块的源代码是不可改动的,任何人都不许修改已有模块的源代码。 基于OCP,利用面向对象中的多态性(Polymorphic),更灵活地处理变更拥抱变化,OCP可以用以下手段实现:(1)抽象和继承,(2)面向接口编程。
产品代码:
abstract class Data{
public abstract void DisplayValue();
}
class Integer extends Data {
int value;
Integer(){
value=100;
}
public void DisplayValue(){
System.out.println(value);
}
}
class Byte extends Data{
byte value;
Byte(){
value=(byte)18;
}
public void DisplayValue(){
System.out.println(value);
}
}
//Pattern Classes
abstract class Factory {
abstract public Data CreateDataObject();
}
class IntFactory extends Factory {
public Data CreateDataObject(){
return new Integer();
}
}
class ByteFactory extends Factory {
public Data CreateDataObject(){
return new Byte();
}
}
//Client Classes
class Document {
Data pd;
Document(Factory pf) {
pd=pf.CreateDataObject();
}
public void DisplayData(){
pd.DisplayValue();
}
}
//Test Classes
public class MyDoc {
static Document d;
public static void main(String[] args) {
d = new Document(new ByteFactory());
d.DisplayData();
}
}
运行结果
(四)练习
任务四:以TDD的方式开发一个复数类Complex
伪代码
// 定义属性并生成getter,setter
double RealPart;
double ImagePart;
// 定义构造函数
public Complex()
public Complex(double R,double I)
//Override Object
public boolean equals(Object obj)
public String toString()
// 定义公有方法:加减乘除
Complex ComplexAdd(Complex a)
Complex ComplexSub(Complex a)
Complex ComplexMulti(Complex a)
Complex ComplexDiv(Complex a)
产品代码
import java.lang.Integer;
import java.util.Objects;
public class Complex {
//定义属性并生成getter,setter
double RealPart;
double ImagePart;
public double getRealPart(){
return RealPart;
}
public double getImagePart(){
return ImagePart;
}
//定义构造函数
public Complex(){
RealPart = 0;
ImagePart = 1;
}
public Complex(double R,double I){
RealPart = R;
ImagePart = I;
}
//Override Object
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(!(obj instanceof Complex)) {
return false;
}
Complex complex = (Complex) obj;
if(complex.RealPart != ((Complex) obj).RealPart) {
return false;
}
if(complex.ImagePart != ((Complex) obj).ImagePart) {
return false;
}
return true;
}
public String toString(){
String s = new String();
if (ImagePart > 0){
s = getRealPart() + "+" + getImagePart() + "i";
}
if (ImagePart == 0){
s = getRealPart() + "";
}
if(ImagePart < 0){
s = getRealPart() + "" + getImagePart() + "i";
}
if(RealPart == 0){
s = getImagePart() + "i";
}
return s;
}
//定义公有方法:加减乘除
Complex ComplexAdd(Complex a){
return new Complex(RealPart + a.RealPart,ImagePart + a.ImagePart);
}
Complex ComplexSub(Complex a){
return new Complex(RealPart - a.RealPart,ImagePart - a.ImagePart);
}
Complex ComplexMulti(Complex a){
return new Complex(RealPart*a.RealPart-ImagePart*a.ImagePart,RealPart*a.ImagePart + ImagePart*a.RealPart);
}
Complex ComplexDiv(Complex a) {
return new Complex((RealPart * a.ImagePart + ImagePart * a.RealPart) / (a.ImagePart * a.ImagePart + a.RealPart * a.RealPart), (ImagePart * a.ImagePart + RealPart * a.RealPart) / (a.RealPart * a.RealPart + a.RealPart * a.RealPart));
}
}
测试代码
import junit.framework.TestCase;
import org.junit.Test;
import static org.junit.Assert.*;
public class ComplexTest extends TestCase {
Complex c1 = new Complex(0.0, 2.0);
Complex c2 = new Complex(-1.0, -1.0);
Complex c3 = new Complex(1.0,2.0);
@Test
public void testgetRealpart() throws Exception{
assertEquals(0.0,c1.getRealPart());
assertEquals(-1.0,c2.getRealPart());
assertEquals(1.0,c3.getRealPart());
}
@Test
public void testgetImagePart() throws Exception{
assertEquals(2.0,c1.getImagePart());
assertEquals(-1.0,c2.getImagePart());
assertEquals(2.0,c3.getImagePart());
}
@Test
public void testComplexAdd() throws Exception{
assertEquals("-1.0+1.0i",c1.ComplexAdd(c2).toString());
assertEquals("1.0+4.0i",c1.ComplexAdd(c3).toString());
assertEquals("1.0i",c2.ComplexAdd(c3).toString());
}
@Test
public void testComplexSub() throws Exception{
assertEquals("1.0+3.0i",c1.ComplexSub(c2).toString());
assertEquals("-1.0",c1.ComplexSub(c3).toString());
assertEquals("-2.0-3.0i",c2.ComplexSub(c3).toString());
}
@Test
public void testComplexMulti() throws Exception{
assertEquals("2.0-2.0i",c1.ComplexMulti(c2).toString());
assertEquals("-4.0+2.0i",c1.ComplexMulti(c3).toString());
assertEquals("1.0-3.0i",c2.ComplexMulti(c3).toString());
}
@Test
public void testComplexDiv() throws Exception{
assertEquals("-1.0-1.0i",c1.ComplexDiv(c2).toString());
assertEquals("0.4+2.0i",c1.ComplexDiv(c3).toString());
assertEquals("-0.6-1.5i",c2.ComplexDiv(c3).toString());
}
public void testtoString() throws Exception{
assertEquals("2.0i",c1.toString());
assertEquals("-1.0-1.0i",c2.toString());
assertEquals("1.0+2.0i",c3.toString());
}
}
运行结果
任务五:使用StarUML对实验二中代码进行建模
UML图
实验总结与体会
通过本周的实验,我主要学会了如何编写测试代码,如何使用UML图,也了解了TDD方式、S.O.L.I.D原则以及设计模式这些程序员必备的编程思想。
本周实验内容比较多,但还好老师的教程比较详细,前期不太懂的时候,多看几遍老师的教程,按照教程一步一步来,在配置过程没有出现什么问题,节省了很多时间。
其实在看老师总结的知识点的时候,对很多内容不是很理解,但在自己上手实践操作过程中,加深了对知识点的理解,也比较熟练的掌握的Junit的用法。最初在使用测试代码的时候还不是明白它的好处,但在动手编写实现复数类以及结对编程的时候,真正认识到了测试代码的好用之处。
代码托管:
码云链接
代码提交截图
参考资料
实验二 Java面向对象程序设计
Intellj IDEA 简易教程
积极主动敲代码,使用JUnit学习Java
capacity()的用法
UML基础教程——简单易懂!