何爲消息?消息就是帶有某種信息的信號,如你用鼠標點擊一個窗口會產生鼠標的消息,鍵盤輸入字符會產生鍵盤的消息,一個窗口大小的改變也會產生消息。
消息從何而來?根據馮·諾依曼的體系結構計算機有運算器、存儲器、控制器和輸入設備和輸出設備五大部件組成,消息主要來自輸入設備,如鍵盤、鼠標、掃描儀等,也可來自已窗口和操做系統。
消息機制的三大要點:消息隊列、消息循環(分發)、消息處理。其結構以下: java
圖 1 :消息機制原理程序員
消息隊列就是存放消息的一種隊列,具備先進先出的特色。每產生一個消息都會添加進消息隊列中,在Window中消息隊列是在操做系統中定義的。消息隊列就如同一羣排隊打飯的少男少女,這羣人中光景較好的排在前面,光景較差的排在後面,能夠理解成是一種優先級隊列!要想更多的瞭解隊列的相關知識,可參見隊列。
消息循環就是經過循環(如while)不斷地從消息隊列中取得隊首的消息,並將消息分發出去。相似於上面的例子中分發飯菜值日生。
消息處理就是在接收到消息以後根據不一樣的消息類型作出不一樣的處理。上面例子中值日生根據學生不一樣類型的飯票給他們不一樣等級的飯菜就是消息處理,學生手中的飯票就是消息所攜帶的信息。
事件是根據接收到的消息的具體信息作出的特定的處理,放在代碼中是事件響應函數。上面的例子中學生拿到飯菜後吃飯就是具體的事件。編程
在這裏咱們以控制檯輸入信息模擬窗口、對話框接收鼠標、鍵盤等消息,以ArrayBlockingQueue對象存放消息隊列。在控制檯中輸入一個數值和一個字符串表明一個消息,輸入-1結束輸入。模擬代碼以下:windows
package message; import java.util.Queue; import java.util.Scanner; import java.util.concurrent.ArrayBlockingQueue; /** * 消息 * @author luoweifu */ class Message { //消息類型 public static final int KEY_MSG = 1; public static final int MOUSE_MSG = 2; public static final int SYS_MSG = 3; private Object source; //來源 private int type; //類型 private String info; //信息 public Message(Object source, int type, String info) { super(); this.source = source; this.type = type; this.info = info; } public Object getSource() { return source; } public void setSource(Object source) { this.source = source; } public int getType() { return type; } public void setType(int type) { this.type = type; } public String getInfo() { return info; } public void setInfo(String info) { this.info = info; } public static int getKeyMsg() { return KEY_MSG; } public static int getMouseMsg() { return MOUSE_MSG; } public static int getSysMsg() { return SYS_MSG; } } interface MessageProcess { public void doMessage(Message msg); } /** * 窗口模擬類 */ class WindowSimulator implements MessageProcess{ private ArrayBlockingQueue msgQueue; public WindowSimulator(ArrayBlockingQueue msgQueue) { this.msgQueue = msgQueue; } public void GenerateMsg() { while(true) { Scanner scanner = new Scanner(System.in); int msgType = scanner.nextInt(); if(msgType < 0) { //輸入負數結束循環 break; } String msgInfo = scanner.next(); Message msg = new Message(this, msgType, msgInfo); try { msgQueue.put(msg); //新消息加入到隊尾 } catch (InterruptedException e) { e.printStackTrace(); } } } @Override /** * 消息處理 */ public void doMessage(Message msg) { switch(msg.getType()) { case Message.KEY_MSG: onKeyDown(msg); break; case Message.MOUSE_MSG: onMouseDown(msg); break; default: onSysEvent(msg); } } //鍵盤事件 public static void onKeyDown(Message msg) { System.out.println("鍵盤事件:"); System.out.println("type:" + msg.getType()); System.out.println("info:" + msg.getInfo()); } //鼠標事件 public static void onMouseDown(Message msg) { System.out.println("鼠標事件:"); System.out.println("type:" + msg.getType()); System.out.println("info:" + msg.getInfo()); } //操做系統產生的消息 public static void onSysEvent(Message msg) { System.out.println("系統事件:"); System.out.println("type:" + msg.getType()); System.out.println("info:" + msg.getInfo()); } } /** * 消息模擬 * @author luoweifu */ public class MessageSimulator { //消息隊列 private static ArrayBlockingQueue<Message> messageQueue = new ArrayBlockingQueue<Message>(100); public static void main(String[] args) { WindowSimulator generator = new WindowSimulator(messageQueue); //產生消息 generator.GenerateMsg(); //消息循環 Message msg = null; while((msg = messageQueue.poll()) != null) { ((MessageProcess) msg.getSource()).doMessage(msg); } } }
這裏模擬用例中只有一個消息輸入源,且是一種線程阻塞的,只有輸入結束後纔會進行消息的處理。真實的Windows操做系統中的消息機制會有多個消息輸入源,且消息輸入的同時也能進行消息的處理。數組
在講以前,咱們先看一個簡單例子:建立一個窗口和兩個按鈕,用來控制窗口的背景顏色。其效果以下: 安全
圖 2 :效果圖服務器
Win32Test.h網絡
#pragma once #include <windows.h> #include <atltypes.h> #include <tchar.h> //資源ID #define ID_BUTTON_DRAW 1000 #define ID_BUTTON_SWEEP 1001 // 註冊窗口類 ATOM AppRegisterClass(HINSTANCE hInstance); // 初始化窗口 BOOL InitInstance(HINSTANCE, int); // 消息處理函數(又叫窗口過程) LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // (白色背景)按鈕事件 void OnButtonWhite(); // (灰色背景)按鈕事件 void OnButtonGray(); // 繪製事件 void OnDraw(HDC hdc);
Win32Test.cppide
#include "stdafx.h" #include "Win32Test.h" //字符數組長度 #define MAX_LOADSTRING 100 //全局變量 HINSTANCE hInst; // 當前實例 TCHAR g_szTitle[MAX_LOADSTRING] = TEXT("Message process"); // 窗口標題 TCHAR g_szWindowClass[MAX_LOADSTRING] = TEXT("AppTest"); // 窗口類的名稱 HWND g_hWnd; // 窗口句柄 bool g_bWhite = false; // 是否爲白色背景 //WinMain入口函數 int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); // 註冊窗口類 if(!AppRegisterClass(hInstance)) { return (FALSE); } // 初始化應用程序窗口 if (!InitInstance (hInstance, nCmdShow)) { return FALSE; } // 消息循環 MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int) msg.wParam; } // 註冊窗口類 ATOM AppRegisterClass(HINSTANCE hInstance) { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = NULL; wcex.lpszClassName = g_szWindowClass; wcex.hIconSm = NULL; return RegisterClassEx(&wcex); } // 保存實例化句柄並建立主窗口 BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { hInst = hInstance; // 保存handle到全局變量 g_hWnd = CreateWindow(g_szWindowClass, g_szTitle, WS_OVERLAPPEDWINDOW, 0, 0, 400, 300, NULL, NULL, hInstance, NULL); // 建立按鈕 HWND hBtWhite = CreateWindowEx(0, L"Button", L"白色", WS_CHILD | WS_VISIBLE | BS_TEXT, 100, 100, 50, 20, g_hWnd, (HMENU)ID_BUTTON_DRAW, hInst, NULL); HWND hBtGray = CreateWindowEx(0, L"Button", L"灰色", WS_CHILD | WS_VISIBLE | BS_CENTER, 250, 100, 50, 20, g_hWnd, (HMENU)ID_BUTTON_SWEEP, hInst, NULL); if (!g_hWnd) { return FALSE; } ShowWindow(g_hWnd, nCmdShow); UpdateWindow(g_hWnd); return TRUE; } // (窗口)消息處理 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int wmId, wmEvent; PAINTSTRUCT ps; HDC hdc; switch (message) { case WM_COMMAND: wmId = LOWORD(wParam); //wmEvent = HIWORD(wParam); switch (wmId) { case ID_BUTTON_DRAW: OnButtonWhite(); break; case ID_BUTTON_SWEEP: OnButtonGray(); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps); OnDraw(hdc); EndPaint(hWnd, &ps); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } //事件處理 //按下hBtWhite時的事件 void OnButtonWhite() { g_bWhite = true; InvalidateRect(g_hWnd, NULL, FALSE); //刷新窗口 } //按下hBtGray時的事件 void OnButtonGray() { g_bWhite = false; InvalidateRect(g_hWnd, NULL, FALSE); //刷新窗口 } //繪製事件(每次刷新時從新繪製圖像) void OnDraw(HDC hdc) { POINT oldPoint; SetViewportOrgEx(hdc, 0, 0, &oldPoint); RECT rcView; GetWindowRect(g_hWnd, &rcView); // 得到句柄的畫布大小 HBRUSH hbrWhite = (HBRUSH)GetStockObject(WHITE_BRUSH); HBRUSH hbrGray = (HBRUSH)GetStockObject(GRAY_BRUSH); if (g_bWhite) { FillRect(hdc, &rcView, hbrWhite); } else { FillRect(hdc, &rcView, hbrGray); } SetViewportOrgEx(hdc, oldPoint.x, oldPoint.y, NULL); }
在上面這個例子中,消息的流通過程以下: 函數
圖 3 :消息的流通過程
這與《編程思想之消息機制》中圖1(消息機制原理)是相吻合的,這就是Windows消息機制的核心部分,也是Windows API開發的核心部分。Windows系統和Windows下的程序都是以消息爲基礎,以事件爲驅動。
RegisterClassEx的做用是註冊一個窗口,在調用CreateWindow建立一個窗口前必須向windows系統註冊獲唯一的標識。
while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
這個while循環就是消息循環,不斷地從消息隊列中獲取消息,並經過DispatchMessage(&msg)將消息分發出去。消息隊列是在Windows操做系統中定義的(咱們沒法看到對應定義的代碼),對於每個正在執行的Windows應用程序,系統爲其創建一個「消息隊列」,即應用程序隊列,用來存放該程序可能建立的各類窗口的消息。DispatchMessage會將消息傳給窗口函數(即消息處理函數)去處理,也就是WndProc函數。WndProc是一個回調函數,在註冊窗口時經過wcex.lpfnWndProc將其傳給了操做系統,因此DispatchMessage分發消息後,操做系統會調用窗口函數(WndProc)去處理消息。關於回調函數可參考:回調函數。
每個窗口都應該有一個函數負責消息處理,程序員必須負責設計這個所謂的窗口函數WndProc。LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
中的四個參數就是消息的相關信息(消息來自的句柄、消息類型等),函數中經過switch/case根據不一樣的消息類型分別進行不一樣的處理。在收到相應類型的消息以後,可調用相應的函數去處理,如OnButtonWhite、OnButtonGray、OnDraw,這就是事件處理的雛形。 在default中調用了DefWindowProc,DefWindowProc是操做系統定義的默認消息處理函數,這是由於全部的消息都必須被處理,應用程序不處理的消息須要交給操做系統處理。
Windows消息都以WM_爲前綴,意思是」Windows Message」,如WM_CREATE、WM_PAINT等。消息的定義以下:
typedef struct tagMsg { HWND hwnd; //接受該消息的窗口句柄 UINT message; //消息常量標識符,也就是咱們一般所說的消息號 WPARAM wParam; //32位消息的特定附加信息,確切含義依賴於消息值 LPARAM lParam; //32位消息的特定附加信息,確切含義依賴於消息值 DWORD time; //消息建立時的時間 POINT pt; //消息建立時的鼠標/光標在屏幕座標系中的位置 }MSG;
消息主要有三種類型:
1. 命令消息(WM_COMMAND):命令消息是程序員須要程序作某些操做的命令。凡UI對象產生的消息都是這種命令消息,可能來自菜單、加速鍵或工具欄按鈕等,都以WM_COMMAND呈現。
2. 標準窗口消息:除WM_COMMAND之處,任何以WM_開頭的消息都是這一類。標準窗口消息是系統中最爲常見的消息,它是指由操做系統和控制其餘窗口的窗口所使用的消息。例如CreateWindow、DestroyWindow和MoveWindow等都會激發窗口消息,以及鼠標移動、點擊,鍵盤輸入都是屬於這種消息。
3. Notification:這種消息由控件產生,爲的是向其父窗口(一般是對話框窗口)通知某種狀況。當一個窗口內的子控件發生了一些事情,而這些是須要通知父窗口的,此刻它就上場啦。通知消息只適用於標準的窗口控件如按鈕、列表框、組合框、編輯框,以及Windows公共控件如樹狀視圖、列表視圖等。
Windows中有一個系統消息隊列,對於每個正在執行的Windows應用程序,系統爲其創建一個「消息隊列」,即應用程序隊列,用來存放該程序可能建立的各類窗口的消息。
(1)隊列消息(Queued Messages)
消息會先保存在消息隊列中,經過消息循環從消息隊列中獲取消息並分發到各窗口函數去處理,如鼠標、鍵盤消息就屬於這類消息。
(2)非隊列消息(NonQueued Messages)
就是消息會直接發送到窗口函數處理,而不通過消息隊列。 如: WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR, WM_WINDOWPOSCHANGED就屬於此類。
PostMessage發送的消息是隊列消息,它會把消息Post到消息隊列中; SendMessage發送的消息是非隊列消息, 被直接送到窗口過程處理,等消息被處理後才返回。
圖 4 :消息隊列示意圖
爲證實這一過程,咱們能夠改動一下上面的這個例子。
1.在Win32Test.h中添加ID_BUTTON_TEST的定義
#define ID_BUTTON_TEST 1002
2.在OnButtonWhite中分別用SendMessage和PostMessage發送消息
//按下hBtWhite時的事件
void OnButtonWhite() { g_bWhite = true; InvalidateRect(g_hWnd, NULL, FALSE); //刷新窗口 SendMessage(g_hWnd, WM_COMMAND, ID_BUTTON_TEST, 0); //PostMessage(g_hWnd, WM_COMMAND, ID_BUTTON_TEST, 0); }
3.在消息循環中增長ID_BUTTON_TEST的判斷
while (GetMessage(&msg, NULL, 0, 0)) { if (LOWORD(msg.wParam) == ID_BUTTON_TEST) { OutputDebugString(L"This is a ID_BUTTON_TEST message."); // [BreakPoint1] } TranslateMessage(&msg); DispatchMessage(&msg); }
4.在窗口處理函數WndProc增長ID_BUTTON_TEST的判斷
case ID_BUTTON_TEST: { OutputDebugString(L"This is a ID_BUTTON_TEST message."); // [BreakPoint2] } break; case ID_BUTTON_DRAW: OnButtonWhite(); break; case ID_BUTTON_SWEEP: OnButtonGray(); break;
用斷點調試的方式咱們發現,用SendMessage發送的ID_BUTTON_TEST消息只會進入BreakPoint2,而PostMessage發送的ID_BUTTON_TEST會進入到BreakPoint1和BreakPoint2。
一樣,咱們仍是先看一個簡單例子:建立一個窗口實現加法的計算功能。其效果以下:
圖1: 加法計算
Calculator.Java:
import javax.swing.*; import javax.swing.border.BevelBorder; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * Created with IntelliJ IDEA. * User: luoweifu * Date: 15-5-5 * Time: 下午9:14 * To change this template use File | Settings | File Templates. */ public class Calculator { /** * 主窗口的寬度 */ public static final int WIDTH = 500; /** * 主窗口的高度 */ public static final int HEIGHT = 100; private JFrame frameCalculator; private JEditorPane editAddend1; private JEditorPane editAddend2; private JEditorPane editResult; private JLabel labelPlus; private JButton btEqual; public Calculator() { frameCalculator = new JFrame(); } public void launchFrame() { frameCalculator.setSize(WIDTH, HEIGHT); frameCalculator.setLocationRelativeTo(null); frameCalculator.setTitle("加法計算"); Container container = frameCalculator.getContentPane(); container.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 10)); editAddend1 = new JEditorPane(); editAddend1.setBorder(new BevelBorder(BevelBorder.LOWERED)); editAddend2 = new JEditorPane(); editAddend2.setBorder(new BevelBorder(BevelBorder.LOWERED)); labelPlus = new JLabel("+"); btEqual = new JButton("="); editResult = new JEditorPane(); editResult.setBorder(new BevelBorder(BevelBorder.LOWERED)); editResult.setEditable(false); container.add(editAddend1); container.add(labelPlus); container.add(editAddend2); container.add(btEqual); container.add(editResult); frameCalculator.setVisible(true); //frameCalculator.setDefaultCloseOperation(EXIT_ON_CLOSE); class AdditionCalculate implements ActionListener { @Override public void actionPerformed(ActionEvent e) { int add1 = Integer.parseInt(editAddend1.getText()); int add2 = Integer.parseInt(editAddend2.getText()); int result = add1 + add2; editResult.setText(result + ""); } } AdditionCalculate additionCalculate = new AdditionCalculate(); btEqual.addActionListener(additionCalculate); } public static void main(String args[]) { Calculator calculator = new Calculator(); calculator.launchFrame(); } }
上面這個例子中,窗口和全部的控件建立完成以後,btEqual按鈕邦定了一個監聽對象additionCalculate,一旦這個按鈕被點擊,就會通知additionCalculate對象,additionCalculate對象監聽到點擊事件,就會調用actionPerformed方法做出相應的響應。additionCalculate是內部類AdditionCalculate的對象,AdditionCalculate實現了ActionListener 接口。
經過上面的例子,你也許看出來了Java Swing/AWT包中窗口、控件的響應方式是一種源-監聽器(Source/Listener)模式,也叫作觀察者模式,這種機制常稱爲事件機制。
Windows API能夠開發窗口(界面)程序,Java經過Swing/AWT包也能夠開發窗口(界面)程序,那麼他們之間有什麼異同呢?
1. 實現方式不一樣,Windows API主要是經過回調,提供對外的接口由用戶去實現對應的處理,內部由操做系統實現,咱們看不到;Java中的Swing/AWT主要源-監聽器(觀察者)模式,實現窗口(控件)對象與事件處理對象的邦定。
2. Windows API的消息機制有一個消息循環一直在接收消息,它是線程阻塞的。而Java的的Swing/AWT是一個通知方式,只有窗口(控件)有變化(被鼠標、鍵盤等觸發)時纔會通知監聽者去處理,是非阻塞的。
3. 相同點:都有消息源——窗口(控件),都有消息處理,Windows API是窗口處理函數,Java中是監聽者的處理方法,都有消息(Java叫事件Event)。若是把Windows API中消息隊列和消息循環去掉,二者就很像了,就如同使用SendMessage直接把消息發送到窗口處理函數。因此,事件機制也能夠認爲是特殊的消息機制。
既然Java中的窗口程序是經過源-監聽器(觀察者)模式實現的,咱們就有必要討論一下觀察者模式了。
觀察者模式,顧名思意就是觀察與被觀察的關係,好比你在燒開水得時時看着它開沒開,你就是觀察者,開水就是被觀察者;再好比說你在帶小孩,你關注她是否是餓了,是否是喝了,是否是撒尿了,你就是觀察者,小孩就被觀察者。觀察者模式是對象的行爲模式,又叫發佈-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。當你看這些模式的時候,不要以爲陌生,它們就是觀察者模式。
觀察者模式通常是一種一對多的關係,能夠有任意個(一個或多個)觀察者對象同時監聽某一個對象。監聽的對象叫觀察者(後面提到監聽者,其實就指觀察者,二者是等價的),被監聽的對象叫被觀察者(Observable,也叫主題Subject)。被觀察者對象在狀態上發生變化時,會通知全部觀察者對象,使它們可以作出相應的變化(如自動更新本身的信息)。
咱們就以上面提到的燒開水的一個簡單生活實例來模擬一下觀察者模式。
代碼ObserverModule.java:
//人,觀察者 class Person { public void update(String data) { System.out.println(data + "關電源..."); } } //水,被觀察者 class Water { private Person person; private boolean isBoiled; public Water() { isBoiled = false; } public void SetBoiled() { isBoiled = true; notifyObserve(); } public void addObserver(Person person) { this.person = person; } public void removeObserver() { if (person != null) { person = null; } } public void notifyObserve() { if (isBoiled && person != null) { person.update("水開了,"); isBoiled = false; } } } //客戶端 public class ObserverModule { public static void main(String args[]) { Person person = new Person(); Water water = new Water(); water.addObserver(person); water.SetBoiled(); } }
結果以下:
水開了,關電源…
這個代碼很是簡單,水開了就會通知人,人就去關電源。但也有一個問題,就是拓展性很差,不靈活。若是咱們燒的開水不是用來喝,而用來洗澡,我就要監測它的溫度,可能50度就關電源,也可能要60度才行,這樣一個監聽就不夠了,還監聽溫度的隨時變化;再好比水開了以後,我不是關電源,而是讓它保溫。你的updae又得改了……
因此上面這個代碼拓展行是很差,但已經實現了咱們的基本想法,咱們算是咱們的第一個版本(版本)。接下來咱們再看一下,升級版:
版本2:ObserverModule.java
//觀察者 interface Observer { public void update(Observable observable); } //被觀察者 abstract class Observable { protected boolean isChanaged; protected List<Observer> observers = new ArrayList<Observer>(); public Observable() { isChanaged = false; } public void addObserver(Observer observer) { observers.add(observer); } public void removeObserver(Observer observer) { observers.remove(observer); } public void removeObservers() { observers.clear(); } public void notifyObservers() { if (isChanaged) { for (int i = 0; i < observers.size(); i ++) { observers.get(i).update(this); } isChanaged = false; } } } //人,溫度監測 class TemperatureObserver implements Observer{ @Override public void update(Observable observable) { Water water = (Water)observable; System.out.println("溫度:" + water.getTemperature() + " 狀態:" + water.getStatus()); System.out.println("TemperatureObserver observing..."); } } class BoildObserver implements Observer { String doSomthing; BoildObserver(String doSomthing) { this.doSomthing = doSomthing; } @Override public void update(Observable observable) { Water water = (Water)observable; if (water.getTemperature() >= 100) { System.out.println("狀態:" + water.getStatus()); System.out.println("BoildObserver:" + doSomthing); } } } //水,被觀察者 class Water extends Observable{ private double temperature; private String status; public Water() { super(); this.temperature = 0; this.status = "冷水"; } public Water(Observer observer) { this(); observers.add(observer); } public double getTemperature() { return temperature; } public String getStatus() { return status; } public void change(double temperature) { this.temperature = temperature; if (temperature < 40) { status = "冷水"; } else if (temperature >= 40 && temperature < 60) { status = "溫水"; }else if (temperature >= 60 && temperature < 100 ) { status = "熱水"; } else { status = "開水"; } this.isChanaged = true; notifyObservers(); } } //客戶端 public class ObserverModule { public static void main(String args[]) { TemperatureObserver temperatureObserver = new TemperatureObserver(); BoildObserver boildObserver1 = new BoildObserver("關閉電源..."); BoildObserver boildObserver2 = new BoildObserver("繼續保溼..."); Water water = new Water(temperatureObserver); water.addObserver(boildObserver1); water.addObserver(boildObserver2); water.change(45); water.change(80); water.change(100); } }
結果以下:
溫度:45.0 狀態:溫水
TemperatureObserver observing…
溫度:80.0 狀態:熱水
TemperatureObserver observing…
溫度:100.0 狀態:開水
TemperatureObserver observing…
狀態:開水
BoildObserver:關閉電源…
狀態:開水
BoildObserver:繼續保溼…
經過上面這個活生生的例子,咱們總結一下觀察者模式的設計。
觀察者模式的類結構關係以下:
觀察者模式的類圖結構
在設計觀察者模式的程序時要注意如下幾點:
1. 要明確誰是觀察者誰是被觀察者,只要明白誰是關注對象,問題也就明白了。通常觀察者與被觀察者之間的是多對一的關係,一個被觀察對象能夠有多個監聽對象(觀察者)。如一個編輯框,有鼠標點擊的監聽者,也有鍵盤的監聽者,還有內容改變的監聽者。
2. Observable在發送廣播通知的時候,無須指定具體的Observer,Observer能夠本身決定是否要訂閱Subject的通知。
3. 被觀察者至少須要有三個方法:添加監聽者、移除監聽者、通知Observer的方法;觀察者至少要有一個方法:更新方法,更新當前的內容,做出相應的處理。
注:添加監聽者、移除監聽者在不一樣的模型中可能會有不一樣命名,如觀察者模型中通常,addObserver、removeObserver;在源-監聽器(Source/Listener)模型中通常是attach/detach,應用在桌面編程的窗口中,還多是attachWindow/detachWindow,或Register/UnRegister。不要被名稱迷糊了,無論他們是什麼名稱,其實功能都是同樣的,就是添加/刪除觀察者。
4. 觀察者模式的應用場景: <1>.對一個對象狀態的更新須要其餘對象同步更新;,或者一個對象的更新須要依賴另外一個對象的更新;<2>.對象僅須要將本身的更新通知給其餘對象而不須要知道其餘對象的細節,如消息推送。
觀察者模式根據其側重的功能還能夠分爲推模型和拉模式
推模型:被觀察者對象向觀察者推送主題的詳細信息,無論觀察者是否須要,推送的信息一般是主題對象的所有或部分數據。通常這種模型的實現中,會把被觀察者對象中的所有或部分信息經過update的參數傳遞給觀察者[update(Object obj) ]。
拉模型:被觀察者在通知觀察者的時候,只傳遞少許信息。若是觀察者須要更具體的信息,由觀察者主動到被觀察者對象中獲取,至關因而觀察者從被觀察者對象中拉數據。通常這種模型的實現中,會把被觀察者對象自身經過update方法傳遞給觀察者[update(Observable observable ) ],這樣在觀察者須要獲取數據的時候,就能夠經過這個引用來獲取了。
其實JDK已經提供了對觀察者模式接口的定義了。在java.util庫裏面,提供了一個Observable類以及一個Observer接口,構成JAVA語言對觀察者模式的支持。咱們能夠看一下Java中的源碼:
Observable接口:
package java.util; public class Observable { private boolean changed = false; private Vector 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; } public synchronized boolean hasChanged() { return changed; } public synchronized int countObservers() { return obs.size(); } }
Observer接口:
package java.util; public interface Observer { void update(Observable o, Object arg); }
經過前面的分析,再來看Java的源碼,相信不會太難了。這裏有個比較好的地方是Observable類中的addObserver、deleteObserver、notifyObservers等方法已經幫咱們考慮了線程同步的問題,這樣更安全。
咱們再回顧一下加法計算器的例子。經過觀察者模式的分析,也許你已經清楚了,AdditionCalculate的對象additionCalculate就是觀察者;JButton的對象btEqual就是被觀察者,同時也是消息源;btEqual.addActionListener(additionCalculate);就是添加監聽者。ActionListener中的public void actionPerformed(ActionEvent e)就至關於update方法,只不過參數e消息源產生的消息(事件)。
觀察者模式還能夠用於網絡中的客戶端和服務器,好比手機中的各類App的消息推送,服務端是觀察者,各個手機App是被觀察者,一旦服務器上的數據(如App升級信息)有更新,就會被推送到手機客戶端。