你的位置:首页 > Java教程

[Java教程]java 不可变类


0.final修饰的类就是不可变类吗?

答:不是.final修饰的类叫不可继承类.两者并无关系.也就是说不可变类的类名可以用final修饰也可以不用.

 

1.不可变类的特点是什么?即什么是不可变类?

答:特点是一旦创建了类的实例,实例内容(状态)不可被修改.典型类就是java中的String.

 

2.既然String是不可变类,为何可以对它重新赋值而改变了其内容?

答:new会在堆中开辟内容," "会缓存在字符串常量池. +会隐式调用toString方法. 拼接相同字符串并不会重新开辟内存

只要记得比较字符串用equals就行了.但是深入理解这个问题将对我们优化程序大有裨益.注意String重写了equals方法,该equals比较对象内容相同就返回true.

== 更严格些,它要求对象引用不变.

 

右上图可知(1) " "的内容相同指同一个对象,这是由于java的String pool的缓存作用.jvm会将" "内的内容作为常量存在pool内,除非显示new一个,否则找pool中有没有,有就指向,如上例的 a == b;没有就在pool里再创建一个.

(2)new出来的String,就算内容相同也是不同的对象. 由c == d可知.

(3)new出来的对象和" "就算内容相同,也不是同一个对象.//很明显这句话可能有误区,""的内容有的书上称为常量池,常量池是编译时就确定了的,常量池里的内容不光是字符串类型,

也就是 说 常量池里并未保存的是对象而是常量. 本质上对象和常量在内存中都是一样的,

(4)由a == e , c== e可知只要new了就不同了.字符串拼接也是先从pool中找.

 

4.final修饰基本类型和引用类型的区别?

答:final修饰基本类型变量不可改变;修饰引用类型,由于其只是保存了一个引用,final只能保证这个引用的地址不会改变,即一直引用同一个对象,对象内容改变与否无关,

注意final修饰引用类型变量和不变类的区别.

一个是引用不变,一个是引用的对象不变.

 

5.那么如何实现不可变类?

答:参考String.

使用private final 修饰该类的field//注意该field如果是基本类型还好,如果是引用类型要保证其不可变,否则将可能导致创建不变类失败.

                                          //filed要为基本类型或不可变类的引用类型

提供带参构造器,用于根据传入的参数来初始化该field

仅仅为该类提供getter方法,不提供setter方法

如有必要重写hashCode和equals方法

该程序来自<<疯狂java>>

