上一節(jié)課,我們講解了工廠模式、建造者模式、裝飾器模式、適配器模式在 Java JDK 中的應用,其中,Calendar 類用到了工廠模式和建造者模式,Collections 類用到了裝飾器模式、適配器模式。學習的重點是讓你了解,在真實的項目中模式的實現(xiàn)和應用更加靈活、多變,會根據(jù)具體的場景做實現(xiàn)或者設計上的調(diào)整。
今天,我們繼續(xù)延續(xù)這個話題,再重點講一下模板模式、觀察者模式這兩個模式在 JDK 中的應用。除此之外,我還會對在理論部分已經(jīng)講過的一些模式在 JDK 中的應用做一個匯總,帶你一塊回憶復習一下。
話不多說,讓我們正式開始今天的學習吧!
模板模式在 Collections 類中的應用
我們前面提到,策略、模板、職責鏈三個模式常用在框架的設計中,提供框架的擴展點,讓框架使用者,在不修改框架源碼的情況下,基于擴展點定制化框架的功能。Java 中的 Collections 類的 sort() 函數(shù)就是利用了模板模式的這個擴展特性。
首先,我們看下 Collections.sort() 函數(shù)是如何使用的。我寫了一個示例代碼,如下所示。這個代碼實現(xiàn)了按照不同的排序方式(按照年齡從小到大、按照名字字母序從小到大、按照成績從大到?。?students 數(shù)組進行排序。
public class Demo {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Alice", 19, 89.0f));
students.add(new Student("Peter", 20, 78.0f));
students.add(new Student("Leo", 18, 99.0f));
Collections.sort(students, new AgeAscComparator());
print(students);
Collections.sort(students, new NameAscComparator());
print(students);
Collections.sort(students, new ScoreDescComparator());
print(students);
}
public static void print(List<Student> students) {
for (Student s : students) {
System.out.println(s.getName() + " " + s.getAge() + " " + s.getScore());
}
}
public static class AgeAscComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();
}
}
public static class NameAscComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getName());
}
}
public static class ScoreDescComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
if (Math.abs(o1.getScore() - o2.getScore()) < 0.001) {
return 0;
} else if (o1.getScore() < o2.getScore()) {
return 1;
} else {
return -1;
}
}
}
}
結合剛剛這個例子,我們再來看下,為什么說 Collections.sort() 函數(shù)用到了模板模式?
Collections.sort() 實現(xiàn)了對集合的排序。為了擴展性,它將其中“比較大小”這部分邏輯,委派給用戶來實現(xiàn)。如果我們把比較大小這部分邏輯看作整個排序邏輯的其中一個步驟,那我們就可以把它看作模板模式。不過,從代碼實現(xiàn)的角度來看,它看起來有點類似之前講過的 JdbcTemplate,并不是模板模式的經(jīng)典代碼實現(xiàn),而是基于 Callback 回調(diào)機制來實現(xiàn)的。
不過,在其他資料中,我還看到有人說,Collections.sort() 使用的是策略模式。這樣的說法也不是沒有道理的。如果我們并不把“比較大小”看作排序邏輯中的一個步驟,而是看作一種算法或者策略,那我們就可以把它看作一種策略模式的應用。
不過,這也不是典型的策略模式,我們前面講到,在典型的策略模式中,策略模式分為策略的定義、創(chuàng)建、使用這三部分。策略通過工廠模式來創(chuàng)建,并且在程序運行期間,根據(jù)配置、用戶輸入、計算結果等這些不確定因素,動態(tài)決定使用哪種策略。而在 Collections.sort() 函數(shù)中,策略的創(chuàng)建并非通過工廠模式,策略的使用也非動態(tài)確定。
觀察者模式在 JDK 中的應用
在講到觀察者模式的時候,我們重點講解了 Google Guava 的 EventBus 框架,它提供了觀察者模式的骨架代碼。使用 EventBus,我們不需要從零開始開發(fā)觀察者模式。實際上,Java JDK 也提供了觀察者模式的簡單框架實現(xiàn)。在平時的開發(fā)中,如果我們不希望引入 Google Guava 開發(fā)庫,可以直接使用 Java 語言本身提供的這個框架類。
不過,它比 EventBus 要簡單多了,只包含兩個類:java.util.Observable 和 java.util.Observer。前者是被觀察者,后者是觀察者。它們的代碼實現(xiàn)也非常簡單,為了方便你查看,我直接 copy-paste 到了這里。
public interface Observer {
void update(Observable o, Object arg);
}
public class Observable {
private boolean changed = false;
private Vector<Observer> obs;
public Observable() {
obs = new Vector<>();
}
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}
public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}
public void notifyObservers() {
notifyObservers(null);
}
public void notifyObservers(Object arg) {
Object[] arrLocal;
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
public synchronized void deleteObservers() {
obs.removeAllElements();
}
protected synchronized void setChanged() {
changed = true;
}
protected synchronized void clearChanged() {
changed = false;
}
}
對于 Observable、Observer 的代碼實現(xiàn),大部分都很好理解,我們重點來看其中的兩個地方。一個是 changed 成員變量,另一個是 notifyObservers() 函數(shù)。
我們先來看 changed 成員變量。
它用來表明被觀察者(Observable)有沒有狀態(tài)更新。當有狀態(tài)更新時,我們需要手動調(diào)用 setChanged() 函數(shù),將 changed 變量設置為 true,這樣才能在調(diào)用 notifyObservers() 函數(shù)的時候,真正觸發(fā)觀察者(Observer)執(zhí)行 update() 函數(shù)。否則,即便你調(diào)用了 notifyObservers() 函數(shù),觀察者的 update() 函數(shù)也不會被執(zhí)行。
也就是說,當通知觀察者被觀察者狀態(tài)更新的時候,我們需要依次調(diào)用 setChanged() 和 notifyObservers() 兩個函數(shù),單獨調(diào)用 notifyObservers() 函數(shù)是不起作用的。你覺得這樣的設計是不是多此一舉呢?這個問題留給你思考,你可以在留言區(qū)說說你的看法。
我們再來看 notifyObservers() 函數(shù)。
為了保證在多線程環(huán)境下,添加、移除、通知觀察者三個操作之間不發(fā)生沖突,Observable 類中的大部分函數(shù)都通過 synchronized 加了鎖,不過,也有特例,notifyObservers() 這函數(shù)就沒有加 synchronized 鎖。這是為什么呢?在 JDK 的代碼實現(xiàn)中,notifyObservers() 函數(shù)是如何保證跟其他函數(shù)操作不沖突的呢?這種加鎖方法是否存在問題?又存在什么問題呢?
notifyObservers() 函數(shù)之所以沒有像其他函數(shù)那樣,一把大鎖加在整個函數(shù)上,主要還是出于性能的考慮。
notifyObservers() 函數(shù)依次執(zhí)行每個觀察者的 update() 函數(shù),每個 update() 函數(shù)執(zhí)行的邏輯提前未知,有可能會很耗時。如果在 notifyObservers() 函數(shù)上加 synchronized 鎖,notifyObservers() 函數(shù)持有鎖的時間就有可能會很長,這就會導致其他線程遲遲獲取不到鎖,影響整個 Observable 類的并發(fā)性能。
我們知道,Vector 類不是線程安全的,在多線程環(huán)境下,同時添加、刪除、遍歷 Vector 類對象中的元素,會出現(xiàn)不可預期的結果。所以,在 JDK 的代碼實現(xiàn)中,為了避免直接給 notifyObservers() 函數(shù)加鎖而出現(xiàn)性能問題,JDK 采用了一種折中的方案。這個方案有點類似于我們之前講過的讓迭代器支持”快照“的解決方案。
在 notifyObservers() 函數(shù)中,我們先拷貝一份觀察者列表,賦值給函數(shù)的局部變量,我們知道,局部變量是線程私有的,并不在線程間共享。這個拷貝出來的線程私有的觀察者列表就相當于一個快照。我們遍歷快照,逐一執(zhí)行每個觀察者的 update() 函數(shù)。而這個遍歷執(zhí)行的過程是在快照這個局部變量上操作的,不存在線程安全問題,不需要加鎖。所以,我們只需要對拷貝創(chuàng)建快照的過程加鎖,加鎖的范圍減少了很多,并發(fā)性能提高了。
為什么說這是一種折中的方案呢?這是因為,這種加鎖方法實際上是存在一些問題的。在創(chuàng)建好快照之后,添加、刪除觀察者都不會更新快照,新加入的觀察者就不會被通知到,新刪除的觀察者仍然會被通知到。這種權衡是否能接受完全看你的業(yè)務場景。實際上,這種處理方式也是多線程編程中減小鎖粒度、提高并發(fā)性能的常用方法。
單例模式在 Runtime 類中的應用
JDK 中 java.lang.Runtime 類就是一個單例類。這個類你有沒有比較眼熟呢?是的,我們之前講到 Callback 回調(diào)的時候,添加 shutdown hook 就是通過這個類來實現(xiàn)的。
每個 Java 應用在運行時會啟動一個 JVM 進程,每個 JVM 進程都只對應一個 Runtime 實例,用于查看 JVM 狀態(tài)以及控制 JVM 行為。進程內(nèi)唯一,所以比較適合設計為單例。在編程的時候,我們不能自己去實例化一個 Runtime 對象,只能通過 getRuntime() 靜態(tài)方法來獲得。
Runtime 類的的代碼實現(xiàn)如下所示。這里面只包含部分相關代碼,其他代碼做了省略。從代碼中,我們也可以看出,它使用了最簡單的餓漢式的單例實現(xiàn)方式。
/**
* Every Java application has a single instance of class
* <code>Runtime</code> that allows the application to interface with
* the environment in which the application is running. The current
* runtime can be obtained from the <code>getRuntime</code> method.
* <p>
* An application cannot create its own instance of this class.
*
* @author unascribed
* @see java.lang.Runtime#getRuntime()
* @since JDK1.0
*/
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
//....
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
//...
}
其他模式在 JDK 中的應用匯總
實際上,我們在講解理論部分的時候,已經(jīng)講過很多模式在 Java JDK 中的應用了。這里我們一塊再回顧一下,如果你對哪一部分有所遺忘,可以再回過頭去看下。
在講到模板模式的時候,我們結合 Java Servlet、JUnit TestCase、Java InputStream、Java AbstractList 四個例子,來具體講解了它的兩個作用:擴展性和復用性。
在講到享元模式的時候,我們講到 Integer 類中的 -128~127 之間的整型對象是可以復用的,還講到 String 類型中的常量字符串也是可以復用的。這些都是享元模式的經(jīng)典應用。
在講到職責鏈模式的時候,我們講到Java Servlet 中的 Filter 就是通過職責鏈來實現(xiàn)的,同時還對比了 Spring 中的 interceptor。實際上,攔截器、過濾器這些功能絕大部分都是采用職責鏈模式來實現(xiàn)的。
在講到的迭代器模式的時候,我們重點剖析了 Java 中 Iterator 迭代器的實現(xiàn),手把手帶你實現(xiàn)了一個針對線性數(shù)據(jù)結構的迭代器。
重點回顧
好了,今天的內(nèi)容到此就講完了。我們一塊來總結回顧一下,你需要重點掌握的內(nèi)容。
這兩節(jié)課主要剖析了 JDK 中用到的幾個經(jīng)典設計模式,其中重點剖析的有:工廠模式、建造者模式、裝飾器模式、適配器模式、模板模式、觀察者模式,除此之外,我們還匯總了其他模式在 JDK 中的應用,比如:單例模式、享元模式、職責鏈模式、迭代器模式。
實際上,源碼都很簡單,理解起來都不難,都沒有跳出我們之前講解的理論知識的范疇。學習的重點并不是表面上去理解、記憶某某類用了某某設計模式,而是讓你了解我反復強調(diào)的一點,也是標題中突出的一點,在真實的項目開發(fā)中,如何靈活應用設計模式,做到活學活用,能夠根據(jù)具體的場景、需求,做靈活的設計和實現(xiàn)上的調(diào)整。這也是模式新手和老手的最大區(qū)別。文章來源:http://www.zghlxwxcb.cn/news/detail-502737.html
課堂討論
針對 Java JDK 中觀察者模式的代碼實現(xiàn),我有兩個問題請你思考。文章來源地址http://www.zghlxwxcb.cn/news/detail-502737.html
- 每個函數(shù)都加一把 synchronized 大鎖,會不會影響并發(fā)性能?有沒有優(yōu)化的方法?
- changed 成員變量是否多此一舉?
到了這里,關于【開源與項目實戰(zhàn):開源實戰(zhàn)】77 | 開源實戰(zhàn)一(下):通過剖析Java JDK源碼學習靈活應用設計模式的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!