OO2022第一單元個人總結
前言
本文是對第一次面向物件課程作業的總結,文章首先總結了本次作業我的總體架構思路,接著分析了三次作業中我的架構迭代歷程,之後對於我的最終架構給出了程式碼度量分析,且分析了架構的優缺點。之後文章分析了在Hack過程中的收穫,以及在本次作業設計,編寫中我學到的東西,尤其是關於深淺克隆這一部分我分享了我認為比較好的一些資料,和我自己的理解,希望uu們不要跟我一樣在這一知識點再犯錯誤。最後文章闡述了本次作業中我的心得體會。文章如有錯誤,謝謝指正!
一.總體架構思路
在本次作業中,我們的最終需求是對一個表示式進行化簡計算,並進行一些必要的合併與優化。然而要進行計算,首要任務是進行表示式的解析。總而言之,要完成解析和計算這兩個需求,我設計瞭如下架構,我的架構簡而言之是:遞歸向下解析,自底向上計算,輸出前再進行合理優化。形象化的表示一下就是如下的流程圖。
在我的架構中,將表示式抽象為了四個層次,在作業指導書中,由於已經給出了表示式的形式化表示,我們可以很容易抽象出以下層次:
-
Expr
:表示式層 -
Term
:Expr
以+-
的形式包含多個Term
項層 -
Factor
:Term
項以*
形式包含多個Factor
因子 -
Element
:上述三個層次均包含一個屬性,其型別是一個Element
陣列,有了Element
層,方便進行自底向上的運算。-
在第一次作業中
Element = a*x**b
-
在第二、三次作業中
Element = a*x*b*[sin][cos]
,其中[sin][cos]
指Sin、Cos
類的陣列
-
有了以上這些層次,我們首先對輸入進行遞歸向下解析,解析出表示式中包含哪些層次,經過自底向上的計算,可以得到每個層次的[Elements]
Expr
表示式的[Elements]
陣列便是我們得到的初步計算結果,經過合併同類項等優化便可以進行輸出。
二.迭代過程
在這一部分簡要闡述了我的三次迭代心路歷程,其中並沒有放UML
類圖,因為三次作業我沒有做大規模的重構,三次作業的UML
類圖重複相似部分較多,我將會在程式碼度量分析一節中分析UML
類圖,其中標明瞭每次作業的新增與修改部分。
2.1第一次作業
- 分析
在第一次作業中,首次上手java
作業打了我一個措手不及,因為之前寫的都是流程式的程式,開始時構思面向物件式的架構沒有很好地思路,屬於是一頭霧水了,花了很多時間去理解題目並和同學交流,並且在助教給的訓練專案中也得到了一些啟發,最終構思了上述的程式架構。
-
解析方法上,在
HW1
中,由於因子層面只有常數因子,變數因子與表示式因子,總的來說型別較少,因此採用了正則表示式進行識別,識別流程示意圖如下:
-
計算方法上,在
HW1
中,涉及到a*x**b
之間的乘法與加減法,只需係數與係數的計算,指數與指數的計算,不再贅述,但在這裡我第一次遇到了深克隆和淺克隆的問題,這個問題在本次作業中多次困擾我,這一問題在之後的Bug部分詳細闡述。 -
優化方法上,由於最後的形式是許多
a*x**b
進行相加,優化方法上只涉及到合併同類項,之後輸出時再進行簡單的優化即可,例如指數為1不輸出指數,變數因子係數為1不輸出係數等。
2.2第二次作業
- 分析
在本次作業中,新增了自變數函式,求和函式,三角函式三種因子,這要求我們對新增的這三種因子進行識別與計算。此外,由於在解析f與sum
時需要進行自變數的替換,替換後可能出現表示式括號巢狀的形式,這要求架構中的解析過程要支援巢狀括號的形式,這是本次作業一個潛在的需求。
-
重構解析方法:在本次解析過程中,由於因子的形式多樣,如果繼續使用正則表示式進行識別,一來正則表示式很難準確構造,二來由於正則式的不準確所帶來的潛在Bug也比較多。因此我對每個層次解析方法
Parse
進行了重構,採用類似棧的方法,對括號層數進行解析,根據當前括號所處的層數和簡單的字元判斷,可以依此解析出Term
,Factor
等層次。這樣寫的好處是不需要絞盡腦汁構造正則表示式,正確性也容易驗證,對於新增的幾個因子-
sin,cos
,將括號內的內容當做常數或變數因子重新解析 -
f,sum
因子,進行字串的替換,展開等處理後看做表示式重新解析
-
-
修改計算方法:
本次作業中的計算,不僅僅是
a*x**b
之間的乘法與加減法,此時Element
是a*x*b*[sin][cos]
,需要修改兩處:-
[Elements]*[Elements]
,除了修改係數a,指數b,另需要將二者[sin][cos]數組合並,這裡又涉及到了深拷貝與淺拷貝問題。然而梅開二度,我在這裡又沒有意識到犯了淺拷貝這個“錯誤”,雖然HW2
中沒有出現問題,但導致我在HW3
優化二倍角時出現了錯誤,這裡會在Bug部分詳細闡述。 -
[Elements]+-[Elements]
,這裡需要修改equals
方法,合併同類項時考慮[sin][cos]陣列是否一致。
-
-
新增優化方法:
- 在解析過程中進行初步優化,對於
sin(0),cos(0),sin()**0,cos()**0
,在解析的過程中就將其解析為對應常數,進行初步優化。 - 三角函式計算的優化
- 在
HW2
,因為時間關係,只增加了簡單的Acos(B)**2+Asin(B)**2=A
的合併,其中A
是任意項,B
是三角函式內的常數或變數因子。
- 在
- 在解析過程中進行初步優化,對於
2.3第三次作業
- 分析
在本次作業中,新增了函式間的相互呼叫,三角函式內巢狀等新需求。關於函式之間的相互呼叫,在HW2
中對於自定義函式的處理方法是,進行自變數的替換後直接看做表示式進行處理,因此HW2
架構已經可以實現函式之間的互呼叫。在本次作業中主要實現了三角函式的內巢狀,相較於第二部分修改較少。
-
修改解析方法, 如上文所述,此次迭代,只需要修改三角函式內部巢狀部分的解析方式。顯而易見,將括號內部的部分當做
Expr
表示式繼續解析即可。
- 計算方法:不需要修改。
-
新增優化方法
- 二倍角公式,在
HW3
中實現了二倍角的簡化,2Asin(B)cos(B) = Asin(2B)
,遺憾的是這裡將HW2
中的Bug顯現了出來,因為在優化時對B進行了係數的加倍,而在計算時採用的淺克隆方式,修改B導致了其他某些sin
,cos
內部的內容也隨之修改。
- 二倍角公式,在
三.程式碼度量分析
先放一下HW3
最終的UML
類圖
-
第二次作業修改了
Caculate
內的方法,新增Sin,Cos
,自定義函式Fun,Sum
類,修改了Element
屬性,其他UML
部分與第一次作業基本相同。 -
第三次作業修改了
Sin,Cos
內部屬性,同時更新了Caculate
計算方法,其他UML
類基本未做變動。 -
優缺點分析
- 優點是進行了比較系統的抽象,不同層次代表了表示式中不同的組合,
- 缺點是首先
Sin,Cos
兩個類基本相同,可以合併,在編寫程式碼時我為了區分Cos,Sin
,建立了兩個類,但其實二者基本相同,完全可以合併為一個類,以減少複雜度。此外,程式碼之間內在的的關聯度高,不容易達到“高內聚低耦合”的原則,程式的結構化程度不是很好。
之前我並未接觸過程式碼度量的含義,瞭解之後學習到其一些指標含義如下:
-
CogC
:認知複雜度,反應一個方法的可理解性,迴圈分支等結構越多,可理解性越差,數值越高。 -
ev(G)
:基本複雜度是用來衡量程式非結構化程度的。 -
Iv(G)
:模組設計複雜度是用來衡量模組判定結構,即模組和其他模組的呼叫關係,越高說明模組和其他模組之間的呼叫關係複雜,耦合程度越高. -
v(G)
:是用來衡量一個模組判定結構的複雜程度,數量上表現為獨立路徑的條數,數值越高說明程式越難以維護
方法複雜度
PreFun.printans(ArrayList) | 48.0 | 3.0 | 16.0 | 17.0 |
---|---|---|---|---|
PreFun.mergecs2(ArrayList) | 42.0 | 10.0 | 9.0 | 12.0 |
Factor.parse() | 31.0 | 4.0 | 12.0 | 15.0 |
Element.matchcs(Element) | 30.0 | 9.0 | 10.0 | 12.0 |
Element.Element(String) | 28.0 | 1.0 | 15.0 | 18.0 |
Fun.parse() | 26.0 | 3.0 | 10.0 | 11.0 |
PreFun.neg(ArrayList) | 19.0 | 1.0 | 8.0 | 8.0 |
Expr.parse() | 12.0 | 1.0 | 8.0 | 9.0 |
Term.parse() | 12.0 | 1.0 | 7.0 | 8.0 |
PreFun.addcos(StringBuilder, Element) | 10.0 | 1.0 | 6.0 | 6.0 |
PreFun.addsin(StringBuilder, Element) | 10.0 | 1.0 | 6.0 | 6.0 |
PreFun.issingle(ArrayList) | 8.0 | 5.0 | 11.0 | 12.0 |
Cos.cossequal(ArrayList, ArrayList) | 7.0 | 4.0 | 3.0 | 4.0 |
Element.elementsequal(ArrayList, ArrayList) | 7.0 | 4.0 | 3.0 | 4.0 |
PreFun.mergecs(ArrayList) | 7.0 | 1.0 | 5.0 | 5.0 |
Sin.sinsequal(ArrayList, ArrayList) | 7.0 | 4.0 | 3.0 | 4.0 |
(由於方法較多,篇幅限制只列出了分析結果中數值較大,標紅的部分資料。)
接下來進行程式碼度量結果的分析:
-
從方法複雜度來看,可以看出其集中出現在
PreFun
內的優化函式上,原因是我在優化過程中採用的for
與if-else
較多,為優化效能對許多情況進行分別討論,這導致了複雜度過高,這從某種程度上說明了我在優化部分容易出錯的原因。 -
同時每個類內
Parse
方法,和Element
方法的實現也比較複雜,這是因為耦合性過強,例如在我的架構中,他們都是為解析功能服務的函式,Expr
的parse
需要依賴Term
中的Parse
,而Term
的Parse
又需要依賴Factor
的Parse
,方法之間的依賴性很強,不容易達到“高內聚低耦合”的原則,程式的結構化程度不是很好。
四.Hack與被Hack
4.1 個人Bug發現與分析
關於本次作業的Bug較多的集中在深克隆與淺克隆這個問題上,這可能與我的程式碼架構有關,在對ArrayList
操作時使用了較多的add
方法。
在一次作業中我首次遇到了深克隆與淺克隆的問題,但在課下時我已經發現了這個Bug並予以解決。但在第二次作業時,又再一次出現了深克隆淺克隆的問題,這個Bug原因是我在計算[Elements]
陣列時為了實現合併兩陣列的[sin][cos],直接採用了add
方法,因為第二次作業我並未對Sin
,Cos
內部進行操作,所以當時我認為直接採用add
方法是正確的,簡便的,因而這個Bug在第二次作業時並沒有顯現。
然而在第三次作業中,在優化部分我新增了二倍角優化方法,直接對Sin
,Cos
內部項的係數進行了操作,此時我忽略了之前採用add
方法合併陣列時採用了淺克隆而非深克隆,直接引發了第二次作業所埋下的隱患。以後再也不瞎優化了(bushi,所以有時候不優化,保證正確性可能會更好一點。不過言歸正傳,這個Bug十分隱蔽,我在課下並沒有發現,導致一個強測點直接寄掉。
這個Bug出現的原因有二:
-
一方面是因為對於深克隆淺克隆的理解還是不夠深刻,在編寫程式碼是沒有意識到自己當前所寫程式碼實際上是再進行克隆操作,也沒有進一步思考這裡的克隆是需要深克隆還是淺克隆,在第二次作業中我採用的淺克隆方式對於
HW2
的需求來說是“正確”的(因為恰巧第二次作業中sin
,cos
內部只有常數與自變數,不需要對其內部進行操作),但其實是並不合理的,因為在架構中需要新構造一個項,進一步思考不難意識到我們需要的實際上是深克隆,關於深淺克隆的問題,為了加深我自己的理解,將在“我學到的”這一部分進行進一步闡述。 -
另一方面是因為在作業迭代的過程中,上一次作業的某些架構在本次作業中埋下了隱患。在進行作業的迭代時,編寫完程式碼後我只檢查了本次修改的,迭代的程式碼正確性,“理所當然的認為”之前的程式碼都通過了上一次強測,那肯定是正確的吧。但事實證明這樣做法是錯誤的,我忽略了兩次迭代之間的關聯性,新增的需求可能會是上次架構中"正確"但不合理的操作變為Bug。以後的作業中,應該有意識的去思考迭代過程中兩次架構中有哪些可能產生Bug的關聯之處,不能只著眼與本次作業的新增部分、修改部分,以防止這種現象的發生。
4.2 互測他人Bug發現與分析
本次互測發現了其他同學如下幾個Bug
-
-1+1
等簡單的操作無法輸出,這一Bug產生的原因是一些同學的架構進行迭代優化後,由於架構的不合理性,反而無法實現最基本的測試點,這也提醒了我們測試全面覆蓋的重要性。 -
sum
上下界定義為了int
,而非BigInteger
,這一Bug產生的原因我認為可能是沒有仔細閱讀指導書,先入為主習慣性的將一些整數定義為了int
,測試時 -
sum(i,下界,上界,i**2)
出現了問題,這一Bug產生的原因是進行字串替換時沒有加括號,這裡的i
是常數,只有替換時加括號才是正確的形式化表述。
五.我學到的
5.1 深克隆與淺克隆
深克隆與淺克隆在本次作業中對我的“身心摧殘”已在上文闡述,接下來需要進一步深入理解深淺克隆的區別與實現,以防以後出現類似的問題。
1.首先我們需要知道什麼時候我們在進行克隆?
- 簡而言之,如果此時我們在根據一個已有資料建立一個新資料的時候,我們便在進行克隆。意識到自己正在進行克隆十分重要,只有意識到了這一點才能進一步去思考深淺克隆的問題。
2.深淺克隆的區別是什麼?
- 淺克隆不會克隆原物件中的引用型別,僅僅拷貝了引用型別的指向。深克隆則拷貝了所有。也就是說深克隆能夠做到原物件和新物件之間完全沒有影響。
3.我在深淺克隆中犯的錯誤
- 首先第一點,我沒有意識到自己在進行克隆,之前並沒有在程式碼中瞭解過克隆這個概念。
- 第二點,我在克隆操作中為了實現類中非基本資料型別的屬性的克隆(在本次架構中是一個[Elements]陣列),錯誤的使用了
addall
或add
方法,這僅僅拷貝了引用型別的指向,並非深克隆。或從某種意義上講,這都不是規範的克隆方法。
4.如何進行規範的克隆?
- 分享兩篇部落格資料,寫的比較好,我在這裡找到了答案。
[(40條訊息) Java物件克隆——淺克隆和深克隆的區別_JeffCoding的部落格-CSDN部落格_淺克隆和深克隆的區別]
java 深克隆(深拷貝)與淺克隆(拷貝)詳解 - mindcarver - 部落格園 (cnblogs.com)(這一篇比較長,其實上一篇對於理解和實用來說就完全足夠)
我來簡單總結一下,java中已經為類準備了clone方法,我們不需要重複造輪子,要實現規範的clone
,還需要以下兩點:
- 實現Cloneable介面,只需在類的後面加implements Cloneable即可
- Override重寫Clone方法,如何重寫在上面兩篇博文中都有很好的闡述。
接下來,我認為重要的是理解為什麼要重寫Clone
方法,對於只包含基本資料型別屬性的類來說,clone
方法完全夠用,不需重寫,clone
後的新舊物件互不影響。重寫Clone
方法是為了應對類中包含了其他自定義的物件屬性這種情況(比如在本次HW2
作業中,我在Sin
,Cos
類中包含了[Elements]
陣列作為屬性),這時Clone
方法在拷貝這個屬性時,拷貝的只是引用型別的指向,也就是我們所說的淺拷貝,後續再對這個屬性進行操作時,便會在不經意間對新舊物件同時操作,這樣的Bug很難查找出來,因此在設計時就要予以避免。
如何進行避免?方法也很簡單,只需要在本類中修改重寫的Clone
方法,包含的其他自定義的物件裡面也重寫Clone
方法,多個淺拷貝便可以實現深拷貝。引用第一篇部落格中的例子如下(Customer
類中包含顧客地址Address
類),在兩個類中均重寫Clone
方法,之後再呼叫Clone
方法可以對Customer
深克隆。
//Address
@Override
public Address clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
//Class Customer(Address是Customer的一個屬性成員)
@Override
public Customer clone() throws CloneNotSupportedException {
Customer customer = (Customer) super.clone();
customer.address = address.clone();
return customer;
}
總而言之,我認為對於java深淺拷貝的這樣一種理解很準確:Java中定義的clone沒有深淺之分,都是統一的呼叫Object的clone方法。為什麼會有深克隆的概念?是由於我們在實現的過程中刻意的嵌套了clone方法的呼叫。也就是說深克隆就是在需要克隆的物件型別的類中全部實現克隆方法。
5.2 去哪裡找Bug
這一點其實在理論課上有講,我認為總結的是在太好了,這裡再將其羅列一下。
- 呼叫是否對返回值進行了接收和檢查
- 類庫及方法的使用是否符合要求
- 容器訪問的越界保護(課下自己測試過程中幾次出現的陣列越界問題)
- 物件拷貝是否徹底(強測出現深淺克隆的Bug)
- 數值計算是否溢位
- 是否對迴圈體內對迴圈變數進行修改
可以看到,我屬於是精準踩雷有木有。所以在靜態檢查時,可以有意識的去思考以上檢查要點,這能幫助我們更快更好的找到Bug,雖然不能覆蓋所有Bug,但我覺得可以覆蓋很大一部分非理解題意錯誤而導致的Bug。
5.3 測試資料
主要採用隨機測試的方式,手動構造了一些我認為比較容易Error和Wrong Answer的邊界資料,雖然也有一定的Hack量,但Hack成果並沒有很顯著,如果寫個自動評測機可能會更好一點,週末開著自動評測機讓其自動Hack,自己可以做其他的事情,多是一件美事啊。所以在這裡我反思一下,下一次一定要寫個自動評測機,下次一定。
六. 心得體會
通過第一單元的作業,我有以下幾點收穫
- 架構真的很重要,好的架構不僅正確度高,邏輯性強,在DeBug的時候很容易找到Bug的出處,有時候好的架構甚至會實現意想不到的功能,例如實現括號巢狀的支援等等。因此我們在開始動手之前,一定要多與同學進行交流,做好初步的設計工作,之後再逐步優化細節,進行實現。這樣既可以減少之後重構的可能性,減少工作量,同時最終實現的效果正確性,準確性也會更好。
- 初步體會了面向物件與面向過程程式設計的不同,通過面向物件的程式設計,可以更好的實現程式碼維護與迭代更新,一次可能只需要修改某幾個類就可以實現更多的需求,這一點是面向過程設計所做不到的。
- 最後一點肯定是更加熟悉了java中的各種語法以及容器的使用,從
HW1
的無從下手與茫然到慢慢熟練運用java中的常用類與常用功能,很像一個升級打怪的過程,在這個過程中我還是很滿足的。第一單元的OO作業從心態和知識上都讓我提升了很多,也收穫了很多設計,程式設計經驗,期待OO課程中之後的學習!