Showing posts with label java. Show all posts
Showing posts with label java. Show all posts

12.07.2014

Java的十個物件導向設計原則

對於開發者而言,想必大家都知道design pattern的重要性;但這篇不是要講design pattern,我們要來看看物件導向程式設計的基本原則,其中包含了SOLID(Single responsibility, Open-closed, Liskov substitution, Interface segregation 和 Dependency inversion),這些基本原則可以幫助你寫出更簡潔、易維護、低耦合的系統。

不要自我重覆(DRY:Don't Repeat Yourself)

不要寫出重覆的程式(功能)!若有一部份的程式出現在不同地方,你可以考慮使用抽象化(Abstraction: 將相同的邏輯往parent搬)或委派(Delegation:將相同的任務委派給另一個物件,或另一個method完成),若有hard-code value重覆出現,則使用public final取代之,藉此可以提升維護性
這個原則很重要,但要小心不能濫用,這裡的程式並不是指程式碼,而是指功能。假設我們使用同樣的程式來驗證資料A以及資料B,雖然現在A及B有著相同格式,若我們使用委派,將相同的驗證抽成一個共用method,代表我們認為A和B的格式永遠不會變動,一旦未來其中一個格式有變動,改了驗證程式就會造成另一個資料驗證問題(若沒有Unit test特別容易發生)。因此要特別注意,這個原則適用在相同的功能而不是相似的程式


將變動封裝起來(Encapsulate Changes)

軟體開發中唯一不變的就是”變動”,變動可能來自於需求的改變、使用不同的工具等,要減少變動影響範圍,我們要將可能會變動的程式封裝起來。例如要發動車子,內部有很多零件必需協同和作才能完成,但用戶不需要知道這些細節,他們只需要將鑰匙轉動即可。由於封裝將細節隱藏起來,只提供主要介面供使用者驅動,若內部實作細節有任何異動,用戶端程式可以不用做任何異動。封裝簡單說就是將變數及method儘可能private化,至於要怎麼判斷程式碼是否有做到封裝,依我的經驗,若有段程式從物件A get不同參數值或呼叫A的不同method,很可能這段程式是屬於A的責任,你已經違反了封裝的概念。不要小看封裝,若你的實作散落各地,實作內容變動愈大,你所要修改的類別可能愈多

開放關閉設計原則(Open Close Design Principle)

有點難理解的原則,又開放又關閉的!其實指的是不同東西,開放指的是類別及方法要能夠擴充(加新功能或不同實作),關閉指的是關閉修改(refactoring例外)。理想上當你要加入新功能,應該引入新的類別、方法或變數,而不是去修改既有已經完成測試的程式,這可以避免修改造成的regression bug。
但要如何使程式能夠擴充呢?基本上我們可以運用多型(polymorphism),藉由相同介面(interface或super class),讓我們可以在擴充行為時不影響client端以及既有的程式

單一責任原則(Single Responsibility Principle)

責任指的是變動的理由,一個類別應該只有一個變動的理由例如一個類別負責編輯報表以及列印報表,這個類別就有二個變動的理由;一個是資料的異動,另一個是報表的格式異動。將二個不同責任綁在一起,除了會造成閱讀困難外,也會使其不好維護,畢竟我們不能保證修改其中一個功能時,對另一個功能不會造成任何影響。


相依性注入或反轉原則(Dependency Injection or Inversion Principle)

相信有使用Spring Framework的都對這個原則不漠生,一般我們要使用一個物件可能會直接new 一個instance,但DI告訴我們不要這樣,你應該動態注入相依的物件,這種方式可以帶來下面二種好處:
  1. 讓我們在撰寫測試時可以利用mock object,讓我們專注在該測的功能上
  2. 可藉由修改設定檔注入不同實作(Polymorphism),而不必修改程式。

傾向使用Composition多於繼承(Favor Composition over Inheritance)

使用繼承我們可以將共同的程式碼移至super class,使用composition我們則是將共同的程式碼移至composed class,在不考慮使用繼承是不是好的設計狀況下,二種差別在於彈性:使用繼承我們無法動態變動實作,使用composition,我們可以藉由polymorphism,讓不同實作有相同介面,藉由執行期的相依性注入抽換不同實作,任何時候我們都可以切換不同的實作且不用改程式。

介面分離原則(ISP:Interface Segregation Principle)

這個原則指的是client不應實作一個他不提供的功能介面會違反這個原則,大多是因為一個介面負責了超過一個功能(違反單一責任原則),而client只需要其中部份功能。在介面的設計上必須特別小心,因為一旦發佈了後,日後所做的任何修改都會使現有的實作無法運行。

Liskov 替換原則(Liskov Substitution Principle)

這個原則指的是子類別的實作必需可以替代父類別!也就是說父類別的實作必需在子類別也能運作LSP和單一責任原則以及介面分離原則有很強的關聯,若一個類別負責許多許責任,子類別一旦無法支援所有行為,就會違反LSP。要遵守LSP,則子類別必須是加強父類別的功能,不能減少。舉個教科書常見的錯誤例子來說:父類別Rectangle定義了長及寬變數,以及取得面積的方法,子類別Square繼承了父類別的長及寬變數以及行為,當我們以polymorphism的角度使用Square類別(Rectangle r = new Square())時,使用者認為他必需設定長及寬來計算面積,但當他設定長=10,寬=5並計算面積時,發現回傳是25(因為正方形四邊都一樣)而不是50!因為polymorphism開發時操作的是super class或interface,因此違反了這個原則可能帶來用戶錯誤的預期

針對介面而非實作(Programming for Interface not implementation)

這個原則其實就是要我們善用polymorphism,定義變數或方法變數或回傳變數時使用介面而非實作可以帶來彈性,當要替換不同實作時,這可以讓你減少異動

委派原則(Delegation Principle)

不要試著在一個類別中做所有的事,把責任委派給該負責的類別。例如hash code和equals method,要比較二個物件是否相同,我們並不會在client端寫程式去比,而是交由二個物件本身自己去比較,這樣的好處是可以減少重覆的程式碼,讓系統更好維護。

你可能對下面主題有興趣:
  1. [OO概念]封裝,繼承,多型
  2. [Java 概念]Interface and abstract class

12.06.2014

Garbage Collector的種類

前面(什麼是Garbage Collection)提到身為開發者,Java會自動回收記憶體,讓開發者可以專注在業務邏輯的處理,但在回收的過程中,會造成系統無法回應(取決於Major/Minor GC);因此Java提供了許多不同的Garbage Collector,讓我們可以依據系統的效能需求,選擇最適合的Garbage Collector。
如下圖所示,有七種Garbage Collector,分為上下二個區塊,上面為Young generation,下面為Old/Tenured generation,若二個Garbage Collector之間存在連線,代表可以搭配使用。

http://blog.csdn.net/java2000_wl/article/details/8030172

Serial Collector(-XX:+UseSerialGC)

屬於單一執行緒的copy collector,運作原理是將存活的物件從"from" space (ex.survivor 1)搬至"to" space(ex.survivor 2),並回收"from" space,他主要是設計給heap size需求很小的應用程式使用,由於屬於"Stop-the-world" (當他在執行時,其他任務都必需暫停) collector,所以並不適用於對response time要求高的系統,但相對地也因為單執行緒,讓他可以有最高的GC效率,較為適合用於不要求即時性(ex background job server)的系統。


ParNew Collector(-XX:+UseParNewGC)

Parallel copy collector其實就是multi-thread版的Serial Collector,其餘行為都跟Serial collector相同,效率則因為multi-thread較Serial Collector好。


Parallel Scavenge Collector(-XX:+UseParallelGC)

運作方式跟Parallel copy collector相同,但演算法有針對超過10GB heaps的multi-CPU環境做過最佳化。他的目標是盡可能地取得最大throughput[註1]同時降低GC造成的暫停。若你使用這個collector,則在old generation你可以使用原使的mark-sweep collector(Serial Old)。


CMS(-XX:+UseConcMarkSweepGC)

CMS不同於其他garbage collector,他不會進行heap space compaction(因為compaction的過程很耗時且會造成系統停頓),使他適用於高互動式的系統;但相對地,他也較容易造成記憶體碎片化(fragmentation);雖然CMS不會進行Full GC,但當出現promotion failed/concurrent mode failure時,此時會使用Mark Compact garbage collector (Serial Old GC)進行Full GC,CMS運作分為下面幾個階段:
Initial mark - 這個階段會找出application有直接(root)reference reference的活物件, stop-the-world 。
Concurrent mark – 這個階段會針對上階段結果,追蹤整個object graph,並標記活著的物件,整個過程會和application同時運行。
Concurrent pre clean – 這個階段是針對上一階段標記為活著的物件再做檢查,目的是為了減少Remark造成stop-the-world的時間,整個過程會和application同時運行。
Remark - 因為在進行mark階段,application是持續在運行,新的物件並不會被標記,此時application再度停止運行,並針對在Concurrent mark階段有變動的物件進行最終標記 , stop-the-world 。

Serial Old(-XX:+UseSerialGC)

Serial Old (Mark Sweep Compact:MSC) - 屬於stop-the-world collector,運作原理是先找出活著的物件做標記,之後清除未用的物件,最後再進行compaction。


Parallel Old(-XX:+UseParallelOldGC)

Parallel Scavenge的Old generation版本。


G1(-XX:+UseG1GC)

G1GC在Java7時加入,他的目標是在不犧牲throughput的狀況下,達成low latency。傳統GC會將heap分為3個區塊:young generation,old generation以及permanent generation,每個區塊都設定了固定大小;G1GC則是將heap分割成一塊塊相同大小的region,每個resion大約是1MB~32MB的連續記憶體空間,再將一個個region組成region sets,同一個region set扮演相同的角色(eden, survivor或old),因為region set沒固定大小,若服務中大部份的物件存活期都很短,則young generation會佔大部份;反之則tenured generation佔大部份,因此使用G1GC時,調整新世代大小的參數就不重要了,這種方式提供了記憶體使用上的更大彈性。

G1GC的GC過程跟CMS很類似,都是分階段標記後再回收,差別在於回收的方式。G1GC將記憶體分割為許多region,GC時並不會一次對所有region都進行回收,而是會依據允許的時間(預設為200ms),能回收多少region就做多少,藉此精準地控制停頓的時間,GC會先回收垃圾最多的region,以釋放最多空間,這也就是為什麼他叫Garbage First的原因。回收的過程採用驅離(evacuation)的方式,從一個或多個region將活著的物件搬至另一個region,藉此同時進行記憶體回收以及compaction,避免了heap破碎化的問題。



[註1]Throughput的定義是扣除GC花費的時間,系統可以使用的時間比;也就是[系統可用時間]/[總系統執行時間]。

12.01.2014

什麼是Garbage Collection以及他在Java如何運作

JAVA並不像C或C++必需由開發者負責記憶體分配/回收的任務,JVM會依據設定,找出沒被參照的物件,自動進行記憶體空間回收,這使得讓開發者在開發時可以專注在業務邏輯上。雖然開發者可以不用管記憶體的分配/回收,但為了系統效能最佳化,我們還是有必要了解Garbage Collection(以下簡稱GC)是如何運作的,在開始之前,我們必須強調GC雖然可以自動回收沒被參照的物件,但無法解決程式設計不當所造成的memory leak。

首先來看GC的幾個重點

  1. 不論物件的scope(local variable/member variable),所有的物件都是儲存在Heap space!
  2. GC是JVM用來回收記憶體的機制,符合GC條件的物件會在GC過程中,釋放其佔用的記憶體空間(Heap space)
  3. GC讓開發者不用處理記憶體管理,在開發時可以專注在業務邏輯上
  4. Java透過一個叫Garbage Collector的daemon(背景)thread來進行GC
  5. 開發者無法強制執行GC,GC只會在JVM認為需要(根據Heap Size)時才會執行
  6. System.gc()和Runtime.gc()[註1]會送GC請求給JVM,但JVM並不一定會執行GC
  7. Heap space沒空間存放新建立的物作,則JVM會丟出OutOfMemoryError java.lang.OutOfMemoryError heap space
  8. Minor GC和Major/Full都是"Stop the World"事件,只是Minor GC時間非常短(幾百milli-seconds),使用者較不容易察覺;而Full GC時間相對上長很多,且heap size愈大時間愈久;因此應儘量避免或減少Full GC發生。


物件什麼時候會符回GC條件

物件若被JVM認為沒有在使用,即符合GC條件。判斷方式則是:JVM會從根物件開始走訪所有reference,把走到的做註記,當全部走完後,沒被註記的物件代表沒有人能夠取得,即符合GC條件。
一般而言,假設我們要讓物件A符合回收條件,可以通過下列方式達成:
  1. 將所有參照到A的變數設為null (ex. sample = null;)
  2. 在method中建立的物件A,一旦離開了該method,則所有local variable參照都會失效
  3. 將parent object reference設為null。parent object會參照到物件A,當parent object的reference消失時,則物件A也符合GC條件
  4. 若物件A只有被WeakHashMap參照到,則符合條件

Heap Generations

Heap可分為3個generations,分別稱為: Young/New generation, Tenured/Old generation和Permanent(Perm) generation,其中Young generation可以進一步分為3個區塊: Eden space, Survivor 1和Survivor 2。當物件一開始被產生時,他被放置在Eden space,當Eden space滿了,無法存放新物件時,JVM會啟動Minor GC,把存活的物件往Survivor 1或2移及以原本在Survivor 1或2的存活物件往另一個Survivor空間移,當JVM執行多次Minor GC後,會把符合條件的存活物件往Tenured generation移,這個過程我們稱為Minor GC。Tenured generation滿了時,JVM會執行GC,回收Tenured generation的空間,我們稱之為Major/Full GC
http://javahash.com/java-memory-model-structures/

Permanent generation儲存class和method相關的metadata以及interned String[註2]。至於perm space會不會進行GC,則是依JVM而定,Sun/Oracle實作的JVM會進行GC,要確認你用的JVM是否會進行GC,可以寫支小程式建立數百萬個字串看看是否會出現GC log或 OutofMemoryError。

物件的旅程

在了解Heap Generation後,我們來看看一個物件生命週期中,他在這些generation間的旅程(並不一定會走完全程,若期間符合GC條件,則提前結束)。
  1. JVM配置Eden記憶體空間給新建立的物件
  2. 當Minor GC時(Eden space無法分配記憶體空間給新建立的物件),物件從Eden被移到Survival space
  3. 當Minor GC時,物件從Survival space被移到另一個Survival space(根據設定,這可能會發生很多次)
  4. 當Minor GC時,物件從Survival space移(promote)至Tenured/Old generation
  5. Major GC(old generation無法分配空間給被promote的物件)

[註1] System.gc()和Runtime.gc()其實做一樣的事,System.gc()轉發request給Runtime.gc(),差別是System.gc()是class level而Runtime.gc()是instance level,System.gc()較為方便而已
[註2]All literal strings and string-valued constant expressions are interned.透過String.intern(),可以讓相同值的字串只存一份在記憶體,在JDK6,intern的字串存放於Permanent generation,在JDK7之後移至Young/Old generation
你可能對下面主題有興趣:
  1. 關於GC的JVM參數

2.24.2013

[Java 概念]Interface and abstract class

在上一篇介紹了OO的基本概念:封裝,繼承和多型,這篇要來介紹一下Java的介面和抽象類別,他們的差別是Java面試時常見的問題,下面來介紹一下他們的差別,以及使用時機。

介面和抽象類別的差別


  1. 介面本身並不包含實作,他只定義行為,實作介面的類別代表他有此行為。相對地,抽象類別本身可以包含抽象和實體方法,可以提供共同/預設的行為。
  2. Java class可以實作多個介面,但只能繼承一個抽象類別,介面能提供更多的多型支援。
  3. 要實作一個介面,你必需實作其所有行為,一旦行為一多,對於實作類別來說相當痛苦,因此較好的設計是一個介面最多提供1~2個行為,不要設計一個介面包含過多行為;而抽象類別則是一開始就提供了預設的實作。
在前文提到,多型本身可利用介面或繼承達成late-binding,以前的我會搞不太清楚何時用介面,何時用抽象類別(繼承),在這裡分享一下我的心得:

介面和抽象類別的使用時機


  1. 因為Java不支援多重繼承但可以實作多個介面,若你想要更多的多型支援,那就必需使用介面。
  2. IS-A關係,同型態的物件本身的行為相同時,就適合用繼承。至於super class本身需不需要Abstract,則是看情形,我認為大部份都是Abstract為多數,通常Abstract class會定義演算法,而將不同的部份定義為abstract method,由各子類別進行實作,這也就是Template Method pattern。
  3. 介面用於定義支援的行為,譬如:Runnable支援run(),Callable支援call(),當只需定義行為,而每個型態的物件本身的行為不同時,就適合用介面。

你可能對下面主題有興趣:
  1. [OO概念]封裝,繼承,多型
  2. Java的十個物件導向設計原則