1. 程式人生 > >建立物件不優雅?Java大牛手把手教你如何建立優雅的物件

建立物件不優雅?Java大牛手把手教你如何建立優雅的物件

建立物件不優雅?Java大牛手把手教你如何建立優雅的物件

 

在 Java 中有多種方式可以建立物件,總結起來主要有下面的 4 種方式:

正常建立。通過 new 操作符

反射建立。呼叫 Class 或 java.lang.reflect.Constructor 的 newInstance()方法

克隆建立。呼叫現有物件的 clone()方法

發序列化。呼叫 java.io.ObjectInputStream 的 getObject()方法反序列化

Java 物件的建立方式是其語法明確規定,使用者不可能從外部改變的。本文仍然要使用上面

的方式來建立物件,所以本文只能說是構建物件,而非建立物件也。

假設有這樣一個場景,現在要構建一個大型的物件,這個物件包含許多個引數的物件,有

些引數有些是必填的,有些則是選填的。那麼如何構建優雅、安全地構建這個物件呢?

單一建構函式

通常,我們第一反應能想到的就是單一建構函式方式。直接 new 的方式構建,通過構造函

數來傳遞引數,見下面的程式碼:

/***

* 單一建構函式

*/

public class Person {

// 姓名(必填)

private String name;

// 年齡(必填)

private int age;

// 身高(選填)

private int height;

// 畢業學校(選填)

private String school;

// 愛好(選填)

private String hobby;
 public Person(String name, int age, int height, String school, String hobby) {
 this.name = name;
 this.age = age;
 this.height = height;
 this.school = school;
 this.hobby = hobby;
 }
}

上面的構建方式有下面的缺點:

有些引數是可以選填的(如 height, school),在構建 Person 的時候必須要傳入可能並不需

要的引數。

現在上面才 5 個引數,建構函式就已經非常長了。如果是 20 個引數,建構函式都可以直接

上天了!

構建的這樣的物件非常容易出錯。客戶端必須要對照 Javadoc 或者引數名來講實參傳入對

應的位置。如果引數都是 String 型別的,一旦傳錯引數,編譯是不會報錯的,但是執行結

果卻是錯誤的。

多建構函式

對於第 1 個問題,我們可以通過建構函式過載來解決。見下面的程式碼:

/***

* 多建構函式

*/

public class Person {

// 姓名(必填)

private String name;

// 年齡(必填)

private int age;

// 身高(選填)

private int height;

// 畢業學校(選填)

private String school;

// 愛好(選填)

private String hobby;
 public Person(String name, int age) {
 this.name = name;
 this.age = age;
 }
 public Person(String name, int age, int height) {
 this.name = name;
 this.age = age;
 this.height = height;
 }
 public Person(String name, int age, int height, String school) {
this.name = name;
 this.age = age;
 this.height = height;
 this.school = school;
 }
 public Person(String name, int age, String hobby, String school) {
 this.name = name;
 this.age = age;
 this.hobby = hobby;
 this.school = school;
 }
}

上面的方式確實能在一定程度上降低建構函式的長度,但是卻有下面的缺陷:

導致類過長。這種方式會使得 Person 類的建構函式成階乘級增長。按理來說,應該要寫的

建構函式數是可選成員變數的組合數(實際並沒有這麼多,原因見第 2 點)。如果讓我呼叫

這樣的類,絕對會在心裡默唸 xx!!

有些引數組合無法重構。因為 Java 中過載是有限制的,相同方法簽名的方法不能構成重

載,編譯時無法通過。譬如包含(name, age, school)和(name, age, hobby)的建構函式是不

能過載的,因為 shcool 和 hobby 同為 String 型別。Java 只認變數的型別,管你變數是什麼

含義呢。 (看臉的社會唉)

建立物件不優雅?Java大牛手把手教你如何建立優雅的物件

 

JavaBean 方式

上面的方法不行,莫急!還有法寶——JavaBean。一個物件的構建通過多個方法來完成。

直接見下面的程式碼:

public class Person {

// 姓名(必填)

private String name;

// 年齡(必填)

private int age;

// 身高(選填)

private int height;

// 畢業學校(選填)

private String school;

// 愛好(選填)

private String hobby;
 public Person(String name, int age) {
this.name = name;
 this.age = age;
 }
 public void setHeight(int height) {
 this.height = height;
 }
 public void setSchool(String school) {
 this.school = school;
 }
 public void setHobby(String hobby) {
 this.hobby = hobby;
 }
}

客戶端使用這個物件的程式碼如下:

public class Client {
 public static void main(String[] args) {
 Person person = new Person("james", 12);
 person.setHeight(170);
 person.setHobby("reading");
 person.setSchool("xxx university");
 }
}

這樣看起來完美的解決了 Person 物件構建的問題,使用起來非常優雅便捷。確實,在單一

執行緒的環境中這確實是一個非常好的構建物件的方法,但是如果是在多執行緒環境中仍有其

