行為型設(shè)計模式范圍
- 觀察者模式
- 模板方法
- 策略模式
- 職責(zé)鏈模式
- 狀態(tài)模式
- 迭代器模式
- 訪問者模式
- 備忘錄模式
- 命令模式
- 解釋器模式
- 中介模式
行為型設(shè)計模式作用
行為型設(shè)計模式主要關(guān)注的是類與類之間的交互問題典尾。
1. 觀察者模式
1.1 定義
在對象之間定義一個一對多的依賴考榨,當一個對象狀態(tài)改變的時候蚊逢,所有依賴的對象都會自動收到通知悯仙。
1.2 作用
- 將原本一個復(fù)雜的更新數(shù)據(jù)的方法熙卡,拆分成基于接口的多個粒度更小,職責(zé)更單一的小方法芥挣,降低代碼實現(xiàn)的復(fù)雜性。
- 由于是觀察者是基于接口實現(xiàn)的核偿,如果有新的邏輯加入進來時,只需要實現(xiàn)相應(yīng)的接口顽染,并完成注冊就可以接收到更新數(shù)據(jù)的通知漾岳,提供了代碼的擴展性。
1.3 類結(jié)構(gòu)圖
1.4 經(jīng)典實現(xiàn)
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(Message message);
}
public class ConcreteSubject implements Subject {
private List<Observer> mObservers = new ArrayList<>();
@Override
public void registerObserver(Observer observer) {
mObservers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
mObservers.remove(observer);
}
@Override
public void notifyObservers(Message message) {
mObservers.forEach(observer -> {
observer.update(message);
});
}
}
public interface Observer {
void update(Message message);
}
public class ConcreteObserver1 implements Observer {
@Override
public void update(Message message) {
System.out.println("Observer1: " + message.content);
}
}
public class ConcreteObserver2 implements Observer {
@Override
public void update(Message message) {
System.out.println("Observer2: " + message.content);
}
}
public class Client {
public static void main(String[] args) {
Observer observer1 = new ConcreteObserver1();
Observer observer2 = new ConcreteObserver2();
Subject subject = new ConcreteSubject();
subject.registerObserver(observer1);
subject.registerObserver(observer2);
Message message = new Message();
message.content = "notified information";
subject.notifyObservers(message);
}
}
1.5 設(shè)計模式的本質(zhì)
設(shè)計模式要干的事情就是解耦粉寞。創(chuàng)建型模式是將對象的創(chuàng)建與使用解耦尼荆,結(jié)構(gòu)型模式是將不同功能代碼解耦,行為型模式是將不同的行為代碼解耦唧垦。
這里的觀察者模式是將觀察者與被觀察者解耦捅儒。
1.6 觀察都模式的種類
1. 同步阻塞
觀察者和被觀察者的代碼都是在同一個線程中執(zhí)行的。被觀察都直到所有觀察都的代碼都執(zhí)行完成后,才會繼續(xù)往后執(zhí)行代碼巧还。
2. 異步非阻塞
最簡單的實現(xiàn)是在每個 update 方法中開啟一個線程來執(zhí)行更新鞭莽。另外一種方式是直接在被觀察者中使用一個線程池來執(zhí)行對各觀察者的調(diào)用。當然麸祷,可以使用事件總線 EventBus 來實現(xiàn)異步非阻塞的澎怒。
3. 進程內(nèi)實現(xiàn)
主要分為同步阻塞和異步非阻塞兩種情況。
4. 跨進程實現(xiàn)
在不同的系統(tǒng)中需要實現(xiàn)觀察者模式的時候阶牍,有兩種方法:
- 直接調(diào)用 RPC 接口通知觀察者
- 使用消息隊列(MessageQueue)來通知觀察者
消息隊列的具體做法是:被觀察者向消息隊列中發(fā)送更新消息喷面,觀察者從消息隊列中取出消息來執(zhí)行相應(yīng)的業(yè)務(wù)邏輯。在這個過程中走孽,被觀察者感知不到觀察者的存在乖酬,觀察者也感知不到被觀察者是存在,是一種更加徹底的解耦實現(xiàn)融求。
1.7 應(yīng)用場景
EventBus
框架的作用
- 隱藏實現(xiàn)細節(jié)
- 降低開發(fā)難度
- 代碼復(fù)用
- 解耦業(yè)務(wù)與非業(yè)務(wù)代碼
- 讓程序員聚焦功能的開發(fā)
應(yīng)用示例
public class UserController {
private UserService userService; // 依賴注入
private EventBus eventBus;
private static final int DEFAULT_EVENTBUS_THREAD_POOL_SIZE = 20;
public UserController() {
//eventBus = new EventBus(); // 同步阻塞模式
eventBus = new AsyncEventBus(Executors.newFixedThreadPool(DEFAULT_EVENTBUS_THREAD_POOL_SIZE)); // 異步非阻塞模式
}
public void setRegObservers(List<Object> observers) {
for (Object observer : observers) {
eventBus.register(observer);
}
}
public Long register(String telephone, String password) {
//省略輸入?yún)?shù)的校驗代碼
//省略userService.register()異常的try-catch代碼
long userId = userService.register(telephone, password);
eventBus.post(userId);
return userId;
}
}
public class RegPromotionObserver {
private PromotionService promotionService; // 依賴注入
@Subscribe
public void handleRegSuccess(Long userId) {
promotionService.issueNewUserExperienceCash(userId);
}
}
public class RegNotificationObserver {
private NotificationService notificationService;
@Subscribe
public void handleRegSuccess(Long userId) {
notificationService.sendInboxMessage(userId, "...");
}
}
使用方法:
- 通過調(diào)用 EventBus 的 register 方法進行注冊工作
- 通過 @Subscribe 來注冊哪些方法用來接收更新
- 通過 @Subscribe 所注冊方法中的參數(shù)來判斷 EventBus 發(fā)送的數(shù)據(jù)哪些注冊了 EventBus 的哪些方法可以接收到此更新
與傳統(tǒng)觀察者不同的地方
- 傳統(tǒng)的實現(xiàn)中被觀察者只接收實現(xiàn)了同一接口的觀察者,而 EventBus 可以接收任何對象
- 傳統(tǒng)的實現(xiàn)中訂閱了被觀察者的觀察者都能夠接收到消息更新算撮,而 EventBus 是根據(jù) post 中的參數(shù)來匹配當前消息需要發(fā)送給哪些觀察者的生宛,是部分發(fā)送
EventBus 的注冊過程
當調(diào)用 EventBus 的 register 方法將類對象作為 Observer 注冊到 EventBus 時,EventBus 會掃描當前類對象中的所有使用了 @Subscribe 注解的方法肮柜,并獲取方法中的參數(shù)類型陷舅,然后通過將參數(shù)類型與當前類對象和函數(shù)進行關(guān)聯(lián)。當通過 EventBus 調(diào)用 post 方法發(fā)送消息時审洞,就會通過當前發(fā)送消息的類型去之前的映射關(guān)系中找到對應(yīng)的對象和函數(shù)莱睁,完成調(diào)用。
EventBus 核心實現(xiàn)原理圖
統(tǒng)一同步阻塞和異步非阻塞的邏輯
異步非阻塞是通過線程池來完成的芒澜。為了統(tǒng)一邏輯處理仰剿,同步阻塞也可以依賴線程池,只不過這個線程池里只有一個線程痴晦。
2. 模板方法模式
2.1 定義
在一個方法中定義一個算法骨架(業(yè)務(wù)邏輯)南吮,并將某些步驟延遲到子類中實現(xiàn)。模板方法模式可以讓子類在不改變算法整體結(jié)構(gòu)的情況下誊酌,重新定義算法中的某些步驟部凑。
其中,模板指的是算法的骨架碧浊,而模板方法指的是包含算法骨架的方法涂邀。
2.2 作用
- 通過繼承的實現(xiàn)方式,復(fù)用邏輯箱锐,提高復(fù)用性
- 通過提供抽象方法由子類來實現(xiàn)比勉,提高擴展性
2.3. 類結(jié)構(gòu)圖
2.4. 經(jīng)典實現(xiàn)
public abstract class AbstractClass {
public final void templateMethod() {
//...
method1();
//...
method2();
//...
}
protected abstract void method1();
protected abstract void method2();
}
public class ConcreteClass1 extends AbstractClass {
@Override
protected void method1() {
//...
}
2.5 應(yīng)用場景
Java InputStream
public abstract class InputStream implements Closeable {
.... ignore codes
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
public abstract int read() throws IOException;
}
public class ByteArrayInputStream extends InputStream {
public synchronized int read() {
return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
}
read(byte b[], int off, int len)
是模板方法,而 read()
是需要子類擴展的方法。
Java AbstractList
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
boolean modified = false;
for (E e : c) {
add(index++, e);
modified = true;
}
return modified;
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
addAll 為模板方法敷搪,而 add(int index, E element)
是需要子類實現(xiàn)的方法兴想。
Java Servlet
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException
{
HttpServletRequest request;
HttpServletResponse response;
if (!(req instanceof HttpServletRequest &&
res instanceof HttpServletResponse)) {
throw new ServletException("non-HTTP request or response");
}
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
service(request, response);
}
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
HttpSevlet 中的 service()
是模板方法,而 doGet()
和 doPost()
是需要子類進行實現(xiàn)的方法赡勘。
Junit TestCase
public abstract class TestCase extends Assert implements Test {
public void runBare() throws Throwable {
Throwable exception = null;
setUp();
try {
runTest();
} catch (Throwable running) {
exception = running;
} finally {
try {
tearDown();
} catch (Throwable tearingDown) {
if (exception == null) exception = tearingDown;
}
}
if (exception != null) throw exception;
}
/**
* Sets up the fixture, for example, open a network connection.
* This method is called before a test is executed.
*/
protected void setUp() throws Exception {
}
/**
* Tears down the fixture, for example, close a network connection.
* This method is called after a test is executed.
*/
protected void tearDown() throws Exception {
}
}
runBare 為模塊方法嫂便,而 setUp()
和 tearDown()
則是需要子類選擇性進行擴展的方法。
2.6 模板方法和回調(diào)的關(guān)系
Callback 回調(diào)機制
比如:A 類事先注冊了 F 函數(shù)到了 B 類闸与,然后毙替,A 類調(diào)用了 B 類的 P 函數(shù),B 類返過來調(diào)用 A 類的 F 函數(shù)践樱。這里的 F 函數(shù)就是“回調(diào)函數(shù)”厂画。A 調(diào)用 B,B 反過來又調(diào)用 A拷邢,這種機制稱為“回調(diào)”袱院。
A 類如何將回調(diào)函數(shù)傳遞給 B
在 Java 中使用包裹了回調(diào)函數(shù)的類對象(通常是接口的實現(xiàn)對象)。
public interface ICallback {
void callback();
}
public class ClassA {
public void test(ICallback callback) {
callback.callback();
}
}
public class Client {
public static void main(String[] args) {
ClassA classA = new ClassA();
classA.test(new ICallback() {
@Override
public void callback() {
System.out.println("callback method is called");
}
});
}
}
回調(diào)分類
回調(diào)分為同步回調(diào)和異步回調(diào)瞭稼。同步回調(diào)是指函數(shù)返回之前執(zhí)行回調(diào)函數(shù)忽洛。異步回調(diào)是指在函數(shù)返回之后再執(zhí)行回調(diào)函數(shù)。
異步回調(diào)的應(yīng)用有:業(yè)務(wù)系統(tǒng)與第三方支付系統(tǒng)之間的支付結(jié)果的回調(diào)环肘。
同步回調(diào)看起來更像模板模式欲虚。
異步回調(diào)看起來更像觀察者模式。
應(yīng)用一:JdbcTemplate
public class JdbcTemplateDemo {
private JdbcTemplate jdbcTemplate;
public User queryUser(long id) {
String sql = "select * from user where id="+id;
return jdbcTemplate.query(sql, new UserRowMapper()).get(0);
}
class UserRowMapper implements RowMapper<User> {
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setTelephone(rs.getString("telephone"));
return user;
}
}
}
UserRowMapper 所實現(xiàn)的mapRow(ResultSet rs, int rowNum)
就是一個回調(diào)函數(shù)悔雹,負責(zé)將通過 JDBC 查詢到的數(shù)據(jù)轉(zhuǎn)換為對象复哆,并返回。
下面是 JDBCTemplate 的部分實現(xiàn)源碼:
@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return query(sql, new RowMapperResultSetExtractor<T>(rowMapper));
}
@Override
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL query [" + sql + "]");
}
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
@Override
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
try {
rs = stmt.executeQuery(sql);
ResultSet rsToUse = rs;
if (nativeJdbcExtractor != null) {
rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
}
return rse.extractData(rsToUse);
}
finally {
JdbcUtils.closeResultSet(rs);
}
}
@Override
public String getSql() {
return sql;
}
}
return execute(new QueryStatementCallback());
}
@Override
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(getDataSource());
Statement stmt = null;
try {
Connection conToUse = con;
if (this.nativeJdbcExtractor != null &&
this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
stmt = conToUse.createStatement();
applyStatementSettings(stmt);
Statement stmtToUse = stmt;
if (this.nativeJdbcExtractor != null) {
stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
}
T result = action.doInStatement(stmtToUse);
handleWarnings(stmt);
return result;
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
}
finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
JDBCTemplate 通過 execute 函數(shù)封裝了通過 JDBC 獲取數(shù)據(jù)過程中不變的執(zhí)行邏輯腌零,主要是獲取到操作數(shù)據(jù)庫的 Statement 對象梯找,并通過StatementCallback<T>
接口將 Statement 對象傳遞給用戶自己去做業(yè)務(wù)邏輯。上面代碼中 QueryStatementCallback 回調(diào)實現(xiàn)中莱没,進行了數(shù)據(jù)查詢的操作初肉。并通過實現(xiàn)ResultSetExtractor<T>
接口,完成數(shù)據(jù)到對象的轉(zhuǎn)換工作饰躲。
簡單來說牙咏,就是通過一個回調(diào)嵌套來完成了數(shù)據(jù)的查詢操作,同時嘹裂,完成了數(shù)據(jù)到對象的轉(zhuǎn)換操作妄壶。StatementCallback<T>
接口實現(xiàn)中持有了ResultSetExtractor<T>
接口的實現(xiàn)。 然后將StatementCallback<T>
中接口的實現(xiàn)傳進 excute 方法中寄狼,通過在 excute 方法中回調(diào)StatementCallback<T>
接口丁寄,接著在StatementCallback<T>
實現(xiàn)中回調(diào)ResultSetExtractor<T>
接口氨淌,來完成整個查詢數(shù)據(jù)到數(shù)據(jù)轉(zhuǎn)換到對象的操作。
應(yīng)用二:Android SetOnclickListener
Button button = (Button)findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("I am clicked.");
}
});
這里的 Android 點擊事件伊磺,從代碼實現(xiàn)來看盛正,點擊事件監(jiān)聽器就是一個回調(diào)接口;而從業(yè)務(wù)實現(xiàn)來看屑埋,當用戶點擊了按鈕后豪筝,監(jiān)聽器就會接收到響應(yīng),就是一個注冊了點擊事件的觀察者摘能。
這里的 setOnclickListener 是一個異步回調(diào)续崖。當注冊好回調(diào)函數(shù)后,并不需要等待回調(diào)函數(shù)執(zhí)行团搞,而是直接執(zhí)行后面的業(yè)務(wù)注冊后面的業(yè)務(wù)邏輯严望。這也再一次驗證了異步回調(diào)比較像觀察者模式。
應(yīng)用三:addShutdownHook
Hook 和 Callback 的關(guān)系:Callback 更側(cè)重于語法機制的描述逻恐,而 Hook 更側(cè)重于應(yīng)用場景的描述像吻。Hook 是 Callback 的一種應(yīng)用。
public class ShutdownThread extends Thread {
@Override
public void run() {
System.out.println("I am called during shutting down");
}
}
public static void main(String[] args) {
ShutdownThread thread = new ShutdownThread();
Runtime.getRuntime().addShutdownHook(thread);
System.out.println("I'm dying");
}
上面的代碼中复隆,當 main 方法中的I'm dying
打印后萧豆,該程序就執(zhí)行完成,程序關(guān)閉昏名。在關(guān)閉之間會回調(diào) ShutdownThread 中的方法。
模板方法和回調(diào)的區(qū)別
在應(yīng)用場景上阵面,模板方法和同步回調(diào)基本上沒有什么差別轻局,都是在一個大的算法骨架中,自由替換某些步驟样刷,起到代碼復(fù)用和擴展的目的仑扑。而異步回調(diào)跟模塊模式有較大差異,異步回調(diào)更像觀察者模式置鼻。
在代碼實現(xiàn)上镇饮,回調(diào)基于組合來實現(xiàn),把一個對象傳遞到另一個對象箕母,是一種對象之間的關(guān)系储藐。而模板方法基于繼承實現(xiàn),子類重寫父類的抽象方法嘶是,是一種類之間的關(guān)系钙勃。
回調(diào)相比模板方法實現(xiàn)的好處
- Java 只支持單繼承,如果使用模板方法實現(xiàn)后聂喇,該類就不在具有繼承能力辖源。
- 回調(diào)可以使用匿名對象,可以不用事先定義類,而模板方法針對不同的實現(xiàn)克饶,都要創(chuàng)建不同的子類來實現(xiàn)酝蜒。
- 如果某個類中實現(xiàn)了多個模板方法,每個模板方法都有抽象方法矾湃,需要子類來實現(xiàn)亡脑。即便子類只用到了其中的一個模板方法都需要實現(xiàn)所有模板方法中的所有抽象方法。而如果是使用回調(diào)就更加靈活洲尊,只需要向用到的模板方法中注入對應(yīng)的回調(diào)對象即可远豺。
3. 策略模式
3.1 定義
定義一簇算法類,將每個算法分別封裝起來坞嘀,讓它們可以相互替換躯护。策略模式可以使算法的變化獨立于使用它們的客戶端(使用算法的代碼)。
3.2 作用
- 解耦算法的定義丽涩、創(chuàng)建和使用棺滞,降低算法的復(fù)雜度。同時矢渊,滿足單一職責(zé)继准,避免由于算法的切換帶來的算法代碼的個性。
- 通過動態(tài)指定算法策略矮男,提高程序切換算法的靈活性移必。
- 基于接口實現(xiàn),向客戶端屏蔽算法實現(xiàn)細節(jié)毡鉴,易擴展崔泵。
- 將多個算法獨立成類,提高了代碼的復(fù)用性猪瞬。
3.3 類結(jié)構(gòu)圖
3.4 經(jīng)典實現(xiàn)
1. 策略的定義
public interface Strategy {
void algorithm();
}
public class ConcreteStaragyA implements Strategy {
@Override
public void algorithm() {
System.out.println("A algorithm");
}
}
public class ConcreteStaragyB implements Strategy {
@Override
public void algorithm() {
System.out.println("B algorithm");
}
}
2. 策略的創(chuàng)建
public class StrategyFactory {
static Map<String, Strategy> strategyMap = new HashMap<>();
static {
strategyMap.put("A", new ConcreteStaragyA());
strategyMap.put("B", new ConcreteStaragyB());
}
public static Strategy getStrategy(String type) {
return strategyMap.get(type);
}
}
3. 策略的使用
- 運行時動態(tài)指定策略:根據(jù)配置憎瘸、用戶輸入或計算結(jié)果等這些不確定性因素,動態(tài)決定使用哪種策略陈瘦。
// 運行時動態(tài)確定幌甘,根據(jù)配置文件的配置決定使用哪種策略
public class Application {
public static void main(String[] args) throws Exception {
EvictionStrategy evictionStrategy = null;
Properties props = new Properties();
props.load(new FileInputStream("./config.properties"));
String type = props.getProperty("eviction_type");
evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);
UserCache userCache = new UserCache(evictionStrategy);
//...
}
}
// 非運行時動態(tài)確定,在代碼中指定使用哪種策略
public class Application {
public static void main(String[] args) {
//...
EvictionStrategy evictionStrategy = new LruEvictionStrategy();
UserCache userCache = new UserCache(evictionStrategy);
//...
}
}
非運行時動態(tài)確定痊项,并沒有發(fā)揮策略模式的優(yōu)勢锅风。在這種場景下,策略模式就退化成了“面向?qū)ο蠖鄳B(tài)編程”或者“基于接口而非實現(xiàn)編程原則”鞍泉。
3.5 使用策略模式避免分支判斷
常見多個算法耦合在一起的例子
public class OrderService {
public double discount(Order order) {
double discount = 0.0;
OrderType type = order.getType();
if (type.equals(OrderType.NORMAL)) { // 普通訂單
//...省略折扣計算算法代碼
} else if (type.equals(OrderType.GROUPON)) { // 團購訂單
//...省略折扣計算算法代碼
} else if (type.equals(OrderType.PROMOTION)) { // 促銷訂單
//...省略折扣計算算法代碼
}
return discount;
}
}
改造流程
- 將不同訂單的打折策略設(shè)計成獨立的類
- 使用工廠類來對不同的策略進行統(tǒng)一管理
改造后代碼
// 策略的定義
public interface DiscountStrategy {
double calDiscount(Order order);
}
// 省略NormalDiscountStrategy遏弱、GrouponDiscountStrategy、PromotionDiscountStrategy類代碼...
// 策略的創(chuàng)建
public class DiscountStrategyFactory {
private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>();
static {
strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
}
public static DiscountStrategy getDiscountStrategy(OrderType type) {
return strategies.get(type);
}
}
// 策略的使用
public class OrderService {
public double discount(Order order) {
OrderType type = order.getType();
DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
return discountStrategy.calDiscount(order);
}
}
對分支判斷的改造本質(zhì)上是對借助“查表”法替代了根據(jù) type 的分支判斷塞弊。
3.6 應(yīng)用場景
給文件中數(shù)字排序
根據(jù)數(shù)據(jù)的規(guī)模將使用 4 種不同的排序算法:
- 針對一次性讀取到內(nèi)存的數(shù)據(jù)柠辞,可以直接使用快排或者其它算法
- 針對文件較大,比如:10 G盐捷,無法一次性讀取到內(nèi)存中柿扣,可以使用外部排序算法(如:桶排序、計數(shù)排序)
- 針對文件更大,比如:100G,可以在外部排序的基礎(chǔ)上使用多線程,實現(xiàn)一個單機版的 MapReduce
- 針對文件巨大仗处,比如:1T,這時可以通過使用真正的 MapReduce 框架來實現(xiàn)
代碼實現(xiàn)
public interface ISortAlg {
void sort(String filePath);
}
public class QuickSort implements ISortAlg {
@Override
public void sort(String filePath) {
//...
}
}
public class ExternalSort implements ISortAlg {
@Override
public void sort(String filePath) {
//...
}
}
public class ConcurrentExternalSort implements ISortAlg {
@Override
public void sort(String filePath) {
//...
}
}
public class MapReduceSort implements ISortAlg {
@Override
public void sort(String filePath) {
//...
}
}
public class Sorter {
private static final long GB = 1000 * 1000 * 1000;
public void sortFile(String filePath) {
// 省略校驗邏輯
File file = new File(filePath);
long fileSize = file.length();
ISortAlg sortAlg;
if (fileSize < 6 * GB) { // [0, 6GB)
sortAlg = new QuickSort();
} else if (fileSize < 10 * GB) { // [6GB, 10GB)
sortAlg = new ExternalSort();
} else if (fileSize < 100 * GB) { // [10GB, 100GB)
sortAlg = new ConcurrentExternalSort();
} else { // [100GB, ~)
sortAlg = new MapReduceSort();
}
sortAlg.sort(filePath);
}
}
對上面代碼通過引入工廠類枣宫,使用“查表”法來緩存這些無狀態(tài)的算法類婆誓。
public class SortAlgFactory {
private static final Map<String, ISortAlg> algs = new HashMap<>();
static {
algs.put("QuickSort", new QuickSort());
algs.put("ExternalSort", new ExternalSort());
algs.put("ConcurrentExternalSort", new ConcurrentExternalSort());
algs.put("MapReduceSort", new MapReduceSort());
}
public static ISortAlg getSortAlg(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("type should not be empty.");
}
return algs.get(type);
}
}
public class Sorter {
private static final long GB = 1000 * 1000 * 1000;
public void sortFile(String filePath) {
// 省略校驗邏輯
File file = new File(filePath);
long fileSize = file.length();
ISortAlg sortAlg;
if (fileSize < 6 * GB) { // [0, 6GB)
sortAlg = SortAlgFactory.getSortAlg("QuickSort");
} else if (fileSize < 10 * GB) { // [6GB, 10GB)
sortAlg = SortAlgFactory.getSortAlg("ExternalSort");
} else if (fileSize < 100 * GB) { // [10GB, 100GB)
sortAlg = SortAlgFactory.getSortAlg("ConcurrentExternalSort");
} else { // [100GB, ~)
sortAlg = SortAlgFactory.getSortAlg("MapReduceSort");
}
sortAlg.sort(filePath);
}
}
再次優(yōu)化,移除 if-else 邏輯也颤。
public class Sorter {
private static final long GB = 1000 * 1000 * 1000;
private static final List<AlgRange> algs = new ArrayList<>();
static {
algs.add(new AlgRange(0, 6*GB, SortAlgFactory.getSortAlg("QuickSort")));
algs.add(new AlgRange(6*GB, 10*GB, SortAlgFactory.getSortAlg("ExternalSort")));
algs.add(new AlgRange(10*GB, 100*GB, SortAlgFactory.getSortAlg("ConcurrentExternalSort")));
algs.add(new AlgRange(100*GB, Long.MAX_VALUE, SortAlgFactory.getSortAlg("MapReduceSort")));
}
public void sortFile(String filePath) {
// 省略校驗邏輯
File file = new File(filePath);
long fileSize = file.length();
ISortAlg sortAlg = null;
for (AlgRange algRange : algs) {
if (algRange.inRange(fileSize)) {
sortAlg = algRange.getAlg();
break;
}
}
sortAlg.sort(filePath);
}
private static class AlgRange {
private long start;
private long end;
private ISortAlg alg;
public AlgRange(long start, long end, ISortAlg alg) {
this.start = start;
this.end = end;
this.alg = alg;
}
public ISortAlg getAlg() {
return alg;
}
public boolean inRange(long size) {
return size >= start && size < end;
}
}
}
目前這種實現(xiàn)洋幻,在添加新的策略類時,需要同時在 Sorter 類和 SortAlgFactory 類中添加對應(yīng)的邏輯翅娶,并不完全符合開閉原則文留。有什么辦法讓上面的代碼完全符合開閉原則呢?
對于工廠類的修改竭沫,可以通過反射 + 注解的方式燥翅。這里有兩種實現(xiàn)方式:
- 通過配置文件配置對應(yīng)的策略類,程序啟動時蜕提,讀取配置文件森书,并通過反射將所有的策略類對象添加到工廠類中。
- 為每個策略類設(shè)置注解谎势,程序啟動時拄氯,會通過掃描所有的注解,并通過反射將所有的策略類對象添加到工廠類中它浅。
對于 Sorter 類的修改,可以通過配置文件的方式镣煮,將文件區(qū)間大小和算法之間的對應(yīng)關(guān)系放到配置文件中姐霍。當添加新算法時,只需要改動區(qū)間與算法的對應(yīng)關(guān)系即可典唇。
4. 責(zé)任鏈模式
其實現(xiàn)可以參考我的另一篇博客镊折。里面做了更加詳細的說明,點擊跳轉(zhuǎn)介衔。
5. 狀態(tài)模式
5.1 定義
允許對象在其內(nèi)部狀態(tài)改變時恨胚,更改其對應(yīng)的行為。和有限狀態(tài)機的概念非常相似炎咖。
什么是有限狀態(tài)機
有限狀態(tài)機簡稱狀態(tài)機赃泡,由狀態(tài)(State)寒波、事件(Event)、和動作(Action)三部分組成升熊。其中俄烁,事情也稱為狀態(tài)轉(zhuǎn)移條件。事件觸發(fā)狀態(tài)的轉(zhuǎn)移及動作的執(zhí)行级野,當然页屠,動作不是必須的,也可以是發(fā)生狀態(tài)的轉(zhuǎn)移蓖柔。
5.2 作用
5.3 類結(jié)構(gòu)圖
5.4 狀態(tài)機的實現(xiàn)方式
1. 狀態(tài)模式實現(xiàn)
2. 分支邏輯法
3. 查表法
5.5 應(yīng)用場景
超級瑪麗奧
馬里奧有多種形態(tài):小巴里奧辰企、超級馬里奧、火焰馬里奧和斗篷馬里奧况鸣。
在不同的游戲情節(jié)下牢贸,各種形態(tài)會相互轉(zhuǎn)化,并相應(yīng)的增減積分懒闷。比如:小馬里奧吃了一個蘑菇后十减,變成超級巴里奧,并增加 100 積分愤估。
吃了一個蘑菇對應(yīng)事件(Event),變成超級馬里奧對應(yīng)狀態(tài)的變化帮辟,增加 100 積分對應(yīng)狀態(tài)變化后的動作執(zhí)行。
各狀態(tài)之間的轉(zhuǎn)換如下圖:
分支邏輯法
public enum State {
SMALL(0),
SUPER(1),
FIRE(2),
CAPE(3);
private int value;
private State(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
public class MarioStateMachine {
private int score;
private State currentState;
public MarioStateMachine() {
this.score = 0;
this.currentState = State.SMALL;
}
public void obtainMushRoom() {
if (currentState.equals(State.SMALL)) {
this.currentState = State.SUPER;
this.score += 100;
}
}
public void obtainCape() {
if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) {
this.currentState = State.CAPE;
this.score += 200;
}
}
public void obtainFireFlower() {
if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) {
this.currentState = State.FIRE;
this.score += 300;
}
}
public void meetMonster() {
if (currentState.equals(State.SUPER)) {
this.currentState = State.SMALL;
this.score -= 100;
return;
}
if (currentState.equals(State.CAPE)) {
this.currentState = State.SMALL;
this.score -= 200;
return;
}
if (currentState.equals(State.FIRE)) {
this.currentState = State.SMALL;
this.score -= 300;
return;
}
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState;
}
}
如果分支邏輯比較簡單玩焰,是可以接受的由驹。但如果對于復(fù)雜的狀態(tài)機來說,這種情況極有可能漏寫代碼昔园,同時蔓榄,每個事件中所對應(yīng)狀態(tài)改變及動作執(zhí)行會包含大量的 if-else 邏輯,可讀性和可維護性非常差默刚。而且甥郑,當要新增加一種狀態(tài)的話,每個事件里里的代碼都會被修改荤西,代碼將非常難以維護澜搅。
查表法
在上面的二維表中,第一維表示當前狀態(tài)邪锌,第二維表示事件勉躺,值表示當前狀態(tài)經(jīng)過事件之后,轉(zhuǎn)移到新的狀態(tài)及要執(zhí)行的動作觅丰。
public enum Event {
GOT_MUSHROOM(0),
GOT_CAPE(1),
GOT_FIRE(2),
MET_MONSTER(3);
private int value;
private Event(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
public class MarioStateMachine {
private int score;
private State currentState;
private static final State[][] transitionTable = {
{SUPER, CAPE, FIRE, SMALL},
{SUPER, CAPE, FIRE, SMALL},
{CAPE, CAPE, CAPE, SMALL},
{FIRE, FIRE, FIRE, SMALL}
};
private static final int[][] actionTable = {
{+100, +200, +300, +0},
{+0, +200, +300, -100},
{+0, +0, +0, -200},
{+0, +0, +0, -300}
};
public MarioStateMachine() {
this.score = 0;
this.currentState = State.SMALL;
}
public void obtainMushRoom() {
executeEvent(Event.GOT_MUSHROOM);
}
public void obtainCape() {
executeEvent(Event.GOT_CAPE);
}
public void obtainFireFlower() {
executeEvent(Event.GOT_FIRE);
}
public void meetMonster() {
executeEvent(Event.MET_MONSTER);
}
private void executeEvent(Event event) {
int stateValue = currentState.getValue();
int eventValue = event.getValue();
this.currentState = transitionTable[stateValue][eventValue];
this.score = actionTable[stateValue][eventValue];
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState;
}
}
上面使用兩個二維數(shù)組非常巧妙的將觸發(fā)事件后所對應(yīng)的狀態(tài)和對應(yīng)狀態(tài)所需要執(zhí)行的動作都表示了出來饵溅。在具體實現(xiàn)的時候,只需要通過當前事件去這兩個二維數(shù)組中獲取對應(yīng)的狀態(tài)和需要執(zhí)行的動作即可妇萄,非常的簡單蜕企,代碼實現(xiàn)也非常的簡潔咬荷。
但也存在問題,就是這里的動作只是簡單的加減積分糖赔,如果是一系列的操作萍丐,這種實現(xiàn)有無法滿足需求了。
狀態(tài)模式實現(xiàn)
狀態(tài)模式實際上是對分支邏輯法的一種優(yōu)化處理放典。狀態(tài)模式是將事件觸發(fā)的狀態(tài)轉(zhuǎn)移和動作執(zhí)行逝变,拆分到了不同的狀態(tài)類中,來避免分支判斷邏輯的奋构。
public interface IMario { //所有狀態(tài)類的接口
State getName();
//以下是定義的事件
void obtainMushRoom();
void obtainCape();
void obtainFireFlower();
void meetMonster();
}
public class SmallMario implements IMario {
private MarioStateMachine stateMachine;
public SmallMario(MarioStateMachine stateMachine) {
this.stateMachine = stateMachine;
}
@Override
public State getName() {
return State.SMALL;
}
@Override
public void obtainMushRoom() {
stateMachine.setCurrentState(new SuperMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 100);
}
@Override
public void obtainCape() {
stateMachine.setCurrentState(new CapeMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 200);
}
@Override
public void obtainFireFlower() {
stateMachine.setCurrentState(new FireMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster() {
// do nothing...
}
}
public class SuperMario implements IMario {
private MarioStateMachine stateMachine;
public SuperMario(MarioStateMachine stateMachine) {
this.stateMachine = stateMachine;
}
@Override
public State getName() {
return State.SUPER;
}
@Override
public void obtainMushRoom() {
// do nothing...
}
@Override
public void obtainCape() {
stateMachine.setCurrentState(new CapeMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 200);
}
@Override
public void obtainFireFlower() {
stateMachine.setCurrentState(new FireMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster() {
stateMachine.setCurrentState(new SmallMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() - 100);
}
}
// 省略CapeMario壳影、FireMario類...
public class MarioStateMachine {
private int score;
private IMario currentState; // 不再使用枚舉來表示狀態(tài)
public MarioStateMachine() {
this.score = 0;
this.currentState = new SmallMario(this);
}
public void obtainMushRoom() {
this.currentState.obtainMushRoom();
}
public void obtainCape() {
this.currentState.obtainCape();
}
public void obtainFireFlower() {
this.currentState.obtainFireFlower();
}
public void meetMonster() {
this.currentState.meetMonster();
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState.getName();
}
public void setScore(int score) {
this.score = score;
}
public void setCurrentState(IMario currentState) {
this.currentState = currentState;
}
}
MarioStateMachine 是一個狀態(tài)維護類,所有狀態(tài)的變更都是通過這里維護的弥臼。而實現(xiàn)了 IMario 接口的這些類宴咧,主要是負責(zé)維護在當前狀態(tài)下,事件觸發(fā)后的動作的執(zhí)行径缅。
兩者是雙向依賴關(guān)系掺栅,當事件觸發(fā)后,會先 MarioStateMachine 中對應(yīng)事件的方法纳猪,這些方法會調(diào)用到對應(yīng)狀態(tài)實現(xiàn)類中對應(yīng)的動作的執(zhí)行氧卧。而動作執(zhí)行的過程中,又需要反過來更新 MarioStateMachine 中的狀態(tài)氏堤。
由于狀態(tài)類中不包含任何的成員變量(狀態(tài))沙绝,所以,可以將其設(shè)計成單例對象鼠锈。優(yōu)化后的代碼如下:
public interface IMario {
State getName();
void obtainMushRoom(MarioStateMachine stateMachine);
void obtainCape(MarioStateMachine stateMachine);
void obtainFireFlower(MarioStateMachine stateMachine);
void meetMonster(MarioStateMachine stateMachine);
}
public class SmallMario implements IMario {
private static final SmallMario instance = new SmallMario();
private SmallMario() {}
public static SmallMario getInstance() {
return instance;
}
@Override
public State getName() {
return State.SMALL;
}
@Override
public void obtainMushRoom(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(SuperMario.getInstance());
stateMachine.setScore(stateMachine.getScore() + 100);
}
@Override
public void obtainCape(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(CapeMario.getInstance());
stateMachine.setScore(stateMachine.getScore() + 200);
}
@Override
public void obtainFireFlower(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(FireMario.getInstance());
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster(MarioStateMachine stateMachine) {
// do nothing...
}
}
// 省略SuperMario闪檬、CapeMario、FireMario類...
public class MarioStateMachine {
private int score;
private IMario currentState;
public MarioStateMachine() {
this.score = 0;
this.currentState = SmallMario.getInstance();
}
public void obtainMushRoom() {
this.currentState.obtainMushRoom(this);
}
public void obtainCape() {
this.currentState.obtainCape(this);
}
public void obtainFireFlower() {
this.currentState.obtainFireFlower(this);
}
public void meetMonster() {
this.currentState.meetMonster(this);
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState.getName();
}
public void setScore(int score) {
this.score = score;
}
public void setCurrentState(IMario currentState) {
this.currentState = currentState;
}
}
查表法和狀態(tài)模式的應(yīng)用場景
- 查表法:像游戲這種比較復(fù)雜的狀態(tài)機购笆,包含的狀態(tài)比較多粗悯,優(yōu)先推薦查表法,而如果使用狀態(tài)模式的話同欠,會引入非常多的狀態(tài)類样傍,會導(dǎo)致代碼比較難維護。
- 狀態(tài)模式:像電商下單行您、外賣下單這種類型的狀態(tài)機,狀態(tài)相對有限剪廉,而事件觸發(fā)執(zhí)行的動作包含的業(yè)務(wù)邏輯可能比較復(fù)雜的情況下娃循,使用狀態(tài)模式更合適。
6. 迭代器模式
6.1 定義
提供一種方法訪問一個容器(container)對象中各個元素斗蒋,而又不需暴露該對象的內(nèi)部細節(jié)捌斧。
6.2 作用
- 迭代器模式封裝集合內(nèi)部的復(fù)雜數(shù)據(jù)結(jié)構(gòu)笛质,開發(fā)者不需要了解如何遍歷,直接使用窗口提供的迭代器即可捞蚂,提供容器的易用性
- 迭代器模式將對集合的遍歷操作從集合類中拆分出來妇押,放到迭代器中,讓兩者的職責(zé)更加單一
- 迭代器讓新增新的遍歷算法非常的容易姓迅,只需要實現(xiàn)迭代器接口敲霍,并在容器接口中提供對應(yīng)的創(chuàng)建方法即可,更加符合開閉原則
6.3 類結(jié)構(gòu)圖
6.4 經(jīng)典實現(xiàn)
// 接口定義方式一
public interface Iterator<E> {
boolean hasNext();
void next();
E currentItem();
}
public class ArrayIterator<E> implements Iterator<E> {
private int cursor;
private ArrayList<E> arrayList;
public ArrayIterator(ArrayList<E> arrayList) {
this.cursor = 0;
this.arrayList = arrayList;
}
@Override
public boolean hasNext() {
return cursor != arrayList.size(); //注意這里丁存,cursor在指向最后一個元素的時候肩杈,hasNext()仍舊返回true。
}
@Override
public void next() {
cursor++;
}
@Override
public E currentItem() {
if (cursor >= arrayList.size()) {
throw new NoSuchElementException();
}
return arrayList.get(cursor);
}
}
public class Demo {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("xzg");
names.add("wang");
names.add("zheng");
Iterator<String> iterator = new ArrayIterator(names);
while (iterator.hasNext()) {
System.out.println(iterator.currentItem());
iterator.next();
}
}
}
上面的實現(xiàn)是將容器對象通過構(gòu)造函數(shù)直接傳遞給了迭代器對象解寝,而為了封裝迭代器的創(chuàng)建細節(jié)扩然,我們可以在容器對象中定義一個 iterator 方法,來創(chuàng)建對應(yīng)的迭代器聋伦。改造代碼如下:
public interface List<E> {
Iterator iterator();
//...省略其他接口函數(shù)...
}
public class ArrayList<E> implements List<E> {
//...
public Iterator iterator() {
return new ArrayIterator(this);
}
//...省略其他代碼
}
public class Demo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("xzg");
names.add("wang");
names.add("zheng");
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.currentItem());
iterator.next();
}
}
}
下圖是容器對象和迭代器對象的依賴關(guān)系圖:
6.5 迭代器遍歷集合的優(yōu)勢
遍歷集合的3種方式
- for
- foreach
- iterator
而 foreach 是基于 iterator 實現(xiàn)的夫偶,也就是說,本質(zhì)上只有 2 種遍歷集合的方式觉增。
迭代器優(yōu)勢
- 迭代器模式封裝集合內(nèi)部的復(fù)雜數(shù)據(jù)結(jié)構(gòu)兵拢,開發(fā)者不需要了解如何遍歷,直接使用窗口提供的迭代器即可
- 迭代器模式將對集合的遍歷操作從集合類中拆分出來抑片,放到迭代器中卵佛,讓兩者的職責(zé)更加單一
- 迭代器讓新增新的遍歷算法非常的容易,只需要實現(xiàn)迭代器接口敞斋,并在容器接口中提供對應(yīng)的創(chuàng)建方法即可截汪,更加符合開閉原則
6.6 遍歷集合的時候,為什么不能增刪集合中元素
主要是由于集合底層數(shù)據(jù)結(jié)構(gòu)是數(shù)組植捎,當刪除元素時衙解,會將數(shù)組向前移動一位,而再去遍歷元素的時候焰枢,就有可能出現(xiàn)有些數(shù)據(jù)被漏掉的情況蚓峦。
而增加一個新的元素,插入位置之后的元素都會向后移動一位济锄,就有可能出現(xiàn)某個元素會被重復(fù)遍歷的情況暑椰。
ArrayList 的解決思路
在遍歷的過程中,出現(xiàn)增刪元素之后荐绝,直接報錯一汽。
如何確定是否有增刪元素
ArrayList 定義了一個 modCount 變量,集合每次調(diào)用增刪都會將 modCount 加 1低滩。然后召夹,將每次通過 ArrayList 的 iterator 函數(shù)創(chuàng)建迭代器的時候岩喷,會將 modCount 的值賦值給 expectedModCount 變量(expectedModCount 是 Iterator 對象中的變量)。之后监憎,每次調(diào)用當前 Iterator 迭代器對象的 hasNext()纱意、next()函數(shù),都會檢查 modCount 和 exceptedModCount 值是否相等鲸阔。如果不相等偷霉,則說明迭代器在遍歷過程中,元素進行了增刪的操作隶债。此時腾它,遵循 fail-fast 原則,會拋出 ConcurrentModificationException
異常死讹。
什么是 fail-fast 原則
fail-fast 是一種系統(tǒng)的設(shè)計思想瞒滴,當程序有可能出現(xiàn)異常的時候,應(yīng)該盡可能的上報其錯誤信息赞警,讓程序終止運行妓忍,而讓程序設(shè)計者盡早知道問題,并解決問題愧旦。
6.7 如果在遍歷的同時世剖,安全地刪除元素
Iterator 迭代器中提供了 remove()
的方法來支持在遍歷過程中,刪除元素的操作笤虫。但這個 remove 的操作的作用是有限的旁瘫,它只能刪除當前 遍歷游標 cursor 所在位置的前一個元素,而且只能執(zhí)行一次刪除操作琼蚯,連續(xù)調(diào)用兩次操作酬凳,直接會報錯。
具體的刪除操作是:在 Iterator 中定義了 lastRet 變量遭庶,每次調(diào)用 next() 方法后宁仔,會將當前游標所指定的位置賦值給 lastRet,然后游標自加 1 并指向下一個位置峦睡。當調(diào)用了 remove()
方法后翎苫,會刪除 lastRet 所指向位置的元素,并將 lastRet 賦值給當前的 cursor 游標榨了,也就是將游標的位置向前移動了一位煎谍。
cursor = lastRet 賦值操作完成后,會將 lastRet 賦值為 -1龙屉,當再次調(diào)用 remove()
方法時呐粘,如果沒有調(diào)用 next()
方法給 lastRet 重新賦值的話,由于 lastRet 仍然等于 -1,則會拋出異常事哭。
6.8. 實現(xiàn)帶“快照”功能的迭代器,徹底解決增刪問題
方案一:直接拷貝集合數(shù)據(jù)
public class SnapshotArrayIterator<E> implements Iterator<E> {
private int cursor;
private ArrayList<E> snapshot;
public SnapshotArrayIterator(ArrayList<E> arrayList) {
this.cursor = 0;
this.snapshot = new ArrayList<>();
this.snapshot.addAll(arrayList);
}
@Override
public boolean hasNext() {
return cursor < snapshot.size();
}
@Override
public E next() {
E currentItem = snapshot.get(cursor);
cursor++;
return currentItem;
}
}
直接在創(chuàng)建迭代器的時候瓜富,將原有集合中的元素拷貝一份鳍咱。簡單直接,但代價較高与柑。
方案二:添加時間戳標記每個元素
public class ArrayList<E> implements List<E> {
private static final int DEFAULT_CAPACITY = 10;
private int actualSize; //不包含標記刪除元素
private int totalSize; //包含標記刪除元素
private Object[] elements;
private long[] addTimestamps;
private long[] delTimestamps;
public ArrayList() {
this.elements = new Object[DEFAULT_CAPACITY];
this.addTimestamps = new long[DEFAULT_CAPACITY];
this.delTimestamps = new long[DEFAULT_CAPACITY];
this.totalSize = 0;
this.actualSize = 0;
}
@Override
public void add(E obj) {
elements[totalSize] = obj;
addTimestamps[totalSize] = System.currentTimeMillis();
delTimestamps[totalSize] = Long.MAX_VALUE;
totalSize++;
actualSize++;
}
@Override
public void remove(E obj) {
for (int i = 0; i < totalSize; ++i) {
if (elements[i].equals(obj)) {
delTimestamps[i] = System.currentTimeMillis();
actualSize--;
}
}
}
public int actualSize() {
return this.actualSize;
}
public int totalSize() {
return this.totalSize;
}
public E get(int i) {
if (i >= totalSize) {
throw new IndexOutOfBoundsException();
}
return (E)elements[i];
}
public long getAddTimestamp(int i) {
if (i >= totalSize) {
throw new IndexOutOfBoundsException();
}
return addTimestamps[i];
}
public long getDelTimestamp(int i) {
if (i >= totalSize) {
throw new IndexOutOfBoundsException();
}
return delTimestamps[i];
}
}
public class SnapshotArrayIterator<E> implements Iterator<E> {
private long snapshotTimestamp;
private int cursorInAll; // 在整個容器中的下標谤辜,而非快照中的下標
private int leftCount; // 快照中還有幾個元素未被遍歷
private ArrayList<E> arrayList;
public SnapshotArrayIterator(ArrayList<E> arrayList) {
this.snapshotTimestamp = System.currentTimeMillis();
this.cursorInAll = 0;
this.leftCount = arrayList.actualSize();;
this.arrayList = arrayList;
justNext(); // 先跳到這個迭代器快照的第一個元素
}
@Override
public boolean hasNext() {
return this.leftCount >= 0; // 注意是>=, 而非>
}
@Override
public E next() {
E currentItem = arrayList.get(cursorInAll);
justNext();
return currentItem;
}
private void justNext() {
while (cursorInAll < arrayList.totalSize()) {
long addTimestamp = arrayList.getAddTimestamp(cursorInAll);
long delTimestamp = arrayList.getDelTimestamp(cursorInAll);
if (snapshotTimestamp > addTimestamp && snapshotTimestamp < delTimestamp) {
leftCount--;
break;
}
cursorInAll++;
}
}
}
添加三個時間戳,分別是元素添加時間戳 addTimestamp价捧、元素刪除時間戳 delTimestamp 和迭代器創(chuàng)建時時間戳 snapshotTimestamp丑念。當滿足 addTimestamp < snapshotTimestamp < delTimestamp 時,說明當前元素是創(chuàng)建 Iterator 迭代器時结蟋,快照的元素脯倚,需要被遍歷到。
而當 addTimestamp > snapshotTimestamp 時嵌屎,說明當前元素是快照生成之后添加到集合的推正,不屬于快照中的元素;當 snapshotTimestamp > delTimestamp 時宝惰,說明當前元素在創(chuàng)建快照之前就已經(jīng)被刪除了植榕,同樣的,也不屬于快照中的元素尼夺。這兩種情況下尊残,對應(yīng)的元素都不應(yīng)該被 遍歷到。
方案二存在問題:
由于刪除元素只是標記而非刪除淤堵,這樣就導(dǎo)致隨機訪問失效寝衫。相當于為了修復(fù)一個問題,引入了另一個問題粘勒。而本來隨機訪問特性是以數(shù)組為底層數(shù)據(jù)結(jié)構(gòu)的一個優(yōu)勢竞端,通過標記而非刪除的方式實現(xiàn)后,這種優(yōu)勢就被丟棄了庙睡,得不償失事富。
解決方案二中存在的問題:讓容器既支持快照,又支持隨機訪問
可以在 ArrayList 中創(chuàng)建兩個數(shù)組乘陪,一個用來支持標記清除的统台,用來實現(xiàn)快照功能。另一個不支持標記清除的啡邑,用來支持隨機訪問贱勃。
說明
此文是根據(jù)王爭設(shè)計模式之美相關(guān)專欄內(nèi)容整理而來,非原創(chuàng)。