2017-2018-2 20165318 实验二《Java面向对象程序设计》实验报告
一、实验报告封面
课程:Java程序设计 班级:1653班 姓名:孙晓暄 学号:20165318
指导教师:娄嘉鹏 实验日期:2018年4月16日
实验时间:13:45 - 3:25 实验序号:实验二
实验名称:Java面向对象程序设计
实验内容:
- 初步掌握单元测试和TDD
- 理解并掌握面向对象三要素:封装、继承、多态
- 初步掌握UML建模
- 熟悉S.O.L.I.D原则
- 了解设计模式
实验要求:
- 没有Linux基础的同学建议先学习《Linux基础入门(新版)》《Vim编辑器》 课程;
- 完成实验、撰写实验报告,注意实验报告重点是运行结果,遇到的问题(工具查找,安装,使用,程序的编辑,调试,运行等)、解决办法(空洞的方法如“查网络”、“问同学”、“看书”等一律得0分)以及分析(从中可以得到什么启示,有什么收获,教训等);
- 实验报告中统计自己的PSP(Personal Software Process)时间;
- 严禁抄袭。
二、实验内容及步骤
目录
(一)单元测试与TDD使用
(1)三种代码
用程序解决问题时,要学会写以下三种代码:
- 伪代码
- 产品代码
- 测试代码
(2)“测试驱动开发”(TDD)
-
开发顺序:
伪代码
→测试代码
→产品代码
。 -
TDD的一般步骤如下:
-
明确当前要完成的功能,记录成一个测试列表
-
快速完成编写针对此功能的测试用例
-
测试代码编译不通过(没产品代码呢)
-
编写产品代码
-
测试通过
-
对代码进行重构,并保证测试通过(重构下次实验练习)
-
循环完成所有功能的开发
-
-
使用IDEA中Junit进行TDD,参考Intellj IDEA 简易教程
-
TDD的编码节奏是:
-
增加测试代码,JUnit出现红条
-
修改产品代码
-
JUnit出现绿条,任务完成
-
任务一:在一个MyUtil类中解决一个百分制成绩转成“优、良、中、及格、不及格”五级制成绩的功能。
- 首先要明确程序要实现什么功能?要实现这些功能需要哪些操作?
伪代码
从意图层面来解决问题。最终,伪代码是产品代码最自然的、最好的注释。因此,可以利用伪代码
来明确以上这些要求。
注:伪代码与具体编程语言无关,不要写与具体编程语言语法相关的语句。
伪代码
百分制转五分制:
如果成绩小于60,转成“不及格”
如果成绩在60与70之间,转成“及格”
如果成绩在70与80之间,转成“中等”
如果成绩在80与90之间,转成“良好”
如果成绩在90与100之间,转成“优秀”
其他,转成“错误”
- 用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 "错误";
}
}
-
给新建好的test设置环境变量,也就是让IDEA知道这里存的是测试代码。在
test
上右击然后MakeDirectoryas
之后选择testSourceRoot
,这样就把这个文件夹设置成了存放测试代码的源文件的文件夹: -
如下图:创建
测试类
,在test目录中编写测试代码MyUtilTest,其中的测试用例分为测试正常testNormal测试边界testBoundary测试异常testException三部分:
可利用以下测试代码
对写成的产品代码
进行测试,检查是否有不完善的地方。
测试代码
import junit.framework.TestCase;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Created by sxx on 2018/4/14.
*/
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
这几个方法对应的测试代码进行测试。
- 按照老师给的参考代码中StringBufferDemo中调用的方法,将其更改为我的产品代码。
产品代码
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.*;
/**
* Created by sxx on 2018/4/14.
*/
public class StringBufferDemoTest extends TestCase {
StringBuffer string1 = new StringBuffer("Students");
StringBuffer string2 = new StringBuffer("Students of class 1653");
StringBuffer string3 = new StringBuffer("Students of class 1653 and class 1652");
@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("1653"));
}
@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中,当我们用父类声明引用,用子类生成对象时,多态就出现了。
任务三:使用StarUML对实验二中代码进行建模
UML是一种通用的建模语言,可以非常直观地表现出各个结构之间的关系。
通过参考UML基础教程——简单易懂!了解了UML图的大致内容,我自己制作的UML图如下。
UML图
(三)设计模式
面向对象三要素是“封装、继承、多态”,任何面向对象编程语言都会在语法上支持这三要素。如何借助抽象思维用好三要素特别是多态还是非常困难的,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)面向接口编程。
根据已有的支持Int型的代码:
abstract class Data{
public abstract void DisplayValue();
}
class Integer extends Data {
int value;
Integer(){
value=100;
}
public void DisplayValue(){
System.out.println(value);
}
}
class Document {
Data pd;
Document() {
pd=new Integer();
}
public void DisplayData(){
pd.DisplayValue();
}
}
public class MyDoc {
static Document d;
public static void main(String[] args) {
d = new Document();
d.DisplayData();
}
}
要想同时实现Byte型的功能,Document类的构造方法需要修改,这样违背了OCP原则。封装、继承、多态解决不了问题了,这时需要设计模式了,通过增加了一层抽象层使代码符合OCP原则,使代码有良好的可扩充性、可维护性。我的产品代码如下:
产品代码
/**
* Created by sxx on 2018/4/14.
*/
//Server Classes
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;
/**
* Created by sxx on 2018/4/14
*/
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.*;
/**
* Created by sxx on 2018/4/14.
*/
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());
}
}
测试成功截图
实验过程中遇到的问题及解决方法
问题1:creat test
中junit,@Test或者import junit.framework.TestCase;是红色的怎么办?
解决方法:参考单元测试,明白导入相关的junit.jar包即可,步骤如下:
-
首先,进入页面左上角
File
→Project Structure...
:
-
选中
Modules
,根据你的IDEA安装地址导入这两个包就可以了。
问题2:关于StringBuffer()中的capacity()方法。
解决方法:参考capacity()的用法可知,StringBuffer在内部维护一个字符数组,当你使用缺省的构造函数来创建StringBuffer对象的时候, StringBuffer的容量被初始化为16个字符,也就是说缺省容量就是16个字符。当StringBuffer达到最大容 量的时候,它会将自身容量增加到当前的2倍再加2,也就是(2*旧值+2)。
也就是说,对于空字符串,调用capacity()方法初始分配值为16;大小超过16时则扩充容量为34,再次扩充得到70。
对于非空字符串:append(s);是扩容方法如下:
最小容量是:当前的长度+串长,拟扩充的新容量是:(旧容量+1)*2
若:最小容量>拟扩充的新容量,则按最小容量扩充,否则(拟扩充的新容量>=最小容量):就按拟扩充的新容量扩充。
eg1:StringBuffer str = new StringBuffer(); //容量是16
str.append("大家好我们大家都很愿意学习java语言"); //扩容。如何扩?
最小容量是:0(当前长度是0)+19(那个要append的串长)=19
拟扩充的新容量:(16+1)*2=34
因为:最小容量<拟扩充的新容量,则此时按:拟扩充的新容量即34进行扩容。
eg2:StringBuffer str = new StringBuffer(); //容量是16
str.append("我们大家都很愿意学习java语言zzzzzzzzzzzzzzzzzzzz"); //扩容。如何扩?
最小容量是:0(当前长度是0)+36(那个要append的串长)=36
拟扩充的新容量:(16+1)*2=34
因为:最小容量>拟扩充的新容量,则此时按:最小容量即36进行扩容。
问题3:关于StringBuffer()中的构造方法。
在老师给的参考代码中计算出当对象a="student"
的长度小于16的时候,计算的a.capacity()
为16,但我的程序中计算出a.capacity()
为24。
解决方法:
通过观察代码,我发现我的代码和老师的代码不同之处在于,我使用
StringBuffer a = new StringBuffer("StringBuffer");
创建对象a;而老师的代码使用
StringBuffer a = new StringBuffer(); a.append("StringBuffer");
创建对象a。
在查阅资料后发现,这两个写法是有区别的。前者是将"StringBuffer"写入a之后再分配16个字符长度的缓存;后者直接在缓存区里面写入(当缓冲区写完了就会继续分配)。
两者最直接的体现就是a.capacity()的数值不同,前者为24,后者只有16(刚好比前面多出16个字符长,即缓存区的长度)。
实验总结与体会
通过本周的实验,我主要学会了如何编写测试代码,如何使用UML图,也了解了TDD方式、S.O.L.I.D原则以及设计模式这些程序员必备的编程思想。
本周实验内容比较多,但还好老师的教程比较详细,前期不太懂的时候,多看几遍老师的教程,按照教程一步一步来,在配置过程没有出现什么问题,节省了很多时间。
其实在看老师总结的知识点的时候,对很多内容不是很理解,但在自己上手实践操作过程中,加深了对知识点的理解,也比较熟练的掌握的Junit的用法。最初在使用测试代码的时候还不是明白它的好处,但在动手编写实现复数类以及结对编程的时候,真正认识到了测试代码的好用之处。
在利用JUnit测试代码的过程中,也掌握了对于assertEquals
的使用,尤其意识到使用assertEquals
时,要注意比较的两个对象的返回值类型一定要相同。通过利用Junit测试,使我对于代码中一些方法的理解也更加深刻透彻。
通过看老师的教程,查找UML图相关教程以及自己动手实践,使我对UML图更为了解,而且我意识到,有时候做一个UML图可以更好的读懂老师的程序(参考老师给的UML图)。
代码托管
代码提交截图
PSP
步骤 | 耗时 | 百分比 |
---|---|---|
需求分析 | 10min | 6% |
设计 | 20min | 11% |
代码实现 | 60min | 35% |
测试 | 30min | 18% |
分析总结 | 50min | 30% |