class Name {  private String firstName;  private String lastName;  public Name() {}  public Name(String firstName, String lastName) {    this.firstName = firstName;    this.lastName = lastName;  }  public void setFirstName(String firstName) {    this.firstName = firstName;  }  public String getFirstName() {    return this.firstName;  }  public void setLastName(String lastName) {    this.lastName = lastName;  }  public String getLastName() {    return this.lastName;  }}public class Person {  private final Name name;  public Person(Name name) {    this.name = name;//(1)  this.name = new Name(name.getFirstName(), name.getLastName());  }  public Name getName() {    return name; //(2)         return new Name(name.getFirstName(), name.getLastName());
} public static void main(String[] args) { Name n = new Name("wukong", "sun"); Person p = new Person(n); System.out.println(p.getName().getFirstName());//wukong n.setFirstName("bajie"); System.out.println(p.getName().getFirstName()); //bajie }}

可以看到Person类的filed是引用类型的,且在Name类里可以改变其状态.所以Person类并未形成一个不可变类.

书上提供了一种方法将Person类变为不可变类

即把(1)(2)这两句都用匿名对象来处理.

原版本经过Name类来改变Person类里的field,然后从Personl类的构造器返回为改变后的对象. 因此该filed指向的对象容易受到Name类的影响而改变

而改为匿名对象后,从Person类的构造器返回就是临时创建的Name对象,无论你是改变firstName还是lastName,他都只是改变了n指向的firstName.

读者可以在n.setFirstName前一行和后一行分别加上

System.out.println(n.getFirstName());

会发现前后是不一样的.

这就说明n指向的Name对象确实被改变了.

而p的name没改变.

注意:n的filed是两个String类型的;p的filed是一个Name类型的.//类也是一种对象

 

下面定义一个不可变类Address----来自疯狂java

/**private final 基本数据类型 或 不可变类的引用类型提供带参构造器,用于根据传入的参数来初始化该field仅仅为该类提供getter方法,不提供setter方法如有必要重写hashCode和equals方法*/public class Address {  private final String detail;  private final String postCode;    public Address() { //如果类编写了有参构造器通常建议提供一个无参构造器    this.detail = "";    this.postCode = "";  }    public Address(String detail, String postCode) {    this.detail = detail;    this.postCode = postCode;  }    public String getPostCode() {    return this.postCode;  }  public String getDetail() {    return this.detail;  }    public boolean equals(Object obj) {    if(this == obj) {      return true;    }    if (obj != null && obj.getClass() == Address.class) {      Address ad = (Address)obj;      if (this.getDetail().equals(ad.getDetail())         && this.getPostCode().equals(ad.getPostCode())) {          return true;        }    }    return false;  }    public int hashCode() {    return detail.hashCode() + postCode.hashCode() * 31;  }}

由于没提供方法setter方法,所以无法修改Address类的field;

那么难道只要不提供setter方法就可以构成不可变类吗?
不,private final可以保证不可变类创建的实例的field不被改变不被外界访问到.

 

6.不可变类的实例状态不会变化,这样的实例可以安全地被其他与之关联的对象共享,还可以安全地被多个线程共享,为了节约内存空间,应该尽可能重用不可变类的实例,避免重复创建具有相同属性值的实例.那么该如何实现缓存技术呢?缓存技术有很多种,下面用数组作为缓存池,来实现一个缓存实例的不可变类.

 

/*** 此程序用数组模拟缓存池* CachePerson类能控制生成的CachePerson对象个数,只能通过valueof()获取该实例* 如果某个对象只使用一次,那么没必要使用缓存;* 如果某个对象频繁使用,缓存就很必要.* java.lang.Integer采用了相同的策略,如果采用new对象,则每次都是全新的;如果采用valueof则缓存*/class CachePerson {  private static int MAX_SIZE = 10;  //使用数组缓存  private static CachePerson[] cache = new CachePerson[MAX_SIZE];  //记录缓存实例在缓存中的位置:cache[pos-1]是新缓存的实例  private static int pos = 0;  private final String name;    //private构造器,外类无法构造对象  private CachePerson(String name) {    this.name = name;  }    public String getName() {    return name;  }//一旦封装了构造器,一定有public方法来创建对象//static是必要的,因创建对象只能是类方法  public static CachePerson valueof(String name) {    //遍历已缓存的实例    for (int i = 0;i < MAX_SIZE; i++) {      如有相同实例则直接返回改实例      if (cache[i] != null && cache[i].getName().equals(name)) {        return cache[i];      }    }    //缓存已满    if (pos == MAX_SIZE) {      //把缓存的第一个实例覆盖,即把刚生成的对象放在缓存池的开始位置      cache[0] = new CachePerson(name);      pos = 1;    } else {      //新创建的实例缓存起来,pos+1      cache[pos++] = new CachePerson(name);    }    return cache[pos - 1];  }  /*此处equals方法很常用最好记下来:判断两个对象是否相等   == 对于基本数据类型只要值相等就行;对于引用变量,只有指向同一个对象时才true;   == 不可比较没有继承关系的对象  默认的String类的equals方法只要对象的字符序列相等即认为相等;  String类的equals方法是重写了Object类的equals方法,该方法与==区别不大;  一般情况下,我们要求像String类那样只要值相同则认为其相同 @see java.lang.String#964  */  public boolean equals(Object obj) {//判断两个对象是否是同一个对象        if (this == obj) {      return true;    }    //当obj不为null且是CachePerson的实例时    if (obj != null && obj.getClass() == CachePerson.class) {      CachePerson ci = (CachePerson)obj;      return name.equals(ci.getName());    }    return false;  }  public int hashCode() {    return name.hashCode();  }}public class CachePersonTest {  public static void main(String[] args) {    CachePerson c1 = CachePerson.valueof("hello");    CachePerson c2 = CachePerson.valueof("hello");    //output:true    System.out.println(c1 == c2);  }

 

题外话:说实话,关于java中对于String,不可变类,只能懂60%.暂时就写到这了.等以后悟性高了再回头看吧.