看似簡單的Getter/Setter 有多少人踩雷?以此獻上“防坑祕籍”

看似簡單的Getter/Setter 有多少人踩雷?以此獻上“防坑祕籍”

 

Getter/Setter 在 Java 中被廣泛使用。看似簡單,但並非每個 Java 開發人員都能很好理解並正確實現 Getter/Setter 方法。因此,在這篇文章裏,我想深入討論 Java 中的 getter 和 setter 方法,請跟隨我一起來看看吧。

一個簡單的例子

下面的代碼展示了 Getter/Setter 方法的基本使用。

public class GetterAndSetterExample {
 private String name;
 public String getName() {
 return name;
 }
 public void setName(String name) {
 this.name = name;
 }
}

可以看到,我們在類 GetterAndSetterExample 中聲明瞭一個私有變量 name。因爲 name 是私有的,所以我們無法在類外部直接訪問該變量。以下的代碼將無法編譯通過:

GetterAndSetterExample object = new GetterAndSetterExample();
object.name = "yanglbme"; // 編譯出錯
String name = object.name; // 編譯出錯

正確的“姿勢”是調用 getter getName() 和 setter setName() 來讀取或更新變量:

GetterAndSetterExample object = new GetterAndSetterExample();
object.setName("yanglbme");
String name = object.getName();

爲什麼我們需要 Getter/Setter ?

通過使用 Getter/Setter 方法,變量的訪問(get)和更新(set)將變得可控。考慮以下 Setter 方法的代碼:

public void setName(String name) {
 if (name == null || "".equals(name)) {
 throw new IllegalArgumentException();
 }
 this.name = name;
}

這樣可以確保 name 設置的值始終不爲空。倘若可以直接通過 . 操作符設置 name 的值,那麼調用方可以隨意爲 name 設置任何值,這就違反了 name 變量的非空約束。

也就是說,Getter/Setter 方法可以確保變量的值免受外界(調用方代碼)的意外更改。當變量被 private 修飾符隱藏並且只能通過 getter 和 setter 訪問時,它就被“封裝”起來了。封裝是面向對象編程(OOP)的基本特性之一,實現 Getter/Setter 是在程序代碼中強制執行封裝的方法之一。

Getter/Setter 方法的命名約束

Setter 和 Getter 的命名需要遵循 Java bean 的命名約定,如 setXxx() 和 getXxx(),其中 Xxx 是變量的名稱:

public void setName(String name) { }
public String getName() { } // getter

而如果變量是 boolean 類型,那麼 getter 方法可以命名爲 isXxx() 或者 getXxx(),但首選使用前者進行命名:

private boolean single;
public boolean isSingle() { } // getter

Getter/Setter 的常見錯誤實現

錯誤一:實現了 Getter/Setter 方法,但變量不做嚴格的範圍限制

如以下代碼片段所示:

public String name; // 使用public修飾
public void setName(String name) {
 this.name = name;
}
public String getName() {
 return name;
}

變量 name 被聲明爲 public,因此我們可以直接在類外部使用點 . 操作符對其進行訪問,從而使 setter 和 getter 無效。這種情況的解決方法很簡單,直接使用更加“嚴格”的訪問修飾符,例如 protected 和 private。

錯誤二:在 Setter 中直接賦值一個對象引用

考慮以下 Setter 方法:

public class Student {
 private int[] scores;
 // setter
 public void setScores(int[] scores) {
 this.scores = scores;
 }
 
 public void showScores() {
 for (int score : scores) {
 System.out.print(score + " ");
 }
 System.out.println();
 }
}

是不是感覺沒毛病?我們再來看以下代碼:

int[] myScores = {100, 97, 99, 88, 69};
Student yang = new Student();
yang.setScores(myScores);
yang.showScores();
複製代碼

可以看到,整數數組 myScores 先進行了初始化並傳遞給 setScores() 方法,隨後對 scores 進行了簡單打印,產生了以下輸出:

100 97 99 88 69 

現在,我們修改 myScores 數組中第 2 個元素的值,並再次打印 scores:

myScores[1] = 101;
yang.showScores();

程序將會輸出如下:

100 101 99 88 69 

而這樣就意味着我們可以在 Setter 方法之外修改數據,這顯然已經破壞了 Setter 封裝的目的。爲什麼會這樣呢?我們再來看一下 setScores() 方法:

public void setScores(int[] scores) {
 this.scores = scores;
}

