软件构造——不可变

目录

1. 数据类型

2. 改变

3. 不变对象

4. 关键字final

5. 不变量Invariants

6. 例子

1. 数据类型

基本数据类型primitive只有值,没有IDimmutable栈中分配int,char,double等小写
对象数据类型object即有值,又有ID不确定堆中分配String,Integer等首字母大写

基本数据类型封装成对象数据类型:Boolean,Integer,Character,……

基本数据类型及其封装都是不可变immutable类型。

2. 改变

改变变量:将当前变量指向另一个存储空间;

改变变量的值:在当前变量的存储空间写入新值。

3. 不变对象

不变数据类型:值不可变;

不变引用类型:引用不可变。

4. 关键字final

利用关键字final来强制引用不可变。

但是当编译器无法确定声明为final的变量不会改变,就会提示错误。这种机制属于静态类型检查。

静态类型检查:在编译时发现错误,例如返回类型错误、参数类型错误等;

动态类型检查:在运行时发现错误,例如分母为0,越界错误等。

注意:

对可变值的不可变引用:指向同一个对象,但是值可以改变,例如:

private final Date date;

date被强制指向同一个引用,但是其指向的值可以被改变。

同样的,对不可变值的可变引用允许其指向不同的对象

因此为了维护不变性,不仅要声明private final,同时要尽量采用immutable类型的数据。

5. 不变量Invariants

首先明确不变性是典型的不变量。在程序的生命周期中,需要维护不变量始终为true。不变量由抽象数据类型ADT表示(不是Java语言中的抽象类、抽象方法的意思)。

定义两个值的集合:

R:表示空间,被ADT的开发者所关注;

A:抽象空间,被client客户端所关注。

在此基础上,定义AF:R→A为抽象函数abstract function,来具象“合法”的要求;

表示不变量Rep Invariant:RI:R→boolean,来验证表示是否“合法”。

6. 例子

聚焦于下列的程序(部分):

./User.java:

public class User
{
    public static List<Tweet> tw()
    {
	    List<Tweet> list=new ArrayList<Tweet>();
	    Date date=new Date();
	    for(int i=0; i<4; i++)
	    {
	        date.setTime(10000*i);
	        list.add(new Tweet("Typer", date));
	    }
	    return list;
    }
    
    public static void main(String[] args)
    {
	    List<Tweet> list=tw();
	    for(Tweet tt: list)
	    {
	        System.out.println(tt.getAuthor());
	        System.out.println(tt.getTime());
	    }
    }
}

./Tweet.java:

public class Tweet
{
    private final String author;
    private final Date time;
    
    public Tweet(String author, Date time)
    {
	    this.author=author;
	    this.time=time;
    }
    
    public String getAuthor()
    {
	    return author;
    }
    
    public Date getTime()
    {
	    return new Date(time.getTime());
    }
}

我们可以看到Tweet.java中的getTime方法做了一个简单的防御式拷贝,来避免表示泄露。

方法的目的是打印4个时间段的推文信息(作者,时间)。

输出结果如下:

Typer
Thu Jan 01 08:00:30 CST 1970
Typer
Thu Jan 01 08:00:30 CST 1970
Typer
Thu Jan 01 08:00:30 CST 1970
Typer
Thu Jan 01 08:00:30 CST 1970

这并不出人意料,因为已经在第4节中简要的描述了:对于可变值的不可变引用,使其指向同一个对象,但是对象的值可以被改变。这里要求我们严格区分引用不变与对象不变。

我们看到User.java中的tw()方法只声明了一次date,导致每次for循环执行时,date始终指向同一个引用,而其中的值始终在改变,因此每次new的Tweet中的time字段始终在一个引用上且值一直在改变。

解决方案如下:

一种我们修改client端的方法:

public class User
{
    public static List<Tweet> tw()
    {
		List<Tweet> list=new ArrayList<Tweet>();
		for(int i=0; i<4; i++)
		{
		    Date date=new Date();
		    date.setTime(10000*i);
		    list.add(new Tweet("Typer", date));
		}
		return list;
    }
    
    ……
}

在每个循环体内重新声明一遍date,但是这要求client有极高的“道德”,因此推荐第二种方法:

修改开发端的方法:

public class Tweet
{
    private final String author;
    private final Date time;
    
    public Tweet(String author, Date time)
    {
		this.author=author;
		this.time=new Date(time.getTime());
    }
	……
}

在接收的时候,声明一个新的Date类型的变量。

两种方法的输出如下:

Typer
Thu Jan 01 08:00:00 CST 1970
Typer
Thu Jan 01 08:00:10 CST 1970
Typer
Thu Jan 01 08:00:20 CST 1970
Typer
Thu Jan 01 08:00:30 CST 1970

因此所谓的防御式拷贝,不仅要关注getter方法,也要关注setter方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值