2017-2018-2 20165318 實驗二《Java面向對象程序設計》實驗報告
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
- 任務一:在一個MyUtil類中解決一個百分制成績轉成“優、良、中、及格、不及格”五級制成績的功能。
- 任務二:以TDD的方式研究學習StringBuffer
- (二)面向對象三要素
- 任務三:使用StarUML對實驗二中的代碼進行建模
- (三)設計模式
- 任務四:對MyDoc類進行擴充,讓其支持Byte類,初步理解設計模式
- (四)練習
- 任務五:以TDD的方式開發一個復數類Complex
- (五)實驗過程中遇到的問題及解決方法
- 問題1:
creat test
中junit,@Test或者import junit.framework.TestCase;是紅色的怎麽辦? - 關於StringBuffer()中的capacity()方法
- 關於StringBuffer()中的構造方法
- 問題1:
- (六)實驗體會與總結
- 代碼托管
- 代碼提交截圖
- 參考資料
(一)單元測試與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(); a.append("StringBuffer");
創建對象a;而老師的代碼使用
StringBuffer a = new StringBuffer("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% |
返回目錄
參考資料
- 實驗二 Java面向對象程序設計
- Intellj IDEA 簡易教程
- 積極主動敲代碼,使用JUnit學習Java
- capacity()的用法
- UML基礎教程——簡單易懂!
返回目錄
2017-2018-2 20165318 實驗二《Java面向對象程序設計》實驗報告