泛型(generics)與函式界面(functional interface)做為強型別語言的一對翅膀
今天想來聊點泛型 (generics) 與 函式界面 (functional interface) 在強型別(strong-typed)語言中所扮演的重要角色。
身為Java愛好者、加上從事的工作和語義息息相關,不諱言地對於「強型別」的語言有一種執著。尤其在我們成長的年代,電腦並沒有像今天畫面包裝得美美的視窗防呆界面,對於機器的基底有多「固執」總有一種深刻的體會。若機器只接受整數123,你一不小心給他字串"123",就有可能當場「死」給你看。
而強型別語言的一板一眼有時確實也令人頭痛,就算可以運用多形 (polymorphism) 來增加語義上的彈性,仍然存在不少限制。例如我們可以用下面兩個同名函式來解決上面的問題,同時接受整數和字串:
不過其實它們也就是兩個機器不會搞混的函式罷了,語義的一致性是給人類看的,機器認的是方法簽章(method signature),兩個函式實際上並沒有什麼關聯性。
當泛型和函式界面先後導入Java之後,強型別的優勢可說突然如虎添翼。為了不讓講古佔太大的篇幅,以下直接來研究一個最近實際遇到的例子。
最近因為武漢肺炎的關係,時常透過寫程式研究一些COVID-19的數據,茲舉以下這個存放某個國家某日數據的value object為例:
它包含了三種不同型別(String、int及double)的資料欄位。假設我們寫了一個函式list()用來將存放在List<COVIDValue> data 集合中的所有資料按某種順序列印出來,至於要依什麼順序,則由傳入的參數keyExtractor來決定:
這樣的寫法主要是方便使用像list(COVIDValue::getCases)這樣的方式來呼叫並指定排序的欄位,而這類欄位(cases, todayCases...等等)多為整數,並且我們往往希望先看到數值較高的國家,因此加上了反向排序 reverseOrder() 做為第二參數。
排序是運用Java串流界面(Stream API)中的排序程式sorted,並配合排序器Comparator界面中的方法comparing。這個方法有不同參數的版本,為了加上反向排序,這裡用了兩個參數的版本,其簽章為:
然而當casesPerOneMillion這類帶有小數的欄位出現時,這樣的呼叫方式便會出現型別不一致的問題。雖然Double和Integer具有共同的父類別Number,但這個父類別並沒有實作Comparable,也就是無法直接用來做比較,因此沒有辦法將Integer直接改成Number。
按前面提到的多形原理,我們可能會想再寫一個接受Double做為參數的版本:
以便讓下面兩種呼叫的方式都可以成立:
理論上這樣似乎可行,但在目前的Java語法中,光憑泛型的差異並沒有辦法用來區別方法簽章,也就是說:
這兩個方法對編譯器來說是一模一樣的簽章(都是Function)無法並存,因此會產生編譯失敗。可能有人會想到直接將Integer改成Comparable,這樣做並非不行,但經過sorted()之後COVIDValue的型別會被抹除,若要在串流的後段針對COVIDValue物件的內容做進一步運算處理,會增加許多麻煩。
要解決這個問題,我們同樣可以利用多形,首先我們把之前的方法改寫成更抽象的形式:
接下來加上一個同名方法:
如此一來,我們便可以同時使用型別為Integer與Double的屬性來呼叫,如list(COVIDValue::getCases)、list(COVIDValue::getCasesPerOneMillion)等等,甚至使用型別為字串的屬性如list(COVIDValue::getCountry)也可以行得通。
另外我們也可以用自訂的計算方式來排序,例如:
以上善用泛型與函式界面,讓強型別的Java語言一方面擴增其彈性、另一方面又不犧牲掉型別和語義上的嚴謹性,尤其在大型專案中應可發揮不小的作用,減少許多除錯的成本。
身為Java愛好者、加上從事的工作和語義息息相關,不諱言地對於「強型別」的語言有一種執著。尤其在我們成長的年代,電腦並沒有像今天畫面包裝得美美的視窗防呆界面,對於機器的基底有多「固執」總有一種深刻的體會。若機器只接受整數123,你一不小心給他字串"123",就有可能當場「死」給你看。
而強型別語言的一板一眼有時確實也令人頭痛,就算可以運用多形 (polymorphism) 來增加語義上的彈性,仍然存在不少限制。例如我們可以用下面兩個同名函式來解決上面的問題,同時接受整數和字串:
int inc(int n) { return ++n; } int inc(String n) { return Integer.valueOf(n)+1; }
不過其實它們也就是兩個機器不會搞混的函式罷了,語義的一致性是給人類看的,機器認的是方法簽章(method signature),兩個函式實際上並沒有什麼關聯性。
當泛型和函式界面先後導入Java之後,強型別的優勢可說突然如虎添翼。為了不讓講古佔太大的篇幅,以下直接來研究一個最近實際遇到的例子。
最近因為武漢肺炎的關係,時常透過寫程式研究一些COVID-19的數據,茲舉以下這個存放某個國家某日數據的value object為例:
class COVIDValue { String country; int recovered, cases, critical, active, deaths, todayCases, todayDeaths; double casesPerOneMillion, deathsPerOneMillion; // constructors, getters... }
它包含了三種不同型別(String、int及double)的資料欄位。假設我們寫了一個函式list()用來將存放在
void list(Function<COVIDValue, Integer> keyExtractor) { data.stream() .sorted(Comparator.comparing(keyExtractor, Comparator.reverseOrder())) .forEach(System.out::println); }
這樣的寫法主要是方便使用像list(COVIDValue::getCases)這樣的方式來呼叫並指定排序的欄位,而這類欄位(cases, todayCases...等等)多為整數,並且我們往往希望先看到數值較高的國家,因此加上了反向排序 reverseOrder() 做為第二參數。
排序是運用Java串流界面(Stream API)中的排序程式sorted,並配合排序器Comparator界面中的方法comparing。這個方法有不同參數的版本,為了加上反向排序,這裡用了兩個參數的版本,其簽章為:
static <T,U> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor, Comparator<? super U> keyComparator)
然而當casesPerOneMillion這類帶有小數的欄位出現時,這樣的呼叫方式便會出現型別不一致的問題。雖然Double和Integer具有共同的父類別Number,但這個父類別並沒有實作Comparable,也就是無法直接用來做比較,因此沒有辦法將Integer直接改成Number。
按前面提到的多形原理,我們可能會想再寫一個接受Double做為參數的版本:
void list(Function<COVIDValue, Double> keyExtractor) { /* ... */ }
以便讓下面兩種呼叫的方式都可以成立:
list(COVIDValue::getCases); list(COVIDValue::getCasesPerOneMillion);
理論上這樣似乎可行,但在目前的Java語法中,光憑泛型的差異並沒有辦法用來區別方法簽章,也就是說:
void list(Function<COVIDValue, Integer> keyExtractor) { /* ... */ } void list(Function<COVIDValue, Double> keyExtractor) { /* ... */ }
這兩個方法對編譯器來說是一模一樣的簽章(都是Function)無法並存,因此會產生編譯失敗。可能有人會想到直接將Integer改成Comparable,這樣做並非不行,但經過sorted()之後COVIDValue的型別會被抹除,若要在串流的後段針對COVIDValue物件的內容做進一步運算處理,會增加許多麻煩。
要解決這個問題,我們同樣可以利用多形,首先我們把之前的方法改寫成更抽象的形式:
void list(Comparator<COVIDValue> comparator) { data.stream().sorted(comparator).forEach(System.out::println); }
接下來加上一個同名方法:
void list(Function<COVIDValue, Comparable> keyExtractor) { list(Comparator.comparing(keyExtractor, Comparator.reverseOrder()); }
如此一來,我們便可以同時使用型別為Integer與Double的屬性來呼叫,如list(COVIDValue::getCases)、list(COVIDValue::getCasesPerOneMillion)等等,甚至使用型別為字串的屬性如list(COVIDValue::getCountry)也可以行得通。
另外我們也可以用自訂的計算方式來排序,例如:
list(value->1.0*value.getDeaths()/value.getCases());
以上善用泛型與函式界面,讓強型別的Java語言一方面擴增其彈性、另一方面又不犧牲掉型別和語義上的嚴謹性,尤其在大型專案中應可發揮不小的作用,減少許多除錯的成本。
留言