成員變量 scores 直接引用了一個參數變量 scores,這意味着兩個變量都指向了內存中同一個對象,即 myScores 數組對象。因此,對 myScores 變量所做的變更將直接導致成員變量 scores 被同步修改。這種情況下,解決辦法是:將方法參數 scores 拷貝一份賦值給成員變量 scores:

public void setScores(int[] scores) {
 this.scores = new int[scores.length];
 System.arraycopy(scores, 0, this.scores, 0, scores.length);
}

經驗總結:如果我們是將對象作爲參數傳遞給 setter 方法,不要直接使用簡單引用賦值的方式。相反,我們應該找到一些方法,將對象的值賦值到內部成員變量中,比如使用 System.arraycopy() 方法將元素從一個數組複製到另一個數組中。

錯誤三:直接返回對象的引用

考慮以下 Getter 方法的實現:

private int[] scores;
public int[] getScores() {
 return scores;
}

在程序中,我們調用 getScores() 方法,並修改其中某個元素的值:

int[] myScores = {100, 97, 99, 88, 69};
Student yang = new Student();
yang.setScores(myScores);
yang.showScores();
int[] copyScores = yang.getScores();
copyScores[3] = 520;
yang.showScores();

將會產生以下輸出:

100 97 99 88 69 
100 97 99 520 69 

正如你所看到的,數組第 4 個元素已經被修改爲 520。這是由於 Getter 方法直接返回了內部成員變量 scores 的引用,因此,外部代碼可以獲取到該引用並對元素進行修改。

這種情況的解決方法是:應該返回對象的副本,而不是直接返回引用:

public int[] getScores() {
 int[] copy = new int[this.scores.length];
 System.arraycopy(this.scores, 0, copy, 0, copy.length);
 return copy; // 返回副本
}

經驗總結:不要在 Getter 方法中返回原始對象的引用。相反,它應該返回原始對象的副本。

實現基本類型的 Getter/Setter 方法

在 Java 中,基本類型有 int, float, double, boolean, char...,你可以直接自由設置或者返回值,因爲 Java 是將一個基本變量的值複製到另一個變量中,而不是複製對象的引用,因此,錯誤二、三都能夠輕鬆避免。

private float amount;
public void setAmount(float amount) {
 this.amount = amount;
}
public float getAmount() {
 return amount;
}

也就是說,對於基本數據類型,用不着一些正確實現 Getter/Setter 的特殊技巧。

實現對象類型的 Getter/Setter 方法

String 對象的 Getter/Setter 方法

String 是一種對象類型,但是它是不可變的,這意味着我們一旦創建了 String 對象,就無法更改其內容。換句話說,對 String 對象的每次更改都會導致新創建一個 String 對象。因此,像原始類型一樣,我們可以安全地爲 String 變量實現 Getter/Setter,就像這樣:

private String address;
public void setAddress(String address) {
 this.address = address;
}
public String getAddress() {
 return address;
}

Date 對象的 Getter/Setter 方法

java.util.Date 類實現了 Object 類中的 clone() 方法。clone() 方法返回對象的副本,因此我們可以將其用於 getter 和 setter,如以下代碼所示:

private Date birthDate;
public void setBirthDate(Date birthDate) {
 this.birthDate = (Date) birthDate.clone();
}
public Date getBirthDate() {
 return (Date) this.birthDate.clone();
}

clone() 方法返回一個 Object 類型的對象,因此我們必須將其強制轉換爲 Date 類型。

Collection 對象的 Getter/Setter 方法

對於 Collection 對象,正如上面錯誤二、三所描述,我們不能這樣簡單實現 Getter/Setter 方法。

private List<String> listTitles;
public void setListTitles(List<String> titles) {
 this.listTitles = titles;
}
public List<String> getListTitles() {
 return listTitles;
}

對於字符串集合,一種解決方法是使用一個構造函數,該構造函數接收另一個集合作爲參數。比如:

public void setListTitles(List<String> titles) {
 // 將titles傳遞給ArrayList的構造函數
 this.listTitles = new ArrayList<String>(titles);
}
public List<String> getListTitles() {
 return new ArrayList<String>(this.listTitles); 
}

注意,上面的構造方法僅僅適用於字符串型的集合。但不適用於 Object 類型的集合。考慮以下示例,我們定義了類 CollectionGetterSetterObject 和 Person:

