1. 程式人生 > >可變類與不可變類的區別

可變類與不可變類的區別

 所謂不可變類,是指當建立了這個類的例項後,就不允許修改它的屬性值。在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物件的屬性值。