final关键的一个应用场景——不可变类
什么是不可变类?
由不可以变类创建出来的对象是不可以改变的。
对于final修饰引用类型的变量,虽然该变量存储的地址是永远不能改变的,即永远指向对该引用变量执行初始化时候的那个变量。但是变量的本身是可以改变的。而不可变类的作用是使得该类创建出来的对象永远不会改变。如果二者结合就是可以实现引用类型变量永远只能指向一个对象,且对象本身也永远无法再被改变。通过不可变类在配合final修饰符"即完全的实现由final修饰的变量,一但获得初始值之后,该值将永远不会被改变。"
Java中有哪些别人已经定义好的不可变类呢?
包装了和java.lang.String类
如何才能实现不可变类呢?
1、利用private和final关键字修饰该类的成员变量。
使用final修饰的目的是因为final修饰的变量一但获得初始值,改变量将永远不会改变。对于一个对象来说,如果该对象是永远不变的其实指的就是它里面有关该对象的信息是永远不变的,即只要它里面的成员变量无法改变该对象也就无法改变了。
2、提供带参数的构造器,使得通过该构造器在创建对象的时候进行初始化操作。
注意,对于成员变量来说,类成员变量可以在静态初始化块和定义类成员变量的时候进行初始化操作。而对于普通成员变量来说,则可以在定义的时候,普通初始化块和构造器中进行初始化操作,但是注意了,由于final修饰的变量一但被初始化将永远不会改变,同时只有构造器可以实现在创建对象的时候由用户对对象普通成员变量进行初始化操作。无论是定义时进行初始化还是在普通初始化块中都是由程序员在定义类的时候指定的。所以对于不可变类来说,一定要通过带参数的构造器为普通成员变量进行初始化才可以根据用户的需要来创建对象,否则所有创造出来的对象就都完全一样了。
3、只提供gettter()方法不提供setter()方法,个人理解由于不可变类中的成员变量都是由final修饰符修饰的,而setter()方法的主要作用就是修改成员变量用的,而final关键字修饰的变量一但初始化之后数值也就无法被改变了,所以不需要setter()方法。
同时由于成员变量是被private修饰的,所以如果没有getter()方法外界也就无法访问的到该对象的信息了。
4、重写equals()方法和hashCode()方法,要求如果两个对象的通过equals比较相等那么通过hashCode比较也要相等。定义equals方法主要可以实现两个对象的比较。hashCode方法目前还不知道为什么要重写。
看一个不可变类的实例。取自疯狂Java讲义中的例子。
定义一个用来存储地址信息的类,里面有两个字符串类型的成员变量,一个用来存储邮编信息,一个用来存储地址信息。
class Address//定义一个地址类,类名为Address
{
private final String detail;//用来存储详细的地址信息
private final String postCode;//用来存储邮编
public String getDetail(){//定义getter方法
return this.detail;
}
public String getPostCode(){//定义getter方法
return this.postCode;
}
public Address(String detail, String postCode)
{
this.detail = detail;
this.postCode = postCode;
}
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj != null && obj.getClass == Address.class)
{
Address temp = (Address)obj;
if (this.detail.equals(temp.getDetail()) && this.postCode.equalls(temp.getPostCode())){
return true;
}
}
return false;
}
public int hashCode
{
return this.detail.hashCode() +this.postCode.hashCode() * 31;
}//因为定义的两个成员变量都是String类型,该类型以重写好了hashCode()方法,可直接使用。乘以31的目的是乘以任意的一个系数,个人感觉是避免和其他类对象的hashCode重复。假设如果定义的其他类中,成员变量也是两个String类型的,那么如果没有系数的话在重写hashCode
的时候是很容易重现重复的。
}//关于equals的重写博客中有详细解释
与不可变类相对的是可变类,我们通常写的类都是可变类。我们在回一下final修饰引用类型变量的时候,虽然该变量中的值不会改变但是不影响其引用对象的改变。虽然,我们上述定义的Address类,里面的成员是String类型的,属于引用类型变量,但由于String类本身是不可变类所以可以写成上述样子。但是如果类中引用的成员本身是可变类的话,在将该类定义成不可以变类的时候还需要进行相应的加密。
看下面的例子
//定义一个名字类
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 void setLastNanme(String lastName){
this.lastName = lastName;
}
public String getFirstName(){
return this.firstName;
}
public String getLastName(){
return this.lastName;
}
}
//定义一个Person类,用来表示人这个类。
class Person
{
private final Name name;//用来表示人的名字
public Person(Name name){
this.name = name;
}
public Name getName()
{
return this.name;
}
//省略equals方法和hashcode方法
public static void main(String[] args)
{
Name name = new Name(“aaa”, “aaa”);
Person p = new Person(name);
//此时p中name成员存储的信息为aaa bbb
//但是仍然可以改变通过name对象进行改变
name.setter(“ccc”, “ddd”);
}
}
如何解决?
方法一、将Name类也定义成不可改变类,但是指标不治本,而且Name类如果本身就是由特殊需要不允许定义成可变类。接下来主要看第二种方式,这种方式不好理解。
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 void setLastNanme(String lastName){
this.lastName = lastName;
}
public String getFirstName(){
return this.firstName;
}
public String getLastName(){
return this.lastName;
}
}
class Person
{
private final Name name;
public Person(Name name)
{
this.name = new Name(name.getFirstName(),name.getLastName() );//关键步骤一
}
public Name getName(){
return new Name(this.name.getFirstName(), this.name.getLastName());//关键步骤二
}
//在此温习下重写equals方法和hashCode方法,本例主要看上述两个关键步骤
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj != null && obj.getClass() == Person.class){
Person temp = (Person)obj;
if (this.name.getFirstName() == temp.getName().getFirstName() &&
this.name.getLastName() == temp.getName().getLastName())
return true;
}
return false;
}
public int hashCode(){
return this.name.getFirstName().hashCode() + this.name.getLastName().hashCode() * 33;
}
}
接下来重点分析关键步骤一和关键步骤二
先看下此时用Person是如何创建对象的
public class PersonTest
{
public static void main(String[] args){
Name name1 = new Name(“aaa”, “bbb”);
Person p = new Person(name);
Name name2 = p.getName();
}
}
在看一下关键步骤一的代码:
public Person(Name name)
{
name = new Name(name.getFirstName(), name.getLastName());
}
注意,这里的构造器是利用参数变量name,重新的创建除了一个新的Name类对象,只是该新创建出来的对象中的数值和参数变量name指向的对象的数值是一样的。
所以此时Person中的成员变量name和main()函数中的name1变量指向的是不同的Name类对象,只是两个对象中的数值是完全一样的。进而无法在通过name1对成员变量name指向的对象进行修改了。
再看第二段关键代码
public Name getName(){
return new Name(this.name.getFirstName(), this.name.getLastName());
}
在main函数的语句中该语句Name name2 = p.getName();调用的上段代码。注意此时,上述第二段关键代码返回的又是一个全新的对象,只是指该对象的内容和成员变量name的内容是完全一样的。
所以通过上述两段关键代码,使得Person中的成员变量name一但被初始化,那么将永远不会被真正的访问到其本身,每次访问的name成员变量的时候,实际访问的都是利用该对象创建出来的“副本对象”。
个人分析和总结对:
于一个类中的引用类型的成员变量,能够被访问到其对象本身的途径只有两种。
第一种就是通过给其进行初始化的那个引用变量。
例如
public Person(Name name){
this.name = name;
}
//对于这段代码来说,成员变量的name和参数name指向的是同一个对象。所以既可以通过成员变量name修改其所指向的对象,也可以通过参数变量name修饰其所指向的对象。
但是对于代码
public Person(Name name)
{
name = new Name(name.getFirstName(), name.getLastName());
}
此时成员变量name和参数name指向的是不同的对象,所以此时通过参数name是无法修改到成员变量指向的那个对象的。
第二种途径就是利用getter方法
public Name getName()
{
return this.name;
}
Name name1 = p.getName();
注意,如果是这种方式,name1和成员变量指向的又是同一个对象了,所以又可以通过name1来修改成员变量name所指向的那个对象了。
但是改成这种形式
public Name getName()
{
return new Name(this.name.getFirstName(), this.name.getLastName());
}
此时Name name = p.getName();此时name1和成员变量name又指向了不同的对象,只是两个对象的内容都是一样的。
所以通过这种"洁源结流"的方式就可以创建出成员变量为引用数据类型的不可变类了。