Java Getter/Setter 「防坑指南」 來了

本文由 yanglbme 原創,首發於公衆號「Doocs開源社區」,歡迎轉載。java

Getter/Setter 在 Java 中被普遍使用。看似簡單,但並不是每一個 Java 開發人員都能很好理解並正確實現 Getter/Setter 方法。所以,在這篇文章裏,我想深刻討論 Java 中的 getter 和 setter 方法,請跟隨我一塊兒來看看吧。git

一個簡單的例子

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

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 變量的非空約束。bash

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

Getter/Setter 方法的命名約束

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

public void setName(String name) { }

public String getName() { } // getter
複製代碼

而若是變量是 boolean 類型,那麼 getter 方法能夠命名爲 isXxx() 或者 getXxx(),但首選使用前者進行命名:spa

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 是將一個基本變量的值複製到另外一個變量中,而不是複製對象的引用,所以,錯誤2、三都可以輕鬆避免。

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 對象,正如上面錯誤2、三所描述,咱們不能這樣簡單實現 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 類型的集合。考慮如下示例,咱們定義了類 CollectionGetterSetterObjectPerson

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 的參數並從中獲取對象來輕易地「蹂躪」你的程序。

請當心使用,避免踩坑。

相關文章
相關標籤/搜索