致命缺陷。在多執行緒環境中,這個物件不能安全地被構建,因為它不是不可變物件。一旦

Person 物件被構建,我們隨時可通過 setXXX()方法改變物件的內部狀態。假設有一個執行緒

正在執行與 Person 物件相關的業務方法,另外一個執行緒改變了其內部狀態,這樣得到莫名

其妙的結果。由於執行緒執行的無規律性,使得這問題有可能不能重現,這個時候真的就只

能哭了。(程式設計師真苦逼。。。)

Builder 方式

為了完美地解決這個問題,下面引出本文中的主角(等等等等!)。我們使用構建器

(Builder)來優雅、安全地構建 Person 物件。廢話不說,直接程式碼:

/**

* 待構建的物件。該物件的特點:

* <ol>

* <li>需要使用者手動的傳入多個引數,並且有多個引數是可選的、順序隨意</li>

* <li>該物件是不可變的(所謂不可變,就是指物件一旦建立完成,其內部狀態不可變,

更通俗的說是其成員變數不可改變)。

* 不可變物件本質上是執行緒安全的。</li>

* <li>物件所屬的類不是為了繼承而設計的。</li>

* </ol>

* 滿足上面特點的物件的構建可是使用下面的 Build 方式構建。這樣構建物件有下面的好

處:

* <ol>

* <li>不需要寫多個建構函式,使得物件的建立更加便捷</li>

* <li>建立物件的過程是執行緒安全的</li>

* </ol>
* @author xialei
* @date 2015-5-2
*/
public class Person {

// 姓名(必填),final 修飾 name 一旦被初始化就不能再改變,保證了物件的不可變

性。

private final String name;

// 年齡(必填)

private final int age;

// 身高(選填)

private final int height;

// 畢業學校(選填)

private final String school;

// 愛好(選填)

private final String hobby;

/**

* 這個私有建構函式的作用:

* <ol>

* <li>成員變數的初始化。final 型別的變數必須進行初始化,否則無法編譯成功</li>

* <li>私有建構函式能夠保證該物件無法從外部建立,並且 Person 類無法被繼承</li>

* </ol>
 */
 private Person(String name, int age, int height, String school, String hobby) {
 this.name = name;
 this.age = age;
 this.height = height;
 this.school = school;
 this.hobby = hobby;
}
 /**

* 要執行的動作

*/
 public void doSomething() {
 // TODO do what you want!!
 }
 /**

* 構建器。為什麼 Builder 是內部靜態類?

* <ol>

* <li>必須是 Person 的內部類。否則,由於 Person 的建構函式私有,不能通過 new 的

方式建立 Person 物件</li>

* <li>必須是靜態類。由於 Person 物件無法從外部建立,如果不是靜態類,則外部無

法引用 Builder 物件。</li>

* </ol>

* <b>注意</b>:Builder 內部成員變數要與 Person 的成員變數保持一致。

* @author xialei

*

*/

public static class Builder {

// 姓名(必填)。注意:這裡不能是 final 的

private String name;

// 年齡(必填)

private int age;

// 身高(選填)

private int height;

// 畢業學校(選填)

private String school;

// 愛好(選填)

private String hobby;
 public Builder(String name, int age) {
 this.name = name;
 this.age = age;
 }
 public Builder setHeight(int height) {
 this.height = height;
return this;
 }
 public Builder setSchool(String school) {
 this.school = school;
 return this;
 }
 public Builder setHobby(String hobby) {
 this.hobby = hobby;
 return this;
 }
 /**

* 構建物件

* @return 返回待構建的物件本身

*/
 public Person build() {
 return new Person(name, age, height, school, hobby);
 }
 }
}

客戶端構建物件的方式見下面的程式碼:

/**

* 使用 Person 物件的客戶端

* @author xialei
* @date 2015-5-2
*/
public class Client {
 public static void main(String[] args) {
 /*

* 通過鏈式呼叫的方式建立 Person 物件,非常優雅!

*/
 Person person = new Person.Builder("james", 12)
 .setHeight(170)
 .setHobby("reading")
 .build();
 person.doSomething();
 }
}

如果不想看程式碼,可看下面對於上面程式碼的總結:

通過 private Person(..)使得 Person 類不可被繼承

通過將 Person 類的成員變數設定為 final 型別,使得其不可變

通過 Person 內部的 static Builder 類來構建 Person 物件

通過將 Builder 類內部的 setXXX()方法返回 Builder 型別本身,實現鏈式呼叫構建 Person 對

總結

至此,我們就相對完美地解決這一型別的物件建立問題!下面來總結一下本文的重點。待

建立的物件特點:

需要使用者手動的傳入多個引數,並且有多個引數是可選的、順序任意

物件不可變

物件所屬的類不是為了繼承而設計的。即類不能被繼承

依次使用的物件構建方法:

單一建構函式

多建構函式

JavaBean 方式

Builder 方式

最終,通過比較得出 Builder 方法最