import java.util.*; 
public class CollectionGetterSetterObject {
 // 元素類型是Person的List集合
 private List<Person> listPeople; 
 public void setListPeople(List<Person> list) { 
 this.listPeople = new ArrayList<Person>(list); 
 } 
 public List<Person> getListPeople() { 
 return new ArrayList<Person>(this.listPeople); 
 } 
} 
class Person { 
 private String name; 
 public Person(String name) { 
 this.name = name; 
 } 
 public String getName() { 
 return name; 
 } 
 public void setName(String name) { 
 this.name = name; 
 } 
 public String toString() { 
 return name; 
 } 
}

對於 String,每複製一個 String 對象都會爲之創建新對象,而其他 Object 類型的對象則不會,它們僅複製引用,因此這就是兩個集合不同但它們包含相同對象的原因。

查看 Collection API,我們發現 ArrayList、HashMap、HashSet 等實現了自己的 clone() 方法。這些方法返回淺表副本,這些淺表副本不會將元素從源 Collection 複製到目標。

比如,ArrayList 類的 clone() 方法的 Javadoc 描述如下:

/**
 * Returns a shallow copy of this <tt>ArrayList</tt> instance. (The
 * elements themselves are not copied.)
 *
 * @return a clone of this <tt>ArrayList</tt> instance
 */
public Object clone() { }

因此,我們不能使用這些 Collection 類的 clone() 方法。解決方案是爲我們自己定義的對象(上例中的 Person 類)實現 clone() 方法。我們在 Person 類中重新實現 clone() 方法,如下所示:

public Object clone() {
 Person aClone = new Person(this.name);
 return aClone;
}

listPeople 的 Setter 方法應該修改爲如下:

public void setListPeople(List<Person> list) {
 for (Person aPerson : list) {
 this.listPeople.add((Person) aPerson.clone());
 }
}

而相應地,Getter 方法也應該被修改,如下所示:

public List<Person> getListPeople() {
 List<Person> listReturn = new ArrayList<Person>();
 for (Person aPerson : this.listPeople) {
 listReturn.add((Person) aPerson.clone());
 }
 return listReturn;
}

因此,新的 CollectionGetterSetterObject 類代碼應該是這樣的:

import java.util.*;
public class CollectionGetterSetterObject {
 private List<Person> listPeople = new ArrayList<Person>();
 public void setListPeople(List<Person> list) {
 for (Person aPerson : list) {
 this.listPeople.add((Person) aPerson.clone());
 }
 }
 public List<Person> getListPeople() {
 List<Person> listReturn = new ArrayList<Person>();
 for (Person aPerson : this.listPeople) {
 listReturn.add((Person) aPerson.clone());
 }
 return listReturn;
 }
}

小結一下,實現 Collection 類型的 Getter/Setter 的關鍵點是:

  • 對於 String 對象的集合,由於 String 對象是不可變的,因此不需要任何特殊的調整。
  • 對於對象的自定義類型的集合:
  • 實現自定義類型的 clone() 方法。
  • 對於 setter,將克隆的項目從源集合添加到目標集合。
  • 對於 getter,創建一個新的 Collection,並將其返回。將原始集合中的克隆項添加到新集合中。

自定義對象的 Getter/Setter 方法

如果定義對象的自定義類型,則應針對自己的類型實現 clone() 方法。

class Person {
 private String name;
 public Person(String name) {
 this.name = name;
 }
 public String getName() {
 return this.name;
 }
 public void setName(String name) {
 this.name = name;
 }
 public String toString() {
 return this.name;
 }
 // 自己實現clone方法
 public Object clone() {
 Person aClone = new Person(this.name);
 return aClone;
 }
}

如我們所見,類 Person 實現了其 clone() 方法以返回其自身的克隆版本。然後,setter 方法應該實現如下:

public void setFriend(Person person) {
 this.friend = (Person) person.clone();
}

而對於 getter 方法:

public Person getFriend() {
 return (Person) this.friend.clone();
}

小結一下,爲自定義對象類型實現 getter 和 setter 的規則是:

  • 爲自定義類型實現一個 clone() 方法。
  • 從 getter 返回一個克隆的對象。
  • 在 setter 中分配一個克隆的對象。

總結

Java 的 Getter/Setter 看起來很簡單,但是如果實現不當,可能會很危險,它甚至可能是導致你代碼行爲異常的問題的根源。或者更糟糕的是,別人可以通過隱式操縱 Getter 或者 Setter 的參數並從中獲取對象來輕易地“蹂躪”你的程序。

請小心使用,避免踩坑。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章