可變類與不可變類的區別
所謂不可變類,是指當建立了這個類的例項後,就不允許修改它的屬性值。在JDK的基本類庫中,所有基本型別的包裝類,如Integer和Long類,都是不可變類,java.lang.String也是不可變類。以下程式碼建立了一個String物件和Integer物件,它們的值分別為“Hello”和 10,在程式程式碼中無法再改變這兩個物件的值,因為Integer和String類沒有提供修改其屬性值的介面。
String s=new String("Hello");
Integer i=new Integer(10);
使用者在建立自己的不可變類時,可以考慮採用以下設計模式:
1..把屬性定義為private final型別。
2.不對外公開用於修改屬性的setXXX()方法。
public Name(String firstname, String lastname) {
3.只對外公開用於讀取屬性的getXXX()方法。
4.在構造方法中初始化所有屬性。
5.覆蓋Object類的equals()和hashCode()方法。在equals()方法中根據物件的屬性值來比較兩個物件是否相等,並且保證用 equals()方法判斷為相等的兩個物件的hashCode()方法的返回值也相等,這可以保證這些物件能正確地放到HashMap或HashSet集合中。
如果需要的話,提供例項快取和靜態工廠方法,允許使用者根據特定引數獲得與之匹配的例項。
例程11-9的Name類就是不可變類,它僅僅提供了讀取sex和description屬性的getXXX()方法,但沒有提供修改這些屬性的setXXX()方法。
例程11-9 Name.java
public class Name {
private final String firstname;
private final String lastname;
this.firstname = firstname;
this.lastname = lastname;
}
public String getFirstname(){
return firstname;
}
public String getLastname(){
return lastname;
}
public boolean equals(Object o){
if (this == o) return true;
if (!(o instanceof Name)) return false;
final Name name = (Name) o;
if(!firstname.equals(name.firstname)) return false;
if(!lastname.equals(name.lastname)) return false;
return true;
}
public int hashCode(){
int result;
result= (firstname==null?0:firstname.hashCode());
result = 29 * result + (lastname==null?0:lastname.hashCode());
return result;
}
public String toString(){
return lastname+" "+firstname;
}
}
假定Person類的name屬性定義為Name型別:
public class Person{
private Name name ;
private Gender gender;
…
}
以下程式碼建立了兩個Person物件,他們的姓名都是“王小紅”,一個是女性,另一個是男性。在最後一行程式碼中,把第一個Person物件的姓名改為“王小虹”。
Name name=new Name("小紅","王");
Person person1=new Person(name,Gender.FEMALE);
Person person2=new Person(name,Gender.MALE);
name=new Name("小虹","王");
person1.setName(name); //修改名字
與不可變類對應的是可變類,可變類的例項屬性是允許修改的。如果把以上例程11-9的Name類的firstname屬性和lastname屬性的 final修飾符去除,並且增加相應的public型別的setFirstname()和setLastname()方法,Name類就變成了可變類。以下程式程式碼本來的意圖也是建立兩個Person物件,他們的姓名都是“王小紅”,接著把第一個Person物件的姓名改為“王小虹”:
//假定以下Name類是可變類
Name name=new Name("小紅","王");
Person person1=new Person(name,Gender.FEMALE);
Person person2=new Person(name,Gender.MALE);
name.setFirstname(" 小虹"); //試圖修改第一個Person物件的名字
以上最後一行程式碼存在錯誤,因為它會把兩個Person物件的姓名都改為“王小虹”。由此可見,使用可變類更容易使程式程式碼出錯。因為隨意改變一個可變類物件的狀態,有可能會導致與之關聯的其他物件的狀態被錯誤地改變。
不可變類的例項在例項的整個生命週期中永遠保持初始化的狀態,它沒有任何狀態變化,簡化了與其他物件之間的關係。不可變類具有以下優點:
l 不可變類能使程式更加安全,不容易出錯。
l 不可變類是執行緒安全的,當多個執行緒訪問不可變類的同一個例項時,無須進行執行緒的同步。
由此可見,應該優先考慮把類設計為不可變類,假使必須使用可變類,也應該把可變類儘可能多的屬性設計為不可變的,即用final修飾符來修飾,並且不對外公開用於改變這些屬性的方法。
在建立不可變類時,假如它的屬性的型別是可變型別,在必要的情況下,必須提供保護性拷貝,否則,這個不可變類例項的屬性仍然有可能被錯誤地修改。這條建議同樣適用於可變類中用final修飾的屬性。
例如例程11-10的Schedule類包含學校的開學時間和放假時間資訊,它是不可變類,它的兩個屬性start和end都是final型別,表示不允許被改變,但是這兩個屬性都是Date型別,而Date類是可變類。
例程11-10 Schedule.java
import java.util.Date;
public final class Schedule{
private final Date start; //開學時間,不允許被改變
private final Date end; //放假時間,不允許被改變
public Schedule(Date start,Date end){
//不允許放假日期在開學日期的前面
if(start.compareTo(end)>0)
throw new IllegalArgumentException(start +" after " +end);
this.start=start;
this.end=end;
}
public Date getStart(){return start;}
public Date getEnd(){return end;}
}
儘管以上Schedule類的start和end屬性是final型別的,但由於它們引用Date物件,在程式中可以修改所引用Date物件的屬性。以下程式程式碼建立了一個Schedule物件,接下來把開學時間和放假時間都改為當前系統時間。
Calendar c= Calendar.getInstance();
c.set(2006,9,1);
Date start=c.getTime();
c.set(2007,1,25);
Date end=c.getTime();
Schedule s=new Schedule(start,end);
end.setTime(System.currentTimeMillis()); //修改放假時間
start=s.getStart();
start.setTime(System.currentTimeMillis()); //修改開學時間
為了保證Schedule物件的start屬性和end屬性值不會被修改,必須為這兩個屬性使用保護性拷貝,參見例程11-11。
例程11-11 採用了保護性拷貝的Schedule.java
import java.util.Date;
public final class Schedule {
private final Date start;
private final Date end;
public Schedule(Date start,Date end){
//不允許放假日期在開學日期的前面
if(start.compareTo(end)>0)throw new IllegalArgumentException(start +" after " +end);
this.start=new Date(start.getTime()); // 採用保護性拷貝
this.end=new Date(end.getTime()); // 採用保護性拷貝
}
public Date getStart(){return (Date)start.clone() ;} // 採用保護性拷貝
public Date getEnd(){return (Date)end.clone() ;} // 採用保護性拷貝
}
通過採用保護性拷貝,其他程式無法獲得與Schedule物件關聯的兩個Date物件的引用,因此也就無法修改這兩個Date物件的屬性值。