需求
聚合數據API提供了搜索某個城市未來一周每天的天氣詳細信息,包含最低最高氣溫、風速、濕度等數據。我們的任務就是:獲取南京市未來一周的天氣情況,找出溫度最高的那一天的天氣詳細信息,把這條信息保存在數據庫中。
同步方法實現
我們只關注獲取數據、處理數據和保存數據的三個核心功能。
Model 和 API
簡單的描述一下Weather的數據模型
public class Weather implements Comparable<Weather> {
private int max;//最高溫度
private int min;//最低溫度
private String wind;
@Override
public int compareTo(@NonNull Weather weather) {
return Integer.compare(max,weather.max);
}
}
下面是我們請求數據和存儲數據的API(同步),我們暫且忽略具體的實現
public interface Api {
List<Weather> queryWeathers(String city);
Uri save(Weather weather);
}
現在,開始編寫我們的業務邏輯代碼
public class WeathersHelper {
Api api;
public Uri saveTheMaxDay(String city){
List<Weather> weathers = api.queryWeathers(city);
Weather max = findMaxDay(weathers);
Uri savedUri = api.save(max);
return savedUri;
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
(@ο@) 哇~,這也太簡單太清晰了吧!來看看上面的代碼是多么的酷。主要的函數 saveTheMaxDay 只包含了 3個函數調用。使用參數來調用這些函數,并接收返回的參數。 并且等待每個函數執行并返回結果。如此簡單、如此有效。下面來看看這種簡單函數的其他優點。
-
組合(Composition)
可以看到我們的 saveTheMaxDay 由其他三個函數調用所組成的。我們通過函數來把一個大功能分割為每個容易理解的小功能。通過函數調用來組合使用這些小功能。使用和理解起來都相當簡單。 -
異常傳遞
另外一個使用函數的好處就是方便處理異常。每個函數都可以通過拋出異常來結束運行。該異常可以在拋出異常的函數里面處理,也可以在調用該函數的外面處理,所以我們無需每次都處理每個異常,我們可以在一個地方處理所有可能拋出的異常。
try{
List<Weather> weathers = api.queryWeathers(city);
Weather max = findMaxDay(weathers);
Uri savedUri = api.save(max);
return savedUri;
} catch (Exception e) {
e.printStackTrace()
return someDefaultValue;
}
這樣,我們就可以處理這三個函數中所拋出的任何異常了。如果沒有 try catch 語句,我們也可以把異常繼續傳遞下去。
異步回調方法實現
但是,現實世界中我們往往沒法等待。有些時候你沒法只使用阻塞調用。在 Android 中你需要處理各種異步操作。
就拿 Android 的 OnClickListener 接口來說吧, 如果你需要處理一個 View 的點擊事件,你必須提供一個 該 Listener 的實現來處理用戶的點擊事件。下面來看看如何處理異步調用。
現在我們把請求網絡放在異步操作中,使用回調獲取結果
public interface Api {
interface WeathersQueryCallback {
void onWeatherListReceived(List<Weather> weathers);
void onError(Exception e);
}
void queryWeathers(String city, WeathersQueryCallback weathersQueryCallback);
Uri save(Weather weather);
}
這樣我們查詢天氣的操作就變為異步的了, 通過 WeathersQueryCallback 回調接口來結束查詢的數據和處理異常情況。
我們的業務邏輯也需要跟著改變一下:
public class WeathersHelper {
public interface MaxDayCallback {
void onMaxDaySaved(Uri uri);
void onQueryFailed(Exception e);
}
Api api;
public void saveTheMaxDay(String city, MaxDayCallback maxDayCallback){
api.queryWeathers(city, new Api.WeathersQueryCallback() {
@Override
public void onWeatherListReceived(List<Weather> weathers) {
Weather max = findMaxDay(weathers);
Uri savedUri = api.save(max);
maxDayCallback.onMaxDaySaved(savedUri);
}
@Override
public void onError(Exception e) {
maxDayCallback.onQueryFailed(e);
}
});
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
這樣我們就沒法使用阻塞式函數調用了, 我們無法繼續使用阻塞調用來使用這些 API 了。所以,我們沒法讓 saveTheMaxDay 函數返回一個值了, 我們需要一個回調接口來異步的處理結果。
這里我們再進一步,使用兩個異步操作來實現我們的功能, 例如 我們還使用異步 IO 來寫文件。
public interface Api {
interface WeathersQueryCallback {
void onWeatherListReceived(List<Weather> weathers);
void onError(Exception e);
}
interface SaveCallback{
void onWeatherSaved(Uri uri);
void onSaveFailed(Exception e);
}
void queryWeathers(String city, WeathersQueryCallback weathersQueryCallback);
void save(Weather weather, SaveCallback saveCallback);
}
業務邏輯變為:
public class WeathersHelper {
public interface MaxDayCallback {
void onMaxDaySaved(Uri uri);
void onQueryFailed(Exception e);
}
Api api;
public void saveTheMaxDay(String city, MaxDayCallback maxDayCallback){
api. queryWeathers(city, new Api.WeathersQueryCallback() {
@Override
public void onWeatherListReceived(List< Weather > weathers) {
Weather max = findMaxDay(weathers);
api.save(max, new Api.SaveCallback() {
@Override
public void onWeatherSaved(Uri uri) {
maxDayCallback.onMaxDaySaved(uri);
}
@Override
public void onStoreFailed(Exception e) {
maxDayCallback.onError(e);
}
});
}
@Override
public void onQueryFailed(Exception e) {
maxDayCallback.onError(e);
}
});
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
現在再來看看我們的業務邏輯代碼,是不是和之前的阻塞式調用那么簡單、那么清晰? 當然不一樣了,上面的異步操作代碼看起來太恐怖了! 這里有太多的干擾代碼了,太多的匿名類了,但是不可否認,他們的業務邏輯其實是一樣的。查詢天氣的列表數據、找出最溫度最高的那一天。
組合功能也不見了! 現在你沒法像阻塞操作一樣來組合調用每個功能了。異步操作中,每次你都必須通過回調接口來手工的處理結果。
那么關于異常傳遞和處理呢? 沒了!異步代碼中的異常不會自動傳遞了,我們需要手工的重新傳遞出去。(onSaveFailed 和 onQueryFailed 就是干這事用的)
上面的代碼非常難懂也更難發現潛在的 BUG。
代碼優化
-
泛型接口
仔細的看看這些回調接口,你會發現一個通用的模式:
1,這些接口都有一個函數來返回結果(onMaxDaySaved, onWeatherListReceived, onWeatherSaved)。
2,這里還都有一個返回異常情況的函數(onError, onQueryFailed, onSaveFailed)。
所以我們可以使用一個泛型接口來替代這三個接口。 由于我們無法修改 API 調用的參數類型, 必須要創建一個包裝類來調用泛型接口。新的接口定義如下:
public interface Callback<T> {
void onResult(T result);
void onError(Exception e);
}
然后使用 ApiWrapper 來改變調用的參數。
public class ApiWrapper {
Api api;
public void queryWeathers(String city, final Callback<List<Weather>> weathersCallback) {
api.queryWeathers(city, new Api.WeathersQueryCallback() {
@Override
public void onWeatherListReceived(List<Weather> weathers) {
weathersCallback.onResult(weathers);
}
@Override
public void onError(Exception e) {
weathersCallback.onError(e);
}
});
}
public void save(Weather weather, final Callback<Uri> uriCallback) {
api.save(weather, new Api.SaveCallback() {
@Override
public void onWeatherSaved(Uri uri) {
uriCallback.onResult(uri);
}
@Override
public void onSaveFailed(Exception e) {
uriCallback.onError(e);
}
});
}
}
上面的代碼使用同樣的邏輯在 Callback 接口中來處理結果和異常情況。
最后 WeathersHelper 的代碼如下:
public class WeatherHelper {
ApiWrapper apiWrapper;
public void saveTheMaxDay(String city, final Callback<Uri> maxDayCallback){
apiWrapper.queryWeathers(city, new Callback<List<Weather>>() {
@Override
public void onResult(List<Weather> result) {
Weather maxDay = findMaxDay(result);
apiWrapper.save(maxDay,maxDayCallback);
}
@Override
public void onError(Exception e) {
maxDayCallback.onError(e);
}
});
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
由于使用了泛型回調接口,這里的maxDayCallback可以直接設置為函數 apiWrapper.save的參數, 所以 上面的代碼比前面的代碼要少一層匿名類。看起來簡單一點。
-
分離參數和回調接口
看看這些新的異步操作(queryWeathers, save和 saveTheMaxDay)。這些函數都有同樣的模式。使用一些參數來調用這些函數(city,weather),同時還有一個回調接口作為參數。甚至,所有的異步操作都帶有一些常規參數和一個額外的回調接口參數。如果我們把他們分離開會如何,讓每個異步操作只有一些常規參數而把返回一個臨時的對象來操作回調接口。下面來試試看看這種方式能否有效。
如果我們返回一個臨時的對象作為異步操作的回調接口處理方式,我們需要先定義這個對象。由于對象遵守通用的行為(有一個回調接口參數),我們定義一個能用于所有操作的對象。 我們稱之為 AsyncJob。
注意: 我非常想把這個名字稱之為 AsyncTask。但是由于 Android 系統已經有個 AsyncTask 類了, 為了 避免混淆,所以就用 AsyncJob了。
該對象如下:
public abstract class AsyncJob<T> {
public abstract void start(Callback<T> callback);
}
start 函數有個Callback 回調接口參數,并開始執行該操作。
ApiWrapper 修改為:
public class ApiWrapper {
Api api;
public AsyncJob<List<Weather>> queryWeathers(final String city) {
return new AsyncJob<List<Weather>>() {
@Override
public void start(final Callback<List<Weather>> weathersCallback) {
api.queryWeathers(city, new Api.WeathersQueryCallback() {
@Override
public void onWeatherListReceived(List<Weather> weathers) {
weathersCallback.onResult(weathers);
}
@Override
public void onError(Exception e) {
weathersCallback.onError(e);
}
});
}
};
}
public AsyncJob<Uri> save(final Weather weather) {
return new AsyncJob<Uri>() {
@Override
public void start(final Callback<Uri> uriCallback) {
api.save(weather, new Api.SaveCallback() {
@Override
public void onWeatherSaved(Uri uri) {
uriCallback.onResult(uri);
}
@Override
public void onSaveFailed(Exception e) {
uriCallback.onError(e);
}
});
}
};
}
}
目前看起來還不錯。現在可以使用 AsyncJob 來啟動每個操作了。 這些功能在 WeathersHelper 中:
public class WeatherHelper {
ApiWrapper apiWrapper;
public AsyncJob<Uri> saveTheMaxDay(final String city) {
return new AsyncJob<Uri>() {
@Override
public void start(final Callback<Uri> maxDayCallback) {
apiWrapper.queryWeathers(city)
.start(new Callback<List<Weather>>() {
@Override
public void onResult(List<Weather> result) {
Weather maxDay = findMaxDay(result);
apiWrapper.save(maxDay)
.start(new Callback<Uri>() {
@Override
public void onResult(Uri result) {
maxDayCallback.onResult(result);
}
@Override
public void onError(Exception e) {
maxDayCallback.onError(e);
}
});
}
@Override
public void onError(Exception e) {
}
});
}
};
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
看起來比前面一個版本更加復雜啊,這樣有啥好處啊?
這里其實我們返回的是一個 AsyncJob 對象,該對象和客戶端代碼組合使用,這樣 在 Activity或者 Fragment 客戶端代碼中就可以操作這個返回的對象了。
代碼雖然目前看起來比較復雜,下面我們就來改進一下。
-
分解
下面是數據流圖:
(async) (sync) (async)
query ===========> List<Weather> -------------> Weather==========> Uri
queryWeathers findMax save
為了讓代碼具有可讀性,我們把這個流程分解為每個操作。同時我們再進一步假設,如果一個操作是異步的,則每個調用該異步操作的函數也是異步的。例如:如果查詢天氣是個異步操作,則找到最最高溫度的那一天也是異步的。
因此,我們可以使用 AsyncJob 來把每個操作分解為一個非常小的函數里面去。
public class WeatherHelper {
ApiWrapper apiWrapper;
public AsyncJob<Uri> saveTheMaxDay(final String city) {
//第一步,請求數據
final AsyncJob<List<Weather>> weatherListAsyncJob = apiWrapper.queryWeathers(city);
//第二步,處理數據
final AsyncJob<Weather> maxDayAsyncJob = new AsyncJob<Weather>() {
@Override
public void start(final Callback<Weather> callback) {
weatherListAsyncJob.start(new Callback<List<Weather>>() {
@Override
public void onResult(final List<Weather> result) {
Weather maxDay = findMaxDay(result);
callback.onResult(maxDay);
}
@Override
public void onError(Exception e) {
callback.onError(e);
}
});
}
};
//第三步,存儲數據
AsyncJob<Uri> savedUriAsyncJob = new AsyncJob<Uri>() {
@Override
public void start(final Callback<Uri> callback) {
maxDayAsyncJob.start(new Callback<Weather>() {
@Override
public void onResult(Weather result) {
apiWrapper.save(result)
.start(new Callback<Uri>() {
@Override
public void onResult(Uri result) {
callback.onResult(result);
}
@Override
public void onError(Exception e) {
callback.onError(e);
}
});
}
@Override
public void onError(Exception e) {
}
});
}
};
return savedUriAsyncJob;
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
雖然代碼量多了,但是看起來更加清晰了。 嵌套的回調函數沒那么多層級了,異步操作的名字也更容易理解了(weatherListAsyncJob , maxDayAsyncJob , savedUriAsyncJob )。
看起來還不錯,但是還可以更好。
-
簡單的映射
先來看看我們創建 AsyncJob maxDayAsyncJob 的代碼:
AsyncJob<Weather> maxDayAsyncJob = new AsyncJob<Weather>() {
@Override
public void start(final Callback<Weather> callback) {
weatherListAsyncJob.start(new Callback<List<Weather>>() {
@Override
public void onResult(final List<Weather> result) {
Weather maxDay = findMaxDay(result);
callback.onResult(maxDay);
}
@Override
public void onError(Exception e) {
callback.onError(e);
}
});
}
};
這 16 行代碼中,只有一行代碼是我們的業務邏輯代碼:findMaxDay(result)
其他的代碼只是為了啟動 AsyncJob 并接收結果和處理異常的干擾代碼。 但是這些代碼是通用的,我們可以把他們放到其他地方來讓我們更加專注業務邏輯代碼。
那么如何實現呢?需要做兩件事情:
1,轉換 AsyncJob 的結果
2,轉換的函數
但是由于 Java 的限制,無法把函數作為參數, 所以需要用一個接口(或者類)并在里面定義一個轉換函數:
public interface Function<T, R> {
R call(T t);
}
該接口很簡單。 有兩個泛型類型定義, T 代表參數的類型; R 代表返回值的類型。
當我們把 AsyncJob 的結果轉換為其他類型的時候, 我們需要把一個結果值映射為另外一種類型,這個操作我們稱之為 map。 把該函數定義到 AsyncJob 類中比較方便,這樣就可以通過 this 來訪問 AsyncJob 對象了。
public abstract class AsyncJob<T> {
public abstract void start(Callback<T> callback);
/**
* 簡單映射
* @param func
* @param <R> 輸入類型
* @return 輸出類型
*/
public <R> AsyncJob<R> map(final Function<T, R> func) {
final AsyncJob<T> source = this;
return new AsyncJob<R>() {
@Override
public void start(final Callback<R> callback) {
source.start(new Callback<T>() {
@Override
public void onResult(T result) {
R mapped = func.call(result);
callback.onResult(mapped);
}
@Override
public void onError(Exception e) {
callback.onError(e);
}
});
}
};
}
}
看起來不錯, 現在的 CatsHelper 如下:
public class WeatherHelper {
ApiWrapper apiWrapper;
public AsyncJob<Uri> saveTheMaxDay(final String city) {
//第一步,請求數據
final AsyncJob<List<Weather>> weatherListAsyncJob = apiWrapper.queryWeathers(city);
//第二步,處理數據
final AsyncJob<Weather> maxDayAsyncJob = weatherListAsyncJob.map(new Function<List<Weather>, Weather>() {
@Override
public Weather call(List<Weather> weathers) {
return findMaxDay(weathers);
}
});
//第三步,存儲數據
AsyncJob<Uri> savedUriAsyncJob = new AsyncJob<Uri>() {
@Override
public void start(final Callback<Uri> callback) {
maxDayAsyncJob.start(new Callback<Weather>() {
@Override
public void onResult(Weather result) {
apiWrapper.save(result)
.start(new Callback<Uri>() {
@Override
public void onResult(Uri result) {
callback.onResult(result);
}
@Override
public void onError(Exception e) {
callback.onError(e);
}
});
}
@Override
public void onError(Exception e) {
}
});
}
};
return savedUriAsyncJob;
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
新的創建 AsyncJob maxDayAsyncJob 的代碼只有 6行,并且只有一層嵌套。
-
高級映射
但是 AsyncJob storedUriAsyncJob 看起來還是非常糟糕。 這里也能使用映射嗎? 下面就來試試吧!
public class WeatherHelper {
ApiWrapper apiWrapper;
public AsyncJob<Uri> saveTheMaxDay(final String city) {
//第一步,請求數據
final AsyncJob<List<Weather>> weatherListAsyncJob = apiWrapper.queryWeathers(city);
//第二步,處理數據
final AsyncJob<Weather> maxDayAsyncJob = weatherListAsyncJob.map(new Function<List<Weather>, Weather>() {
@Override
public Weather call(List<Weather> weathers) {
return findMaxDay(weathers);
}
});
//第三步,存儲數據
AsyncJob<Uri> savedUriAsyncJob = maxDayAsyncJob.map(new Function<Weather, Uri>() {
@Override
public Uri call(Weather weather) {
return apiWrapper.save(weather);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 將會導致無法編譯
// Incompatible types:
// Required: Uri
// Found: AsyncJob<Uri>
}
});
return savedUriAsyncJob;
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
哎。。。 看起來沒這么簡單啊, 下面修復返回的類型再試一次:
public class WeatherHelper {
ApiWrapper apiWrapper;
public AsyncJob<Uri> saveTheMaxDay(final String city) {
//第一步,請求數據
final AsyncJob<List<Weather>> weatherListAsyncJob = apiWrapper.queryWeathers(city);
//第二步,處理數據
final AsyncJob<Weather> maxDayAsyncJob = weatherListAsyncJob.map(new Function<List<Weather>, Weather>() {
@Override
public Weather call(List<Weather> weathers) {
return findMaxDay(weathers);
}
});
//第三步,存儲數據
AsyncJob<AsyncJob<Uri>> savedUriAsyncJob = maxDayAsyncJob.map(new Function<Weather, AsyncJob<Uri>>() {
@Override
public AsyncJob<Uri> call(Weather weather) {
return apiWrapper.save(weather);
}
});
return savedUriAsyncJob;
//^^^^^^^^^^^^^^^^^^^^^^^ 將會導致無法編譯
// Incompatible types:
// Required: AsyncJob<Uri>
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
這里我們只能拿到 AsyncJob<AsyncJob> 。 看來還需要更進一步。我們需要壓縮一層AsyncJob ,把兩個異步操作當做一個單一的異步操作來對待。
現在我們需要一個參數為 AsyncJob 的 map 轉換操作而不是 R。 該操作類似于 map, 但是該操作會把嵌套的 AsyncJob 壓縮為(flatten )一層 AsyncJob. 我們稱之為 flatMap, 實現如下:
public abstract class AsyncJob<T> {
public abstract void start(Callback<T> callback);
/**
* 簡單映射
* @param func
* @param <R> 輸入類型
* @return 輸出類型
*/
public <R> AsyncJob<R> map(final Function<T, R> func) {
final AsyncJob<T> source = this;
return new AsyncJob<R>() {
@Override
public void start(final Callback<R> callback) {
source.start(new Callback<T>() {
@Override
public void onResult(T result) {
R mapped = func.call(result);
callback.onResult(mapped);
}
@Override
public void onError(Exception e) {
callback.onError(e);
}
});
}
};
}
/**
* 高級映射
* @param func
* @param <R>
* @return
*/
public <R> AsyncJob<R> flatMap(final Function<T, AsyncJob<R>> func) {
final AsyncJob<T> source = this;
return new AsyncJob<R>() {
@Override
public void start(final Callback<R> callback) {
source.start(new Callback<T>() {
@Override
public void onResult(T result) {
AsyncJob<R> mapped = func.call(result);
mapped.start(new Callback<R>() {
@Override
public void onResult(R result) {
callback.onResult(result);
}
@Override
public void onError(Exception e) {
callback.onError(e);
}
});
}
@Override
public void onError(Exception e) {
callback.onError(e);
}
});
}
};
}
}
看起來很多干擾代碼,但是還好這些代碼在客戶端代碼中并不會出現。 現在我們的 WeathersHelper 如下:
public class WeatherHelper {
ApiWrapper apiWrapper;
public AsyncJob<Uri> saveTheMaxDay(final String city) {
//第一步,請求數據
final AsyncJob<List<Weather>> weatherListAsyncJob = apiWrapper.queryWeathers(city);
//第二步,處理數據
final AsyncJob<Weather> maxDayAsyncJob = weatherListAsyncJob.map(new Function<List<Weather>, Weather>() {
@Override
public Weather call(List<Weather> weathers) {
return findMaxDay(weathers);
}
});
//第三步,存儲數據
AsyncJob<Uri> savedUriAsyncJob = maxDayAsyncJob.flatMap(new Function<Weather, AsyncJob<Uri>>() {
@Override
public AsyncJob<Uri> call(Weather weather) {
return apiWrapper.save(weather);
}
});
return savedUriAsyncJob;
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
現在看起來簡單多了,最終的代碼 看起來是不是有點眼熟啊? 再仔細看看。還沒發現? 如果把匿名類修改為 Java 8 的 lambdas 表達式(邏輯是一樣的,只是讓代碼看起來更清晰點)就很容易發現了。
public class WeatherHelper {
ApiWrapper apiWrapper;
public AsyncJob<Uri> saveTheMaxDay(final String city) {
AsyncJob<List<Weather>> weatherListAsyncJob = apiWrapper.queryWeathers(city);
AsyncJob<Weather> maxDayAsyncJob = weatherListAsyncJob.map(weathers -> findMaxDay(weathers));
AsyncJob<Uri> savedUriAsyncJob = maxDayAsyncJob.flatMap(weather -> apiWrapper.save(weather));
return savedUriAsyncJob;
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
這樣看起來是不是就很清晰了。 這個代碼和剛剛開頭的阻塞式代碼是不是非常相似:
public class WeathersHelper {
Api api;
public Uri saveTheMaxDay(String city){
List<Weather> weathers = api.queryWeathers(city);
Weather max = findMaxDay(weathers);
Uri savedUri = api.save(max);
return savedUri;
}
private Weather findMaxDay(List<Weather> weathers) {
return Collections.max(weathers);
}
}
現在他們不僅邏輯是一樣的,語義上也是一樣的。 太棒了!
同時我們還可以使用組合操作,現在把兩個異步操作組合一起并返還另外一個異步操作。
異常處理也會傳遞到最終的回調接口中。
總結
下面來看看 RxJava 吧。
你沒必要把上面代碼應用到您的項目中去, 這些簡單的、線程不安全的代碼只是 RxJava 的一部分。只有一些名字上的不同:
AsyncJob 等同于 Observable, 不僅僅可以返回一個結果,還可以返回一系列的結果,當然也可能沒有結果返回。
Callback 等同于 Observer, 除了onNext(T t), onError(Throwable t)以外,還有一個onCompleted()函數,該函數在結束繼續返回結果的時候通知Observable 。
abstract void start(Callback callback) 和 Subscription subscribe(final Observer observer) 類似,返回一個Subscription ,如果你不再需要后面的結果了,可以取消該任務。除了 map 和 flatMap 以外, Observable 還有很多其他常見的轉換操作。
下一節,讓我們認識一下RxJava 中關鍵的類。