Java中的集合和封裝
面向物件程式設計的核心租戶是封裝:不應允許呼叫者直接訪問類的欄位。這是新的語言,包括Kotlin,Swift和Ceylon,已經很好地解決了一流的屬性。
Java沒有第一類屬性的概念。相反,JavaBeans規範是作為Java執行封裝的方法引入的。編寫JavaBeans意味著您需要將類的欄位設定為私有,僅通過getter和setter方法公開它們。
如果你像我一樣,你在編寫JavaBeans時經常會感覺到你正在編寫一堆很少用於任何實際目的的理論樣板。我的大多數JavaBeans都包含私有欄位及其相應的getter和setter,它們只能獲取和設定這些私有欄位。不止一次,我一直試圖簡單地將這些領域公之於眾,並免除了getter / setter的大肆宣傳,至少在IDE發出嚴厲的警告後,我的雙腿之間的尾巴,以及JavaBeans標準。
但是,最近,我意識到封裝和JavaBean / getter / setter模式在常見場景中非常有用:集合型別欄位。怎麼會這樣?讓我們編寫一個簡單的類:
公共 類 MyClass {
private List < String > myStrings ;
}
我們有一個欄位 - 一個字串列表 - 呼叫 myStrings
,它被封裝在中MyClass
。現在,我們需要提供訪問器方法:
公共 類 MyClass {
private List < String > myStrings ;
public void setMyStrings(List < String > s){
這個。myStrings = s ;
}
public List < String > getMyStrings(){
歸還 這個。myStrings ;
}
}
在這裡,我們有一個適當封裝的 - 如果不是詳細的 - 類。所以我們做得很好,對嗎?堅持這個想法。
可選課程
考慮在Java 8中引入的Optional類。如果你已經使用Optionals做了很多工作,你可能聽說過你應該永遠不會null
從返回Optional的方法返回的咒語 。為什麼?考慮以下人為的例子:
公共 課 Foo {
私人 字串 欄 ;
public 可選< String > getBar(){
return(bar == null)? null:可選。的(巴);
}
}
現在,客戶可以這樣使用該方法:
foo。getBar()。ifPresent(log :: info);
冒險投擲 NullPointerException
。或者,他們可以執行 null
檢查:
如果(FOO。getBar()!= 空){
foo。getBar()。ifPresent(log :: info);
}
當然,這樣做會擊敗Optionals的目的。事實上,這樣違背了選配的目的,它的成為標準做法是,返回可選將任何API 從未返回一個 null
值。
回到收藏。就像Optional包含none或one一樣,Collection包含none或some。和Optionals一樣,沒有理由返回 null
集合(除了可能在罕見的,特殊情況下,我目前無法想到的任何情況)。只需返回一個空(零大小)Collection即可指示缺少任何元素。
正是由於這個原因,確保返回Collection型別(包括陣列)的方法永遠不會返回null
值變得越來越常見 ,與返回Optional型別的方法相同。也許您或您的組織已經在編寫新程式碼時採用了此規則。如果沒有,你應該。畢竟,你(或你的客戶)會這麼做嗎?:
boolean isUnique = personDao。getPersonsByName(name)。size()== 1 ;
或者,讓你的程式碼亂七八糟?:
列表< 人> 人 = 人道。getPersonsByName(name);
boolean isUnique =(persons == null)? 虛假:人。size()== 1 ;
那麼這與封裝有什麼關係呢?
保持對我們收藏的控制
回到我們MyClass
班。實際上,一個例項MyClass
很容易null
從該getMyStrings()
方法返回 ; 事實上,一個新的例項就是這樣做的。因此,為了遵守我們新的永不迴歸零收集指南,我們需要解決這個問題:
公共 類 MyClass {
private List < String > myStrings = new ArrayList <>();
public void setMyStrings(List < String > s){
這個。myStrings = s ;
}
public List < String > getMyStrings(){
歸還 這個。myStrings ;
}
}
問題解決了?不完全是。任何客戶都可以打電話aMyClass.setMyStrings(null)
,在這種情況下我們會回到原點。
在這一點上,封裝聽起來像一個實際的 - 而不是單獨的理論 - 概念。讓我們擴充套件setMyStrings()
方法:
public void setMyStrings(List < String > s){
if(s == null){
這個。myStrings。clear();
} else {
這個。myStrings = s ;
}
}
現在,即使 null
傳遞給setter, myStrings
也會保留一個有效的引用(在這裡的例子中,我們 null
認為應該清除元素,這是一個合理的假設)。當然,呼叫aMyClass.getMyStrings() = null
對MyClass
“基礎myStrings
變數” 沒有影響 。我們都這樣做了嗎?
呃,好吧,有點兒。我們可以在這裡停下來 但實際上,我們應該做的更多。
考慮到我們正在取代我們的私人 ArrayList
與 List
呼叫者傳遞給我們。這有兩個問題:首先,我們不再知道所List
使用的確切 實現 myStrings
。從理論上講,這應該不是問題,對嗎?好吧,考慮一下:
myClass。setMyStrings(收藏。unmodifiableList(“嘿,疑難雜症!” ));
因此,如果我們更新MyClass
它試圖修改內容 myStrings
,那麼壞事可能會在執行時開始發生。
第二個問題是呼叫者保留對我們的底層的引用 List
。所以現在,呼叫者現在可以直接操縱我們的 List
。
我們應該做的是將傳遞給我們的元素儲存在 初始化的 ArrayList
to中 myStrings
。雖然我們正在努力,但我們真正擁抱封裝。我們應該從外部呼叫者隱藏我們班級的內部。實際情況是我們類的呼叫者不應該關心是否存在底層List,Set,陣列或一些執行時動態程式碼生成voodoo,它們儲存我們傳遞給它的字串。他們應該知道的是,字串以某種方式儲存。所以讓我們setMyStrings()
這樣更新方法:
public void setMyStrings(Collection < String > s){
這個。myStrings。clear();
if(s != null){
這個。myStrings。addAll(s);
}
}
這樣可以確保 myStrings
以輸入引數中包含的相同元素結束(如果傳遞null,則為 空),同時確保呼叫者沒有引用 myStrings
。
現在 myStrings
'參考不能改變,讓我們把它變成一個常數:
公共 類 MyClass {
private final List < String > myStrings = new ArrayList <>();
...
}
雖然我們在這裡,但我們不應該通過我們的getter返回我們的基礎List。這也會使呼叫者直接引用myStrings。為了解決這個問題,請回想一下有效Java擊敗我們頭腦的“防禦性副本”口號(或至少應該有):
public List < String > getMyStrings(){
//取決於我們想要返回的確切內容
返回 新 的ArrayList <> (此。myStrings);
}
此時,我們有一個封裝良好的類,null
無論何時呼叫其getter ,都不需要 -checking。但是,我們已經從客戶那裡獲得了一些控制權。由於他們不再可以直接訪問我們的基礎列表,因此他們不能再新增或刪除單個字串。
沒問題。如果我們可以簡單地新增像
public void addString(String s){
這個。myStrings。新增(小號);
}
和
public void removeString(String s){
這個。myStrings。除去(小號);
}
我們的呼叫者是否需要一次向MyClass
例項新增多個字串?那也沒關係:
public void addStrings(Collection < String > c){
if(c != null){
這個。myStrings。addAll(c);
}
}
等等...
public void clearStrings(){
這個。myStrings。clear();
}
public void replaceStrings(Collection < String > c){
clearStrings();
addStrings(c);
}
收集我們的想法
以下是我們班級最終的樣子:
公共 類 MyClass {
private final List < String > myStrings = new ArrayList <>();
public void setMyStrings(Collection < String > s){
這個。myStrings。clear();
if(s != null){
這個。myStrings。addAll(s);
}
}
public List < String > getMyStrings(){
返回 新 的ArrayList <> (此。myStrings);
}
public void addString(String s){
這個。myStrings。新增(小號);
}
public void removeString(String s){
這個。myStrings。除去(小號);
}
//也許還有一些更有幫助的方法......
}
有了這個,我們實現了一個類:
- 仍然基本上是一個符合JavaBean規範的POJO
- 完全封裝其私人成員
並且,最重要的是,它確保其返回Collection的方法始終只執行 - 返回Collection並且永遠不會返回null
。